test(api): 修复 uploads e2e 与服务端查重(90dcc58)的字节数冲突

90dcc58 引入 init ③.5 服务端查重(同 albumId+uploaderId+bytes 非 deleted
→ 409 PHOTO_DUPLICATE)时未同步更新本 spec:renew/complete describe 内多个
init 沿用 bytes=1000,C-bytes complete 落定 1000 字节照片后,C-type/C-happy/
DEV-1/DEV-2 的 init 即被查重 409 拦截(该提交仅跑了 app 397 测试,api e2e
漏跑)。为各冲突 init 改用互异字节数(1002/1003/1004/1005),并同步
putStaging 与 bytes/expectedAt 断言;各用例语义不变。

本地库补apply 20260928120000_photos_dedup_index(idx_photos_dedup)。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-28 18:38:36 +08:00
parent 3b7d37110a
commit 4b5940c0d8
+18 -12
View File
@@ -676,10 +676,12 @@ describe('uploads renew & complete & history (e2e)', () => {
}); });
it('complete:contentType 不符(presign 不签名 content-type,服务端兜底)→ 400', async () => { it('complete:contentType 不符(presign 不签名 content-type,服务端兜底)→ 400', async () => {
const init = await initAs(memberToken, 'C-type'); // bytes=1002:C-bytes 完成后本册已有 member 的 1000 字节非 deleted 照片,
// 沿用 1000 会被 init ③.5 服务端查重拦截(90dcc58),换字节数绕开。
const init = await initAs(memberToken, 'C-type', { bytes: 1002 });
// 声明 image/jpeg,实传 application/octet-stream:PUT 成功(签名不覆盖该头), // 声明 image/jpeg,实传 application/octet-stream:PUT 成功(签名不覆盖该头),
// complete 时 head().contentType 与申明值比对兜底。 // complete 时 head().contentType 与申明值比对兜底。
expect(await putStaging(init.credential, 1000, 'application/octet-stream')).toBe(true); expect(await putStaging(init.credential, 1002, 'application/octet-stream')).toBe(true);
const res = await complete(memberToken, init.uploadId); const res = await complete(memberToken, init.uploadId);
expect(res.status).toBe(400); expect(res.status).toBe(400);
expect(res.body.code).toBe(ErrorCodes.UPLOAD_VERIFY_FAILED); expect(res.body.code).toBe(ErrorCodes.UPLOAD_VERIFY_FAILED);
@@ -690,9 +692,10 @@ describe('uploads renew & complete & history (e2e)', () => {
}); });
it('complete 成功 → 200 uploaded;photo/session/photo_jobs/audit 同事务落库;重复 complete 幂等(A03)', async () => { it('complete 成功 → 200 uploaded;photo/session/photo_jobs/audit 同事务落库;重复 complete 幂等(A03)', async () => {
const init = await initAs(memberToken, 'C-happy'); // bytes=1003:同上,避开 C-bytes 已完成的 1000 字节照片(init ③.5 查重)。
const init = await initAs(memberToken, 'C-happy', { bytes: 1003 });
cHappy = { uploadId: init.uploadId, photoId: init.photoId, objectKey: init.objectKey }; cHappy = { uploadId: init.uploadId, photoId: init.photoId, objectKey: init.objectKey };
expect(await putStaging(init.credential, 1000)).toBe(true); expect(await putStaging(init.credential, 1003)).toBe(true);
const res = await complete(memberToken, init.uploadId); const res = await complete(memberToken, init.uploadId);
expect(res.status).toBe(200); expect(res.status).toBe(200);
@@ -702,7 +705,7 @@ describe('uploads renew & complete & history (e2e)', () => {
// DB 四处一致性。 // DB 四处一致性。
const photo = await prisma.photos.findUnique({ where: { id: init.photoId } }); const photo = await prisma.photos.findUnique({ where: { id: init.photoId } });
expect(photo).toMatchObject({ expect(photo).toMatchObject({
bytes: 1000, bytes: 1003,
uploadedAt: expect.any(Date), uploadedAt: expect.any(Date),
processingState: 'uploaded', processingState: 'uploaded',
visibility: 'unpublished', visibility: 'unpublished',
@@ -749,8 +752,8 @@ describe('uploads renew & complete & history (e2e)', () => {
const stagingHead = await storage.head(cHappy.objectKey); const stagingHead = await storage.head(cHappy.objectKey);
const finalHead = await storage.head(photo?.objectKey as string); const finalHead = await storage.head(photo?.objectKey as string);
expect(stagingHead?.bytes).toBe(1000); expect(stagingHead?.bytes).toBe(1003);
expect(finalHead?.bytes).toBe(1000); expect(finalHead?.bytes).toBe(1003);
// 手工删除 staging(模拟生命周期清理/超时)后重复 complete:originals 已在, // 手工删除 staging(模拟生命周期清理/超时)后重复 complete:originals 已在,
// copyIfAbsent 幂等返回 created:false,不再依赖 staging 源。 // copyIfAbsent 幂等返回 created:false,不再依赖 staging 源。
@@ -779,7 +782,7 @@ describe('uploads renew & complete & history (e2e)', () => {
const res = await complete(memberToken, cHappy.uploadId); const res = await complete(memberToken, cHappy.uploadId);
expect(res.status).toBe(200); expect(res.status).toBe(200);
expect(res.body.processingState).toBe('uploaded'); expect(res.body.processingState).toBe('uploaded');
expect((await storage.head(photo?.objectKey as string))?.bytes).toBe(1000); expect((await storage.head(photo?.objectKey as string))?.bytes).toBe(1003);
}); });
it('GET /uploads:本人会话新在前,photo 状态内嵌,游标翻页,跨用户隔离', async () => { it('GET /uploads:本人会话新在前,photo 状态内嵌,游标翻页,跨用户隔离', async () => {
@@ -823,7 +826,7 @@ describe('uploads renew & complete & history (e2e)', () => {
expect(page1.body.items[0]).toMatchObject({ expect(page1.body.items[0]).toMatchObject({
state: 'confirmed', state: 'confirmed',
albumId, albumId,
expectedBytes: 1000, expectedBytes: 1003,
contentType: 'image/jpeg', contentType: 'image/jpeg',
photo: { photo: {
photoId: cHappy.photoId, photoId: cHappy.photoId,
@@ -895,8 +898,10 @@ describe('uploads renew & complete & history (e2e)', () => {
}); });
it('sourceDeviceId:init 携带 → 落库并回显;幂等重放换设备保留首值(first-writer)', async () => { it('sourceDeviceId:init 携带 → 落库并回显;幂等重放换设备保留首值(first-writer)', async () => {
// 设备 A 初始化(P3 Task 3 契约增补,A19 设备来源语义)。 // 设备 A 初始化(P3 Task 3 契约增补,A19 设备来源语义)。bytes=1004:
// 避开本册已完成的 1000(C-bytes)字节数(init ③.5 查重)。
const first = await initAs(memberToken, 'DEV-1', { const first = await initAs(memberToken, 'DEV-1', {
bytes: 1004,
sourceDeviceId: 'device-aaaa-1111', sourceDeviceId: 'device-aaaa-1111',
}); });
@@ -912,13 +917,14 @@ describe('uploads renew & complete & history (e2e)', () => {
// 同 clientTaskId 换设备 B 重放:复用原会话,sourceDeviceId 保留首值。 // 同 clientTaskId 换设备 B 重放:复用原会话,sourceDeviceId 保留首值。
const replay = await initAs(memberToken, 'DEV-1', { const replay = await initAs(memberToken, 'DEV-1', {
bytes: 1004,
sourceDeviceId: 'device-bbbb-2222', sourceDeviceId: 'device-bbbb-2222',
}); });
expect(replay.uploadId).toBe(first.uploadId); expect(replay.uploadId).toBe(first.uploadId);
expect((await findByTask('DEV-1'))?.sourceDeviceId).toBe('device-aaaa-1111'); expect((await findByTask('DEV-1'))?.sourceDeviceId).toBe('device-aaaa-1111');
// 未携带 → null(老客户端兼容)。 // 未携带 → null(老客户端兼容)。bytes=1005 同理避开已完成字节数。
await initAs(memberToken, 'DEV-2'); await initAs(memberToken, 'DEV-2', { bytes: 1005 });
expect((await findByTask('DEV-2'))?.sourceDeviceId).toBeNull(); expect((await findByTask('DEV-2'))?.sourceDeviceId).toBeNull();
// 长度越界(<8 / >64)→ 400 VALIDATION_FAILED。 // 长度越界(<8 / >64)→ 400 VALIDATION_FAILED。