175 lines
5.2 KiB
TypeScript
175 lines
5.2 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import { loadServerData, saveServerData, ServerDataConflictError } from './server-data';
|
|
|
|
type MockResponse = {
|
|
status: number;
|
|
body: unknown;
|
|
};
|
|
|
|
test('server data saves include the latest loaded AppData version', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
const calls: Array<{ url: string; method: string; body?: unknown }> = [];
|
|
const responses: MockResponse[] = [
|
|
{ status: 200, body: {} },
|
|
{
|
|
status: 200,
|
|
body: { key: 'dev-tasks', value: [{ id: 'old' }], version: 'version-1' },
|
|
},
|
|
{
|
|
status: 200,
|
|
body: { key: 'dev-tasks', value: [{ id: 'next' }], version: 'version-2' },
|
|
},
|
|
{
|
|
status: 200,
|
|
body: { key: 'dev-tasks', value: [{ id: 'again' }], version: 'version-3' },
|
|
},
|
|
{
|
|
status: 200,
|
|
body: { key: 'bugs', value: [], version: 'bugs-version-1' },
|
|
},
|
|
{
|
|
status: 409,
|
|
body: {
|
|
code: 'APP_DATA_CONFLICT',
|
|
key: 'dev-tasks',
|
|
currentValue: [{ id: 'server-copy' }],
|
|
currentVersion: 'version-4',
|
|
},
|
|
},
|
|
];
|
|
|
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const response = responses.shift();
|
|
if (!response) throw new Error('Unexpected fetch call');
|
|
calls.push({
|
|
url: String(input),
|
|
method: init?.method ?? 'GET',
|
|
body: init?.body ? JSON.parse(String(init.body)) : undefined,
|
|
});
|
|
return new Response(JSON.stringify(response.body), {
|
|
status: response.status,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
await assert.doesNotReject(async () => {
|
|
const loaded = await loadServerData<Array<{ id: string }>>('dev-tasks');
|
|
assert.deepEqual(loaded, [{ id: 'old' }]);
|
|
});
|
|
|
|
await saveServerData('dev-tasks', [{ id: 'next' }]);
|
|
assert.deepEqual(calls[2].body, {
|
|
value: [{ id: 'next' }],
|
|
version: 'version-1',
|
|
});
|
|
|
|
await saveServerData('dev-tasks', [{ id: 'again' }]);
|
|
assert.deepEqual(calls[3].body, {
|
|
value: [{ id: 'again' }],
|
|
version: 'version-2',
|
|
});
|
|
|
|
await saveServerData('bugs', []);
|
|
assert.deepEqual(calls[4].body, { value: [] });
|
|
|
|
await assert.rejects(
|
|
() => saveServerData('dev-tasks', [{ id: 'stale' }]),
|
|
(error: unknown) => {
|
|
if (!(error instanceof ServerDataConflictError)) return false;
|
|
return error.key === 'dev-tasks' && error.currentVersion === 'version-4';
|
|
},
|
|
);
|
|
assert.deepEqual(calls[5].body, {
|
|
value: [{ id: 'stale' }],
|
|
version: 'version-3',
|
|
});
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('server data shares an in-flight load for the same AppData key', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let dataRequests = 0;
|
|
let releaseDataResponse!: () => void;
|
|
const dataResponseReady = new Promise<void>((resolve) => {
|
|
releaseDataResponse = resolve;
|
|
});
|
|
|
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
if (url.endsWith('/config/ai')) {
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
if (url.endsWith('/data/test-cases')) {
|
|
dataRequests += 1;
|
|
await dataResponseReady;
|
|
return new Response(JSON.stringify({
|
|
key: 'test-cases',
|
|
value: [{ id: 'tc-1' }],
|
|
version: 'test-cases-version-1',
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fetch ${init?.method ?? 'GET'} ${url}`);
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const first = loadServerData<Array<{ id: string }>>('test-cases');
|
|
const second = loadServerData<Array<{ id: string }>>('test-cases');
|
|
releaseDataResponse();
|
|
|
|
assert.deepEqual(await first, [{ id: 'tc-1' }]);
|
|
assert.deepEqual(await second, [{ id: 'tc-1' }]);
|
|
assert.equal(dataRequests, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test('server data reuses a fresh cached load for repeated AppData reads', async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let dataRequests = 0;
|
|
|
|
globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
|
|
const url = String(input);
|
|
if (url.endsWith('/config/ai')) {
|
|
return new Response(JSON.stringify({}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
if (url.endsWith('/data/version-plans')) {
|
|
dataRequests += 1;
|
|
return new Response(JSON.stringify({
|
|
key: 'version-plans',
|
|
value: [{ id: 'plan-1' }],
|
|
version: 'version-plans-version-1',
|
|
}), {
|
|
status: 200,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
});
|
|
}
|
|
throw new Error(`Unexpected fetch ${init?.method ?? 'GET'} ${url}`);
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const first = await loadServerData<Array<{ id: string }>>('version-plans');
|
|
const second = await loadServerData<Array<{ id: string }>>('version-plans');
|
|
|
|
assert.deepEqual(first, [{ id: 'plan-1' }]);
|
|
assert.deepEqual(second, [{ id: 'plan-1' }]);
|
|
assert.equal(dataRequests, 1);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|