Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、 回调分发、oapi 接口及单元测试。 保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile, 新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
@@ -0,0 +1,449 @@
|
||||
package com.yunhai.flowcenter.callback;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.sun.net.httpserver.HttpExchange;
|
||||
import com.sun.net.httpserver.HttpServer;
|
||||
import com.yunhai.flowcenter.entity.FlwCallbackLog;
|
||||
import com.yunhai.flowcenter.model.OapiModels;
|
||||
import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwCallbackRegRepository;
|
||||
import com.yunhai.flowcenter.service.FlowInstanceService;
|
||||
import com.yunhai.flowcenter.service.OapiFlowService;
|
||||
import com.yunhai.flowcenter.support.FlowTestSupport;
|
||||
import com.yunhai.flowcenter.support.OapiSignedClient;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.TestConfiguration;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 回调分发 IT:注册(探活)→ 代发起 → AFTER_COMMIT 事件 → 落 flw_callback_log →
|
||||
* 异步推送 X-Flw-* 四头 → stub 接收端验签 + payload 字段断言;推送失败持续重试(T1.1:
|
||||
* 越过退避表封顶 1h 不死信、挂起告警恰一次、瞬时失败自动治愈);挂起可视/标记死信/rePush 复活(T1.2)。
|
||||
* 注意:本类必须单类独跑(平台 IT 判据)。
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
@ActiveProfiles("test")
|
||||
class CallbackDispatchIT extends FlowTestSupport {
|
||||
|
||||
private static final String APP_KEY = "100018";
|
||||
private static final String APP_SECRET = "svc-hall-dev-secret-100018";
|
||||
|
||||
@LocalServerPort
|
||||
private int port;
|
||||
|
||||
@Autowired
|
||||
private OapiFlowService oapiFlowService;
|
||||
@Autowired
|
||||
private FlowInstanceService instanceService;
|
||||
@Autowired
|
||||
private CallbackPushService pushService;
|
||||
@Autowired
|
||||
private com.yunhai.flowcenter.service.FlowCallbackAdminService callbackAdminService;
|
||||
@Autowired
|
||||
private FlwCallbackRegRepository regRepository;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
private HttpServer stubServer;
|
||||
private int stubPort;
|
||||
private final CopyOnWriteArrayList<Received> received = new CopyOnWriteArrayList<>();
|
||||
|
||||
/** 计数版 UserMsgClient:只数挂起告警(hang:* msgNo),不真正外发(@Primary 注入 CallbackPushService) */
|
||||
static final CountingUserMsgClient STUB_USER_MSG = new CountingUserMsgClient();
|
||||
|
||||
@TestConfiguration
|
||||
static class StubConfig {
|
||||
@Bean
|
||||
@Primary
|
||||
public com.yunhai.flowcenter.stub.StubUserClient stubUserClient() {
|
||||
return new com.yunhai.flowcenter.stub.StubUserClient();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
public CountingUserMsgClient countingUserMsgClient() {
|
||||
return STUB_USER_MSG;
|
||||
}
|
||||
}
|
||||
|
||||
static class CountingUserMsgClient extends com.yunhai.flowcenter.notify.UserMsgClient {
|
||||
final java.util.concurrent.atomic.AtomicInteger hangAlerts =
|
||||
new java.util.concurrent.atomic.AtomicInteger();
|
||||
|
||||
CountingUserMsgClient() {
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendUserMsg(Integer schId, List<Integer> userIds, Integer msgType, String bizType,
|
||||
String bizId, String title, String content, String jumpPath, String msgNo,
|
||||
Integer expireDays) {
|
||||
if (msgNo != null && msgNo.startsWith("hang:")) {
|
||||
hangAlerts.incrementAndGet();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class Received {
|
||||
String body;
|
||||
String eventId;
|
||||
String timestamp;
|
||||
String nonce;
|
||||
String signature;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
cleanDb();
|
||||
received.clear();
|
||||
STUB_USER_MSG.hangAlerts.set(0);
|
||||
stubServer = HttpServer.create(new InetSocketAddress(0), 0);
|
||||
stubPort = stubServer.getAddress().getPort();
|
||||
stubServer.createContext("/callback/flow", exchange -> {
|
||||
Received r = new Received();
|
||||
r.body = readBody(exchange);
|
||||
r.eventId = exchange.getRequestHeaders().getFirst("X-Flw-EventId");
|
||||
r.timestamp = exchange.getRequestHeaders().getFirst("X-Flw-Timestamp");
|
||||
r.nonce = exchange.getRequestHeaders().getFirst("X-Flw-Nonce");
|
||||
r.signature = exchange.getRequestHeaders().getFirst("X-Flw-Signature");
|
||||
received.add(r);
|
||||
byte[] resp = "{\"ok\":true}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, resp.length);
|
||||
exchange.getResponseBody().write(resp);
|
||||
exchange.close();
|
||||
});
|
||||
stubServer.start();
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
stubServer.stop(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fullChain_register_apply_pushesSignedEvents() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t20", "回调全链", linearDef("ybb", "审核"));
|
||||
// 1. 注册(探活命中 stub)→ secret 下发
|
||||
OapiModels.CallbackRegResultVo reg = oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flow", null));
|
||||
assertTrue(reg.getSecret() != null && reg.getSecret().length() >= 16);
|
||||
|
||||
// 2. 代发起 → flow.submitted + flow.task.created 两事件先后落库并推送
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-1\",\"bizTitle\":\"张三的回调件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
org.springframework.http.ResponseEntity<JsonNode> applyResp =
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
// 3. 轮询 stub:submitted + task.created 两事件都到齐(探活 ping 不计)
|
||||
waitUntil(() -> received.stream().anyMatch(r -> r.body.contains("flow.submitted"))
|
||||
&& received.stream().anyMatch(r -> r.body.contains("flow.task.created")), 10_000);
|
||||
boolean hasSubmitted = false;
|
||||
boolean hasTaskCreated = false;
|
||||
for (Received r : received) {
|
||||
JsonNode payload = readJson(r.body);
|
||||
String event = payload.get("event").asText();
|
||||
if ("ping".equals(event)) {
|
||||
continue; // 注册探活 ping 不参与事件协议断言
|
||||
}
|
||||
if ("flow.submitted".equals(event)) {
|
||||
hasSubmitted = true;
|
||||
}
|
||||
if ("flow.task.created".equals(event)) {
|
||||
hasTaskCreated = true;
|
||||
}
|
||||
// 协议断言:必带字段
|
||||
assertEquals("1", payload.get("schId").asText());
|
||||
assertEquals(def.getFlowGuid(), payload.get("flowGuid").asText());
|
||||
assertEquals("svc_item", payload.get("bizType").asText());
|
||||
assertEquals("cb-1", payload.get("bizId").asText());
|
||||
assertEquals("张三的回调件", payload.get("bizTitle").asText());
|
||||
assertTrue(payload.get("instanceVersion").asInt() >= 1, "instanceVersion 应为办件乐观锁版本");
|
||||
assertNotNull(payload.get("eventId"));
|
||||
assertNotNull(payload.get("occurredTime"));
|
||||
// 签名断言:Base64(HmacSHA256(secret, ts.nonce.body))
|
||||
String expected = hmac(reg.getSecret(), r.timestamp, r.nonce, r.body);
|
||||
assertEquals(expected, r.signature, "推送必须带合法 X-Flw-Signature");
|
||||
assertEquals(r.eventId, payload.get("eventId").asText(), "头与体 eventId 一致");
|
||||
}
|
||||
assertTrue(hasSubmitted, "应有 flow.submitted 事件");
|
||||
assertTrue(hasTaskCreated, "应有 flow.task.created 事件");
|
||||
// 4. 日志表成功态(异步推送落定,轮询等待)
|
||||
assertTrue(logRepository.findAll().size() >= 2);
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.allMatch(l -> l.getStatus() == FlwEnums.CallbackLogStatus.SUCCESS.getCode()), 10_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushFailure_persistsRetryBeyondBackoffList() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t21", "持续重试", linearDef("ybb", "审核"));
|
||||
// 只订阅 flow.submitted:单事件单日志行,断言确定
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flow", "flow.submitted"));
|
||||
FlwCallbackReg regRow = regRepository.findBySchIdAndAppKeyAndFlowGuid(SCH, APP_KEY, def.getFlowGuid())
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
// 注册指向死端口模拟业务方宕机(探活已过,直接改库)
|
||||
regRow.setCallbackUrl("http://localhost:1/dead");
|
||||
regRepository.save(regRow);
|
||||
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-2\",\"bizTitle\":\"张三的挂起件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
|
||||
// 首推失败:retryCount=1(异步首推,轮询等待)
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.anyMatch(l -> l.getRetryCount() != null && l.getRetryCount() >= 1), 10_000);
|
||||
// 手工逐次触发扫描语义再失败 5 轮(共 6 次,越过退避表 5 项)
|
||||
for (int round = 0; round < 5; round++) {
|
||||
for (FlwCallbackLog row : logRepository.findAll()) {
|
||||
pushService.pushAsync(row.getId());
|
||||
}
|
||||
}
|
||||
FlwCallbackLog hung = logRepository.findAll().get(0);
|
||||
// T1.1 核心语义:持续重试不自动死信——status 仍=0、retryCount=6、nextRetry≈now+60min(封顶)
|
||||
assertEquals(FlwEnums.CallbackLogStatus.PENDING.getCode(), hung.getStatus(),
|
||||
"越过退避表后必须保持待推持续重试,不得自动死信");
|
||||
assertEquals(6, hung.getRetryCount());
|
||||
assertNotNull(hung.getNextRetryTime());
|
||||
java.time.LocalDateTime expectedNext = java.time.LocalDateTime.now().plusMinutes(60);
|
||||
assertTrue(hung.getNextRetryTime().isAfter(expectedNext.minusMinutes(5))
|
||||
&& hung.getNextRetryTime().isBefore(expectedNext.plusMinutes(5)),
|
||||
"越过退避表后 nextRetryTime 应封顶 +60 分钟: " + hung.getNextRetryTime());
|
||||
assertTrue(logRepository.findAll().stream()
|
||||
.allMatch(l -> l.getStatus() == FlwEnums.CallbackLogStatus.PENDING.getCode()),
|
||||
"任何行都不应出现死信(status=2)");
|
||||
assertNotNull(hung.getLastError());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hangAlertSentExactlyOnce() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t23", "挂起告警", linearDef("ybb", "审核"));
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flow", "flow.submitted"));
|
||||
FlwCallbackReg regRow = regRepository.findBySchIdAndAppKeyAndFlowGuid(SCH, APP_KEY, def.getFlowGuid())
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
regRow.setCallbackUrl("http://localhost:1/dead");
|
||||
regRepository.save(regRow);
|
||||
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-4\",\"bizTitle\":\"张三的告警件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.anyMatch(l -> l.getRetryCount() != null && l.getRetryCount() >= 1), 10_000);
|
||||
|
||||
// 失败到第 6 次(=挂起阈值):告警恰一次
|
||||
for (int round = 0; round < 5; round++) {
|
||||
for (FlwCallbackLog row : logRepository.findAll()) {
|
||||
pushService.pushAsync(row.getId());
|
||||
}
|
||||
}
|
||||
assertEquals(1, STUB_USER_MSG.hangAlerts.get(), "第 6 次失败后挂起告警应恰发送一次");
|
||||
|
||||
// 第 7 次失败:仍是 1(恰一次)
|
||||
for (FlwCallbackLog row : logRepository.findAll()) {
|
||||
pushService.pushAsync(row.getId());
|
||||
}
|
||||
assertEquals(1, STUB_USER_MSG.hangAlerts.get(), "第 7 次失败后告警计数仍应为 1");
|
||||
assertEquals(7, logRepository.findAll().get(0).getRetryCount());
|
||||
}
|
||||
|
||||
@Test
|
||||
void transientFailureRecovers() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t24", "瞬时失败治愈", linearDef("ybb", "审核"));
|
||||
// 抖动端点:探活(第1次) 200、首推(第2次) 500、其后 200——模拟消费方瞬时故障后恢复
|
||||
AtomicInteger flakyCount = new AtomicInteger(0);
|
||||
stubServer.createContext("/callback/flaky", exchange -> {
|
||||
boolean fail = flakyCount.incrementAndGet() == 2;
|
||||
byte[] resp = "{\"ok\":true}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(fail ? 500 : 200, resp.length);
|
||||
exchange.getResponseBody().write(resp);
|
||||
exchange.close();
|
||||
});
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flaky", "flow.submitted"));
|
||||
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-5\",\"bizTitle\":\"张三的抖动件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
|
||||
// 首推 500 → 进入重试(status=0, retryCount=1)
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.anyMatch(l -> l.getRetryCount() != null && l.getRetryCount() >= 1), 10_000);
|
||||
// 消费方恢复后(扫描/重推触发)→ 200 → status=1 + pushTime 落定
|
||||
for (FlwCallbackLog row : logRepository.findAll()) {
|
||||
pushService.pushAsync(row.getId());
|
||||
}
|
||||
FlwCallbackLog recovered = logRepository.findAll().get(0);
|
||||
assertEquals(FlwEnums.CallbackLogStatus.SUCCESS.getCode(), recovered.getStatus(),
|
||||
"瞬时失败恢复后应自动治愈为成功");
|
||||
assertNotNull(recovered.getPushTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void hungList_paged_andMarkDead_thenRepushRevives() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t25", "挂起管理", linearDef("ybb", "审核"));
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flow", "flow.submitted"));
|
||||
FlwCallbackReg regRow = regRepository.findBySchIdAndAppKeyAndFlowGuid(SCH, APP_KEY, def.getFlowGuid())
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
regRow.setCallbackUrl("http://localhost:1/dead");
|
||||
regRepository.save(regRow);
|
||||
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-6\",\"bizTitle\":\"张三的管理件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.anyMatch(l -> l.getRetryCount() != null && l.getRetryCount() >= 1), 10_000);
|
||||
for (int round = 0; round < 5; round++) {
|
||||
for (FlwCallbackLog row : logRepository.findAll()) {
|
||||
pushService.pushAsync(row.getId());
|
||||
}
|
||||
}
|
||||
FlwCallbackLog hung = logRepository.findAll().get(0);
|
||||
assertEquals(6, hung.getRetryCount());
|
||||
|
||||
// 挂起列表:status=0 且 retryCount≥阈值的分页行,不含正常行
|
||||
com.yunhai.platform.utils.model.BasePageResult<FlwCallbackLog> hungPage =
|
||||
callbackAdminService.pageHung(SCH, 1, 10);
|
||||
assertTrue(hungPage.getContent().stream().anyMatch(l -> l.getId().equals(hung.getId())));
|
||||
assertTrue(hungPage.getContent().stream()
|
||||
.allMatch(l -> l.getStatus() == FlwEnums.CallbackLogStatus.PENDING.getCode()
|
||||
&& l.getRetryCount() >= 6));
|
||||
|
||||
// 管理端手工标记死信(唯一死信入口)
|
||||
callbackAdminService.markDead(SCH, hung.getId());
|
||||
FlwCallbackLog dead = logRepository.findById(hung.getId()).orElseThrow(IllegalStateException::new);
|
||||
assertEquals(FlwEnums.CallbackLogStatus.DEAD.getCode(), dead.getStatus());
|
||||
|
||||
// 恢复消费方地址后既有 rePush 复活:置回待推立即推送 → 成功
|
||||
regRow.setCallbackUrl("http://localhost:" + stubPort + "/callback/flow");
|
||||
regRepository.save(regRow);
|
||||
callbackAdminService.rePush(SCH, java.util.Collections.singletonList(hung.getId()));
|
||||
waitUntil(() -> {
|
||||
FlwCallbackLog row = logRepository.findById(hung.getId()).orElse(null);
|
||||
return row != null && row.getStatus() == FlwEnums.CallbackLogStatus.SUCCESS.getCode();
|
||||
}, 10_000);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unsubscribedEvent_notDelivered() throws InterruptedException {
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t22", "订阅过滤", linearDef("ybb", "审核"));
|
||||
// 只订阅 flow.completed:submitted/task.created 不推送
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY,
|
||||
regBody(def.getFlowGuid(), "http://localhost:" + stubPort + "/callback/flow",
|
||||
"flow.completed"));
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-3\",\"bizTitle\":\"张三的订阅件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
Thread.sleep(1500);
|
||||
// stub 收到的注册探活 ping 不算事件推送
|
||||
long eventPushes = received.stream()
|
||||
.filter(r -> !r.body.contains("\"event\":\"ping\"")).count();
|
||||
assertEquals(0, eventPushes, "未订阅事件不应推送");
|
||||
}
|
||||
|
||||
/** 组注册请求体(registerCallback 返回下发 secret) */
|
||||
private OapiModels.CallbackRegBody regBody(String flowGuid, String url, String eventTypes) {
|
||||
OapiModels.CallbackRegBody body = new OapiModels.CallbackRegBody();
|
||||
body.setFlowGuid(flowGuid);
|
||||
body.setCallbackUrl(url);
|
||||
body.setEventTypes(eventTypes);
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 轮询等待(替代 Awaitility,离线环境无该依赖) */
|
||||
private static void waitUntil(java.util.function.BooleanSupplier cond, long timeoutMs) {
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
if (cond.getAsBoolean()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Thread.sleep(100);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
org.junit.jupiter.api.Assertions.assertTrue(cond.getAsBoolean(), "等待条件超时");
|
||||
}
|
||||
|
||||
private static String readBody(HttpExchange exchange) {
|
||||
try (InputStream in = exchange.getRequestBody()) {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] buf = new byte[4096];
|
||||
int n;
|
||||
while ((n = in.read(buf)) > 0) {
|
||||
out.write(buf, 0, n);
|
||||
}
|
||||
return out.toString("UTF-8");
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private JsonNode readJson(String s) {
|
||||
try {
|
||||
return objectMapper.readTree(s);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String hmac(String secret, String ts, String nonce, String body) {
|
||||
try {
|
||||
Mac mac = Mac.getInstance("HmacSHA256");
|
||||
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
|
||||
byte[] raw = mac.doFinal((ts + "." + nonce + "." + body).getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(raw);
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user