From 5fea22a8c19a9149905c4a33862f59ea19d3dd85 Mon Sep 17 00:00:00 2001 From: vchuser Date: Tue, 28 Jul 2026 16:03:23 +0200 Subject: [PATCH] Validiere Inhalt in loadSession statt nur Array-Form loadSession pruefte bisher nur, ob stacks/entries Arrays sind, nicht deren Inhalt. Ein fremder oder verfaelschter Eintrag unter dem gleichen Schluessel kam dadurch als vermeintlich gueltige Sitzung durch und liess die App beim naechsten Scan mit TypeError abstuerzen (stack.spec fehlte). Falsch typisierte Felder (nextEntryId als String, stack.count als String) wurden ebenfalls durchgereicht und verdarben die Sitzung lautlos (doppelte entryIds, "3" + 1 = "31"). loadSession verlangt jetzt, dass nextEntryId eine endliche Zahl ist und dass jeder Stapel/Eintrag ein echtes Objekt (kein Array, nicht null) mit den Feldern ist, die session.js tatsaechlich weiterverwendet. Schlaegt eine Pruefung fehl, liefert loadSession null - ein halb brauchbarer Zustand wird nicht repariert oder teilweise uebernommen. --- src/storage.js | 40 ++++++++++++++++++++- test/storage.test.js | 83 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 121 insertions(+), 2 deletions(-) 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); +});