mirror of
https://github.com/hpd840321/craftlabs-authorization-sdk.git
synced 2026-06-09 18:10:30 +08:00
feat(sdk): AuthConfigs, JSON Schema, examples, and release checksum CI
Add craftlabs-auth-config.schema.json, Java AuthConfigs model with tests, example configs aligned to BP-10, C/Java/auth-config documentation, native header notes, RELEASING guide, and workflow to verify SDK artifact checksums on release tags. Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class AuthConfigsTest {
|
||||
|
||||
@Test
|
||||
void parseWharfExampleFromClasspath() throws Exception {
|
||||
String json = readClasspath("examples/config/wharf.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals(1, c.schemaVersion());
|
||||
assertEquals("bitanswer", c.provider());
|
||||
assertEquals("wharf", c.scenario());
|
||||
assertNotNull(c.bitanswer());
|
||||
assertTrue(c.bitanswer().url().startsWith("bit://"));
|
||||
assertEquals(101, c.bitanswerFeatureId("container_number_detect"));
|
||||
assertEquals("group", c.wharf().topology());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseSchoolExample() throws Exception {
|
||||
String json = readClasspath("examples/config/school.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals("school", c.scenario());
|
||||
assertEquals(201, c.bitanswerFeatureId("face"));
|
||||
assertEquals("classroom-gate-01", c.school().edgeDeviceId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void parseFloatingExample_requiresProjectId() throws Exception {
|
||||
String json = readClasspath("examples/config/floating.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
assertEquals("floating", c.scenario());
|
||||
assertEquals("migrant-flow-prj-2026-001", c.floating().projectId());
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateFails_whenFloatingWithoutProject() {
|
||||
String json =
|
||||
"""
|
||||
{"schemaVersion":1,"provider":"bitanswer","scenario":"floating",\
|
||||
"bitanswer":{"url":"http://x"},"floating":{}}\
|
||||
""";
|
||||
AuthConfigException ex = assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
|
||||
assertTrue(ex.getMessage().contains("projectId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validateFails_whenBitanswerWithoutUrl() {
|
||||
String json =
|
||||
"""
|
||||
{"schemaVersion":1,"provider":"bitanswer","scenario":"school",\
|
||||
"bitanswer":{"url":""}}\
|
||||
""";
|
||||
assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json));
|
||||
}
|
||||
|
||||
@Test
|
||||
void roundTrip_toJson() throws Exception {
|
||||
String json = readClasspath("examples/config/school.bitanswer.json");
|
||||
AuthConfig c = AuthConfigs.parse(json);
|
||||
String out = AuthConfigs.toJson(c);
|
||||
AuthConfig again = AuthConfigs.parse(out);
|
||||
assertEquals(c.scenario(), again.scenario());
|
||||
assertEquals(c.bitanswerFeatureId("expression"), again.bitanswerFeatureId("expression"));
|
||||
}
|
||||
|
||||
private static String readClasspath(String path) throws Exception {
|
||||
try (InputStream in = AuthConfigsTest.class.getClassLoader().getResourceAsStream(path)) {
|
||||
assertNotNull(in, "missing classpath resource: " + path);
|
||||
return new String(in.readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package cn.craftlabs.auth.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.networknt.schema.JsonSchema;
|
||||
import com.networknt.schema.JsonSchemaFactory;
|
||||
import com.networknt.schema.SpecVersion;
|
||||
import com.networknt.schema.ValidationMessage;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* BP-10 / I5:{@code examples/config/*.json} 与仓库根 {@code schemas/craftlabs-auth-config.schema.json} 一致。
|
||||
*/
|
||||
class ExamplesConfigSchemaValidationTest {
|
||||
|
||||
private static final ObjectMapper MAPPER = new ObjectMapper();
|
||||
|
||||
@Test
|
||||
void allExampleConfigsValidateAgainstSchema() throws Exception {
|
||||
Path repoRoot = findRepoRoot();
|
||||
Path schemaPath = repoRoot.resolve("schemas/craftlabs-auth-config.schema.json");
|
||||
Path examplesDir = repoRoot.resolve("examples/config");
|
||||
assertTrue(Files.isRegularFile(schemaPath), "schema missing: " + schemaPath);
|
||||
assertTrue(Files.isDirectory(examplesDir), "examples dir missing: " + examplesDir);
|
||||
|
||||
JsonNode schemaNode = MAPPER.readTree(schemaPath.toFile());
|
||||
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012);
|
||||
JsonSchema schema = factory.getSchema(schemaNode);
|
||||
|
||||
try (Stream<Path> stream = Files.list(examplesDir)) {
|
||||
stream.filter(p -> p.toString().endsWith(".json")).forEach(jsonFile -> {
|
||||
try {
|
||||
JsonNode instance = MAPPER.readTree(jsonFile.toFile());
|
||||
Set<ValidationMessage> errors = schema.validate(instance);
|
||||
assertTrue(
|
||||
errors.isEmpty(),
|
||||
() -> jsonFile.getFileName() + ": " + errors);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(jsonFile.toString(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自 {@code user.dir} 向上查找含 {@code schemas/craftlabs-auth-config.schema.json} 的目录(兼容模块目录或仓库根执行)。
|
||||
*/
|
||||
static Path findRepoRoot() {
|
||||
Path p = Path.of(System.getProperty("user.dir", ".")).toAbsolutePath().normalize();
|
||||
for (int i = 0; i < 8 && p != null; i++) {
|
||||
Path candidate = p.resolve("schemas/craftlabs-auth-config.schema.json");
|
||||
if (Files.isRegularFile(candidate)) {
|
||||
return p;
|
||||
}
|
||||
p = p.getParent();
|
||||
}
|
||||
throw new IllegalStateException(
|
||||
"Could not find repo root (schemas/craftlabs-auth-config.schema.json) from user.dir="
|
||||
+ System.getProperty("user.dir"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "floating",
|
||||
"bitanswer": {
|
||||
"url": "https://cloud.bitanswer.example/e3",
|
||||
"loginMode": "REMOTE"
|
||||
},
|
||||
"features": {
|
||||
"face": { "bitanswerFeatureId": 301 }
|
||||
},
|
||||
"floating": {
|
||||
"projectId": "migrant-flow-prj-2026-001",
|
||||
"projectName": "某市流动人口人像核验",
|
||||
"contractRef": "PO-2026-8848"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "school",
|
||||
"bitanswer": {
|
||||
"url": "https://cloud.bitanswer.example/e3",
|
||||
"loginMode": "AUTO",
|
||||
"sn": ""
|
||||
},
|
||||
"features": {
|
||||
"face": { "bitanswerFeatureId": 201 },
|
||||
"expression": { "bitanswerFeatureId": 202 }
|
||||
},
|
||||
"school": {
|
||||
"edgeDeviceId": "classroom-gate-01",
|
||||
"tenantId": "school-district-demo"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"provider": "bitanswer",
|
||||
"scenario": "wharf",
|
||||
"bitanswer": {
|
||||
"url": "bit://license.example.com:8273",
|
||||
"loginMode": "AUTO",
|
||||
"rootPath": "/var/lib/craftlabs/bitanswer"
|
||||
},
|
||||
"features": {
|
||||
"container_number_detect": { "bitanswerFeatureId": 101 },
|
||||
"container_number_recognize": { "bitanswerFeatureId": 102 },
|
||||
"iso_detect": { "bitanswerFeatureId": 103 },
|
||||
"iso_recognize": { "bitanswerFeatureId": 104 }
|
||||
},
|
||||
"wharf": {
|
||||
"topology": "group",
|
||||
"groupServiceUrl": "bit://license.example.com:8273",
|
||||
"notes": "边设备集中连集团服务;特征项与控制台产品一致后替换 id。"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user