feat(sync): 将 pullConfig 的 ACK 确认拆分为独立请求

- 正常 pullConfig 请求不再夹带 ackSuc/ackFail,只保留业务过滤参数
- 在本轮生产数据处理完成后,新增一次 ACK-only 的 pullConfig 确认调用
- ACK 确认请求仅回传 ackSuc/ackFail,不再携带 airportId/appName/configVersion/fileName
- 保持本地 ackFail 落库与定向重拉机制不变,确认成功后再标记 reported
- 更新相关 HTTP/集成测试,并同步 current.md 与 prod-api-v1.md 文档
This commit is contained in:
dark 2026-04-29 11:25:41 +08:00
parent 00b81bf7ef
commit c22eff8950
6 changed files with 251 additions and 64 deletions

View File

@ -11,6 +11,7 @@ pushConfigPOST + JSON数组
pullConfigGET + JSON响应 pullConfigGET + JSON响应
login已支持 token 获取与缓存 login已支持 token 获取与缓存
ackSuc/ackFail已接入回传与本地落库 ackSuc/ackFail已接入回传与本地落库
ackSuc/ackFail已改为 pull 数据处理完成后,再补发一次 ACK-only pullConfig 确认请求(只带 ackSuc/ackFail
ConfigCryptoService已抽出当前默认透传实现后续只需替换该服务内算法 ConfigCryptoService已抽出当前默认透传实现后续只需替换该服务内算法
Git -> PROD已支持最小增量推送删除场景自动回退全量 Git -> PROD已支持最小增量推送删除场景自动回退全量
Git -> PROD已改为先按 git.repo.scan-branch-pattern 批量扫描版本分支,再逐个按“版本分支 + 机场目录 + 模块目录”解析参数 Git -> PROD已改为先按 git.repo.scan-branch-pattern 批量扫描版本分支,再逐个按“版本分支 + 机场目录 + 模块目录”解析参数
@ -45,6 +46,7 @@ PROD -> Git
- 当前提交目标分支为git.repo.snapshot-branch/<configVersion> - 当前提交目标分支为git.repo.snapshot-branch/<configVersion>
- 例如config-prod-snapshot/R_XXX_V3.0.3_XXX - 例如config-prod-snapshot/R_XXX_V3.0.3_XXX
- 若 pullConfig 返回缺少统一版本号,则按当前 result 的 sourceVersion 退化生成动态分支名 - 若 pullConfig 返回缺少统一版本号,则按当前 result 的 sourceVersion 退化生成动态分支名
- pull 数据处理完成后,需要再次调用一次 pullConfig 做 ACK 确认,请求只传 ackSuc/ackFail
关键文件 关键文件
@ -64,6 +66,66 @@ ftp-sync-tool-detail-design.md
configContent 推送前加密 configContent 推送前加密
configContent 拉取后解密 configContent 拉取后解密
本轮评估结论2026-04-29
目标补充
最终目标希望 Git 仓库内容与生产接口库保持一致。
当前结论
- 当前正式模式不变:
- Git -> PROD版本分支直推生产
- PROD -> Git回写到 git.repo.snapshot-branch/<configVersion>
- 当前不建议直接把生产回流写回对应版本分支。
- 原因不是“代码不能写”,而是“当前 pullConfig 语义还不够安全”:
- pullConfig 不带过滤时,返回的是“未同步到 Git 的已审核配置”
- 这不是某个 configVersion 的全量快照
- 但当前 Git 回写语义是“整目录覆盖目标分支”
- 如果直接覆盖版本分支,可能误删未出现在本次 pull 结果里的文件
- 当前 snapshot 分支仍然有必要保留,作为生产回流的隔离区和审计区。
保守方案(推荐)
1. 保持当前双分支职责不变
- 版本分支:作为 Git -> PROD 的发布源
- snapshot 动态分支:作为 PROD -> Git 的生产回流镜像分支
2. 把 snapshot 分支定义为“生产待对账视图”
- 生产回流继续先写到 git.repo.snapshot-branch/<configVersion>
- 不直接改动 R_XXX_* 等版本分支
3. 增加“版本分支 vs snapshot 分支”自动对账能力
- 对比同一 configVersion 下两边目录差异
- 差异至少要区分:新增、修改、删除
- 输出可读结果,供人工确认是否需要把生产变更回收进版本分支
4. 增加“受控回收”动作,而不是自动覆盖
- 建议后续增加管理入口或命令:
- promoteSnapshotToVersion(configVersion)
- 行为:
- 读取 snapshot 分支
- 与对应版本分支做差异确认
- 经人工确认后再回写版本分支
- 这样可以避免生产临时变更直接污染发布分支
5. 明确回环控制
- Git -> PROD 只扫描版本分支
- snapshot 前缀分支不得进入 git.repo.scan-branch-pattern
- 避免生产回流提交再次被推回生产形成闭环
6. 后续如果要升级成“版本分支直接等于生产态”,前提是:
- 生产接口能按 configVersion 提供全量数据
- 或者返回结果具备明确删除语义
- 在此前,不建议取消 snapshot 隔离层
下一步建议
如果继续推进,最稳妥的下一步是:
- 先实现“snapshot 分支 与 对应版本分支”的差异对账能力
- 再决定是否补一个人工确认后的 promote 流程
验证状态 验证状态
mvn -s build-support/maven-settings.xml test 已通过 mvn -s build-support/maven-settings.xml test 已通过

View File

@ -197,25 +197,36 @@ PEK/monitor/jobs/sync-job.json
说明: 说明:
- 由于接口是 `GET`,当前实现按 query string 传参 - 由于接口是 `GET`,当前实现按 query string 传参
- 当前实现已经支持: - 正常拉取请求当前已经支持:
- `airportId` 可选过滤 - `airportId` 可选过滤
- `appName` 可选过滤 - `appName` 可选过滤
- `ackSuc` 自动回传
- `ackFail` 自动回传
- 当前已经支持:
- `configVersion` 可选过滤 - `configVersion` 可选过滤
- `fileName` 可选过滤 - `fileName` 可选过滤
- ACK 确认请求当前已经支持:
- `ackSuc` 自动回传
- `ackFail` 自动回传
### 5.3 当前请求行为 ### 5.3 当前请求行为
当前代码只有在配置了过滤条件时才会发送 当前代码发起两类 `pullConfig` 请求
- `prod.api.airport-id` 1. 正常拉取请求:
- `prod.api.app-name`
- `prod.api.pull-config-version`
- `prod.api.pull-file-name`
如果这两个配置为空,则不会在请求中携带它们,此时依赖生产端返回所有已审核且未同步的配置。 - 只有在配置了过滤条件时才会发送:
- `prod.api.airport-id`
- `prod.api.app-name`
- `prod.api.pull-config-version`
- `prod.api.pull-file-name`
如果这些配置为空,则不会在请求中携带它们,此时依赖生产端返回所有已审核且未同步的配置。
2. ACK 确认请求:
- 在本轮 pull 数据处理完成后额外发起一次
- 请求只传:
- `ackSuc`
- `ackFail`
- 不再夹带 `airportId/appName/configVersion/fileName` 过滤参数
### 5.4 响应体 ### 5.4 响应体
@ -253,6 +264,7 @@ PEK/monitor/jobs/sync-job.json
- 如果某组内所有项的 `configVersion` 相同,则该值作为该组的 `sourceVersion` - 如果某组内所有项的 `configVersion` 相同,则该值作为该组的 `sourceVersion`
- 如果某组缺少统一版本号,则该组退回为内容哈希作为 `sourceVersion` - 如果某组缺少统一版本号,则该组退回为内容哈希作为 `sourceVersion`
- `configContent` 当前已统一经过 `ConfigCryptoService`,默认实现仍为透传 - `configContent` 当前已统一经过 `ConfigCryptoService`,默认实现仍为透传
- pull 结果本地处理完成后,会再补发一次 ACK-only `pullConfig` 请求,只带 `ackSuc/ackFail`
- 当本地处理失败时,会把失败项写入 ACK 重试表,并在下次按 `airportId/appName/configVersion/fileName` 定向重拉 - 当本地处理失败时,会把失败项写入 ACK 重试表,并在下次按 `airportId/appName/configVersion/fileName` 定向重拉
## 6. 接口三:获取 token ## 6. 接口三:获取 token
@ -302,7 +314,7 @@ PEK/monitor/jobs/sync-job.json
- `pushConfig` 使用 `POST + JSON 数组` - `pushConfig` 使用 `POST + JSON 数组`
- `pullConfig` 使用 `GET + JSON 响应` - `pullConfig` 使用 `GET + JSON 响应`
- `login` 使用 `POST + JSON` - `login` 使用 `POST + JSON`
- `ackSuc/ackFail` 已接入请求回传和本地状态更新 - `ackSuc/ackFail` 已接入 ACK-only 二次确认请求和本地状态更新
- `pushConfig` 参数已按 `airportId/appName/fileName` 目录结构解析 - `pushConfig` 参数已按 `airportId/appName/fileName` 目录结构解析
- `pullConfig` 响应已按 `airportId/appName/fileName` 结构恢复到本地目录 - `pullConfig` 响应已按 `airportId/appName/fileName` 结构恢复到本地目录

View File

@ -129,6 +129,7 @@ public class ProdSyncCoordinator {
gitRepoProperties.getSnapshotBranch() gitRepoProperties.getSnapshotBranch()
); );
try {
retryFailedProdPulls(); retryFailedProdPulls();
try { try {
@ -139,6 +140,9 @@ public class ProdSyncCoordinator {
} catch (Exception e) { } catch (Exception e) {
log.error("PROD prod->git sync failed before new snapshot groups were processed", e); log.error("PROD prod->git sync failed before new snapshot groups were processed", e);
} }
} finally {
confirmPendingPullAck();
}
} }
private boolean shouldSkip(Optional<SyncTask> existing) { private boolean shouldSkip(Optional<SyncTask> existing) {
@ -301,6 +305,14 @@ public class ProdSyncCoordinator {
} }
} }
private void confirmPendingPullAck() {
try {
prodConfigApiService.confirmPendingPullAck();
} catch (Exception e) {
log.error("PROD prod->git ACK confirm failed", e);
}
}
/** /**
* 计算本轮 Git -> PROD 实际要推送的目录 * 计算本轮 Git -> PROD 实际要推送的目录
* 1. 首次同步走全量 * 1. 首次同步走全量

View File

@ -115,7 +115,6 @@ public class ProdConfigApiService {
/** /**
* 按指定过滤条件拉取生产配置 * 按指定过滤条件拉取生产配置
* 这里会把 ACK 待回传参数一并带上
*/ */
public List<ProdPullResult> pullConfigSnapshots( public List<ProdPullResult> pullConfigSnapshots(
String airportIdFilter, String airportIdFilter,
@ -123,45 +122,15 @@ public class ProdConfigApiService {
String configVersionFilter, String configVersionFilter,
String fileNameFilter String fileNameFilter
) throws IOException { ) throws IOException {
String url = buildUrl(prodApiProperties.getPullPath());
HttpHeaders headers = defaultJsonHeaders();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
ProdPullAckService.PendingAckSummary pendingAckSummary = prodPullAckService.getPendingAckSummary();
// 过滤条件为空时依赖生产端返回已审核且未同步的全量数据 // 过滤条件为空时依赖生产端返回已审核且未同步的全量数据
if (StringUtils.hasText(airportIdFilter) && !isPlaceholder(airportIdFilter)) { ProdApiResponse<List<ProdPulledConfigItem>> body = executePullConfigRequest(
builder.queryParam("airportId", airportIdFilter.trim()); airportIdFilter,
} appNameFilter,
if (StringUtils.hasText(appNameFilter) && !isPlaceholder(appNameFilter)) { configVersionFilter,
builder.queryParam("appName", appNameFilter.trim()); fileNameFilter,
} null
if (StringUtils.hasText(configVersionFilter) && !isPlaceholder(configVersionFilter)) {
builder.queryParam("configVersion", configVersionFilter.trim());
}
if (StringUtils.hasText(fileNameFilter) && !isPlaceholder(fileNameFilter)) {
builder.queryParam("fileName", fileNameFilter.trim());
}
if (pendingAckSummary.getAckSucIds() != null && !pendingAckSummary.getAckSucIds().isEmpty()) {
builder.queryParam("ackSuc", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckSucIds()));
}
if (pendingAckSummary.getAckFailIds() != null && !pendingAckSummary.getAckFailIds().isEmpty()) {
builder.queryParam("ackFail", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckFailIds()));
}
ResponseEntity<ProdApiResponse<List<ProdPulledConfigItem>>> response = restTemplate.exchange(
builder.build(true).toUri(),
HttpMethod.GET,
new HttpEntity<Void>(headers),
new ParameterizedTypeReference<ProdApiResponse<List<ProdPulledConfigItem>>>() {
}
); );
ProdApiResponse<List<ProdPulledConfigItem>> body = response.getBody();
validateSuccess(body, "Production pullConfig call failed"); validateSuccess(body, "Production pullConfig call failed");
if (pendingAckSummary.hasPendingAck()) {
prodPullAckService.markPendingAsReported();
}
List<ProdPulledConfigItem> items = body.getData(); List<ProdPulledConfigItem> items = body.getData();
if (items == null || items.isEmpty()) { if (items == null || items.isEmpty()) {
@ -170,6 +139,32 @@ public class ProdConfigApiService {
return buildPullResults(items); return buildPullResults(items);
} }
/**
* 生产快照处理完成后使用 ACK-only pull 请求向生产侧确认本轮处理结果
* 当前约定只传 ackSuc / ackFail不再夹带业务过滤参数
*/
public void confirmPendingPullAck() throws IOException {
ProdPullAckService.PendingAckSummary pendingAckSummary = prodPullAckService.getPendingAckSummary();
if (!pendingAckSummary.hasPendingAck()) {
return;
}
ProdApiResponse<List<ProdPulledConfigItem>> body = executePullConfigRequest(
null,
null,
null,
null,
pendingAckSummary
);
validateSuccess(body, "Production pullConfig ACK confirm failed");
prodPullAckService.markPendingAsReported();
log.info(
"Prod pullConfig ACK confirm finished. ackSucCount={}, ackFailCount={}",
pendingAckSummary.getAckSucIds() == null ? 0 : pendingAckSummary.getAckSucIds().size(),
pendingAckSummary.getAckFailIds() == null ? 0 : pendingAckSummary.getAckFailIds().size()
);
}
private HttpHeaders defaultJsonHeaders() { private HttpHeaders defaultJsonHeaders() {
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON)); headers.setAccept(java.util.Collections.singletonList(MediaType.APPLICATION_JSON));
@ -231,6 +226,46 @@ public class ProdConfigApiService {
return cachedToken; return cachedToken;
} }
private ProdApiResponse<List<ProdPulledConfigItem>> executePullConfigRequest(
String airportIdFilter,
String appNameFilter,
String configVersionFilter,
String fileNameFilter,
ProdPullAckService.PendingAckSummary pendingAckSummary
) {
String url = buildUrl(prodApiProperties.getPullPath());
HttpHeaders headers = defaultJsonHeaders();
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(url);
if (StringUtils.hasText(airportIdFilter) && !isPlaceholder(airportIdFilter)) {
builder.queryParam("airportId", airportIdFilter.trim());
}
if (StringUtils.hasText(appNameFilter) && !isPlaceholder(appNameFilter)) {
builder.queryParam("appName", appNameFilter.trim());
}
if (StringUtils.hasText(configVersionFilter) && !isPlaceholder(configVersionFilter)) {
builder.queryParam("configVersion", configVersionFilter.trim());
}
if (StringUtils.hasText(fileNameFilter) && !isPlaceholder(fileNameFilter)) {
builder.queryParam("fileName", fileNameFilter.trim());
}
if (pendingAckSummary != null && pendingAckSummary.getAckSucIds() != null && !pendingAckSummary.getAckSucIds().isEmpty()) {
builder.queryParam("ackSuc", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckSucIds()));
}
if (pendingAckSummary != null && pendingAckSummary.getAckFailIds() != null && !pendingAckSummary.getAckFailIds().isEmpty()) {
builder.queryParam("ackFail", StringUtils.collectionToCommaDelimitedString(pendingAckSummary.getAckFailIds()));
}
ResponseEntity<ProdApiResponse<List<ProdPulledConfigItem>>> response = restTemplate.exchange(
builder.build(true).toUri(),
HttpMethod.GET,
new HttpEntity<Void>(headers),
new ParameterizedTypeReference<ProdApiResponse<List<ProdPulledConfigItem>>>() {
}
);
return response.getBody();
}
/** /**
* Git 分支快照目录转换成 pushConfig 所需的 JSON 数组 * Git 分支快照目录转换成 pushConfig 所需的 JSON 数组
* 当前目录约定必须是airportId/appName/fileName * 当前目录约定必须是airportId/appName/fileName

View File

@ -15,6 +15,7 @@ import com.ftptool.sync.repository.SyncTaskRepository;
import com.ftptool.sync.service.GitClientService; import com.ftptool.sync.service.GitClientService;
import com.ftptool.sync.service.PackageService; import com.ftptool.sync.service.PackageService;
import com.ftptool.sync.service.ProdConfigApiService; import com.ftptool.sync.service.ProdConfigApiService;
import com.ftptool.sync.service.ProdPullAckService;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@ -70,6 +71,9 @@ class ProdSyncCoordinatorIntegrationTest {
@Autowired @Autowired
private ProdSyncCoordinator prodSyncCoordinator; private ProdSyncCoordinator prodSyncCoordinator;
@Autowired
private ProdPullAckService prodPullAckService;
@Autowired @Autowired
private SyncTaskRepository syncTaskRepository; private SyncTaskRepository syncTaskRepository;
@ -93,6 +97,10 @@ class ProdSyncCoordinatorIntegrationTest {
syncTaskRepository.deleteAll(); syncTaskRepository.deleteAll();
syncCheckpointRepository.deleteAll(); syncCheckpointRepository.deleteAll();
prodPullAckRecordRepository.deleteAll(); prodPullAckRecordRepository.deleteAll();
doAnswer(invocation -> {
prodPullAckService.markPendingAsReported();
return null;
}).when(prodConfigApiService).confirmPendingPullAck();
} }
@Test @Test
@ -189,7 +197,7 @@ class ProdSyncCoordinatorIntegrationTest {
assertEquals(2, ackRecords.size()); assertEquals(2, ackRecords.size());
for (ProdPullAckRecord ackRecord : ackRecords) { for (ProdPullAckRecord ackRecord : ackRecords) {
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus()); assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
assertFalse(ackRecord.getReported()); assertTrue(ackRecord.getReported());
} }
verify(gitClientService, times(1)).syncDirectoryToBranch( verify(gitClientService, times(1)).syncDirectoryToBranch(
@ -197,6 +205,7 @@ class ProdSyncCoordinatorIntegrationTest {
eq("config-prod-snapshot/prod-v1"), eq("config-prod-snapshot/prod-v1"),
contains("prod-v1") contains("prod-v1")
); );
verify(prodConfigApiService, times(2)).confirmPendingPullAck();
} }
@Test @Test
@ -255,7 +264,7 @@ class ProdSyncCoordinatorIntegrationTest {
assertEquals(1, ackRecords.size()); assertEquals(1, ackRecords.size());
assertEquals("9", ackRecords.get(0).getRemoteConfigId()); assertEquals("9", ackRecords.get(0).getRemoteConfigId());
assertEquals(ProdPullAckStatus.FAILED, ackRecords.get(0).getAckStatus()); assertEquals(ProdPullAckStatus.FAILED, ackRecords.get(0).getAckStatus());
assertFalse(ackRecords.get(0).getReported()); assertTrue(ackRecords.get(0).getReported());
} }
@Test @Test
@ -306,7 +315,7 @@ class ProdSyncCoordinatorIntegrationTest {
assertEquals(1, ackRecords.size()); assertEquals(1, ackRecords.size());
ProdPullAckRecord ackRecord = ackRecords.get(0); ProdPullAckRecord ackRecord = ackRecords.get(0);
assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus()); assertEquals(ProdPullAckStatus.SUCCESS, ackRecord.getAckStatus());
assertFalse(ackRecord.getReported()); assertTrue(ackRecord.getReported());
assertEquals(Integer.valueOf(0), ackRecord.getRetryCount()); assertEquals(Integer.valueOf(0), ackRecord.getRetryCount());
assertEquals("PEK", ackRecord.getAirportId()); assertEquals("PEK", ackRecord.getAirportId());
assertEquals("monitor", ackRecord.getAppName()); assertEquals("monitor", ackRecord.getAppName());
@ -314,6 +323,7 @@ class ProdSyncCoordinatorIntegrationTest {
verify(prodConfigApiService, times(1)) verify(prodConfigApiService, times(1))
.pullConfigSnapshots("PEK", "monitor", "prod-v3", "jobs/sync-job.json"); .pullConfigSnapshots("PEK", "monitor", "prod-v3", "jobs/sync-job.json");
verify(prodConfigApiService, times(2)).confirmPendingPullAck();
} }
@Test @Test

View File

@ -19,10 +19,12 @@ import java.nio.file.Path;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import static org.springframework.test.web.client.ExpectedCount.once; import static org.springframework.test.web.client.ExpectedCount.once;
@ -139,7 +141,7 @@ class ProdConfigApiServiceHttpTest {
} }
@Test @Test
void shouldSendAckParamsWhenPullingConfig() throws Exception { void shouldPullConfigWithoutAckParams() throws Exception {
RestTemplate restTemplate = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
@ -158,9 +160,6 @@ class ProdConfigApiServiceHttpTest {
prodApiProperties.setPullFileName("a.txt"); prodApiProperties.setPullFileName("a.txt");
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class); ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
when(prodPullAckService.getPendingAckSummary()).thenReturn(
new ProdPullAckService.PendingAckSummary(Arrays.asList("1", "2"), Arrays.asList("9"))
);
ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class); ConfigCryptoService configCryptoService = mock(ConfigCryptoService.class);
when(configCryptoService.decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content")) when(configCryptoService.decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content"))
.thenReturn("decoded-content"); .thenReturn("decoded-content");
@ -180,8 +179,8 @@ class ProdConfigApiServiceHttpTest {
assertTrue(uri.contains("appName=test-app")); assertTrue(uri.contains("appName=test-app"));
assertTrue(uri.contains("configVersion=v1")); assertTrue(uri.contains("configVersion=v1"));
assertTrue(uri.contains("fileName=a.txt")); assertTrue(uri.contains("fileName=a.txt"));
assertTrue(uri.contains("ackSuc=1,2") || uri.contains("ackSuc=1%2C2")); assertFalse(uri.contains("ackSuc="));
assertTrue(uri.contains("ackFail=9")); assertFalse(uri.contains("ackFail="));
}) })
.andExpect(method(HttpMethod.GET)) .andExpect(method(HttpMethod.GET))
.andExpect(header("token", "static-token")) .andExpect(header("token", "static-token"))
@ -205,6 +204,63 @@ class ProdConfigApiServiceHttpTest {
assertEquals("decoded-content", new String(Files.readAllBytes(restoredFile), StandardCharsets.UTF_8)); assertEquals("decoded-content", new String(Files.readAllBytes(restoredFile), StandardCharsets.UTF_8));
verify(configCryptoService).decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content"); verify(configCryptoService).decryptAfterPull("test-airport", "test-app", "v1", "a.txt", "content");
verify(prodPullAckService, times(0)).getPendingAckSummary();
verify(prodPullAckService, times(0)).markPendingAsReported();
server.verify();
}
@Test
void shouldConfirmPendingAckByAckOnlyPullCall() throws Exception {
RestTemplate restTemplate = new RestTemplate();
MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build();
SyncProperties syncProperties = newSyncProperties("./target/http-test-work-pull-ack");
WorkDirectoryService workDirectoryService = new WorkDirectoryService(syncProperties);
workDirectoryService.initialize();
ProdApiProperties prodApiProperties = new ProdApiProperties();
prodApiProperties.setBaseUrl("https://prod.example.com");
prodApiProperties.setPullPath("/pic_bus_manage_monitor/configSync/pullConfig");
prodApiProperties.setToken("static-token");
prodApiProperties.setTokenHeaderName("token");
prodApiProperties.setAirportId("test-airport");
prodApiProperties.setAppName("test-app");
prodApiProperties.setPullConfigVersion("v1");
prodApiProperties.setPullFileName("a.txt");
ProdPullAckService prodPullAckService = mock(ProdPullAckService.class);
when(prodPullAckService.getPendingAckSummary()).thenReturn(
new ProdPullAckService.PendingAckSummary(Arrays.asList("1", "2"), Arrays.asList("9"))
);
ProdConfigApiService service = new ProdConfigApiService(
prodApiProperties,
syncProperties,
restTemplate,
workDirectoryService,
prodPullAckService,
new ConfigCryptoService()
);
server.expect(once(), request -> {
String uri = request.getURI().toString();
assertFalse(uri.contains("airportId="));
assertFalse(uri.contains("appName="));
assertFalse(uri.contains("configVersion="));
assertFalse(uri.contains("fileName="));
assertTrue(uri.contains("ackSuc=1,2") || uri.contains("ackSuc=1%2C2"));
assertTrue(uri.contains("ackFail=9"));
})
.andExpect(method(HttpMethod.GET))
.andExpect(header("token", "static-token"))
.andRespond(withSuccess(
"{\"code\":\"0\",\"data\":[],\"msg\":\"ok\"}",
MediaType.APPLICATION_JSON
));
service.confirmPendingPullAck();
verify(prodPullAckService).getPendingAckSummary();
verify(prodPullAckService).markPendingAsReported(); verify(prodPullAckService).markPendingAsReported();
server.verify(); server.verify();
} }