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:
parent
50973b4f85
commit
867923186e
2 changed files with 10 additions and 11 deletions
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue