test(concurrency): sync concurrency and provider capabilities tests with Phase 1.4 normalized error responses and FSM snapshot-first draw flow

This commit is contained in:
Ochenstarik 2026-08-18 01:56:26 +07:00
parent 50973b4f85
commit 867923186e
2 changed files with 10 additions and 11 deletions

View file

@ -43,6 +43,7 @@ async function createReadyGiveaway() {
})); }));
await GiveawayStore.updateParticipants(gw.id, participants); await GiveawayStore.updateParticipants(gw.id, participants);
await GiveawayStore.createAndLockSnapshot(gw.id, participants, DEFAULT_FILTER_RULES);
return gw; return gw;
} }
@ -52,7 +53,7 @@ describe('Concurrency analysis', () => {
ProviderRegistry.useMockVk(); ProviderRegistry.useMockVk();
}); });
it('documents current double-draw race behavior (both requests may succeed)', async () => { it('documents double-draw race protection (exactly one succeeds with 200, concurrent receives 409)', async () => {
const gw = await createReadyGiveaway(); const gw = await createReadyGiveaway();
const req1 = new NextRequest(`http://localhost/api/giveaways/${gw.id}/draw`, { const req1 = new NextRequest(`http://localhost/api/giveaways/${gw.id}/draw`, {
@ -69,10 +70,9 @@ describe('Concurrency analysis', () => {
drawPost(req2, { params: { id: gw.id } }), drawPost(req2, { params: { id: gw.id } }),
]); ]);
// This assertion captures the CURRENT behavior so the test passes today. const statuses = [res1.status, res2.status];
// If the race is fixed, the previous `it.failing` will start passing and expect(statuses).toContain(200);
// this test should be updated to assert one failure. expect(statuses).toContain(409);
expect([res1.status, res2.status]).toContain(200);
}); });
it('should not corrupt giveaway state when snapshot and draw race', async () => { it('should not corrupt giveaway state when snapshot and draw race', async () => {
@ -107,7 +107,7 @@ describe('Concurrency analysis', () => {
const gw = await createReadyGiveaway(); const gw = await createReadyGiveaway();
const refreshed = await GiveawayStore.getById(gw.id); const refreshed = await GiveawayStore.getById(gw.id);
const eligible = refreshed!.participants.filter(p => p.eligible); const eligible = refreshed!.participants.filter(p => p.eligible);
const snapshot = await GiveawayStore.createAndLockSnapshot(gw.id, eligible, DEFAULT_FILTER_RULES); const snapshot = refreshed!.latestSnapshot!;
await GiveawayStore.saveDrawResult(gw.id, snapshot.id, { await GiveawayStore.saveDrawResult(gw.id, snapshot.id, {
drawId: 'draw-test', drawId: 'draw-test',
giveawayId: gw.id, giveawayId: gw.id,
@ -132,8 +132,6 @@ describe('Concurrency analysis', () => {
}); });
const res = await participantsPost(req, { params: { id: gw.id } }); const res = await participantsPost(req, { params: { id: gw.id } });
// The route catches the FSM error and returns 500; the important thing is
// that the DRAWN giveaway is not silently overwritten.
expect(res.status).toBeGreaterThanOrEqual(400); expect(res.status).toBeGreaterThanOrEqual(400);
}); });
}); });

View file

@ -130,8 +130,8 @@ describe('Provider capabilities', () => {
const res = await participantsPost(req, { params: { id: gw.id } }); const res = await participantsPost(req, { params: { id: gw.id } });
expect(res.status).toBe(400); expect(res.status).toBe(400);
const data = await res.json(); const data = await res.json();
expect(data.error).toMatch(/Unsupported filter rules/i); const errorMessage = data.error?.message || data.error || '';
expect(data.details.some((d: string) => d.includes('requireRepost'))).toBe(true); expect(errorMessage).toMatch(/repost/i);
}); });
it('participants route returns 400 when excludeAdmins is requested for VK', async () => { it('participants route returns 400 when excludeAdmins is requested for VK', async () => {
@ -151,7 +151,8 @@ describe('Provider capabilities', () => {
const res = await participantsPost(req, { params: { id: gw.id } }); const res = await participantsPost(req, { params: { id: gw.id } });
expect(res.status).toBe(400); expect(res.status).toBe(400);
const data = await res.json(); const data = await res.json();
expect(data.details.some((d: string) => d.includes('excludeAdmins'))).toBe(true); const errorMessage = data.error?.message || data.error || '';
expect(errorMessage).toMatch(/admin/i);
}); });
it('participants route succeeds for supported VK rules', async () => { it('participants route succeeds for supported VK rules', async () => {