Files

103 lines
4.1 KiB
JavaScript

import { test } from 'node:test'
import assert from 'node:assert/strict'
import { STORAGE_KEY, readItems, addItem } from '../src/items.mjs'
function memory(initial = null) {
let value = initial
return {
getItem(key) { assert.equal(key, STORAGE_KEY); return value },
setItem(key, next) { assert.equal(key, STORAGE_KEY); value = next },
}
}
test('empty/new/reloaded local items persist exactly', () => {
const storage = memory()
assert.deepEqual(readItems(storage), [])
const first = addItem(storage)
assert.deepEqual(readItems(storage), first)
assert.equal(addItem(storage)[1].id, 2)
})
test('invalid data is preserved, never reset by read or add', () => {
for (const raw of ['broken', '{}', '[null]', '[{"id":1}]', JSON.stringify([{ id: 1, name: 'a', detail: '' }, { id: 1, name: 'b', detail: '' }])]) {
const storage = memory(raw)
assert.throws(() => readItems(storage))
assert.throws(() => addItem(storage))
assert.equal(storage.getItem(STORAGE_KEY), raw)
}
})
test('unavailable storage and quota failures propagate without claiming success', () => {
assert.throws(() => readItems({ getItem() { throw Error('blocked') } }), /blocked/)
const storage = memory()
storage.setItem = () => { throw Error('quota') }
assert.throws(() => addItem(storage), /quota/)
assert.deepEqual(readItems(storage), [])
})
test('existing IDs retained and next ID follows maximum, not length', () => {
const storage = memory(JSON.stringify([{ id: 5, name: 'Existing', detail: 'Preserve' }]))
const items = addItem(storage)
assert.deepEqual(items[0], { id: 5, name: 'Existing', detail: 'Preserve' })
assert.equal(items[1].id, 6)
})
test('size and item count bounds preserve original data', () => {
const huge = ' '.repeat(1024 * 1024 + 1)
assert.throws(() => readItems(memory(huge)), /size/)
const raw = JSON.stringify(Array.from({ length: 1000 }, (_, i) => ({ id: i + 1, name: 'Saved', detail: '' })))
const storage = memory(raw)
assert.throws(() => addItem(storage), /1000/)
assert.equal(storage.getItem(STORAGE_KEY), raw)
})
test('unsafe identifiers, extra properties and invalid names preserve storage', () => {
for (const item of [
{ id: 0, name: 'a', detail: '' },
{ id: 1.5, name: 'a', detail: '' },
{ id: Number.MAX_SAFE_INTEGER + 1, name: 'a', detail: '' },
{ id: 1, name: '', detail: '' },
{ id: 1, name: 'a'.repeat(201), detail: '' },
{ id: 1, name: 'a', detail: 'b'.repeat(501) },
{ id: 1, name: 'a', detail: '', extra: true },
]) {
const raw = JSON.stringify([item])
const storage = memory(raw)
assert.throws(() => addItem(storage))
assert.equal(storage.getItem(STORAGE_KEY), raw)
}
})
test('exhausted safe integer ID refuses a new item without changing original', () => {
const raw = JSON.stringify([{ id: Number.MAX_SAFE_INTEGER, name: 'Last', detail: '' }])
const storage = memory(raw)
assert.throws(() => addItem(storage), /exhausted/)
assert.equal(storage.getItem(STORAGE_KEY), raw)
})
test('an append crossing the serialized-size boundary preserves readable data', () => {
const items = Array.from({ length: 999 }, (_, i) => ({ id: i + 1, name: 'x'.repeat(200), detail: 'x'.repeat(500) }))
const limit = 1024 * 1024
let extra = Math.floor((limit - JSON.stringify(items).length) / 5)
for (const item of items) {
const escaped = Math.min(extra, 200)
item.name = '\u0000'.repeat(escaped) + 'x'.repeat(200 - escaped)
extra -= escaped
}
const raw = JSON.stringify(items)
assert.ok(raw.length <= limit && raw.length > limit - 5)
const storage = memory(raw)
assert.equal(readItems(storage).length, 999)
assert.throws(() => addItem(storage), /size/)
assert.equal(storage.getItem(STORAGE_KEY), raw)
assert.equal(readItems(storage).length, 999)
})
test('quota refusal during append preserves populated storage byte for byte', () => {
const raw = JSON.stringify([{ id: 7, name: 'Keep', detail: 'Existing data' }])
const storage = memory(raw)
storage.setItem = () => { throw Error('quota') }
assert.throws(() => addItem(storage), /quota/)
assert.equal(storage.getItem(STORAGE_KEY), raw)
})