feat(blog): enable live likes and first-party views

This commit is contained in:
DeCentN2Madness authored and AIAMarketing committed 2026-11-06 10:54:18 -07:00
1 parent 89cdb00765
commit 8c19266760
13 files changed
+631 -30

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
/**
* Browser regression for the built live-counter artifact. Every API request is
* intercepted and fulfilled locally; this script never writes production data.
*/
import assert from 'node:assert/strict';
import http from 'node:http';
import net from 'node:net';
import path from 'node:path';
import { spawn } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { setTimeout as delay } from 'node:timers/promises';
const browserName = process.argv.find((argument) => argument.startsWith('--browser='))?.split('=')[1] ?? 'chromium';
assert.ok(['chromium', 'firefox'].includes(browserName));
const moduleName = process.env.PLAYWRIGHT_MODULE
? pathToFileURL(path.resolve(process.env.PLAYWRIGHT_MODULE)).href
: 'playwright';
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const reserve = net.createServer();
await new Promise((resolve, reject) => { reserve.once('error', reject); reserve.listen(0, '127.0.0.1', resolve); });
const port = reserve.address().port;
await new Promise((resolve) => reserve.close(resolve));
const origin = `http://127.0.0.1:${port}`;
const apiOrigin = 'https://api.azinstitute4autism.com';
const calls = [];
let views = 100;
let likes = 5;
let browser;
let preview;
try {
const drivers = await import(moduleName);
const driver = drivers[browserName];
const executablePath = browserName === 'chromium' ? process.env.CHROMIUM_EXECUTABLE : process.env.FIREFOX_EXECUTABLE;
browser = await driver.launch(executablePath ? { headless: true, executablePath } : { headless: true });
preview = spawn(process.execPath, ['node_modules/astro/bin/astro.mjs', 'preview', '--host', '127.0.0.1', '--port', String(port)], {
cwd: root,
env: { ...process.env, ASTRO_TELEMETRY_DISABLED: '1' },
stdio: 'ignore',
});
const reachable = () => new Promise((resolve) => {
const request = http.get(`${origin}/`, (response) => { response.resume(); resolve(response.statusCode === 200); });
request.setTimeout(500, () => request.destroy());
request.on('error', () => resolve(false));
});
for (let attempt = 0; attempt < 60 && !(await reachable()); attempt += 1) await delay(250);
assert.equal(await reachable(), true, 'Loopback Astro preview did not start');
const context = await browser.newContext();
await context.route(`${apiOrigin}/**`, async (route) => {
const request = route.request();
const url = new URL(request.url());
if (request.method() === 'OPTIONS') {
await route.fulfill({ status: 204, headers: {
'access-control-allow-origin': origin,
'access-control-allow-methods': 'POST, DELETE, OPTIONS',
'access-control-allow-headers': 'content-type',
} });
return;
}
const body = request.postDataJSON?.() ?? undefined;
calls.push({ method: request.method(), path: url.pathname, body });
let response;
if (url.pathname === '/stats/batch') {
response = Object.fromEntries(body.slugs.map((slug) => [slug, { likes, views }]));
} else if (url.pathname === '/views') {
views += 1;
response = { views };
} else if (url.pathname === '/likes' && request.method() === 'POST') {
likes += 1;
response = { count: likes };
} else if (url.pathname.startsWith('/likes/') && request.method() === 'DELETE') {
likes = Math.max(0, likes - 1);
response = { count: likes };
} else {
await route.fulfill({ status: 404, body: '{}' });
return;
}
await route.fulfill({
status: 200,
contentType: 'application/json',
headers: { 'access-control-allow-origin': origin },
body: JSON.stringify(response),
});
});
const page = await context.newPage();
const article = '/library/aba-school-readiness-guide';
await page.goto(`${origin}${article}`);
await page.locator('[data-counter][data-track-view="true"] [data-views]').waitFor();
await assert.doesNotReject(async () => page.waitForFunction(() =>
document.querySelector('[data-counter][data-track-view="true"] [data-views]')?.textContent === '101'));
assert.equal(calls.filter((call) => call.path === '/views').length, 1);
assert.deepEqual(calls.find((call) => call.path === '/views').body, { slug: 'aba-school-readiness-guide' });
await page.locator('[data-counter][data-track-view="true"] [data-like]').click();
await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-like]')?.getAttribute('aria-pressed') === 'true');
assert.equal(calls.filter((call) => call.path === '/likes' && call.method === 'POST').length, 1);
assert.equal(await page.evaluate(() => localStorage.length), 1);
assert.deepEqual(await page.evaluate(() => Object.keys(localStorage)), ['aia-liked-articles-v1']);
await page.locator('[data-counter][data-track-view="true"] [data-like]').click();
await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-like]')?.getAttribute('aria-pressed') === 'false');
assert.equal(calls.filter((call) => call.method === 'DELETE').length, 1);
await page.reload();
await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"] [data-views]')?.textContent === '102');
assert.equal(calls.filter((call) => call.path === '/views').length, 2, 'a real reload is a new view');
const beforeListing = calls.filter((call) => call.path === '/views').length;
await page.goto(`${origin}/library`);
await page.waitForFunction(() => document.querySelector('[data-counter] [data-views]')?.textContent !== '—');
assert.equal(calls.filter((call) => call.path === '/views').length, beforeListing, 'listing cards never report views');
for (const route of [
'/es/library/executive-functioning-skills-autism',
'/ar/library/executive-functioning-skills-autism',
]) {
await page.goto(`${origin}${route}`);
await page.waitForFunction(() => document.querySelector('[data-counter][data-track-view="true"]')?.dataset.viewReported === 'true');
}
const translated = calls.filter((call) => call.path === '/views').slice(-2);
assert.deepEqual(translated.map((call) => call.body.slug), [
'executive-functioning-skills-autism',
'executive-functioning-skills-autism',
]);
console.log(JSON.stringify({ browser: browserName, version: browser.version(), checks: 18, productionRequests: 0 }));
await context.close();
} finally {
if (preview) preview.kill('SIGTERM');
if (browser) await browser.close();
}
+209
View File
@@ -0,0 +1,209 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import path from 'node:path';
import test from 'node:test';
import fg from 'fast-glob';
import matter from 'gray-matter';
import {
bindLiveCounters,
fetchCounterStats,
readLikedSlugs,
reportArticleView,
updateLike,
} from '../src/scripts/likes-views.ts';
class FakeClassList {
values = new Set();
toggle(name, enabled) { enabled ? this.values.add(name) : this.values.delete(name); }
}
class FakeButton {
attributes = new Map([['aria-pressed', 'false']]);
classList = new FakeClassList();
disabled = true;
listeners = [];
setAttribute(name, value) { this.attributes.set(name, value); }
getAttribute(name) { return this.attributes.get(name); }
addEventListener(name, callback) { if (name === 'click') this.listeners.push(callback); }
async click() { for (const callback of this.listeners) await callback(); }
}
class FakeCounter {
constructor(slug, { trackView = false, lang = 'en' } = {}) {
this.dataset = {
slug,
counterMode: 'live',
apiBase: 'https://api.example.test',
trackView: String(trackView),
likeLabel: lang === 'ar' ? 'إعجاب' : 'Like',
unlikeLabel: lang === 'ar' ? 'إزالة الإعجاب' : 'Unlike',
readyMessage: '',
errorMessage: lang === 'ar' ? 'خطأ مؤقت' : 'Temporarily unavailable',
};
this.views = { textContent: '—' };
this.likes = { textContent: '—' };
this.status = { textContent: '' };
this.button = new FakeButton();
}
querySelector(selector) {
return selector === '[data-views]' ? this.views
: selector === '[data-likes]' ? this.likes
: selector === '[data-like]' ? this.button
: selector === '.counter-status' ? this.status : null;
}
}
class FakeRoot {
constructor(counters) { this.counters = counters; }
querySelectorAll(selector) {
return selector === '[data-counter][data-counter-mode="live"]' ? this.counters : [];
}
}
class FakeStorage {
values = new Map();
getItem(key) { return this.values.get(key) ?? null; }
setItem(key, value) { this.values.set(key, value); }
}
function response(body, status = 200) {
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
}
test('counter transport batches unique slugs and never sends credentials', async () => {
const calls = [];
const fetcher = async (url, options) => {
calls.push({ url, options });
return response({ article: { likes: 3, views: 9 } });
};
const result = await fetchCounterStats('https://api.example.test/', ['article', 'article'], fetcher);
assert.equal(result.article.views, 9);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, 'https://api.example.test/stats/batch');
assert.equal(calls[0].options.credentials, 'omit');
assert.equal(calls[0].options.referrerPolicy, 'no-referrer');
assert.deepEqual(JSON.parse(calls[0].options.body), { slugs: ['article'] });
});
test('article views and likes use the existing API contracts', async () => {
const calls = [];
const fetcher = async (url, options) => {
calls.push({ url, options });
return response(url.endsWith('/views') ? { views: 42 } : { count: 8 });
};
assert.equal(await reportArticleView('https://api.example.test', 'article', fetcher), 42);
assert.equal(await updateLike('https://api.example.test', 'article', false, fetcher), 8);
assert.equal(await updateLike('https://api.example.test', 'article', true, fetcher), 8);
assert.deepEqual(JSON.parse(calls[0].options.body), { slug: 'article' });
assert.equal(calls[0].options.keepalive, true);
assert.equal(calls[1].options.method, 'POST');
assert.equal(calls[2].options.method, 'DELETE');
assert.equal(calls[2].url, 'https://api.example.test/likes/article');
});
test('one article navigation increments once while cards only retrieve statistics', async () => {
const article = new FakeCounter('shared-slug', { trackView: true });
const duplicate = new FakeCounter('shared-slug');
const card = new FakeCounter('other-slug');
const root = new FakeRoot([article, duplicate, card]);
const storage = new FakeStorage();
const calls = [];
const fetcher = async (url, options) => {
calls.push({ url, options });
if (url.endsWith('/stats/batch')) return response({
'shared-slug': { likes: 4, views: 20 },
'other-slug': { likes: 1, views: 2 },
});
if (url.endsWith('/views')) return response({ views: 21 });
if (options.method === 'POST') return response({ count: 5 });
return response({ count: 4 });
};
await bindLiveCounters(root, fetcher, storage);
await bindLiveCounters(root, fetcher, storage);
assert.equal(calls.filter((call) => call.url.endsWith('/stats/batch')).length, 1);
assert.equal(calls.filter((call) => call.url.endsWith('/views')).length, 1);
assert.equal(article.views.textContent, '21');
assert.equal(duplicate.views.textContent, '21');
assert.equal(card.views.textContent, '2');
await article.button.click();
assert.equal(article.likes.textContent, '5');
assert.equal(duplicate.likes.textContent, '5');
assert.equal(article.button.getAttribute('aria-pressed'), 'true');
assert.deepEqual([...readLikedSlugs(storage)], ['shared-slug']);
await duplicate.button.click();
assert.equal(article.likes.textContent, '4');
assert.equal(duplicate.button.getAttribute('aria-pressed'), 'false');
assert.deepEqual([...readLikedSlugs(storage)], []);
});
test('API failures preserve rendering and expose localized inline status', async () => {
const counter = new FakeCounter('article', { trackView: true, lang: 'ar' });
const root = new FakeRoot([counter]);
await bindLiveCounters(root, async () => response({ error: 'offline' }, 503), new FakeStorage());
assert.equal(counter.status.textContent, 'خطأ مؤقت');
assert.equal(counter.button.disabled, false);
assert.equal(counter.dataset.viewReported, 'true');
});
test('like persistence rejects corrupt and non-array storage values', () => {
const storage = new FakeStorage();
storage.setItem('aia-liked-articles-v1', '{broken');
assert.deepEqual([...readLikedSlugs(storage)], []);
storage.setItem('aia-liked-articles-v1', JSON.stringify(['valid', 4, null]));
assert.deepEqual([...readLikedSlugs(storage)], ['valid']);
});
test('blocked local storage does not duplicate or disable a successful like', async () => {
const counter = new FakeCounter('article');
const root = new FakeRoot([counter]);
const storage = {
getItem() { throw new Error('blocked'); },
setItem() { throw new Error('blocked'); },
};
let writes = 0;
const fetcher = async (url) => {
if (url.endsWith('/stats/batch')) return response({ article: { likes: 2, views: 3 } });
writes += 1;
return response({ count: 3 });
};
await bindLiveCounters(root, fetcher, storage);
await counter.button.click();
assert.equal(writes, 1);
assert.equal(counter.likes.textContent, '3');
assert.equal(counter.button.getAttribute('aria-pressed'), 'true');
assert.equal(counter.button.disabled, false);
});
test('counter requests time out without retrying an ambiguous operation', async () => {
let calls = 0;
const hanging = (_url, options) => {
calls += 1;
return new Promise((_resolve, reject) => options.signal.addEventListener('abort', () => reject(options.signal.reason)));
};
await assert.rejects(fetchCounterStats('https://api.example.test', ['article'], hanging, 5));
assert.equal(calls, 1);
});
test('only article layouts track views and translated publications retain exact legacy slugs', async () => {
const layout = await fs.readFile('src/layouts/BlogPostLayout.astro', 'utf8');
const card = await fs.readFile('src/components/BlogCard.astro', 'utf8');
assert.match(layout, /LikeViewCounter[^>]*trackView/);
assert.doesNotMatch(card, /trackView/);
const groups = new Map();
for (const file of await fg('src/content/blog/**/*.{md,mdx}')) {
const frontmatter = matter(await fs.readFile(file, 'utf8')).data;
if (!frontmatter.translationKey || frontmatter.draft) continue;
const entries = groups.get(frontmatter.translationKey) ?? [];
entries.push({ lang: frontmatter.lang, slug: frontmatter.slug, file: path.basename(file) });
groups.set(frontmatter.translationKey, entries);
}
const multilingual = [...groups.values()].filter((entries) => entries.length > 1);
assert.ok(multilingual.some((entries) => new Set(entries.map(({ lang }) => lang)).size === 3));
for (const entries of multilingual) {
assert.equal(new Set(entries.map(({ slug }) => slug)).size, 1, JSON.stringify(entries));
}
});
+3 -3
View File
@@ -142,7 +142,7 @@ function validationEnvironment() {
async function executeBuild(buildEnv) {
const testEnv = validationEnvironment();
run('npm', ['run', 'check'], { env: testEnv });
run('node', ['--experimental-strip-types', '--test', 'tools/jotform-form-a.test.mjs', 'tools/service-portal.test.mjs', 'tools/phase10-artifact.test.mjs'], { env: testEnv });
run('node', ['--experimental-strip-types', '--test', 'tools/jotform-form-a.test.mjs', 'tools/service-portal.test.mjs', 'tools/phase10-artifact.test.mjs', 'tools/counters.test.mjs'], { env: testEnv });
run('npm', ['run', 'test:publication'], { env: testEnv });
run('npm', ['run', 'audit:links'], { env: testEnv });
run('npm', ['run', 'audit:images'], { env: testEnv });
@@ -187,10 +187,10 @@ async function main() {
} else portal = await syntheticPayload(temporary);
if (!options.dryRun && portal.synthetic) fail('A real deployment requires AIA_PORTAL_PRODUCTION_CIPHERTEXT_FILE');
if (!options.dryRun && !options.confirm) fail('Refusing to upload without --confirm; use --dry-run for local verification');
const env = { ...process.env, PUBLIC_ALLOW_INDEXING: 'true', PUBLIC_AIA_FORMS_MODE: 'live', PUBLIC_COUNTERS_MODE: 'off', AIA_PORTAL_PRODUCTION_APPROVED: 'true', AIA_PORTAL_PRODUCTION_CIPHERTEXT_FILE: portal.file, AIA_PORTAL_LOCAL_PREVIEW_APPROVED: 'false' };
const env = { ...process.env, PUBLIC_ALLOW_INDEXING: 'true', PUBLIC_AIA_FORMS_MODE: 'live', PUBLIC_COUNTERS_MODE: 'live', PUBLIC_AIA_API_BASE: 'https://api.azinstitute4autism.com', PUBLIC_COUNTERS_LIVE_APPROVED: 'true', AIA_PORTAL_PRODUCTION_APPROVED: 'true', AIA_PORTAL_PRODUCTION_CIPHERTEXT_FILE: portal.file, AIA_PORTAL_LOCAL_PREVIEW_APPROVED: 'false' };
await executeBuild(env);
const portalSha256 = sha256(Buffer.from(JSON.stringify(portal.payload)));
const artifact = await verifyArtifact(path.join(toolRoot, 'dist'), 'indexing', 'live', { mode: portal.synthetic ? 'synthetic' : 'production', sha256: portal.synthetic ? undefined : portalSha256 });
const artifact = await verifyArtifact(path.join(toolRoot, 'dist'), 'indexing', 'live', { mode: portal.synthetic ? 'synthetic' : 'production', sha256: portal.synthetic ? undefined : portalSha256 }, 'live');
if (!artifact.passed) fail(`Artifact verification failed: ${artifact.errors.join('; ')}`);
const manifest = await releaseManifest(path.join(toolRoot, 'dist'), sha);
const manifestFile = path.join(temporary, `aia-site-${sha}.manifest.json`);
+27 -5
View File
@@ -85,20 +85,25 @@ function formAAdapters(html) {
return [...html.matchAll(/<section\b[^>]*\bdata-jotform-form-a\b[^>]*>/gi)].map((match) => match[0]);
}
function unsafeSignals(html, allowApprovedFormA = false) {
function unsafeSignals(html, allowApprovedFormA = false, allowApprovedCounters = false) {
const signals = [];
if (!allowApprovedFormA && /<iframe\b[^>]*\bsrc=["'][^"']*jotform/i.test(html)) signals.push('active Jotform iframe');
if (!allowApprovedFormA && /262946078314059/i.test(html)) signals.push('disabled Form A identifier');
if (/<script\b[^>]*\bsrc=["'][^"']*(?:jotform|googletagmanager|google-analytics|doubleclick)/i.test(html)) signals.push('active vendor tracking/form script');
if (!allowApprovedFormA && /data-(?:form|forms)-mode=["']live["']/i.test(html)) signals.push('live form mode');
if (/api\.azinstitute4autism\.com/i.test(html)) signals.push('production API origin');
const counterReviewed = allowApprovedCounters
? html.replace(/<div\b[^>]*\bdata-counter\b[^>]*>/gi, (tag) =>
tag.replace('data-api-base="https://api.azinstitute4autism.com"', ''))
: html;
if (/api\.azinstitute4autism\.com/i.test(counterReviewed)) signals.push('production API origin');
if (/AIza[0-9A-Za-z_-]{20,}|-----BEGIN (?:RSA|OPENSSH|EC) PRIVATE\sKEY-----|(?:api[_-]?key|secret|password)\s*[:=]/i.test(html)) signals.push('credential-like content');
return signals;
}
export async function verifyArtifact(root, mode = 'staging', forms = 'live', portal = { mode: 'none' }) {
export async function verifyArtifact(root, mode = 'staging', forms = 'live', portal = { mode: 'none' }, counters = 'off') {
if (!['staging', 'indexing'].includes(mode)) throw new Error('Artifact mode must be staging or indexing; form availability is selected separately with --forms=live|off|mock.');
if (!['live', 'off', 'mock'].includes(forms)) throw new Error('Artifact forms must be live, off, or mock.');
if (!['live', 'off', 'mock'].includes(counters)) throw new Error('Artifact counters must be live, off, or mock.');
if (!portal || !['none', 'synthetic', 'production'].includes(portal.mode)) throw new Error('Portal artifact mode must be none, synthetic, or production.');
if (portal.mode === 'production' && (!/^[0-9a-f]{64}$/.test(portal.sha256 ?? '') || mode !== 'indexing')) throw new Error('Production portal verification requires indexing mode and its expected payload SHA-256.');
const formAEnabled = forms === 'live';
@@ -115,6 +120,7 @@ export async function verifyArtifact(root, mode = 'staging', forms = 'live', por
let secretSignals = [];
let formAIframes = 0;
let deferredFormAIframes = 0;
let counterInstances = 0;
for (const file of htmlFiles) {
const html = await readText(file);
@@ -127,7 +133,20 @@ export async function verifyArtifact(root, mode = 'staging', forms = 'live', por
const trailingSlashUrls = rootRelativeUrls(html).filter(hasTerminalSlash);
const expectedFormAUrl = formAEnabled ? staticFormAUrls.get(route) : undefined;
const expectedDeferredFormAUrl = formAEnabled ? deferredFormAUrls.get(route) : undefined;
const signals = unsafeSignals(html, formAEnabled);
const signals = unsafeSignals(html, formAEnabled, counters === 'live');
const countersOnPage = [...html.matchAll(/<div\b[^>]*\bdata-counter\b[^>]*>/gi)].map((match) => match[0]);
for (const counter of countersOnPage) {
if (counters === 'live') {
if (!counter.includes('data-counter-mode="live"')
|| !counter.includes('data-api-base="https://api.azinstitute4autism.com"')) {
errors.push(`${route}: live counter lacks the approved mode or API origin`);
}
} else if (counter.includes('data-counter-mode="live"')
|| counter.includes('data-api-base="https://api.azinstitute4autism.com"')) {
errors.push(`${route}: live counter appears in a ${counters} artifact`);
}
}
counterInstances += countersOnPage.length;
const jotformFrames = iframeSources(html).filter((src) => {
try {
return new URL(src).origin === 'https://form.jotform.com';
@@ -212,6 +231,7 @@ export async function verifyArtifact(root, mode = 'staging', forms = 'live', por
if (reviewIndex) errors.push('indexing-enabled artifact contains staging-only /lp review index');
}
if (activeForms) errors.push(`found ${activeForms} non-search native form action(s)`);
if (counters === 'live' && counterInstances === 0) errors.push('live artifact contains no article counters');
if (secretSignals.length) errors.push(`unsafe artifact signals: ${JSON.stringify(secretSignals)}`);
const files = (await fg('**/*', { cwd: root, absolute: true, onlyFiles: true })).sort();
@@ -241,6 +261,7 @@ export async function verifyArtifact(root, mode = 'staging', forms = 'live', por
return {
mode,
forms,
counters,
portal: portal.mode,
htmlRoutes: htmlFiles.length,
sitemapUrls: sitemapRoutes.length,
@@ -264,7 +285,8 @@ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(new URL(im
const forms = process.argv.find((arg) => arg.startsWith('--forms='))?.slice('--forms='.length) ?? 'live';
const portalMode = process.argv.find((arg) => arg.startsWith('--portal='))?.slice('--portal='.length) ?? 'none';
const portalSha256 = process.argv.find((arg) => arg.startsWith('--portal-sha256='))?.slice('--portal-sha256='.length);
const result = await verifyArtifact(path.resolve(root), mode, forms, { mode: portalMode, sha256: portalSha256 });
const counters = process.argv.find((arg) => arg.startsWith('--counters='))?.slice('--counters='.length) ?? 'off';
const result = await verifyArtifact(path.resolve(root), mode, forms, { mode: portalMode, sha256: portalSha256 }, counters);
console.log(JSON.stringify(result, null, 2));
if (!result.passed) process.exitCode = 1;
}
+14
View File
@@ -110,6 +110,19 @@ test('artifact rejects production API origin even when Form A is approved', asyn
assert.ok(result.errors.some((error) => error.includes('unsafe artifact signals')));
});
test('live counters allow only the reviewed API origin and explicit live mode', async () => {
const root = await fixture('indexing');
await fs.appendFile(path.join(root, 'index.html'), '<div data-counter data-counter-mode="live" data-api-base="https://api.azinstitute4autism.com"></div>');
assert.equal((await verifyArtifact(root, 'indexing', 'live', { mode: 'none' }, 'live')).passed, true);
assert.equal((await verifyArtifact(root, 'indexing', 'live', { mode: 'none' }, 'off')).passed, false);
const wrong = await fixture('indexing');
await fs.appendFile(path.join(wrong, 'index.html'), '<div data-counter data-counter-mode="live" data-api-base="https://other.invalid"></div>');
const result = await verifyArtifact(wrong, 'indexing', 'live', { mode: 'none' }, 'live');
assert.equal(result.passed, false);
assert.ok(result.errors.some((error) => error.includes('approved mode or API origin')));
});
test('artifact rejects Form A identity outside the exact approved HTML routes', async () => {
const root = await fixture('staging');
await fs.writeFile(path.join(root, 'assets.js'), 'https://form.jotform.com/262946078314059?formContext=contact');
@@ -182,4 +195,5 @@ test('artifact rejects extra routes, wrong origins and prematurely loaded drawer
test('obsolete artifact modes are rejected with separate publication/form guidance', async () => {
await assert.rejects(verifyArtifact('/unused','isolated-test'), /--forms=live\|off\|mock/);
await assert.rejects(verifyArtifact('/unused','staging','invalid'), /live, off, or mock/);
await assert.rejects(verifyArtifact('/unused','staging','live',{ mode: 'none' },'invalid'), /counters must be live/);
});
+12 -7
View File
@@ -20,7 +20,7 @@ test('counter configuration defaults off and requires explicit approval', () =>
assert.equal(resolveCounterConfig('live', 'https://api.example.test', 'false').mode, 'off');
assert.equal(resolveCounterConfig('live', 'https://api.example.test', 'true').mode, 'live');
assert.equal(resolveCounterConfig('live', 'http://api.example.test', 'true').mode, 'off');
assert.equal(counterModeForRuntime(resolveCounterConfig('live', 'https://api.example.test', 'true')), 'off');
assert.equal(counterModeForRuntime(resolveCounterConfig('live', 'https://api.example.test', 'true')), 'live');
assert.equal(resolveCounterConfig('mock', undefined, undefined).mode, 'mock');
});
@@ -73,10 +73,15 @@ test('inactive source has no native form or live vendor destination', () => {
assert.match(sources, /data-form-mode/);
});
test('counter source has no implicit production fallback or credentialed fetch', () => {
const source = fs.readFileSync('src/components/LikeViewCounter.astro', 'utf8');
assert.doesNotMatch(source, /api\.azinstitute4autism\.com/);
assert.doesNotMatch(source, /credentials:\s*['"]include/);
assert.doesNotMatch(source, /method:\s*['"](?:POST|DELETE)/);
assert.match(source, /data-counter-mode/);
test('counter source requires explicit live configuration and omits credentials', () => {
const component = fs.readFileSync('src/components/LikeViewCounter.astro', 'utf8');
const runtime = fs.readFileSync('src/scripts/likes-views.ts', 'utf8');
const deploy = fs.readFileSync('tools/deploy.mjs', 'utf8');
assert.doesNotMatch(component, /api\.azinstitute4autism\.com/);
assert.doesNotMatch(runtime, /credentials:\s*['"]include/);
assert.match(runtime, /credentials:\s*['"]omit/);
assert.match(component, /data-counter-mode/);
assert.match(deploy, /PUBLIC_COUNTERS_MODE:\s*'live'/);
assert.match(deploy, /PUBLIC_COUNTERS_LIVE_APPROVED:\s*'true'/);
assert.match(deploy, /PUBLIC_AIA_API_BASE:\s*'https:\/\/api\.azinstitute4autism\.com'/);
});