← back to Rentv 2026
test(adslots): cover ad-system admin write paths (video/placement/site-template)
423cfa3d65f663a99b4023746ec5570a3f674720 · 2026-08-08 08:37:30 -0700 · Steve Abrams
Completes src/ad-system.cjs coverage — admin state mutations (uid/id 400 guards,
status-enum validation, Number coercion, persistence to ad-state.json), reservations
read (reversed + empty-safe), and the site-template pick (enum guard + persistence +
read-back + the 'going live is separately gated' note). No production code changed.
Suite 84->93 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Files touched
A test/adslots/admin.test.mjs
Diff
commit 423cfa3d65f663a99b4023746ec5570a3f674720
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sat Aug 8 08:37:30 2026 -0700
test(adslots): cover ad-system admin write paths (video/placement/site-template)
Completes src/ad-system.cjs coverage — admin state mutations (uid/id 400 guards,
status-enum validation, Number coercion, persistence to ad-state.json), reservations
read (reversed + empty-safe), and the site-template pick (enum guard + persistence +
read-back + the 'going live is separately gated' note). No production code changed.
Suite 84->93 green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---
test/adslots/admin.test.mjs | 111 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 111 insertions(+)
diff --git a/test/adslots/admin.test.mjs b/test/adslots/admin.test.mjs
new file mode 100644
index 00000000..e20ecad4
--- /dev/null
+++ b/test/adslots/admin.test.mjs
@@ -0,0 +1,111 @@
+// ─────────────────────────────────────────────────────────────────────────────
+// test/adslots/admin.test.mjs — the admin WRITE paths of src/ad-system.cjs
+// (video/placement state mutation, reservations read, site-template pick) that
+// cycles 1-2 didn't cover. Completes ad-system.cjs coverage. yoloforever TK-10364
+// cycle 3. No production code changes — pure test coverage.
+//
+// The harness bypasses the adminOnly/pickGate middleware (registered before each
+// handler) to unit-test the handler logic itself; gate wiring is a server.js concern
+// out of this module's scope.
+// ─────────────────────────────────────────────────────────────────────────────
+import { test } from 'node:test';
+import assert from 'node:assert/strict';
+import { createRequire } from 'node:module';
+import fs from 'node:fs';
+import os from 'node:os';
+import path from 'node:path';
+
+const require = createRequire(import.meta.url);
+const mountAdSystem = require('../../src/ad-system.cjs');
+
+function mount(DATA) {
+ const routes = {};
+ const reg = m => (p, ...h) => { routes[`${m} ${p}`] = h[h.length - 1]; }; // last handler = real one
+ const app = { get: reg('GET'), post: reg('POST'), put: reg('PUT'), use() {} };
+ mountAdSystem(app, { DATA, PUB: path.join(DATA, '__nopub__'), adminOnly: (_q, _r, n) => n && n() });
+ return routes;
+}
+function hit(routes, key, { query = {}, body = {}, params = {} } = {}) {
+ const req = { query, body, params, headers: {}, ip: '127.0.0.1' };
+ let out = { status: 200, body: null };
+ const res = { set() { return this; }, status(c) { out.status = c; return this; }, json(o) { out.body = o; return this; } };
+ routes[key](req, res);
+ return out;
+}
+const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'adadmin-'));
+const readState = DATA => JSON.parse(fs.readFileSync(path.join(DATA, 'ad-state.json'), 'utf8'));
+
+// ── admin/video: validation + persisted mutation ────────────────────────────
+test('admin/video: missing uid → 400', () => {
+ assert.equal(hit(mount(tmp()), 'POST /api/ad/admin/video', { body: {} }).status, 400);
+});
+
+test('admin/video: sets status/price/advertiser and persists to ad-state.json', () => {
+ const DATA = tmp();
+ const { status, body } = hit(mount(DATA), 'POST /api/ad/admin/video', {
+ body: { uid: 'yt-a1', status: 'sold', price_monthly: '1500', advertiser: 'ACME' },
+ });
+ assert.equal(status, 200);
+ assert.equal(body.state.status, 'sold');
+ assert.equal(body.state.price_monthly, 1500, 'coerced to Number');
+ assert.equal(body.state.advertiser, 'ACME');
+ assert.ok(body.state.updated_at, 'stamps updated_at');
+ assert.equal(readState(DATA).videos['yt-a1'].status, 'sold', 'persisted to disk');
+});
+
+test('admin/video: an invalid status is ignored, not written', () => {
+ const DATA = tmp();
+ hit(mount(DATA), 'POST /api/ad/admin/video', { body: { uid: 'yt-a1', status: 'sold' } });
+ const { body } = hit(mount(DATA), 'POST /api/ad/admin/video', { body: { uid: 'yt-a1', status: 'bogus' } });
+ assert.equal(body.state.status, 'sold', 'bogus status leaves prior value intact');
+});
+
+// ── admin/placement: validation + creative persistence ──────────────────────
+test('admin/placement: missing id → 400', () => {
+ assert.equal(hit(mount(tmp()), 'POST /api/ad/admin/placement', { body: {} }).status, 400);
+});
+
+test('admin/placement: sets creative_url/status and persists to creatives', () => {
+ const DATA = tmp();
+ const { status, body } = hit(mount(DATA), 'POST /api/ad/admin/placement', {
+ body: { id: 'home-leaderboard', status: 'sold', creative_url: '/c.png', click_url: 'https://x.example' },
+ });
+ assert.equal(status, 200);
+ assert.equal(body.state.creative_url, '/c.png');
+ assert.equal(body.state.status, 'sold');
+ assert.equal(readState(DATA).creatives['home-leaderboard'].click_url, 'https://x.example');
+});
+
+// ── admin/reservations: reads the append-only jsonl, newest first ────────────
+test('admin/reservations: returns appended rows reversed with a count', () => {
+ const DATA = tmp();
+ fs.writeFileSync(path.join(DATA, 'ad-reservations.jsonl'),
+ JSON.stringify({ email: 'a@x.com', target_id: 't1' }) + '\n' +
+ JSON.stringify({ email: 'b@x.com', target_id: 't2' }) + '\n');
+ const { status, body } = hit(mount(DATA), 'GET /api/ad/admin/reservations');
+ assert.equal(status, 200);
+ assert.equal(body.count, 2);
+ assert.equal(body.items[0].email, 'b@x.com', 'newest first');
+});
+
+test('admin/reservations: no file → empty list, never throws', () => {
+ const { status, body } = hit(mount(tmp()), 'GET /api/ad/admin/reservations');
+ assert.equal(status, 200);
+ assert.equal(body.count, 0);
+ assert.deepEqual(body.items, []);
+});
+
+// ── site-template pick: enum validation + persistence + read-back ────────────
+test('site-template: rejects a chosen value outside classic|a..e with 400', () => {
+ assert.equal(hit(mount(tmp()), 'POST /api/ad/admin/site-template', { body: { chosen: 'z' } }).status, 400);
+});
+
+test('site-template: valid pick persists and is read back by GET', () => {
+ const DATA = tmp();
+ const post = hit(mount(DATA), 'POST /api/ad/admin/site-template', { body: { chosen: 'C' } });
+ assert.equal(post.status, 200);
+ assert.equal(post.body.chosen, 'c', 'lowercased');
+ assert.match(post.body.note, /separate.*gated deploy/i, 'flags that going live is separately gated');
+ const get = hit(mount(DATA), 'GET /api/ad/site-template');
+ assert.equal(get.body.chosen, 'c', 'persisted + read back');
+});
← 9af4fe93 chore: sync package-lock version to 0.24.0 (trailing the v0.
·
back to Rentv 2026
·
test(adslots): +124 coverage (buildInventory sources/dedup, f709ec78 →