周末修订
build / build-push (push) Successful in 3m59s

This commit is contained in:
chenxing
2026-09-15 23:41:34 +08:00
parent e4b843cf00
commit 1cb95225a6
52 changed files with 1250 additions and 3788 deletions
@@ -0,0 +1,121 @@
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) 按校过滤)。本类冻结三件事:
* <ul>
* <li>鉴权:/oapi/** 无签名 401(RequireAppSignFilter)、错密钥 401;</li>
* <li>契约:schId 可选——不传=全部启用站点(含跨校),传了=按校过滤;</li>
* <li>出参形态:data[] 元素键=siteCode/name/schId(消费方 mapToVos 按这三键解析)。</li>
* </ul>
* 签名 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;
}
}