import "fake-indexeddb/auto" import { beforeEach, describe, expect, it, vi } from "vitest" import { enqueueWrite, flushQueue, listQueue, queueCount } from "./offline-queue" // Unik matchId per test -- IndexedDB-databasen deles på tvers av tester i // samme fil (ingen reset mellom tester), men listQueue/flushQueue filtrerer // alltid på matchId, så unike ID-er isolerer testene fra hverandre helt uten // å måtte slette/gjenopprette databasen for hver test. function uniqueMatchId(): string { return `match-${Math.random().toString(36).slice(2)}` } function jsonResponse(body: unknown, ok = true, status = ok ? 200 : 409): Response { return { ok, status, json: async () => body, } as Response } describe("enqueueWrite / listQueue / queueCount", () => { it("lagrer og henter en oppføring for riktig matchId", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/x", method: "POST", body: { a: 1 }, matchId }) const entries = await listQueue(matchId) expect(entries).toHaveLength(1) expect(entries[0].body).toEqual({ a: 1 }) expect(await queueCount(matchId)).toBe(1) }) it("holder køer for ulike matchId helt adskilt", async () => { const matchA = uniqueMatchId() const matchB = uniqueMatchId() await enqueueWrite({ url: "/x", method: "POST", body: {}, matchId: matchA }) expect(await queueCount(matchA)).toBe(1) expect(await queueCount(matchB)).toBe(0) }) it("bevarer rekkefølgen entries ble lagt til i", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/x", method: "POST", body: { n: 1 }, matchId }) await enqueueWrite({ url: "/x", method: "POST", body: { n: 2 }, matchId }) await enqueueWrite({ url: "/x", method: "POST", body: { n: 3 }, matchId }) const entries = await listQueue(matchId) expect(entries.map((e) => (e.body as { n: number }).n)).toEqual([1, 2, 3]) }) }) describe("flushQueue -- versjonskjeding per resourceKey (ADR-060 regresjonsvern)", () => { beforeEach(() => { vi.restoreAllMocks() }) it("kjeder expected_version fremover for FLERE skrivinger til SAMME resourceKey", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/hole-scores", method: "POST", body: { hole_number: 3, expected_version: 1 }, matchId, resourceKey: "hull-3", }) await enqueueWrite({ url: "/hole-scores", method: "POST", body: { hole_number: 3, expected_version: 1 }, // fortsatt "1" -- lokal state ble aldri oppdatert offline matchId, resourceKey: "hull-3", }) const bodiesSent: unknown[] = [] vi.stubGlobal( "fetch", vi.fn(async (_url: string, init: RequestInit) => { const body = JSON.parse(init.body as string) bodiesSent.push(body) return jsonResponse({ version: body.expected_version === 1 ? 2 : 3 }) }), ) const outcomes = await flushQueue(matchId) expect(outcomes.every((o) => o.ok)).toBe(true) // Første sendt med versjon 1 (som lagret), andre skal ha blitt // OVERSTYRT til 2 (versjonen serveren returnerte fra FØRSTE skrivning i // denne batchen) -- ikke den opprinnelig lagrede (utdaterte) "1". expect((bodiesSent[0] as { expected_version: number }).expected_version).toBe(1) expect((bodiesSent[1] as { expected_version: number }).expected_version).toBe(2) }) it("blander IKKE sammen versjon mellom to ULIKE resourceKey som deler samme URL", async () => { // Dette er nøyaktig regresjonen som ble funnet og rettet i ADR-060: // /hole-scores har samme URL for alle hull i en match. Uten korrekt // nøkling ville hull 7 sin skrivning feilaktig arvet versjonen fra // hull 3 sin skrivning i samme flush-runde. const matchId = uniqueMatchId() await enqueueWrite({ url: "/hole-scores", method: "POST", body: { hole_number: 3, expected_version: 1 }, matchId, resourceKey: "hull-3", }) await enqueueWrite({ url: "/hole-scores", method: "POST", body: { hole_number: 7, expected_version: 5 }, matchId, resourceKey: "hull-7", }) const bodiesSent: unknown[] = [] vi.stubGlobal( "fetch", vi.fn(async (_url: string, init: RequestInit) => { const body = JSON.parse(init.body as string) bodiesSent.push(body) return jsonResponse({ version: 99 }) // hull 3 sin server-versjon etter skriving }), ) await flushQueue(matchId) // Hull 7 sin expected_version skal FORTSATT være 5 -- IKKE 99 (hull 3 // sin returnerte versjon), siden de er to forskjellige ressurser. expect((bodiesSent[0] as { hole_number: number; expected_version: number }).expected_version).toBe(1) expect((bodiesSent[1] as { hole_number: number; expected_version: number }).expected_version).toBe(5) }) it("faller tilbake til url som nøkkel når resourceKey ikke er satt (round_hole-bakoverkompatibilitet)", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/rounds/r1/participants/p1/holes/5", method: "PATCH", body: { expected_version: 1 }, matchId, // ingen resourceKey -- samme mønster som round-detail.tsx bruker i dag }) await enqueueWrite({ url: "/rounds/r1/participants/p1/holes/5", method: "PATCH", body: { expected_version: 1 }, matchId, }) const bodiesSent: unknown[] = [] vi.stubGlobal( "fetch", vi.fn(async (_url: string, init: RequestInit) => { const body = JSON.parse(init.body as string) bodiesSent.push(body) return jsonResponse({ version: 2 }) }), ) await flushQueue(matchId) expect((bodiesSent[0] as { expected_version: number }).expected_version).toBe(1) expect((bodiesSent[1] as { expected_version: number }).expected_version).toBe(2) }) it("fjerner vellykkede oppføringer fra køen, lar mislykkede stå igjen ved ekte nettverksfeil", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/x", method: "POST", body: { n: 1 }, matchId, resourceKey: "a" }) await enqueueWrite({ url: "/x", method: "POST", body: { n: 2 }, matchId, resourceKey: "b" }) let call = 0 vi.stubGlobal( "fetch", vi.fn(async () => { call += 1 if (call === 1) return jsonResponse({ version: 1 }) throw new Error("offline") }), ) const outcomes = await flushQueue(matchId) expect(outcomes).toHaveLength(1) expect(outcomes[0].ok).toBe(true) // Andre oppføringen feilet med en EKTE nettverksfeil -- skal fortsatt // ligge i køen til neste forsøk, ikke fjernet. expect(await queueCount(matchId)).toBe(1) }) it("fjerner en oppføring ved en DEFINITIV HTTP-feil (f.eks. 409) og rapporterer den", async () => { const matchId = uniqueMatchId() await enqueueWrite({ url: "/x", method: "POST", body: {}, matchId, resourceKey: "a" }) vi.stubGlobal( "fetch", vi.fn(async () => jsonResponse({ detail: { code: "STALE_VERSION", message: "Konflikt" } }, false, 409), ), ) const outcomes = await flushQueue(matchId) expect(outcomes).toHaveLength(1) expect(outcomes[0].ok).toBe(false) expect(outcomes[0].message).toBe("Konflikt") expect(await queueCount(matchId)).toBe(0) }) })