feat(platform): I5 callback inbox, internal ingest, and M6 catalog APIs

Made-with: Cursor
This commit is contained in:
2026-04-06 22:40:21 +08:00
parent b6e110acaf
commit fc0c4b1930
30 changed files with 2646 additions and 3 deletions
@@ -0,0 +1,120 @@
package cn.craftlabs.platform.api.callback;
import cn.craftlabs.platform.api.support.JwtTestSupport;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
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 org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.nullValue;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class CallbackInboxControllerTest {
private static final String INTERNAL_TOKEN = "unit-test-internal-token-for-callback-ingest";
private static final String INTERNAL_HEADER = "X-Platform-Internal-Token";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void listDetailStatusLinkAndIntegrationCatalog() throws Exception {
String token = JwtTestSupport.obtainBearerToken(mockMvc, objectMapper);
String auth = "Bearer " + token;
mockMvc.perform(
get("/api/v1/integration/product-lines")
.header("Authorization", auth)
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$.content[0].code").value("default"));
mockMvc.perform(
get("/api/v1/integration/environments")
.header("Authorization", auth)
.param("page", "0")
.param("size", "10"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content.length()").value(2));
String ingestBody = minimalIngestJson("msg-inbox-flow-1");
String ingestResp =
mockMvc.perform(
post("/internal/v1/callback-events")
.header(INTERNAL_HEADER, INTERNAL_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(ingestBody))
.andExpect(status().isOk())
.andReturn()
.getResponse()
.getContentAsString();
long inboxId = objectMapper.readTree(ingestResp).get("inboxId").asLong();
mockMvc.perform(
get("/api/v1/callback-inbox")
.header("Authorization", auth)
.param("page", "0")
.param("size", "20")
.param("eventType", "sn:test"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.content.length()").value(1))
.andExpect(jsonPath("$.content[0].id").value(inboxId))
.andExpect(jsonPath("$.content[0].rawPayload").value(nullValue()));
mockMvc.perform(get("/api/v1/callback-inbox/" + inboxId).header("Authorization", auth))
.andExpect(status().isOk())
.andExpect(jsonPath("$.rawPayload").exists())
.andExpect(jsonPath("$.status").value("PENDING"));
mockMvc.perform(
patch("/api/v1/callback-inbox/" + inboxId + "/status")
.header("Authorization", auth)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"PROCESSED\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.status").value("PROCESSED"));
mockMvc.perform(
patch("/api/v1/callback-inbox/" + inboxId + "/status")
.header("Authorization", auth)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"status\":\"FAILED\"}"))
.andExpect(status().isConflict())
.andExpect(jsonPath("$.message").value(containsString("illegal")));
mockMvc.perform(
patch("/api/v1/callback-inbox/" + inboxId + "/link")
.header("Authorization", auth)
.contentType(MediaType.APPLICATION_JSON)
.content("{\"licenseSnId\":999999}"))
.andExpect(status().isNotFound());
}
private String minimalIngestJson(String externalMessageId) throws Exception {
ObjectNode root = objectMapper.createObjectNode();
root.put("schemaVersion", "1.0");
root.put("sourceSystem", "BITANSWER");
root.put("externalMessageId", externalMessageId);
root.put("eventType", "sn:test");
root.set("rawPayload", objectMapper.createObjectNode().put("sn", "SN-X"));
return objectMapper.writeValueAsString(root);
}
}
@@ -0,0 +1,95 @@
package cn.craftlabs.platform.api.internal;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
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 org.springframework.transaction.annotation.Transactional;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class CallbackInternalControllerTest {
private static final String INTERNAL_TOKEN = "unit-test-internal-token-for-callback-ingest";
private static final String INTERNAL_HEADER = "X-Platform-Internal-Token";
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Test
void unauthorizedWithoutToken() throws Exception {
mockMvc.perform(
post("/internal/v1/callback-events")
.contentType(MediaType.APPLICATION_JSON)
.content("{}"))
.andExpect(status().isUnauthorized());
}
@Test
void ingestIdempotentReturnsSameInboxId() throws Exception {
String body = minimalIngestJson("msg-idempotent-1");
String first =
mockMvc.perform(
post("/internal/v1/callback-events")
.header(INTERNAL_HEADER, INTERNAL_TOKEN)
.header("Idempotency-Key", "idem-1")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.duplicate").value(false))
.andReturn()
.getResponse()
.getContentAsString();
long inboxId = objectMapper.readTree(first).get("inboxId").asLong();
mockMvc.perform(
post("/internal/v1/callback-events")
.header(INTERNAL_HEADER, INTERNAL_TOKEN)
.header("Idempotency-Key", "idem-replay")
.contentType(MediaType.APPLICATION_JSON)
.content(body))
.andExpect(status().isOk())
.andExpect(jsonPath("$.duplicate").value(true))
.andExpect(jsonPath("$.inboxId").value(inboxId));
}
@Test
void rejectsUnsupportedSchemaMajor() throws Exception {
ObjectNode root = objectMapper.createObjectNode();
root.put("schemaVersion", "2.0");
root.put("sourceSystem", "BITANSWER");
root.put("externalMessageId", "msg-major");
root.put("eventType", "t");
root.set("rawPayload", objectMapper.createObjectNode());
mockMvc.perform(
post("/internal/v1/callback-events")
.header(INTERNAL_HEADER, INTERNAL_TOKEN)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(root)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value(containsString("unsupported schema major")));
}
private String minimalIngestJson(String externalMessageId) throws Exception {
ObjectNode root = objectMapper.createObjectNode();
root.put("schemaVersion", "1.0");
root.put("sourceSystem", "BITANSWER");
root.put("externalMessageId", externalMessageId);
root.put("eventType", "sn:test");
root.set("rawPayload", objectMapper.createObjectNode().put("sn", "SN-X"));
return objectMapper.writeValueAsString(root);
}
}
@@ -12,3 +12,5 @@ spring:
platform:
jwt:
secret: unit-test-jwt-secret-at-least-32-chars-ok
internal:
token: unit-test-internal-token-for-callback-ingest