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:
@@ -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。
|
||||||
|
|||||||
Reference in New Issue
Block a user