feat(platform): I1 bootstrap, I2 M1 APIs, OpenAPI SSOT, and CI guards

Deliver dual Spring Boot services (platform API + webhook ingress), JWT
auth, Flyway with isolated history tables, customer/project/dictionary
endpoints, OpenAPI snapshot under contracts/, RUNBOOK, and CI that runs
on services/web/contracts paths plus enforcer + dependency tree ban on
craftlabs-auth-bitanswer.

Made-with: Cursor
This commit is contained in:
2026-04-06 21:04:56 +08:00
parent 76ff98db87
commit 3f577b34d5
57 changed files with 3170 additions and 0 deletions
@@ -0,0 +1,53 @@
package cn.craftlabs.platform.webhook;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
class CallbackIngestControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void rejectsWithoutToken() throws Exception {
mockMvc.perform(
post("/webhook/bitanswer/callback")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isUnauthorized());
}
@Test
void acceptsWithToken() throws Exception {
mockMvc.perform(
post("/webhook/bitanswer/callback")
.header(CallbackIngestController.HEADER_TOKEN, "test-secret")
.header("Idempotency-Key", "k1")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"event\":\"sn:post_activate\"}"))
.andExpect(status().isOk());
}
@Test
void duplicateIdempotencyKeyStillOk() throws Exception {
String body = "{\"event\":\"dup\"}";
for (int i = 0; i < 2; i++) {
mockMvc.perform(
post("/webhook/bitanswer/callback")
.header(CallbackIngestController.HEADER_TOKEN, "test-secret")
.header("Idempotency-Key", "stable-key")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk());
}
}
}
@@ -0,0 +1,20 @@
spring:
datasource:
url: jdbc:h2:mem:webhook_ingress;MODE=PostgreSQL;DATABASE_TO_LOWER=TRUE;DEFAULT_NULL_ORDERING=HIGH
driver-class-name: org.h2.Driver
username: sa
password:
flyway:
enabled: false
sql:
init:
mode: always
schema-locations: classpath:schema-webhook-test.sql
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
craftlabs:
webhook:
expected-token: test-secret
@@ -0,0 +1,8 @@
-- 单测建表(与 db/migration/V1 语义一致);单测关闭 Flyway,由 spring.sql.init 执行
CREATE TABLE IF NOT EXISTS webhook_callback_receipt (
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
idempotency_key VARCHAR(512),
body_bytes INT NOT NULL,
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_webhook_idempotency UNIQUE (idempotency_key)
);