Enforce exact published form version contracts

Local review candidate only. Explicitly selected inherited prerequisites and prior repair patches; unrelated WIP and independent MQ fixes are excluded. No push or deployment authorized.
This commit is contained in:
chenxing
2026-10-23 10:51:13 +08:00
parent e39164a2bd
commit c6e061815a
3 changed files with 108 additions and 1 deletions
@@ -82,6 +82,11 @@ public class OapiFormService {
}
requireSchId(schId);
FormDefService.Published published = defService.resolvePublishedByUuid(schId, uuid, version);
// 精确 schema 用于跨端发布绑定,只允许发布过的不可变版本;不能把草稿签成发布契约。
// 历史答卷读取仍使用既有 resolver,不在此处收紧其生命周期语义。
if (version != null && (version < 1 || published.versionRow().getPublishTime() == null)) {
throw new BizException(ErrorCode.PARAM_ERROR, "指定发布版本不存在或不可读");
}
return toSchemaView(published);
}
@@ -0,0 +1,84 @@
package com.yunhai.formcenter.service;
import com.yunhai.formcenter.entity.Form;
import com.yunhai.formcenter.entity.FormVersion;
import com.yunhai.formcenter.model.FormAccessModels.ConsumerPrincipal;
import com.yunhai.formcenter.repository.AnswerRepository;
import com.yunhai.formcenter.repository.FormRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;
/** 真实服务层发布版本守卫单测;resolver/仓储是桩,不作为真库验收。 */
class OapiSchemaPublishedVersionTest {
private FormDefService defs;
private FormDictService dicts;
private FormConsumerAccessService access;
private OapiFormService service;
private Form form;
private FormVersion row;
private final ConsumerPrincipal caller = new ConsumerPrincipal("100009");
@BeforeEach
void setUp() {
defs = mock(FormDefService.class);
dicts = mock(FormDictService.class);
access = mock(FormConsumerAccessService.class);
service = new OapiFormService(defs, dicts, mock(AnswerRepository.class), mock(FormRepository.class), access);
form = new Form();
form.setSchId(27L);
form.setUuid("FU1");
form.setAppCode("svc-hall");
form.setCode("leave");
row = new FormVersion();
row.setVersion(3);
row.setSchemaJson("{\"groups\":[]}");
when(defs.resolvePublishedByUuid(27L, "FU1", 3)).thenReturn(new FormDefService.Published(form, row));
when(dicts.expandSchema(anyLong(), anyString())).thenAnswer(inv -> inv.getArgument(1));
}
@Test
void exactDraftCannotBecomeCrossServicePublishedContract() {
BizException ex = assertThrows(BizException.class,
() -> service.schema(caller, 27L, null, null, "FU1", 3));
assertEquals(ErrorCode.PARAM_ERROR.getCode(), ex.getErrorCode().getCode());
verify(access).requireRead(caller, 27L, "FU1");
verifyNoInteractions(dicts);
}
@Test
void historicalPublishedVersionStillReadableWhenHeadIsDisabledOrDeleted() {
row.setPublishTime(LocalDateTime.now().minusDays(1));
form.setStatus(2);
form.setIsDeleted(1);
form.setCurrentVersion(4);
assertEquals(3, service.schema(caller, 27L, null, null, "FU1", 3).getVersion());
verify(defs).resolvePublishedByUuid(27L, "FU1", 3);
}
@Test
void missingVersionRowPreservesResourceRejectionCode() {
when(defs.resolvePublishedByUuid(27L, "FU1", 3))
.thenThrow(new BizException(ErrorCode.PARAM_ERROR, "opaque"));
BizException ex = assertThrows(BizException.class,
() -> service.schema(caller, 27L, null, null, "FU1", 3));
assertEquals(2, ex.getErrorCode().getCode());
verifyNoInteractions(dicts);
}
@Test
void readAuthorizationRunsBeforeVersionResolution() {
doThrow(new BizException(ErrorCode.PARAM_ERROR, "opaque"))
.when(access).requireRead(caller, 27L, "FU1");
assertThrows(BizException.class, () -> service.schema(caller, 27L, null, null, "FU1", 3));
verifyNoInteractions(defs, dicts);
}
}