122 lines
5.7 KiB
Java
122 lines
5.7 KiB
Java
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;
|
||
}
|
||
}
|