含10-22精修: 对账有限重试(retry_count/指数退避/work_version防旧覆盖)+接缝身份(source-app-key空拒收)+应急对账闸让位FlowApplyRecoveryService+同attempt指令不重开终态。SQL未执行。
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,202 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.flowcallback.FlowCallbackRegistrar;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import com.yunhai.svchall.controller.api.svc.SvcFormCatalogController;
|
||||
import com.yunhai.svchall.entity.ServiceItem;
|
||||
import com.yunhai.svchall.model.svc.SvcModels.SvcFormCatalogPage;
|
||||
import com.yunhai.svchall.model.svc.SvcModels.SvcItemSaveRequest;
|
||||
import com.yunhai.svchall.repository.ServiceFormRepository;
|
||||
import com.yunhai.svchall.repository.ServiceItemRepository;
|
||||
import com.yunhai.svchall.service.DeptSource;
|
||||
import com.yunhai.svchall.service.FlowClient;
|
||||
import com.yunhai.svchall.service.SiteDirectory;
|
||||
import com.yunhai.svchall.service.SiteGuard;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.transaction.support.TransactionTemplate;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.doThrow;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 事项的表单中心绑定单测(v3/TASK_1)。
|
||||
*
|
||||
* <p>任务书要的证据是「<b>事项保存远端校验失败即拒绝该保存,不写半个绑定</b>」。这里给单元级取证:
|
||||
* 核验点落在 {@code adminSave} 的<em>任何写动作之前</em>({@code validateBindings}),核验一抛,
|
||||
* 事项行与附件重绑都不许被碰到——事务回滚是兜底,"根本没写"才是这条链路的常态。
|
||||
* 中心侧的撤权/跨校/版本行缺失三种情形在客户端都表现为同一次核验失败,本类用"客户端抛错"代表它们
|
||||
* (各自的规则在 form-center 的 {@code FcDiscoveryAccessTest} 里逐条钉住)。</p>
|
||||
*
|
||||
* <p>另外四条:成对提交(单传一个即拒)、本地 {@code form_code} 只读不写(改值即拒、原样回传保留存量绑定,
|
||||
* 不做"顺手清空历史行"这种二次伤害)、没表单的事项保留既有允许形态、目录代理的校域只能由 CAS 站点派生。</p>
|
||||
*/
|
||||
@DisplayName("事项绑定表单中心 - v3 保存链路单测(TASK_1)")
|
||||
class ShFormBindingTest {
|
||||
|
||||
private static final SiteDirectory.SiteInfo SITE = new SiteDirectory.SiteInfo("S1", "站点一", 1L);
|
||||
private static final String FC_UUID = "fcuuid0000000000000000000000ab";
|
||||
|
||||
private ServiceItemRepository itemRepository;
|
||||
private ServiceFormRepository formRepository;
|
||||
private AttachmentService attachmentService;
|
||||
private SiteGuard siteGuard;
|
||||
private FormCenterCatalogClient catalogClient;
|
||||
private ServiceItemService service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
itemRepository = mock(ServiceItemRepository.class);
|
||||
formRepository = mock(ServiceFormRepository.class);
|
||||
attachmentService = mock(AttachmentService.class);
|
||||
siteGuard = mock(SiteGuard.class);
|
||||
when(siteGuard.requireSiteManager(any(), any())).thenReturn(SITE);
|
||||
when(itemRepository.save(any(ServiceItem.class))).thenAnswer(call -> call.getArgument(0));
|
||||
|
||||
catalogClient = mock(FormCenterCatalogClient.class);
|
||||
service = new ServiceItemService(
|
||||
itemRepository, formRepository,
|
||||
mock(SiteDirectory.class), mock(ServiceDictService.class), mock(ServiceFormService.class),
|
||||
attachmentService, siteGuard, mock(DeptSource.class),
|
||||
mock(FlowClient.class), mock(FlowCallbackRegistrar.class), mock(TransactionTemplate.class));
|
||||
// setter 注入(本类那 11 参构造被存量测试逐位钉住,见 ServiceItemService 的字段注释)
|
||||
service.setFormCenterClient(catalogClient);
|
||||
}
|
||||
|
||||
private static SvcItemSaveRequest request() {
|
||||
SvcItemSaveRequest req = new SvcItemSaveRequest();
|
||||
req.setSiteCode("S1");
|
||||
req.setItemCode("svc_apply_01");
|
||||
req.setTitle("测试事项");
|
||||
// 线下形态:不牵动"可在线办理必须绑流程"那条既有规则,本类只管表单坐标
|
||||
req.setOnlineFlag(0);
|
||||
return req;
|
||||
}
|
||||
|
||||
private ServiceItem savedItem() {
|
||||
ArgumentCaptor<ServiceItem> saved = ArgumentCaptor.forClass(ServiceItem.class);
|
||||
verify(itemRepository).save(saved.capture());
|
||||
return saved.getValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("成对提交:只给 uuid 或只给 version 都拒,且不碰远端、不写库")
|
||||
void bindingMustBePaired() {
|
||||
SvcItemSaveRequest uuidOnly = request();
|
||||
uuidOnly.setFcFormUuid(FC_UUID);
|
||||
assertThatThrownBy(() -> service.adminSave(null, uuidOnly))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("必须成对提交");
|
||||
|
||||
SvcItemSaveRequest versionOnly = request();
|
||||
versionOnly.setFcFormVersion(4);
|
||||
assertThatThrownBy(() -> service.adminSave(null, versionOnly))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("必须成对提交");
|
||||
|
||||
verify(catalogClient, never()).requireBindable(any(), any(), any());
|
||||
verify(itemRepository, never()).save(any(ServiceItem.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("远端核验失败=整次保存失败:事项没落库、附件没重绑(不留半个绑定)")
|
||||
void remoteCheckFailureWritesNothing() {
|
||||
SvcItemSaveRequest req = request();
|
||||
req.setFcFormUuid(FC_UUID);
|
||||
req.setFcFormVersion(4);
|
||||
// 撤权 / 跨校 / 版本行缺失,在客户端都是同一次核验失败
|
||||
doThrow(new BizException(ErrorCode.PARAM_ERROR, "表单中心校验未通过"))
|
||||
.when(catalogClient).requireBindable(1L, FC_UUID, 4);
|
||||
|
||||
assertThatThrownBy(() -> service.adminSave(null, req))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("表单中心校验未通过");
|
||||
|
||||
verify(itemRepository, never()).save(any(ServiceItem.class));
|
||||
verify(attachmentService, never()).bindGuide(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("核验通过才落库:按 CAS 站点派生的校域核验,uuid 去空白后成对写入两列")
|
||||
void verifiedPairIsStored() {
|
||||
SvcItemSaveRequest req = request();
|
||||
req.setFcFormUuid(" " + FC_UUID + " ");
|
||||
req.setFcFormVersion(7);
|
||||
|
||||
service.adminSave(null, req);
|
||||
|
||||
verify(catalogClient).requireBindable(eq(1L), eq(FC_UUID), eq(7));
|
||||
assertThat(savedItem().getFcFormUuid()).isEqualTo(FC_UUID);
|
||||
assertThat(savedItem().getFcFormVersion()).isEqualTo(7);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("本地 form_code 停写:新建与改值都拒;原样回传保留存量绑定(不被清空)")
|
||||
void localFormCodeIsReadOnly() {
|
||||
when(formRepository.existsBySchIdAndCode(1L, "legacy_form")).thenReturn(true);
|
||||
when(formRepository.existsBySchIdAndCode(1L, "another_form")).thenReturn(true);
|
||||
|
||||
// 新建事项就带本地 code:即便这张表在本地存在,也不允许形成新绑定
|
||||
SvcItemSaveRequest create = request();
|
||||
create.setFormCode("legacy_form");
|
||||
assertThatThrownBy(() -> service.adminSave(null, create))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("本地表单定义已停用");
|
||||
|
||||
ServiceItem stored = new ServiceItem();
|
||||
stored.setFormCode("legacy_form");
|
||||
when(itemRepository.findByIdAndSchIdAndSiteCode(9, 1L, "S1")).thenReturn(Optional.of(stored));
|
||||
|
||||
// 编辑存量事项、换成另一张本地表:同样拒
|
||||
SvcItemSaveRequest switchCode = request();
|
||||
switchCode.setId(9);
|
||||
switchCode.setFormCode("another_form");
|
||||
assertThatThrownBy(() -> service.adminSave(null, switchCode))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("本地表单定义已停用");
|
||||
|
||||
// 原样回传:历史绑定保留(保存链路不清空存量行)
|
||||
SvcItemSaveRequest echo = request();
|
||||
echo.setId(9);
|
||||
echo.setFormCode("legacy_form");
|
||||
service.adminSave(null, echo);
|
||||
assertThat(stored.getFormCode()).isEqualTo("legacy_form");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("没有表单的事项保留既有允许形态:两坐标都不传即正常保存")
|
||||
void itemWithoutFormStillAllowed() {
|
||||
service.adminSave(null, request());
|
||||
|
||||
assertThat(savedItem().getFcFormUuid()).isNull();
|
||||
assertThat(savedItem().getFcFormVersion()).isNull();
|
||||
verify(catalogClient, never()).requireBindable(any(), any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("目录代理的租户域从 CAS 站点派生:前端没有可越界的校号入口")
|
||||
void catalogProxyDerivesSchoolFromCas() {
|
||||
SiteDirectory.SiteInfo site2 = new SiteDirectory.SiteInfo("S2", "站点二", 999L);
|
||||
when(siteGuard.requireSiteManager(any(), eq("S2"))).thenReturn(site2);
|
||||
when(catalogClient.listForms(any(), any(), any(), any(), any())).thenReturn(new SvcFormCatalogPage());
|
||||
SvcFormCatalogController controller = new SvcFormCatalogController(siteGuard, catalogClient);
|
||||
|
||||
controller.forms(null, "S1", null, "荣誉", 1, 20);
|
||||
verify(catalogClient).listForms(1L, null, "荣誉", 1, 20);
|
||||
|
||||
controller.forms(null, "S2", null, null, null, null);
|
||||
verify(catalogClient).listForms(999L, null, null, null, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.yunhai.svchall.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.svchall.entity.FcAnswerProjection;
|
||||
import com.yunhai.svchall.model.FcApplyModels;
|
||||
import com.yunhai.svchall.model.FcApplyModels.Snapshot;
|
||||
import com.yunhai.svchall.model.FcApplyModels.SnapshotEnvelope;
|
||||
import com.yunhai.svchall.model.FcApplyModels.GrantSnapshotView;
|
||||
import com.yunhai.svchall.repository.FcAnswerProjectionRepository;
|
||||
import com.yunhai.svchall.repository.FcAnswerInboxRepository;
|
||||
import com.yunhai.svchall.repository.FcApplyIntentRepository;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Optional;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.Assertions.*;
|
||||
import static org.mockito.ArgumentMatchers.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
class ShReconcileExtensionTest {
|
||||
private static final String UUID = "00000000000000000000000000000001";
|
||||
private final ObjectMapper mapper = new ObjectMapper();
|
||||
private FcAnswerProjectionRepository projections;
|
||||
private FcAnswerInboxRepository inbox;
|
||||
private FcApplyIntentRepository intents;
|
||||
private ServiceApplyService applies;
|
||||
private FcSubmissionClient submission;
|
||||
private AnswerProjectionStore store;
|
||||
private SnapshotEnvelope envelope;
|
||||
private FcAnswerProjection local;
|
||||
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
projections = mock(FcAnswerProjectionRepository.class);
|
||||
inbox = mock(FcAnswerInboxRepository.class);
|
||||
intents = mock(FcApplyIntentRepository.class);
|
||||
applies = mock(ServiceApplyService.class);
|
||||
submission = mock(FcSubmissionClient.class);
|
||||
store = new AnswerProjectionStore(projections, inbox, intents, submission, applies, mapper);
|
||||
ReflectionTestUtils.setField(store, "consumerAppKey", "100018");
|
||||
GrantSnapshotView grant = new GrantSnapshotView();
|
||||
grant.setFormUuid("form-1");
|
||||
grant.setConsumerSchoolId(5L);
|
||||
grant.setConsumerAppKey("100018");
|
||||
grant.setSubscribeAllowed(true);
|
||||
grant.setGrantVersion(7);
|
||||
when(submission.verifyGrants(eq(5L), any())).thenReturn(Collections.singletonList(grant));
|
||||
Snapshot snapshot = new Snapshot();
|
||||
snapshot.setSchId(5L);
|
||||
snapshot.setValues(mapper.createObjectNode().put("value", "authoritative"));
|
||||
snapshot.setIntent(mapper.createObjectNode().put("intentId", "should-not-link"));
|
||||
envelope = new SnapshotEnvelope();
|
||||
envelope.setProtocolVersion(FcApplyModels.PROTOCOL_VERSION);
|
||||
envelope.setConsumerAppKey("100018");
|
||||
envelope.setConsumerSchoolId(5L);
|
||||
envelope.setGrantVersion(7);
|
||||
envelope.setFormUuid("form-1");
|
||||
envelope.setFormVersion(4);
|
||||
envelope.setAnswerUuid(UUID);
|
||||
envelope.setEventId("reconcile-task-1-answer-1");
|
||||
envelope.setAnswerRevision(3L);
|
||||
envelope.setIsDeleted(false);
|
||||
envelope.setSnapshot(snapshot);
|
||||
envelope.setContentHash(AnswerProjectionStore.canonicalHash(snapshot));
|
||||
local = new FcAnswerProjection();
|
||||
local.setSchId(5L);
|
||||
local.setAnswerUuid(UUID);
|
||||
local.setFormUuid("form-1");
|
||||
local.setRevision(3L);
|
||||
local.setContentHash("local-corrupt-hash");
|
||||
local.setIsDeleted(0);
|
||||
local.setBizExtJson("business-extension");
|
||||
local.setApplyId(42);
|
||||
when(projections.findByAnswerUuidAndSchId(UUID, 5L)).thenAnswer(i -> Optional.ofNullable(local));
|
||||
when(projections.repairIfUnchanged(any(), anyLong(), anyString(), anyInt())).thenReturn(1);
|
||||
}
|
||||
|
||||
private AnswerProjectionStore.RepairResult repair() {
|
||||
return store.repairAuthoritative(envelope, 3L, "local-corrupt-hash", 0, "task-1/proof-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void sameRevisionRepairUsesConditionalSnapshotUpdateWithoutBusinessEffects() {
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.APPLIED);
|
||||
ArgumentCaptor<FcAnswerProjection> replacement = ArgumentCaptor.forClass(FcAnswerProjection.class);
|
||||
verify(projections).repairIfUnchanged(replacement.capture(), eq(3L), eq("local-corrupt-hash"), eq(0));
|
||||
assertThat(replacement.getValue().getContentHash()).isEqualTo(envelope.getContentHash());
|
||||
assertThat(replacement.getValue().getValuesJson()).contains("authoritative");
|
||||
assertThat(replacement.getValue().getConflictFlag()).isZero();
|
||||
assertThat(local.getBizExtJson()).isEqualTo("business-extension");
|
||||
assertThat(local.getApplyId()).isEqualTo(42);
|
||||
verifyNoInteractions(inbox, intents, applies);
|
||||
}
|
||||
|
||||
@Test
|
||||
void absentRepairDoesNotLinkEvenWhenSnapshotContainsIntent() {
|
||||
local = null;
|
||||
assertThat(store.repairAuthoritative(envelope, null, null, null, "proof-absent"))
|
||||
.isEqualTo(AnswerProjectionStore.RepairResult.APPLIED);
|
||||
ArgumentCaptor<FcAnswerProjection> row = ArgumentCaptor.forClass(FcAnswerProjection.class);
|
||||
verify(projections).saveAndFlush(row.capture());
|
||||
assertThat(row.getValue().getLinkState()).isEqualTo(FcAnswerProjection.LINK_UNLINKED);
|
||||
assertThat(row.getValue().getIntentId()).isNull();
|
||||
assertThat(row.getValue().getApplyId()).isNull();
|
||||
verifyNoInteractions(inbox, intents, applies);
|
||||
}
|
||||
|
||||
@Test
|
||||
void localChangeBetweenReadAndUpdateIsNotOverwritten() {
|
||||
when(projections.repairIfUnchanged(any(), anyLong(), anyString(), anyInt())).thenReturn(0);
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.LOCAL_CHANGED);
|
||||
assertThat(local.getContentHash()).isEqualTo("local-corrupt-hash");
|
||||
}
|
||||
|
||||
@Test
|
||||
void changedTupleAndHigherLocalRevisionRequireRecheck() {
|
||||
local.setIsDeleted(1);
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.LOCAL_CHANGED);
|
||||
local.setRevision(4L);
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.LOCAL_AHEAD);
|
||||
verify(projections, never()).repairIfUnchanged(any(), anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
void newerTombstoneUsesSameRepairPrimitive() {
|
||||
envelope.setAnswerRevision(4L);
|
||||
envelope.setIsDeleted(true);
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.APPLIED);
|
||||
ArgumentCaptor<FcAnswerProjection> row = ArgumentCaptor.forClass(FcAnswerProjection.class);
|
||||
verify(projections).repairIfUnchanged(row.capture(), eq(3L), anyString(), eq(0));
|
||||
assertThat(row.getValue().getIsDeleted()).isEqualTo(1);
|
||||
assertThat(row.getValue().getRevision()).isEqualTo(4L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void repairRechecksGrantEvenWithWarmCacheAndRejectsUnknownEpoch() {
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.APPLIED);
|
||||
envelope.setGrantVersion(8);
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.DEFERRED);
|
||||
when(submission.verifyGrants(eq(5L), any())).thenReturn(Collections.emptyList());
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.DEFERRED);
|
||||
verify(submission, times(3)).verifyGrants(eq(5L), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void freshGrantMustBelongToTheSameTenantAndConsumer() {
|
||||
GrantSnapshotView other = new GrantSnapshotView();
|
||||
other.setFormUuid("form-1");
|
||||
other.setConsumerSchoolId(6L);
|
||||
other.setConsumerAppKey("100018");
|
||||
other.setSubscribeAllowed(true);
|
||||
other.setGrantVersion(7);
|
||||
when(submission.verifyGrants(eq(5L), any())).thenReturn(Collections.singletonList(other));
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.DEFERRED);
|
||||
other.setConsumerSchoolId(5L);
|
||||
other.setConsumerAppKey("another-app");
|
||||
assertThat(repair()).isEqualTo(AnswerProjectionStore.RepairResult.DEFERRED);
|
||||
verifyNoInteractions(projections);
|
||||
}
|
||||
|
||||
@Test
|
||||
void malformedProofWrongTenantAndHashAreRejectedWithoutWrites() {
|
||||
assertThatThrownBy(() -> store.repairAuthoritative(envelope, 3L, null, 0, "proof"))
|
||||
.isInstanceOf(BizException.class);
|
||||
envelope.getSnapshot().setSchId(6L);
|
||||
assertThatThrownBy(this::repair).isInstanceOf(BizException.class);
|
||||
envelope.getSnapshot().setSchId(5L);
|
||||
envelope.setContentHash(String.join("", Collections.nCopies(64, "a")));
|
||||
assertThatThrownBy(this::repair).isInstanceOf(BizException.class);
|
||||
verify(projections, never()).repairIfUnchanged(any(), anyLong(), anyString(), anyInt());
|
||||
}
|
||||
|
||||
private FcAnswerProjectionRepository.ManifestRow row(String uuid) {
|
||||
FcAnswerProjectionRepository.ManifestRow row = mock(FcAnswerProjectionRepository.ManifestRow.class);
|
||||
when(row.getAnswerUuid()).thenReturn(uuid);
|
||||
when(row.getCreateTime()).thenReturn(LocalDateTime.of(2026, 10, 21, 1, 0));
|
||||
return row;
|
||||
}
|
||||
|
||||
@Test
|
||||
void uuidCursorTraversesSparseRowsAndReturnsExplicitEnd() {
|
||||
String second = "00000000000000000000000000000050";
|
||||
String third = "00000000000000000000000000000099";
|
||||
FcAnswerProjectionRepository.ManifestRow firstRow = row(UUID);
|
||||
FcAnswerProjectionRepository.ManifestRow secondRow = row(second);
|
||||
FcAnswerProjectionRepository.ManifestRow thirdRow = row(third);
|
||||
when(projections.findManifest(eq(5L), eq("form-1"), eq(""), any()))
|
||||
.thenReturn(Arrays.asList(firstRow, secondRow, thirdRow));
|
||||
when(projections.findManifest(eq(5L), eq("form-1"), eq(second), any()))
|
||||
.thenReturn(Collections.singletonList(thirdRow));
|
||||
AnswerProjectionStore.ManifestPage first = store.manifestPage(5L, "form-1", null, 2);
|
||||
assertThat(first.getRows()).hasSize(2);
|
||||
assertThat(first.isCompleted()).isFalse();
|
||||
assertThat(first.getNextCursor()).isEqualTo(second);
|
||||
assertThat(first.getRows().get(0).getCreateTime()).isNotNull();
|
||||
AnswerProjectionStore.ManifestPage last = store.manifestPage(5L, "form-1", first.getNextCursor(), 2);
|
||||
assertThat(last.getRows()).extracting(FcAnswerProjectionRepository.ManifestRow::getAnswerUuid)
|
||||
.containsExactly(third);
|
||||
assertThat(last.isCompleted()).isTrue();
|
||||
assertThat(last.getNextCursor()).isNull();
|
||||
ArgumentCaptor<Pageable> page = ArgumentCaptor.forClass(Pageable.class);
|
||||
verify(projections).findManifest(eq(5L), eq("form-1"), eq(""), page.capture());
|
||||
assertThat(page.getValue().getPageSize()).isEqualTo(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyAndExactlyFullPagesAreCompletedAndInvalidCursorIsRejected() {
|
||||
FcAnswerProjectionRepository.ManifestRow first = row(UUID);
|
||||
FcAnswerProjectionRepository.ManifestRow second = row("00000000000000000000000000000050");
|
||||
when(projections.findManifest(anyLong(), anyString(), anyString(), any()))
|
||||
.thenReturn(Collections.emptyList(), Arrays.asList(first, second));
|
||||
assertThat(store.manifestPage(5L, "form-1", null, 2).isCompleted()).isTrue();
|
||||
assertThat(store.manifestPage(5L, "form-1", null, 2).isCompleted()).isTrue();
|
||||
assertThatThrownBy(() -> store.manifestPage(5L, "form-1", "invalid", 2))
|
||||
.isInstanceOf(BizException.class);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user