From 64e1be09d3b9d9bc028520e8b2000841a5b8d6de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Mon, 28 Sep 2026 19:06:47 +0800 Subject: [PATCH] test(api): pin earliest-uploadedAt + uploadEnabled-agnostic pre-check; fix comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 评审硬化三项(checkUploadDuplicates 后续,不 amend): - e2e 补「同 bytes 多条可见命中取最早 uploadedAt」:两条同 bytes 不同 uploadedAt 的种子行,断言回显更早值。 - e2e 补「uploadEnabled=false 预检仍 200 且照常命中」:钉住只读预览刻意 不校验开关的偏差——防止未来一致性重构把检查加回来,破坏 App 关册时的 降级体验(init 入队时仍有 409 兜底)。 - 修正 uploads.service checkDuplicates 的 null-uploadedAt 注释:原「谓词 命中与否不受影响」不准确,改为「仅直改库等理论形状(bytes>0 且 uploadedAt=null)下预检可能漏报而 init 仍会 409」。 uploads e2e 30/30 ✅ · api typecheck ✅ Co-Authored-By: Claude Code --- services/api/src/uploads/uploads.service.ts | 4 +-- services/api/test/uploads.e2e-spec.ts | 40 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/services/api/src/uploads/uploads.service.ts b/services/api/src/uploads/uploads.service.ts index dc72c80..ca1b8d3 100644 --- a/services/api/src/uploads/uploads.service.ts +++ b/services/api/src/uploads/uploads.service.ts @@ -594,8 +594,8 @@ export class UploadsService { const earliest = new Map(); for (const row of rows) { // bytes>0 的行只会经 complete 落定(同事务写 uploadedAt);null 仅理论 - // 形状(直改库等),跳过不参与映射——谓词命中与否不受影响(uploadedAt - // 只是回显元数据)。 + // 形状(直改库等,bytes>0 且 uploadedAt=null),跳过不参与映射——该形状 + // 下预检可能漏报而 init 仍会 409(偏差仅限于此理论形状,正常写路径不可达)。 if (row.uploadedAt === null) { continue; } diff --git a/services/api/test/uploads.e2e-spec.ts b/services/api/test/uploads.e2e-spec.ts index 39757fb..31ae24a 100644 --- a/services/api/test/uploads.e2e-spec.ts +++ b/services/api/test/uploads.e2e-spec.ts @@ -1117,6 +1117,46 @@ describe('uploads duplicates check (e2e)', () => { expect(res.body.duplicates).toEqual([]); }); + it('同 bytes 多条可见命中取最早 uploadedAt(force 允许的重复行)', async () => { + const bytes = 555_555; + await seedPhoto(memberAId, bytes, { + uploadedAt: new Date('2026-09-25T08:00:00.000Z'), + }); + await seedPhoto(memberAId, bytes, { + uploadedAt: new Date('2026-09-10T08:00:00.000Z'), // 更早,应回显此值 + }); + const res = await check(memberAToken, { + albumId, + items: [item('e.jpg', bytes)], + }); + expect(res.status).toBe(200); + expect(res.body.duplicates).toEqual([ + { index: 0, bytes, uploadedAt: '2026-09-10T08:00:00.000Z' }, + ]); + }); + + it('uploadEnabled=false:预检仍 200 且照常命中(只读预览刻意不校验开关;init 入队时仍 409 兜底)', async () => { + await prisma.albums.update({ + where: { id: albumId }, + data: { uploadEnabled: false }, + }); + try { + const res = await check(memberAToken, { + albumId, + items: [item('a.jpg', BYTES_A)], + }); + expect(res.status).toBe(200); + expect(res.body.duplicates).toEqual([ + { index: 0, bytes: BYTES_A, uploadedAt: UPLOADED_AT_A }, + ]); + } finally { + await prisma.albums.update({ + where: { id: albumId }, + data: { uploadEnabled: true }, + }); + } + }); + it('非成员 → 404 ALBUM_NOT_FOUND(防枚举,同 init)', async () => { const res = await check(outsiderToken, { albumId,