package com.yunhai.cms.controller.oapi; import com.yunhai.cms.entity.CmsSite; import com.yunhai.cms.support.CmsIntegrationTestBase; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.MvcResult; import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; /** * 站点目录开放接口 IT(P0-1 解堵的 CMS 半边): * 咬合契约=svc-hall CmsSiteDirectoryImpl(appKey=100018 签名调用;resolveSite 不传 schId 拉全量、 * listSites(schId) 按校过滤)。本类冻结三件事: * * 签名 canonical 与平台 AppSignInterceptor 一致:METHOD + "\n" + path[?sortedQuery] + "\n" + ts, * HmacSHA256 输出 Base64(见 SignUtil.signRequest / AppAuthFilter.buildSignedTarget)。 */ @DisplayName("SiteOapi /oapi/site/list - P0-1 解堵:签名鉴权 + schId 可选契约") class SiteOapiIT extends CmsIntegrationTestBase { /** 与 svc-hall application.yml yh.app-auth.app-secret 逐字一致(联调态密钥) */ private static final String SVC_HALL_APP_KEY = "100018"; private static final String SVC_HALL_APP_SECRET = "svc-hall-dev-secret-100018"; @Autowired private MockMvc mockMvc; @BeforeEach void seed() { purgeTables(); persistSite("demo1", 1, "演示站点一"); persistSite("schx", 1, "主站X子站"); persistSite("demo2", 2, "演示站点二"); // 停用站点:任何形态(全量/按校)都不得下发 CmsSite disabled = persistSite("off1", 1, "停用站点"); disabled.setStatus(0); siteRepository.saveAndFlush(disabled); } @AfterEach void clean() { purgeTables(); } @Test @DisplayName("无签名 → 401(RequireAppSignFilter 拦截)") void noSignature_yields401() throws Exception { mockMvc.perform(get("/oapi/site/list")).andExpect(status().isUnauthorized()); mockMvc.perform(get("/oapi/site/list?schId=1")).andExpect(status().isUnauthorized()); } @Test @DisplayName("错误密钥签名 → 401") void wrongSecret_yields401() throws Exception { mockMvc.perform(get("/oapi/site/list") .header("X-App-Key", SVC_HALL_APP_KEY) .header("X-Timestamp", String.valueOf(System.currentTimeMillis())) .header("X-Signature", "bad-signature")) .andExpect(status().isUnauthorized()); } @Test @DisplayName("带签名不传 schId → 全部启用站点(跨校全量,供 resolveSite 反解 siteCode)") void signed_withoutSchId_returnsAllEnabledSites() throws Exception { MvcResult result = mockMvc.perform(get("/oapi/site/list").headers(signHeaders("/oapi/site/list"))) .andExpect(status().isOk()) .andReturn(); String body = result.getResponse().getContentAsString(StandardCharsets.UTF_8); assertThat(body).contains("\"siteCode\":\"demo1\"").contains("\"siteCode\":\"demo2\""); assertThat(body).doesNotContain("off1").doesNotContain("停用站点"); } @Test @DisplayName("带签名传 schId → 仅该校启用站点") void signed_withSchId_filtersBySchool() throws Exception { MvcResult result = mockMvc.perform(get("/oapi/site/list?schId=1") .headers(signHeaders("/oapi/site/list?schId=1"))) .andExpect(status().isOk()) .andReturn(); String body = result.getResponse().getContentAsString(StandardCharsets.UTF_8); assertThat(body).contains("\"siteCode\":\"demo1\"").contains("\"siteCode\":\"schx\""); assertThat(body).doesNotContain("demo2"); } @Test @DisplayName("出参形态冻结:{code,msg,data[]}, 元素键=siteCode/name/schId(schId 为数字)") void responseShape_frozenForSvcHallConsumer() throws Exception { MvcResult result = mockMvc.perform(get("/oapi/site/list").headers(signHeaders("/oapi/site/list"))) .andExpect(status().isOk()) .andReturn(); String body = result.getResponse().getContentAsString(StandardCharsets.UTF_8); // 信封(IDHttp 按code/data 解包)+ 消费方三键(CmsSiteDirectoryImpl.mapToVos) assertThat(body).startsWith("{\"code\":0").contains("\"data\":["); assertThat(body).contains("\"name\":\"演示站点一\"").contains("\"schId\":1"); } /** 按平台 AppSignInterceptor 规则生成签名头(path + 排序 query,与 AppAuthFilter 服务端验签对齐) */ private static org.springframework.http.HttpHeaders signHeaders(String target) { String ts = String.valueOf(System.currentTimeMillis()); String sign = com.yunhai.platform.utils.crypto.SignUtil.signRequest( "GET", target, ts, SVC_HALL_APP_SECRET); org.springframework.http.HttpHeaders headers = new org.springframework.http.HttpHeaders(); headers.add("X-App-Key", SVC_HALL_APP_KEY); headers.add("X-Timestamp", ts); headers.add("X-Signature", sign); return headers; } }