200 lines
7.2 KiB
TypeScript
200 lines
7.2 KiB
TypeScript
|
|
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)
|
||
|
|
})
|
||
|
|
})
|