@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user