Files
bitsforfree/tests/store.test.js
T

152 lines
7.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { createStore } from '../src/store.js';
import { validateImage } from '../src/image.js';
import { encodePng } from '../src/png.js';
test('stores memes in dated hash shards and paginates', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-protocol-'));
const store = await createStore({ dataDir: dir });
const buffer = encodePng(16, 16, () => [0, 255, 65, 255]);
const image = validateImage(buffer, {
maxBytes: 5 * 1024 * 1024,
maxWidth: 6000,
maxHeight: 6000,
maxPixels: 20_000_000
});
const meme = await store.save({
buffer,
image,
originalName: 'test.png',
createdAt: '2026-05-08T12:00:00.000Z'
});
assert.equal(store.count(), 1);
assert.equal(store.list({ page: 1, pageSize: 12 }).memes[0].id, meme.id);
assert.equal(store.list({ page: 1, pageSize: 12 }).memes[0].viewCount, 0);
assert.match(store.get(meme.id).storageKey, /^memes\/2026\/05\/08\/[a-f0-9]{2}\/[a-f0-9]{2}\//);
await fs.access(path.join(dir, store.get(meme.id).storageKey));
});
test('increments view and download counters', async () => {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-protocol-'));
const store = await createStore({ dataDir: dir });
const buffer = encodePng(16, 16, () => [0, 255, 65, 255]);
const image = validateImage(buffer, {
maxBytes: 5 * 1024 * 1024,
maxWidth: 6000,
maxHeight: 6000,
maxPixels: 20_000_000
});
const meme = await store.save({
buffer,
image,
originalName: 'test.png',
createdAt: '2026-05-08T12:00:00.000Z'
});
await store.incrementMetric(meme.id, 'viewCount');
await store.incrementMetric(meme.id, 'downloadCount');
const listed = store.list({ page: 1, pageSize: 12 }).memes[0];
assert.equal(listed.viewCount, 1);
assert.equal(listed.downloadCount, 1);
});
async function metadataStore(t) {
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'meme-metadata-'));
t.after(() => fs.rm(dir, { recursive: true, force: true }));
const store = await createStore({ dataDir: dir });
let seed = 0;
async function save(options = {}) {
const color = ++seed;
const buffer = encodePng(8, 8, () => [color, 200, 50, 255]);
const image = validateImage(buffer, { maxBytes: 100_000, maxWidth: 6000, maxHeight: 6000, maxPixels: 20_000_000 });
return store.save({ buffer, image, originalName: 'test.png', ...options });
}
return { dir, store, save };
}
test('persists editable metadata and managed tags, removing assignments on tag deletion', async (t) => {
const { dir, store, save } = await metadataStore(t);
await store.addTag(' Cats ');
await store.addTag('work');
await assert.rejects(store.addTag('CATS'), { statusCode: 409 });
await assert.rejects(store.addTag('<script>'), { statusCode: 400 });
const meme = await save({ caption: 'An angry cat', tags: ['cats'], status: 'pending' });
await assert.rejects(store.updateMetadata(meme.id, { tags: ['invented'] }), { statusCode: 400 });
await assert.rejects(store.updateMetadata(meme.id, { caption: 'x'.repeat(2001) }), { statusCode: 400 });
await assert.rejects(store.updateMetadata(meme.id, { tags: null }), { statusCode: 400 });
await store.updateMetadata(meme.id, { caption: 'A cat sleeping at work', tags: ['WORK', 'cats', 'cats'] });
const reloaded = await createStore({ dataDir: dir });
assert.deepEqual(reloaded.listTags(), ['cats', 'work']);
assert.equal(reloaded.get(meme.id).caption, 'A cat sleeping at work');
assert.deepEqual(reloaded.get(meme.id).tags, ['work', 'cats']);
assert.equal(reloaded.get(meme.id).status, 'pending');
await reloaded.deleteTag('cats');
const finalStore = await createStore({ dataDir: dir });
assert.deepEqual(finalStore.get(meme.id).tags, ['work']);
assert.deepEqual(finalStore.listTags(), ['work']);
const diskRecord = JSON.parse(await fs.readFile(path.join(dir, finalStore.get(meme.id).metaKey), 'utf8'));
assert.deepEqual(diskRecord.tags, ['work']);
});
test('searches captions with relevance, typo tolerance, accents, topic intersections, and approved-only visibility', async (t) => {
const { store, save } = await metadataStore(t);
await store.addTag('cats');
await store.addTag('work');
const older = await save({ caption: 'An angry cat at the office', tags: ['cats', 'work'], createdAt: '2026-01-01T00:00:00Z' });
const newer = await save({ caption: 'An angry catfish sleeping', tags: [], createdAt: '2026-02-01T00:00:00Z' });
const accent = await save({ caption: 'A café worker', tags: ['work'] });
const pending = await save({ caption: 'An angry cat at the office', status: 'pending', tags: ['cats', 'work'] });
const list = (options = {}) => store.list({ page: 1, pageSize: 12, ...options });
assert.deepEqual(list({ q: 'ANGRY cat' }).memes.map((meme) => meme.id), [older.id, newer.id]);
assert.deepEqual(list({ q: 'angry catt' }).memes.map((meme) => meme.id), [older.id]);
assert.equal(list({ q: 'cafe' }).memes[0].id, accent.id);
assert.equal(list({ q: 'ang' }).memes[0].id, newer.id); // Equal relevance uses newest-first order.
assert.deepEqual(list({ tags: ['cats', 'work'] }).memes.map((meme) => meme.id), [older.id]);
assert.equal(list({ q: 'cat banana' }).total, 0);
assert.equal(list({ q: '!!!' }).total, 0);
assert.equal(list({ tags: ['unknown'] }).total, 0);
assert.equal(list({ q: older.id }).total, 0);
assert.ok(!list().memes.some((meme) => meme.id === pending.id));
assert.equal(list({ page: 99, pageSize: 1 }).page, 3);
const missing = await save();
assert.deepEqual(store.listForAdmin({ missingCaption: true }).memes.map((meme) => meme.id), [missing.id]);
assert.equal(store.listForAdmin({ status: 'all' }).total, 5);
assert.equal(list().memes.some((meme) => meme.id === newer.id), true);
});
test('prevents AI generation from overwriting concurrent edits and serializes writes', async (t) => {
const { dir, store, save } = await metadataStore(t);
const meme = await save({ caption: 'Original' });
const version = store.get(meme.id).metadataVersion;
await Promise.all([
store.updateMetadata(meme.id, { caption: 'Admin correction' }),
store.incrementMetric(meme.id, 'viewCount')
]);
await assert.rejects(store.updateMetadata(meme.id, { caption: 'AI result' }, { expectedVersion: version }), { statusCode: 409 });
const reloaded = await createStore({ dataDir: dir });
assert.equal(reloaded.get(meme.id).caption, 'Admin correction');
assert.equal(reloaded.get(meme.id).viewCount, 1);
assert.equal('metadataVersion' in reloaded.list({ page: 1, pageSize: 12 }).memes[0], false);
});
test('loads legacy images with empty metadata without changing their status', async (t) => {
const { dir, store, save } = await metadataStore(t);
const meme = await save();
const record = { ...store.get(meme.id) };
delete record.caption;
delete record.tags;
delete record.metadataVersion;
await fs.writeFile(path.join(dir, 'index', 'memes.jsonl'), `${JSON.stringify(record)}\n`);
const reloaded = await createStore({ dataDir: dir });
const result = reloaded.list({ page: 1, pageSize: 12 }).memes[0];
assert.equal(result.caption, '');
assert.deepEqual(result.tags, []);
assert.equal(result.status, 'approved');
});