152 lines
7.2 KiB
JavaScript
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');
|
|
});
|