test(01-03): add failing tests for Base64 codec + full round-trip [RED]
- round-trip.test.ts (3 tests): full pipeline EXPORT -> IMPORT -> MIGRATE -> WRAP -> UNWRAP -> IDB PUT -> IDB GET exercising every save layer file end-to-end (CORE-09 + CORE-04 + CORE-06 + CORE-07); plus DoS-cap rejection at MAX_IMPORT_BYTES + 1; plus malformed-Base64 rejection RED phase per TDD plan-level gate. Tests fail because codec.ts does not exist yet.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import 'fake-indexeddb/auto';
|
||||
import { wrap, unwrap } from './envelope';
|
||||
import { migrate, CURRENT_SCHEMA_VERSION } from './migrations';
|
||||
import { exportToBase64, importFromBase64 } from './codec';
|
||||
import { openSaveDB } from './db';
|
||||
|
||||
// CORE-09 + CORE-04 + CORE-06 + CORE-07: full save round-trip exercising
|
||||
// every save layer file end-to-end. This is the load-bearing integration
|
||||
// test for Phase 1 — if this passes, Phase 2 can reasonably trust that
|
||||
// the save subsystem is wired correctly.
|
||||
|
||||
beforeEach(async () => {
|
||||
// Same store-contents reset pattern as the unit tests — see db.test.ts
|
||||
// and snapshots.test.ts for why we don't deleteDatabase.
|
||||
const db = await openSaveDB();
|
||||
for (const store of ['saves', 'save_snapshots'] as const) {
|
||||
const all = await db.getAll(store);
|
||||
for (const e of all) {
|
||||
await db.delete(store, e.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('CORE-09 + CORE-04 + CORE-06 + CORE-07: full save round-trip', () => {
|
||||
it('synthetic v0 envelope migrates, round-trips through Base64, validates, persists', async () => {
|
||||
// Pretend a player had an old v0 save lying around (CONTEXT D-05 synthetic v0).
|
||||
const v0Payload = { garden: [{ id: 'tile-1' }, { id: 'tile-2' }] };
|
||||
// v0 envelope: schemaVersion 0, with a placeholder checksum that we won't
|
||||
// verify (the v0 era didn't have our checksum scheme, but the schema
|
||||
// accepts it because checksum just has to be 8 hex chars).
|
||||
const v0Envelope = {
|
||||
schemaVersion: 0,
|
||||
payload: v0Payload,
|
||||
checksum: '00000000', // 8-char hex placeholder
|
||||
};
|
||||
|
||||
// EXPORT through Base64 codec
|
||||
const exported = exportToBase64(v0Envelope);
|
||||
expect(exported.length).toBeGreaterThan(0);
|
||||
|
||||
// IMPORT (simulating a fresh browser) — note: import returns a parsed
|
||||
// envelope that PASSES our SaveEnvelopeSchema (schemaVersion 0 is allowed
|
||||
// since z.number().nonnegative()).
|
||||
const imported = importFromBase64(exported);
|
||||
expect(imported.schemaVersion).toBe(0);
|
||||
|
||||
// MIGRATE the imported payload
|
||||
const { payload, toVersion } = migrate(imported.payload, imported.schemaVersion);
|
||||
expect(toVersion).toBe(CURRENT_SCHEMA_VERSION);
|
||||
expect(payload).toMatchObject({
|
||||
garden: { tiles: [{ id: 'tile-1' }, { id: 'tile-2' }] },
|
||||
plants: [],
|
||||
harvestedFragmentIds: [],
|
||||
lastTickAt: expect.any(Number),
|
||||
settings: {
|
||||
musicVolume: expect.any(Number),
|
||||
ambientVolume: expect.any(Number),
|
||||
sfxVolume: expect.any(Number),
|
||||
},
|
||||
});
|
||||
|
||||
// WRAP with current version and a valid checksum, UNWRAP to verify
|
||||
const v1Envelope = wrap(payload, toVersion);
|
||||
expect(unwrap(v1Envelope)).toEqual(payload);
|
||||
|
||||
// PERSIST to IDB and read back (CORE-04)
|
||||
const db = await openSaveDB();
|
||||
await db.put('saves', {
|
||||
id: 'main',
|
||||
envelope: v1Envelope,
|
||||
savedAt: new Date().toISOString(),
|
||||
});
|
||||
const retrieved = await db.get('saves', 'main');
|
||||
expect(retrieved?.envelope).toEqual(v1Envelope);
|
||||
expect(unwrap(retrieved!.envelope)).toEqual(payload);
|
||||
});
|
||||
|
||||
it('rejects oversized Base64 import (DoS cap)', () => {
|
||||
const huge = 'A'.repeat(50 * 1024 * 1024 + 1);
|
||||
expect(() => importFromBase64(huge)).toThrow(/exceeds/);
|
||||
});
|
||||
|
||||
it('rejects malformed Base64', () => {
|
||||
expect(() => importFromBase64('not-valid-base64-)(*&^%$')).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user