103 lines
4.1 KiB
JavaScript
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)
|
|
})
|