feat!: client storage is read-only, storage.save and storage.delete removed

This commit is contained in:
edmand46
2026-09-29 16:43:15 +03:00
parent b115450715
commit 17a55888a5
11 changed files with 28 additions and 104 deletions
+1 -1
View File
@@ -117,7 +117,7 @@ describe('transport error mapping', () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(
new Response(null, { status: 204, statusText: 'No Content' }),
));
const result = await client.storage.save('t', '{}');
const result = await client.auth.linkWithGoogle('id-token');
expect(result).toBeUndefined();
});
+2 -10
View File
@@ -2,18 +2,10 @@ import { describe, it, expect } from 'vitest';
import { freshPlayer } from './_setup/harness.js';
describe('e2e-prod: storage', () => {
it('save -> get -> delete round-trip', async () => {
it('a fresh player reads empty storage', async () => {
const client = await freshPlayer();
const payload = JSON.stringify({ level: 7 });
await client.storage.save('savegame', payload);
const got = await client.storage.reload();
expect(got.items?.some((i) => i.type === 'savegame' && i.data === payload)).toBe(true);
await client.storage.delete('savegame');
const after = await client.storage.reload();
expect(after.items?.some((i) => i.type === 'savegame')).toBeFalsy();
expect(got.items ?? []).toEqual([]);
});
});
+11 -31
View File
@@ -8,56 +8,36 @@ describe('E2E: player storage', () => {
let client: RudderClient;
beforeEach(() => {
gateway = createFakeGateway();
gateway = createFakeGateway({
storage: new Map([
['progress', { id: 'id-progress', type: 'progress', data: JSON.stringify({ level: 7, gold: 1200 }) }],
['prefs', { id: 'id-prefs', type: 'prefs', data: JSON.stringify({ sound: false }) }],
]),
});
gateway.install();
client = createTestClient();
});
afterEach(() => vi.unstubAllGlobals());
it('saves items then reads them back (round-trip through the gateway)', async () => {
it('reads the items written for the player', async () => {
await client.auth.loginWithDevice();
await client.storage.save('progress', JSON.stringify({ level: 7, gold: 1200 }));
await client.storage.save('prefs', JSON.stringify({ sound: false }));
const all = await client.storage.reload();
expect(all.items).toHaveLength(2);
const progress = all.items!.find((i) => i.type === 'progress');
expect(JSON.parse(progress!.data!)).toEqual({ level: 7, gold: 1200 });
const put = gateway.recorded.find((r) => r.method === 'PUT' && r.path === '/sdk/v1/storage');
expect(put?.body).toEqual({ items: [{ type: 'progress', data: '{"level":7,"gold":1200}' }] });
const getReq = gateway.recorded.findLast((r) => r.method === 'GET' && r.path === '/sdk/v1/storage');
expect(getReq?.query.get('limit')).toBe('100');
});
it('save overwrites the item of the same type', async () => {
await client.auth.loginWithDevice();
await client.storage.save('progress', '1');
await client.storage.save('progress', '2');
const all = await client.storage.reload();
expect(all.items?.map((i) => i.data)).toEqual(['2']);
});
it('deletes a type and the item is gone', async () => {
await client.auth.loginWithDevice();
await client.storage.save('progress', '{}');
await client.storage.save('prefs', '{}');
await client.storage.delete('progress');
const remaining = await client.storage.reload();
expect(remaining.items?.map((i) => i.type)).toEqual(['prefs']);
});
it('storage writes carry the auth token', async () => {
it('storage reads carry the auth token', async () => {
const token = (await client.auth.loginWithDevice()).accessToken;
await client.storage.save('t', '{}');
await client.storage.reload();
const put = gateway.recorded.find((r) => r.method === 'PUT' && r.path === '/sdk/v1/storage');
expect(put?.authToken).toBe(token);
const getReq = gateway.recorded.findLast((r) => r.method === 'GET' && r.path === '/sdk/v1/storage');
expect(getReq?.authToken).toBe(token);
});
});
-15
View File
@@ -15,7 +15,6 @@ export interface RecordedRequest {
export interface FakeGatewayState {
storage: Map<string, StorageItem>;
currencies: CatalogCurrency[];
/** Response per `module/fn`; falls back to `{ result: {} }`. */
moduleCalls: Map<string, CallModuleFunctionClientResponse>;
}
@@ -32,7 +31,6 @@ function json(body: unknown, status = 200): Response {
});
}
const noContent = (): Response => new Response(null, { status: 204 });
export function createFakeGateway(seed?: Partial<FakeGatewayState>): FakeGateway {
const state: FakeGatewayState = {
@@ -79,19 +77,6 @@ export function createFakeGateway(seed?: Partial<FakeGatewayState>): FakeGateway
);
return json({ items, nextCursor: '' });
}
if (method === 'PUT') {
const items = (body as { items?: StorageItem[] }).items ?? [];
for (const item of items) {
if (item.type) state.storage.set(item.type, { id: `id-${item.type}`, ...item });
}
return noContent();
}
if (method === 'DELETE') {
const type = query.get('type');
if (type) state.storage.delete(type);
else state.storage.clear();
return noContent();
}
}
return json({ error: `unhandled route: ${method} ${path}` }, 500);