feat: 增加 EasyFlow license 启动校验能力
- 新增 license 解析、验签、机器指纹校验与失败日志分流 - 支持开发态 classpath 读取与打包排除 lic 资源
This commit is contained in:
@@ -0,0 +1,478 @@
|
||||
package tech.easyflow.autoconfig.license;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.core.env.MapPropertySource;
|
||||
import org.springframework.core.env.StandardEnvironment;
|
||||
import org.springframework.core.io.DefaultResourceLoader;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.Signature;
|
||||
import java.time.Clock;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link EasyflowLicenseVerifier} 测试。
|
||||
*/
|
||||
public class EasyflowLicenseVerifierTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-10T12:00:00Z"), ZoneOffset.UTC);
|
||||
|
||||
/**
|
||||
* 合法 license 应校验通过。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldVerifyValidLicense() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
EasyflowLicenseVerificationResult result = verifier.verify(files.licenseLocation);
|
||||
|
||||
Assert.assertEquals("LIC-20260510-120000", result.licenseId());
|
||||
Assert.assertEquals("prod-2026-01", result.keyId());
|
||||
Assert.assertEquals("STANDARD", result.licenseType());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当配置目录时,应自动找到唯一的 .lic 文件。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldResolveSingleLicFileFromDirectory() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
EasyflowLicenseVerificationResult result = verifier.verify("file:" + Path.of(files.licensePath).getParent());
|
||||
|
||||
Assert.assertEquals("LIC-20260510-120000", result.licenseId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 当配置的固定文件不存在时,应回退到同目录唯一的 .lic 文件。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFallbackToSingleLicFileInSameDirectory() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
EasyflowLicenseVerificationResult result = verifier.verify("file:" + Path.of(files.licensePath).getParent().resolve("easyflow.lic"));
|
||||
|
||||
Assert.assertEquals("LIC-20260510-120000", result.licenseId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 缺失 license 文件时应抛出缺失异常。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenLicenseFileMissing() {
|
||||
EasyflowLicenseVerifier verifier = createVerifier("classpath:missing-public.pem",
|
||||
new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF"));
|
||||
|
||||
try {
|
||||
verifier.verify("file:/path/not-exists.lic");
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.isMissing());
|
||||
Assert.assertTrue(ex.getMessage().contains("不存在"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同目录存在多个 .lic 文件时应报定位失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenMultipleLicFilesExist() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
|
||||
Files.writeString(Path.of(files.licensePath).getParent().resolve("other.lic"), "dummy", StandardCharsets.UTF_8);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
try {
|
||||
verifier.verify("file:" + Path.of(files.licensePath).getParent());
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.isMissing());
|
||||
Assert.assertTrue(ex.getMessage().contains("多个 .lic"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非法前缀应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenPrefixInvalid() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, true);
|
||||
Files.writeString(Path.of(files.licensePath), "BAD." + Files.readString(Path.of(files.licensePath)), StandardCharsets.UTF_8);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
try {
|
||||
verifier.verify(files.licenseLocation);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertFalse(ex.isMissing());
|
||||
Assert.assertTrue(ex.getMessage().contains("前缀"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 非法 product 应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenProductInvalid() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "OtherFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
try {
|
||||
verifier.verify(files.licenseLocation);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains("product"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已过期的 STANDARD license 应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenLicenseExpired() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-05-01T00:00:00Z", "EasyFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
try {
|
||||
verifier.verify(files.licenseLocation);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains("过期"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DEV license 可不包含 expiresAt。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldAllowDevLicenseWithoutExpiresAt() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "DEV", null, "EasyFlow"), identity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
EasyflowLicenseVerificationResult result = verifier.verify(files.licenseLocation);
|
||||
Assert.assertNull(result.expiresAt());
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器指纹不匹配时应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenFingerprintMismatch() throws Exception {
|
||||
MachineIdentity licenseIdentity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
MachineIdentity runtimeIdentity = new MachineIdentity("machine-two", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(licenseIdentity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), licenseIdentity, false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, runtimeIdentity);
|
||||
try {
|
||||
verifier.verify(files.licenseLocation);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains("指纹不匹配"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* classpath 资源应可被正确读取并校验。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldVerifyClasspathLicense() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
TestLicenseFiles files = createLicenseFiles(createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow"), identity, false);
|
||||
writeClasspathTestResource("test-license/public.pem", Files.readString(Path.of(files.publicKeyPath), StandardCharsets.UTF_8));
|
||||
writeClasspathTestResource("test-license/license.lic", Files.readString(Path.of(files.licensePath), StandardCharsets.UTF_8));
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier("classpath:test-license/public.pem", identity);
|
||||
EasyflowLicenseVerificationResult result = verifier.verify("classpath:test-license/license.lic");
|
||||
Assert.assertEquals("LIC-20260510-120000", result.licenseId());
|
||||
}
|
||||
|
||||
/**
|
||||
* formatVersion 非 1 时应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenFormatVersionInvalid() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
|
||||
TestLicenseFiles files = createLicenseFiles(payload, identity, false, 2, "Ed25519", payload.get("fingerprintAlgorithm").toString(), false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
assertInvalid(verifier, files.licenseLocation, "formatVersion");
|
||||
}
|
||||
|
||||
/**
|
||||
* alg 非 Ed25519 时应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenAlgorithmInvalid() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
|
||||
TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "RSA", payload.get("fingerprintAlgorithm").toString(), false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
assertInvalid(verifier, files.licenseLocation, "签名算法");
|
||||
}
|
||||
|
||||
/**
|
||||
* fingerprintAlgorithm 不匹配时应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenFingerprintAlgorithmInvalid() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
|
||||
TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "Ed25519", "sha256(other)", false);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
assertInvalid(verifier, files.licenseLocation, "fingerprintAlgorithm");
|
||||
}
|
||||
|
||||
/**
|
||||
* 篡改签名后应校验失败。
|
||||
*
|
||||
* @throws Exception 测试失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenSignatureTampered() throws Exception {
|
||||
MachineIdentity identity = new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF");
|
||||
Map<String, Object> payload = createPayload(identity, "STANDARD", "2026-12-31T00:00:00Z", "EasyFlow");
|
||||
TestLicenseFiles files = createLicenseFiles(payload, identity, false, 1, "Ed25519", payload.get("fingerprintAlgorithm").toString(), true);
|
||||
|
||||
EasyflowLicenseVerifier verifier = createVerifier(files.publicKeyLocation, identity);
|
||||
assertInvalid(verifier, files.licenseLocation, "签名校验失败");
|
||||
}
|
||||
|
||||
/**
|
||||
* MAC 地址应完成归一化、去重与排序。
|
||||
*/
|
||||
@Test
|
||||
public void shouldNormalizeMacAddresses() {
|
||||
Assert.assertEquals("aa:bb:cc:dd:ee:ff",
|
||||
EasyflowLicenseVerifier.normalizeMacAddress("AA-BB-CC-DD-EE-FF"));
|
||||
Assert.assertEquals(
|
||||
java.util.List.of("11:22:33:44:55:66", "aa:bb:cc:dd:ee:ff"),
|
||||
EasyflowLicenseVerifier.normalizeMacAddresses("AA-BB-CC-DD-EE-FF\n11:22:33:44:55:66\naa:bb:cc:dd:ee:ff"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动前校验器应在缺失 license 时直接失败。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailBootstrapValidatorWhenLicenseMissing() {
|
||||
EasyflowLicenseBootstrapValidator validator = new EasyflowLicenseBootstrapValidator(
|
||||
createVerifier("classpath:missing-public.pem",
|
||||
new MachineIdentity("machine-one", "product-one", "AA-BB-CC-DD-EE-FF"))
|
||||
);
|
||||
StandardEnvironment environment = new StandardEnvironment();
|
||||
environment.getPropertySources().addFirst(
|
||||
new MapPropertySource("test", Map.of("easyflow.license.location", "file:/path/not-exists.lic"))
|
||||
);
|
||||
validator.setEnvironment(environment);
|
||||
|
||||
try {
|
||||
validator.postProcessBeanFactory(null);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (IllegalStateException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains("不存在") || ex.getMessage().contains("未配置"));
|
||||
}
|
||||
}
|
||||
|
||||
private EasyflowLicenseVerifier createVerifier(String publicKeyLocation, MachineIdentity identity) {
|
||||
return new EasyflowLicenseVerifier(
|
||||
new DefaultResourceLoader(),
|
||||
new StubMachineIdentityCollector(identity),
|
||||
FIXED_CLOCK,
|
||||
publicKeyLocation
|
||||
);
|
||||
}
|
||||
|
||||
private TestLicenseFiles createLicenseFiles(Map<String, Object> payload,
|
||||
MachineIdentity identity,
|
||||
boolean keepOriginalPrefix) throws Exception {
|
||||
return createLicenseFiles(payload, identity, keepOriginalPrefix, 1, "Ed25519",
|
||||
payload.get("fingerprintAlgorithm").toString(), false);
|
||||
}
|
||||
|
||||
private TestLicenseFiles createLicenseFiles(Map<String, Object> payload,
|
||||
MachineIdentity identity,
|
||||
boolean keepOriginalPrefix,
|
||||
int formatVersion,
|
||||
String algorithm,
|
||||
String fingerprintAlgorithm,
|
||||
boolean tamperSignature) throws Exception {
|
||||
KeyPair keyPair = KeyPairGenerator.getInstance("Ed25519").generateKeyPair();
|
||||
payload.put("fingerprintAlgorithm", fingerprintAlgorithm);
|
||||
String canonicalPayload = buildCanonicalPayloadJson(payload);
|
||||
String signature = sign(canonicalPayload, keyPair);
|
||||
if (tamperSignature) {
|
||||
signature = Base64.getEncoder().encodeToString("tampered-signature".getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
Map<String, Object> envelope = new LinkedHashMap<>();
|
||||
envelope.put("formatVersion", formatVersion);
|
||||
envelope.put("alg", algorithm);
|
||||
envelope.put("keyId", "prod-2026-01");
|
||||
envelope.put("payload", payload);
|
||||
envelope.put("signature", signature);
|
||||
|
||||
String licenseContent = EasyflowLicenseVerifier.LICENSE_FILE_PREFIX
|
||||
+ Base64.getUrlEncoder().withoutPadding()
|
||||
.encodeToString(OBJECT_MAPPER.writeValueAsBytes(envelope));
|
||||
if (!keepOriginalPrefix) {
|
||||
licenseContent = licenseContent.trim();
|
||||
}
|
||||
|
||||
Path tempDir = Files.createTempDirectory("easyflow-license-test");
|
||||
Path publicKeyPath = tempDir.resolve("public.pem");
|
||||
Path licensePath = tempDir.resolve("license.lic");
|
||||
Files.writeString(publicKeyPath, toPublicPem(keyPair), StandardCharsets.UTF_8);
|
||||
Files.writeString(licensePath, licenseContent, StandardCharsets.UTF_8);
|
||||
return new TestLicenseFiles(publicKeyPath.toUri().toString(),
|
||||
licensePath.toUri().toString(),
|
||||
publicKeyPath.toString(),
|
||||
licensePath.toString());
|
||||
}
|
||||
|
||||
private Map<String, Object> createPayload(MachineIdentity identity,
|
||||
String licenseType,
|
||||
String expiresAt,
|
||||
String product) {
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("licenseId", "LIC-20260510-120000");
|
||||
payload.put("product", product);
|
||||
payload.put("customerName", "Test Customer");
|
||||
payload.put("environment", "PROD");
|
||||
payload.put("licenseType", licenseType);
|
||||
payload.put("issuedAt", "2026-05-10T12:00:00Z");
|
||||
payload.put("expiresAt", expiresAt);
|
||||
payload.put("fingerprintAlgorithm", EasyflowLicenseVerifier.FINGERPRINT_ALGORITHM);
|
||||
payload.put("machineFingerprint", EasyflowLicenseVerifier.computeMachineFingerprint(identity));
|
||||
return payload;
|
||||
}
|
||||
|
||||
private String buildCanonicalPayloadJson(Map<String, Object> payload) {
|
||||
return "{"
|
||||
+ "\"customerName\":\"" + payload.get("customerName") + "\","
|
||||
+ "\"environment\":\"" + payload.get("environment") + "\","
|
||||
+ "\"expiresAt\":" + quoteOrNull(payload.get("expiresAt")) + ","
|
||||
+ "\"fingerprintAlgorithm\":\"" + payload.get("fingerprintAlgorithm") + "\","
|
||||
+ "\"issuedAt\":\"" + payload.get("issuedAt") + "\","
|
||||
+ "\"licenseId\":\"" + payload.get("licenseId") + "\","
|
||||
+ "\"licenseType\":\"" + payload.get("licenseType") + "\","
|
||||
+ "\"machineFingerprint\":\"" + payload.get("machineFingerprint") + "\","
|
||||
+ "\"product\":\"" + payload.get("product") + "\""
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private String quoteOrNull(Object value) {
|
||||
return value == null ? "null" : "\"" + value + "\"";
|
||||
}
|
||||
|
||||
private String sign(String canonicalPayload, KeyPair keyPair) throws Exception {
|
||||
Signature signer = Signature.getInstance("Ed25519");
|
||||
signer.initSign(keyPair.getPrivate());
|
||||
signer.update(canonicalPayload.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(signer.sign());
|
||||
}
|
||||
|
||||
private String toPublicPem(KeyPair keyPair) {
|
||||
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
|
||||
.encodeToString(keyPair.getPublic().getEncoded());
|
||||
return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC KEY-----\n";
|
||||
}
|
||||
|
||||
private void assertInvalid(EasyflowLicenseVerifier verifier, String location, String expectedMessagePart) {
|
||||
try {
|
||||
verifier.verify(location);
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (EasyflowLicenseException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains(expectedMessagePart));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeClasspathTestResource(String relativePath, String content) throws Exception {
|
||||
Path path = Path.of("target/test-classes").resolve(relativePath);
|
||||
Files.createDirectories(path.getParent());
|
||||
Files.writeString(path, content, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用固定机器信息采集器。
|
||||
*/
|
||||
private static class StubMachineIdentityCollector extends MachineIdentityCollector {
|
||||
|
||||
private final MachineIdentity identity;
|
||||
|
||||
private StubMachineIdentityCollector(MachineIdentity identity) {
|
||||
this.identity = identity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接返回固定机器信息。
|
||||
*
|
||||
* @return 固定机器信息
|
||||
*/
|
||||
@Override
|
||||
public MachineIdentity collect() {
|
||||
return identity;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试临时文件信息。
|
||||
*
|
||||
* @param publicKeyLocation 公钥位置
|
||||
* @param licenseLocation license 位置
|
||||
* @param licensePath license 文件路径
|
||||
*/
|
||||
private record TestLicenseFiles(String publicKeyLocation,
|
||||
String licenseLocation,
|
||||
String publicKeyPath,
|
||||
String licensePath) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package tech.easyflow.autoconfig.license;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
/**
|
||||
* {@link LicenseCanonicalJsonSerializer} 测试。
|
||||
*/
|
||||
public class LicenseCanonicalJsonSerializerTest {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
/**
|
||||
* 应按 key 递归排序对象,且保持数组顺序。
|
||||
*
|
||||
* @throws Exception 解析失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldSerializeCanonicalJson() throws Exception {
|
||||
JsonNode jsonNode = OBJECT_MAPPER.readTree("{\"b\":1,\"arr\":[{\"b\":2,\"a\":1},3],\"a\":{\"d\":4,\"c\":3}}");
|
||||
String canonicalJson = LicenseCanonicalJsonSerializer.serialize(jsonNode);
|
||||
Assert.assertEquals("{\"a\":{\"c\":3,\"d\":4},\"arr\":[{\"a\":1,\"b\":2},3],\"b\":1}", canonicalJson);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
package tech.easyflow.autoconfig.license;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* {@link MachineIdentityCollector} 测试。
|
||||
*/
|
||||
public class MachineIdentityCollectorTest {
|
||||
|
||||
/**
|
||||
* Linux 采集命令应按约定返回三项机器参数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldCollectLinuxIdentity() {
|
||||
TestMachineIdentityCollector collector = new TestMachineIdentityCollector("Linux");
|
||||
collector.shellOutputs.put("cat /etc/machine-id", "linux-machine");
|
||||
collector.shellOutputs.put("cat /sys/class/dmi/id/product_uuid", "linux-product");
|
||||
collector.shellOutputs.put("cat /sys/class/net/$(ip route | awk '/default/ {print $5; exit}')/address", "aa:bb:cc:dd:ee:ff");
|
||||
|
||||
MachineIdentity identity = collector.collect();
|
||||
Assert.assertEquals("linux-machine", identity.machineId());
|
||||
Assert.assertEquals("linux-product", identity.productUuid());
|
||||
Assert.assertEquals("aa:bb:cc:dd:ee:ff", identity.macAddresses());
|
||||
}
|
||||
|
||||
/**
|
||||
* macOS 采集命令应按约定返回三项机器参数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldCollectMacIdentity() {
|
||||
TestMachineIdentityCollector collector = new TestMachineIdentityCollector("Mac OS X");
|
||||
collector.shellOutputs.put("ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/ {print $(NF-1)}'", "mac-platform-uuid");
|
||||
collector.shellOutputs.put("networksetup -listallhardwareports | awk '/Device/ {device=$2} /Ethernet Address/ {print $3; exit}'", "AA-BB-CC-DD-EE-FF");
|
||||
|
||||
MachineIdentity identity = collector.collect();
|
||||
Assert.assertEquals("mac-platform-uuid", identity.machineId());
|
||||
Assert.assertEquals("mac-platform-uuid", identity.productUuid());
|
||||
Assert.assertEquals("AA-BB-CC-DD-EE-FF", identity.macAddresses());
|
||||
}
|
||||
|
||||
/**
|
||||
* Windows 采集命令应按约定返回三项机器参数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldCollectWindowsIdentity() {
|
||||
TestMachineIdentityCollector collector = new TestMachineIdentityCollector("Windows 11");
|
||||
collector.powerShellOutputs.put("(Get-ItemProperty 'HKLM:\\SOFTWARE\\Microsoft\\Cryptography').MachineGuid", "windows-machine");
|
||||
collector.powerShellOutputs.put("(Get-CimInstance Win32_ComputerSystemProduct).UUID", "windows-product");
|
||||
collector.powerShellOutputs.put("(Get-NetAdapter | Where-Object {$_.Status -eq 'Up' -and $_.MacAddress} | Select-Object -First 1 -ExpandProperty MacAddress)", "AA-BB-CC-DD-EE-FF");
|
||||
|
||||
MachineIdentity identity = collector.collect();
|
||||
Assert.assertEquals("windows-machine", identity.machineId());
|
||||
Assert.assertEquals("windows-product", identity.productUuid());
|
||||
Assert.assertEquals("AA-BB-CC-DD-EE-FF", identity.macAddresses());
|
||||
}
|
||||
|
||||
/**
|
||||
* 空输出应被识别为采集失败。
|
||||
*/
|
||||
@Test
|
||||
public void shouldFailWhenCommandOutputIsBlank() {
|
||||
TestMachineIdentityCollector collector = new TestMachineIdentityCollector("Linux");
|
||||
collector.shellOutputs.put("cat /etc/machine-id", "");
|
||||
|
||||
try {
|
||||
collector.collect();
|
||||
Assert.fail("预期应抛出异常");
|
||||
} catch (IllegalStateException ex) {
|
||||
Assert.assertTrue(ex.getMessage().contains("输出为空"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试用机器信息采集器。
|
||||
*/
|
||||
private static class TestMachineIdentityCollector extends MachineIdentityCollector {
|
||||
|
||||
private final String osName;
|
||||
private final Map<String, String> shellOutputs = new HashMap<>();
|
||||
private final Map<String, String> powerShellOutputs = new HashMap<>();
|
||||
|
||||
private TestMachineIdentityCollector(String osName) {
|
||||
this.osName = osName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回固定的操作系统名称。
|
||||
*
|
||||
* @return 操作系统名称
|
||||
*/
|
||||
@Override
|
||||
protected String currentOsName() {
|
||||
return osName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回预设 shell 输出。
|
||||
*
|
||||
* @param description 描述
|
||||
* @param command 命令
|
||||
* @return 命令输出
|
||||
*/
|
||||
@Override
|
||||
protected String executeShellCommand(String description, String command) {
|
||||
return lookupOutput(description, command, shellOutputs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回预设 PowerShell 输出。
|
||||
*
|
||||
* @param description 描述
|
||||
* @param command 命令
|
||||
* @return 命令输出
|
||||
*/
|
||||
@Override
|
||||
protected String executePowerShellCommand(String description, String command) {
|
||||
return lookupOutput(description, command, powerShellOutputs);
|
||||
}
|
||||
|
||||
private String lookupOutput(String description, String command, Map<String, String> outputs) {
|
||||
if (!outputs.containsKey(command)) {
|
||||
throw new IllegalStateException(description + "失败,命令未配置输出");
|
||||
}
|
||||
String output = outputs.get(command);
|
||||
if (output == null || output.trim().isEmpty()) {
|
||||
throw new IllegalStateException(description + "失败,输出为空");
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user