diff --git a/src/storage.js b/src/storage.js index 4cf3f7a..9b5b469 100644 --- a/src/storage.js +++ b/src/storage.js @@ -14,13 +14,51 @@ export function saveSession(session, store) { } } -/** @returns {object|null} */ +// Ein Objekt im Sinne dieser Pruefung: kein Array und nicht null - beides +// liefert typeof 'object', ist fuer stack.spec/entry.spec aber unbrauchbar. +function isPlainObject(value) { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isFiniteNumber(value) { + return typeof value === 'number' && Number.isFinite(value); +} + +function isUsableStack(stack) { + return ( + isPlainObject(stack) + && typeof stack.id === 'string' + && isFiniteNumber(stack.count) + && isPlainObject(stack.spec) + ); +} + +function isUsableEntry(entry) { + return ( + isPlainObject(entry) + && isFiniteNumber(entry.entryId) + && typeof entry.stackId === 'string' + && isPlainObject(entry.spec) + ); +} + +/** + * @returns {object|null} + * + * Nur eine Sitzung, die session.js unveraendert weiterverwenden kann, wird + * zurueckgegeben. Ein halb brauchbarer Zustand wird nicht repariert oder + * teilweise uebernommen - dann lieber die Sitzung verlieren (null) als mit + * falschen Zaehlern oder doppelten entryIds lautlos weiterarbeiten. + */ export function loadSession(store) { try { const raw = store.getItem(KEY); if (!raw) return null; const parsed = JSON.parse(raw); if (!Array.isArray(parsed.stacks) || !Array.isArray(parsed.entries)) return null; + if (!isFiniteNumber(parsed.nextEntryId)) return null; + if (!parsed.stacks.every(isUsableStack)) return null; + if (!parsed.entries.every(isUsableEntry)) return null; return parsed; } catch { return null; diff --git a/test/storage.test.js b/test/storage.test.js index 522c225..4822897 100644 --- a/test/storage.test.js +++ b/test/storage.test.js @@ -1,7 +1,13 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { saveSession, loadSession, clearSession } from '../src/storage.js'; -import { createSession, commitAssignment } from '../src/session.js'; +import { + createSession, + commitAssignment, + proposeAssignment, + moveEntry, + removeEntry, +} from '../src/session.js'; import { emptySpec } from '../src/spec.js'; function fakeStore() { @@ -52,3 +58,78 @@ test('Sichern ohne funktionierenden Speicher wirft nicht', () => { }; assert.doesNotThrow(() => saveSession(createSession(), kaputt)); }); + +test('stacks mit Nicht-Objekten liefert null', () => { + const store = fakeStore(); + store.setItem('ram-sortierhilfe:session', JSON.stringify({ + stacks: ['x', 'y'], + entries: [], + nextEntryId: 1, + })); + assert.equal(loadSession(store), null); +}); + +test('fehlender nextEntryId liefert null', () => { + const store = fakeStore(); + store.setItem('ram-sortierhilfe:session', JSON.stringify({ + stacks: [], + entries: [], + })); + assert.equal(loadSession(store), null); +}); + +test('nextEntryId als Zeichenkette liefert null', () => { + const store = fakeStore(); + store.setItem('ram-sortierhilfe:session', JSON.stringify({ + stacks: [], + entries: [], + nextEntryId: '2', + })); + assert.equal(loadSession(store), null); +}); + +test('Stapel mit Zaehler als Zeichenkette liefert null', () => { + const store = fakeStore(); + store.setItem('ram-sortierhilfe:session', JSON.stringify({ + stacks: [{ id: 'A', count: '3', spec: emptySpec() }], + entries: [], + nextEntryId: 1, + })); + assert.equal(loadSession(store), null); +}); + +test('Eintrag ohne Spec liefert null', () => { + const store = fakeStore(); + store.setItem('ram-sortierhilfe:session', JSON.stringify({ + stacks: [], + entries: [{ entryId: 1, stackId: 'A' }], + nextEntryId: 2, + })); + assert.equal(loadSession(store), null); +}); + +test('nach Sichern und Laden funktioniert Weiterarbeiten weiterhin', () => { + const store = fakeStore(); + const session = createSession(); + const spec = { ...emptySpec(), capacityGb: 64 }; + commitAssignment(session, spec, 'barcode', 'A'); + const zweiterEintrag = commitAssignment(session, spec, 'barcode', 'A'); + saveSession(session, store); + + const wieder = loadSession(store); + + const vorschlag = proposeAssignment(wieder, spec); + assert.equal(vorschlag.kind, 'match'); + assert.equal(vorschlag.stackId, 'A'); + commitAssignment(wieder, spec, 'barcode', vorschlag.stackId); + assert.equal(wieder.stacks[0].count, 3); + assert.equal(wieder.nextEntryId, 4); + + moveEntry(wieder, zweiterEintrag.entryId, 'B'); + assert.equal(wieder.stacks.find((s) => s.id === 'A').count, 2); + assert.equal(wieder.stacks.find((s) => s.id === 'B').count, 1); + + removeEntry(wieder, zweiterEintrag.entryId); + assert.equal(wieder.entries.find((e) => e.entryId === zweiterEintrag.entryId), undefined); + assert.equal(wieder.stacks.find((s) => s.id === 'B'), undefined); +});