openvibers/openvibe.community Commits
Tests: SSR, /api/pastes proxy, OAuth client and SEO against mocked Live and Network
Changes
nnew file mode 100644@@@ -0,0 +1,112 @@++'use strict';++/** The OAuth client: login (incl. silent), callback, logout, me, refresh, and next sanitising. */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++ const cookieNamed = (r, name) => r.setCookies.find((c) => c.startsWith(`${name}=`));++++ await check('GET /auth/login builds the Network authorize URL and sets the state cookie', async () => {++ const r = await t.get('/auth/login?next=/pastes%3Fsort%3Dviews');++ assert.strictEqual(r.status, 302);++ const u = new URL(r.headers.get('location'));++ assert.strictEqual(u.origin, t.network.url);++ assert.strictEqual(u.pathname, '/oauth/authorize');++ assert.strictEqual(u.searchParams.get('client_id'), 'community');++ assert.strictEqual(u.searchParams.get('redirect_uri'), 'https://openvibe.community/auth/callback');++ assert.strictEqual(u.searchParams.get('response_type'), 'code');++ assert.strictEqual(u.searchParams.get('scope'), 'profile theme');++ assert.strictEqual(u.searchParams.get('prompt'), null);++ assert.ok(/^[0-9a-f]{32}$/.test(u.searchParams.get('state')));++ const state = cookieNamed(r, 'ov_oauth_state');++ assert.ok(state && /HttpOnly/i.test(state) && /Path=\/auth/.test(state) && /Secure/.test(state) && !/Domain=/i.test(state), 'state cookie: httpOnly, /auth, secure, host-only');++ assert.ok(state.includes(u.searchParams.get('state')));++ assert.strictEqual(decodeURIComponent(cookieNamed(r, 'ov_oauth_next').split(';')[0].split('=').slice(1).join('=')), '/pastes?sort=views');++ });++++ await check('silent=1 adds prompt=none; login_required bounces back to next + ?sso=none', async () => {++ const r = await t.get('/auth/login?silent=1&next=/p/amber-fox-42');++ const u = new URL(r.headers.get('location'));++ assert.strictEqual(u.searchParams.get('prompt'), 'none');++ assert.ok(cookieNamed(r, 'ov_oauth_silent'));++ const back = await t.get(`/auth/callback?error=login_required&state=${u.searchParams.get('state')}`);++ assert.strictEqual(back.status, 302);++ assert.strictEqual(back.headers.get('location'), '/p/amber-fox-42?sso=none');++ assert.ok(!t.jar.has('ov_token'), 'no session was created');++ });++++ await check('?sso=none is appended correctly to a next that already has a query or a Network URL', async () => {++ const { withParam, sanitizeNext } = require('../server/auth/routes');++ const cfg = require('../server/config');++ assert.strictEqual(withParam('/pastes?sort=views', 'sso', 'none'), '/pastes?sort=views&sso=none');++ assert.strictEqual(withParam('/pastes#x', 'sso', 'none'), '/pastes?sso=none#x');++ assert.strictEqual(sanitizeNext('/my', cfg), '/my');++ assert.strictEqual(sanitizeNext('//evil.example/x', cfg), '/');++ assert.strictEqual(sanitizeNext('https://evil.example/x', cfg), '/');++ assert.strictEqual(sanitizeNext('https://openvibe.community/pastes', cfg), 'https://openvibe.community/pastes');++ assert.strictEqual(sanitizeNext(`${t.network.url}/sso/next?step=2`.replace(/^http:/, 'https:'), cfg), `${t.network.url}/sso/next?step=2`.replace(/^http:/, 'https:'));++ assert.strictEqual(sanitizeNext('javascript:alert(1)', cfg), '/');++ });++++ await check('callback exchanges the code, sets host-only ov_token (JS-readable) + ov_refresh + ov_sso_hint=account, redirects to next', async () => {++ const login = await t.get('/auth/login?next=/my');++ const state = new URL(login.headers.get('location')).searchParams.get('state');++ const cb = await t.get(`/auth/callback?code=good-code&state=${state}`);++ assert.strictEqual(cb.status, 302, cb.text);++ assert.strictEqual(cb.headers.get('location'), '/my');++ const tok = cookieNamed(cb, 'ov_token');++ assert.ok(tok && !/HttpOnly/i.test(tok) && /SameSite=Lax/i.test(tok) && /Secure/.test(tok) && !/Domain=/i.test(tok) && /Path=\//.test(tok));++ const ref = cookieNamed(cb, 'ov_refresh');++ assert.ok(ref && /HttpOnly/i.test(ref) && /Path=\/auth/.test(ref));++ const hint = cookieNamed(cb, 'ov_sso_hint');++ assert.ok(hint && hint.startsWith('ov_sso_hint=account') && !/HttpOnly/i.test(hint) && /Max-Age=31536000/.test(hint));++ const grant = t.network.grants.pop();++ assert.strictEqual(grant.grant_type, 'authorization_code');++ assert.strictEqual(grant.client_id, 'community');++ assert.strictEqual(grant.redirect_uri, 'https://openvibe.community/auth/callback');++ });++++ await check('/auth/me verifies the cookie offline via the Network JWKS', async () => {++ const me = await t.get('/auth/me');++ assert.strictEqual(me.status, 200);++ const body = me.json();++ assert.strictEqual(body.user.username, 'alex');++ assert.strictEqual(body.user.exp, undefined);++ assert.ok(body.expires_at > Date.now());++ const bad = await t.get('/auth/me', { cookies: ['ov_token=garbage'], headers: { authorization: 'Bearer garbage' } });++ assert.strictEqual(bad.status, 401);++ });++++ await check('callback rejects a missing or mismatched state', async () => {++ const r = await t.get('/auth/callback?code=good-code&state=deadbeef');++ assert.strictEqual(r.status, 400);++ });++++ await check('POST /auth/refresh rotates tokens; a rejected refresh clears the session', async () => {++ t.jar.set('ov_refresh', 'refresh-1');++ const ok = await t.get('/auth/refresh', { method: 'POST' });++ assert.strictEqual(ok.status, 200);++ assert.strictEqual(ok.json().user.username, 'alex');++ assert.strictEqual(t.jar.get('ov_refresh'), 'refresh-2');++ t.jar.set('ov_refresh', 'stale');++ const no = await t.get('/auth/refresh', { method: 'POST' });++ assert.strictEqual(no.status, 401);++ assert.ok(!t.jar.has('ov_token'));++ });++++ await check('logout clears the session, sets ov_sso_hint=guest and honours a Network next', async () => {++ t.jar.set('ov_token', 'x'); t.jar.set('ov_refresh', 'refresh-2');++ const next = `${t.network.url}/logout?everywhere=1`.replace(/^http:/, 'https:');++ const r = await t.get(`/auth/logout?next=${encodeURIComponent(next)}`);++ assert.strictEqual(r.status, 302);++ assert.strictEqual(r.headers.get('location'), next);++ assert.ok(cookieNamed(r, 'ov_sso_hint').startsWith('ov_sso_hint=guest'));++ assert.ok(!t.jar.has('ov_token') && !t.jar.has('ov_refresh'));++ const evil = await t.get('/auth/logout?next=https://evil.example/');++ assert.strictEqual(evil.headers.get('location'), '/');++ });++++ await t.close();++ done();++})();nnew file mode 100644@@@ -0,0 +1,56 @@++'use strict';++/** Boots the mocks, points the app at them, and returns a tiny HTTP client that keeps cookies. */++const http = require('http');++const mockLive = require('./mock-live');++const mockNetwork = require('./mock-network');++++async function boot() {++ const liveSrv = await mockLive.start();++ const netSrv = await mockNetwork.start();++ process.env.NODE_ENV = 'test';++ process.env.BASE_URL = 'https://openvibe.community';++ process.env.OV_LIVE_INTERNAL_URL = liveSrv.url;++ process.env.OV_LIVE_URL = 'https://openvibe.live';++ process.env.OV_MEDIA_URL = 'https://openvibe.media';++ process.env.OV_NETWORK_URL = netSrv.url;++ process.env.OV_NETWORK_INTERNAL_URL = netSrv.url;++ process.env.OV_OAUTH_CLIENT_ID = 'community';++ process.env.OV_OAUTH_CLIENT_SECRET = 'shh';++ process.env.OV_OAUTH_REDIRECT_URI = 'https://openvibe.community/auth/callback';++ process.env.COOKIE_SECURE = 'true';++ process.env.TRUST_PROXY = '1';++ for (const k of Object.keys(require.cache)) if (k.includes('/server/')) delete require.cache[k];++ const { createApp } = require('../../server/app');++ const app = createApp();++ const server = await new Promise((resolve) => { const s = http.createServer(app); s.listen(0, '127.0.0.1', () => resolve(s)); });++ const base = `http://127.0.0.1:${server.address().port}`;++++ const jar = new Map();++ async function get(path, opts = {}) {++ const headers = { ...(opts.headers || {}) };++ const cookies = [...jar.entries()].map(([k, v]) => `${k}=${v}`).concat(opts.cookies || []);++ if (cookies.length) headers.cookie = cookies.join('; ');++ const res = await fetch(base + path, { method: opts.method || 'GET', headers, body: opts.body, redirect: 'manual', duplex: opts.body && typeof opts.body.pipe === 'function' ? 'half' : undefined });++ for (const sc of res.headers.getSetCookie ? res.headers.getSetCookie() : []) {++ const [pair, ...attrs] = sc.split(';');++ const [k, v] = pair.split('=');++ const expired = attrs.some((a) => /max-age=0|expires=thu, 01 jan 1970/i.test(a.trim()));++ if (expired) jar.delete(k.trim()); else jar.set(k.trim(), v);++ }++ const text = await res.text();++ return { status: res.status, headers: res.headers, text, setCookies: res.headers.getSetCookie ? res.headers.getSetCookie() : [], json() { return JSON.parse(text); } };++ }++ return {++ app, base, get, jar, live: liveSrv, network: netSrv,++ close: async () => { await new Promise((r) => server.close(r)); await liveSrv.close(); await netSrv.close(); },++ };++}++++let failures = 0;++async function check(name, fn) {++ try { await fn(); console.log(' ✓', name); }++ catch (e) { failures++; console.log(' ✗', name, '\n ', (e.stack || String(e)).split('\n').slice(0, 4).join('\n ')); }++}++function done() { console.log(failures ? `\n${failures} failed` : '\nall passed'); process.exit(failures ? 1 : 0); }++++module.exports = { boot, check, done };nnew file mode 100644@@@ -0,0 +1,75 @@++'use strict';++/**++ * A stand-in for OpenVibe.Live's /api/pastes surface: enough of the list / get / by-user /++ * create endpoints to render every page, plus an echo of whatever else arrives so the proxy++ * tests can see exactly what was forwarded.++ */++const http = require('http');++++const FIXTURES = [++ { id: 1, slug: 'amber-fox-42', user_id: 7, username: 'alex', display_name: 'Alex', avatar_url: '/data/avatars/alex.png', type: 'paste', title: 'Hello world in JavaScript', content: 'const greet = (name) => {\n return `Hello, ${name}!`;\n};\nconsole.log(greet("OpenVibe"));\n', language: 'javascript', visibility: 'public', views: 120, likes: 3, created_at: '2026-09-15 10:00:00', updated_at: '2026-09-15 10:00:00' },++ { id: 2, slug: 'blue-lake-17', user_id: 9, username: 'sam', display_name: 'Sam', type: 'paste', title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx', visibility: 'public', views: 900, likes: 0, created_at: '2026-09-14 09:00:00', updated_at: '2026-09-14 09:00:00' },++ { id: 3, slug: 'cold-ridge-88', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python', visibility: 'public', views: 15, likes: 1, created_at: '2026-09-13 08:00:00', updated_at: '2026-09-13 08:00:00' },++ { id: 4, slug: 'dark-owl-55', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'secret notes', content: 'unlisted <script>alert(1)</script>', language: 'text', visibility: 'unlisted', views: 2, likes: 0, created_at: '2026-09-12 08:00:00', updated_at: '2026-09-12 08:00:00' },++ { id: 5, slug: 'fair-moon-23', user_id: 9, username: 'sam', display_name: 'Sam', type: 'screenshot', title: 'Desktop shot', content: 'my desktop', language: 'text', visibility: 'public', screenshot_url: '/p/fair-moon-23/screenshot', views: 40, likes: 0, created_at: '2026-09-11 08:00:00', updated_at: '2026-09-11 08:00:00' },++ { id: 6, slug: 'grim-vale-61', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'private thing', content: 'private', language: 'text', visibility: 'private', views: 0, likes: 0, created_at: '2026-09-10 08:00:00', updated_at: '2026-09-10 08:00:00' },++];++++function start() {++ const calls = [];++ const server = http.createServer((req, res) => {++ const chunks = [];++ req.on('data', (c) => chunks.push(c));++ req.on('end', () => {++ const body = Buffer.concat(chunks);++ const url = new URL(req.url, 'http://live');++ const call = { method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams), headers: req.headers, body };++ calls.push(call);++ const auth = req.headers.authorization || '';++ const signedIn = auth === 'Bearer good-token' || auth.startsWith('Bearer eyJ');++ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };++ const publicOnly = FIXTURES.filter((p) => p.visibility === 'public');++++ if (req.method === 'GET' && url.pathname === '/api/pastes') {++ let rows = publicOnly;++ if (url.searchParams.get('type')) rows = rows.filter((p) => p.type === url.searchParams.get('type'));++ const q = url.searchParams.get('search');++ if (q) rows = rows.filter((p) => (p.title + p.content).toLowerCase().includes(q.toLowerCase()));++ const limit = Number(url.searchParams.get('limit') || 50), offset = Number(url.searchParams.get('offset') || 0);++ return json(200, { pastes: rows.slice(offset, offset + limit).map((p) => ({ ...p, content: p.type === 'paste' ? p.content.slice(0, 300) : null })), total: rows.length, limit, offset });++ }++ let m;++ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/pastes\/by-user\/([^/]+)$/))) {++ const username = decodeURIComponent(m[1]);++ if (!FIXTURES.some((p) => p.username === username)) return json(404, { error: 'User not found' });++ const rows = FIXTURES.filter((p) => p.username === username && (signedIn || p.visibility === 'public'));++ return json(200, { pastes: rows, total: rows.length, username });++ }++ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/pastes\/([^/]+)$/))) {++ const p = FIXTURES.find((x) => x.slug === decodeURIComponent(m[1]));++ if (!p || (p.visibility === 'private' && !signedIn)) return json(404, { error: 'Paste not found' });++ if (p.slug === 'burned') return json(410, { error: 'burned' });++ return json(200, { paste: { ...p, views: p.views + (url.searchParams.get('no_view') === '1' ? 0 : 1) } });++ }++ if (req.method === 'POST' && url.pathname === '/api/pastes') {++ let parsed = {}; try { parsed = JSON.parse(body.toString('utf8')); } catch { /* */ }++ if (!parsed.content) return json(400, { error: 'Content is required' });++ if (parsed.title === 'slow down') return json(429, { error: 'Please wait 30s', cooldown: 30 });++ return json(201, { id: 99, slug: 'new-paste-99', url: '/p/new-paste-99', paste: { slug: 'new-paste-99', ...parsed, user_id: signedIn ? 7 : null } });++ }++ if (req.method === 'POST' && url.pathname === '/api/pastes/screenshot') {++ return json(201, { paste: { slug: 'shot-100' }, url: '/p/shot-100', received: body.length, contentType: req.headers['content-type'] || null });++ }++ // Everything else: echo, so proxy tests can inspect the forwarded request.++ json(200, { echo: true, method: req.method, path: url.pathname, query: call.query, headers: req.headers, body: body.toString('utf8') });++ });++ });++ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve({++ url: `http://127.0.0.1:${server.address().port}`,++ calls,++ fixtures: FIXTURES,++ close: () => new Promise((r) => server.close(r)),++ })));++}++++module.exports = { start, FIXTURES };nnew file mode 100644@@@ -0,0 +1,41 @@++'use strict';++/** A stand-in for OpenVibe.Network: JWKS + the token endpoint, with a real RS256 key pair. */++const http = require('http');++const crypto = require('crypto');++const jwt = require('jsonwebtoken');++++function start() {++ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });++ const publicPem = publicKey.export({ type: 'spki', format: 'pem' });++ const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });++ const grants = [];++ let issuer = 'http://network.test';++ const server = http.createServer((req, res) => {++ const chunks = [];++ req.on('data', (c) => chunks.push(c));++ req.on('end', () => {++ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };++ if (req.url === '/api/.well-known/jwks') return json(200, { public_key: publicPem, algorithm: 'RS256' });++ if (req.url === '/oauth/token' && req.method === 'POST') {++ let body = {}; try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* */ }++ grants.push(body);++ if (body.client_secret !== 'shh') return json(401, { error: 'invalid_client' });++ if (body.grant_type === 'authorization_code' && body.code !== 'good-code') return json(400, { error: 'invalid_grant' });++ if (body.grant_type === 'refresh_token' && body.refresh_token !== 'refresh-1') return json(400, { error: 'invalid_grant' });++ return json(200, { access_token: sign({ id: 7, username: 'alex', display_name: 'Alex', role: 'user' }), refresh_token: 'refresh-2', token_type: 'Bearer', expires_in: 86400 });++ }++ if (req.url === '/oauth/revoke') return json(200, { ok: true });++ json(404, { error: 'not found' });++ });++ });++ function sign(claims, opts = {}) {++ return jwt.sign({ sub: claims.id, ...claims }, privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h', ...opts });++ }++ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => {++ const url = `http://127.0.0.1:${server.address().port}`;++ issuer = url;++ resolve({ url, grants, sign, publicPem, close: () => new Promise((r) => server.close(r)) });++ }));++}++++module.exports = { start };nnew file mode 100644@@@ -0,0 +1,83 @@++'use strict';++/** /api/pastes/* is a transparent proxy to Live that forwards the visitor's identity and address. */++const assert = require('assert');++const { Readable } = require('stream');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++++ await check('GET forwards path, query, the ov_token cookie as Bearer and the client address', async () => {++ const r = await t.get('/api/pastes/echo/thing?limit=5&x=y', { cookies: ['ov_token=cookie-jwt'], headers: { 'x-forwarded-for': '203.0.113.9' } });++ assert.strictEqual(r.status, 200);++ const e = r.json();++ assert.strictEqual(e.path, '/api/pastes/echo/thing');++ assert.deepStrictEqual(e.query, { limit: '5', x: 'y' });++ assert.strictEqual(e.headers.authorization, 'Bearer cookie-jwt');++ assert.strictEqual(e.headers['x-forwarded-for'], '203.0.113.9');++ assert.strictEqual(e.headers.cookie, undefined, 'cookies never leave this site');++ });++++ await check('Authorization header wins over the cookie; anonymous calls carry no Authorization', async () => {++ const r = await t.get('/api/pastes/echo/x', { cookies: ['ov_token=cookie-jwt'], headers: { authorization: 'Bearer header-jwt' } });++ assert.strictEqual(r.json().headers.authorization, 'Bearer header-jwt');++ const anon = await t.get('/api/pastes/echo/x');++ assert.strictEqual(anon.json().headers.authorization, undefined);++ });++++ await check('JSON POST body reaches Live byte-for-byte with its content-type', async () => {++ const body = JSON.stringify({ title: 't', content: 'hello', language: 'text' });++ const r = await t.get('/api/pastes', { method: 'POST', body, headers: { 'content-type': 'application/json' }, cookies: ['ov_token=good-token'] });++ assert.strictEqual(r.status, 201);++ assert.strictEqual(r.json().slug, 'new-paste-99');++ const call = t.live.calls.filter((c) => c.method === 'POST' && c.path === '/api/pastes').pop();++ assert.strictEqual(call.body.toString(), body);++ assert.strictEqual(call.headers['content-type'], 'application/json');++ assert.strictEqual(call.headers.authorization, 'Bearer good-token');++ });++++ await check('multipart screenshot upload streams through untouched', async () => {++ const boundary = 'xxBOUNDARYxx';++ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3, 255, 254]);++ const body = Buffer.concat([++ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="title"\r\n\r\nShot\r\n--${boundary}\r\nContent-Disposition: form-data; name="screenshot"; filename="a.png"\r\nContent-Type: image/png\r\n\r\n`),++ png, Buffer.from(`\r\n--${boundary}--\r\n`),++ ]);++ const r = await t.get('/api/pastes/screenshot', { method: 'POST', body: Readable.from([body]), headers: { 'content-type': `multipart/form-data; boundary=${boundary}`, 'content-length': String(body.length) } });++ assert.strictEqual(r.status, 201);++ assert.strictEqual(r.json().received, body.length);++ const call = t.live.calls.filter((c) => c.path === '/api/pastes/screenshot').pop();++ assert.ok(call.body.equals(body), 'body identical');++ assert.strictEqual(call.headers['content-type'], `multipart/form-data; boundary=${boundary}`);++ });++++ await check('upstream status codes and error bodies pass through (404, 429)', async () => {++ const nf = await t.get('/api/pastes/does-not-exist');++ assert.strictEqual(nf.status, 404);++ assert.deepStrictEqual(nf.json(), { error: 'Paste not found' });++ const rl = await t.get('/api/pastes', { method: 'POST', body: JSON.stringify({ title: 'slow down', content: 'x' }), headers: { 'content-type': 'application/json' } });++ assert.strictEqual(rl.status, 429);++ assert.strictEqual(rl.json().cooldown, 30);++ });++++ await check('DELETE and the copy/like sub-routes are forwarded with the method intact', async () => {++ const del = await t.get('/api/pastes/amber-fox-42', { method: 'DELETE', cookies: ['ov_token=good-token'] });++ assert.strictEqual(del.json().method, 'DELETE');++ const copy = await t.get('/api/pastes/amber-fox-42/copy', { method: 'POST', body: '{}', headers: { 'content-type': 'application/json' } });++ assert.strictEqual(copy.json().path, '/api/pastes/amber-fox-42/copy');++ });++++ await check('an unreachable Live answers 502 JSON instead of hanging', async () => {++ const { createApp } = require('../server/app');++ const http = require('http');++ const app = createApp({ liveUrl: 'http://127.0.0.1:1' });++ const srv = await new Promise((resolve) => { const s = http.createServer(app); s.listen(0, '127.0.0.1', () => resolve(s)); });++ const res = await fetch(`http://127.0.0.1:${srv.address().port}/api/pastes`);++ assert.strictEqual(res.status, 502);++ assert.deepStrictEqual(await res.json(), { error: 'Could not reach the paste service' });++ await new Promise((r) => srv.close(r));++ });++++ await t.close();++ done();++})();nnew file mode 100644@@@ -0,0 +1,56 @@++#!/usr/bin/env node++/**++ * Runs every test in test/ — the files named *.test.js — each in its own process, and fails++ * if any of them fails. They use in-process mocks of OpenVibe.Live and OpenVibe.Network only;++ * none of them needs the network or a running site.++ *++ * npm test # everything++ * npm test -- seo proxy # only files whose name contains one of the words++ */++'use strict';++const { spawn } = require('child_process');++const fs = require('fs');++const path = require('path');++++const filters = process.argv.slice(2).filter((a) => !a.startsWith('-'));++const files = fs.readdirSync(__dirname)++ .filter((f) => f.endsWith('.test.js'))++ .filter((f) => !filters.length || filters.some((w) => f.includes(w)))++ .sort();++const TIMEOUT_MS = 60000;++++function runOne(file) {++ return new Promise((resolve) => {++ const started = Date.now();++ const child = spawn(process.execPath, [path.join(__dirname, file)], {++ cwd: path.join(__dirname, '..'),++ env: { ...process.env, NODE_ENV: 'test' },++ stdio: ['ignore', 'pipe', 'pipe'],++ });++ let output = '';++ child.stdout.on('data', (c) => { output += c; });++ child.stderr.on('data', (c) => { output += c; });++ const timer = setTimeout(() => { output += `\n[run] timed out after ${TIMEOUT_MS}ms`; child.kill('SIGKILL'); }, TIMEOUT_MS);++ child.on('close', (code, signal) => {++ clearTimeout(timer);++ resolve({ file, ok: code === 0, code: code ?? signal, ms: Date.now() - started, output });++ });++ });++}++++(async () => {++ if (!files.length) { console.error('no test files matched'); process.exit(1); }++ const results = [];++ for (const f of files) {++ const r = await runOne(f);++ results.push(r);++ console.log(`${r.ok ? '✓' : '✗'} ${r.file.padEnd(32)} ${String(r.ms).padStart(6)}ms`);++ }++ const failed = results.filter((r) => !r.ok);++ for (const r of failed) {++ console.log(`\n── ${r.file} (exit ${r.code}) ──`);++ console.log(r.output.split('\n').slice(-60).join('\n'));++ }++ console.log(`\n${results.length - failed.length}/${results.length} test files passed`);++ process.exit(failed.length ? 1 : 0);++})();nnew file mode 100644@@@ -0,0 +1,62 @@++'use strict';++/** robots.txt, sitemap.xml, feed.xml, and the static-asset cache policy. */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++++ await check('robots.txt allows crawling, blocks the private routes, points at the sitemap', async () => {++ const r = await t.get('/robots.txt');++ assert.strictEqual(r.status, 200);++ assert.ok(r.headers.get('content-type').startsWith('text/plain'));++ assert.ok(r.text.includes('User-agent: *\nAllow: /'));++ for (const d of ['/api/', '/auth/', '/my', '/new']) assert.ok(r.text.includes(`Disallow: ${d}`));++ assert.ok(r.text.includes('Sitemap: https://openvibe.community/sitemap.xml'));++ });++++ await check('sitemap.xml lists home, /pastes and every recent public paste — never unlisted/private', async () => {++ const r = await t.get('/sitemap.xml');++ assert.strictEqual(r.status, 200);++ assert.ok(r.headers.get('content-type').includes('xml'));++ assert.ok(r.text.startsWith('<?xml version="1.0" encoding="UTF-8"?>'));++ assert.ok(r.text.includes('<loc>https://openvibe.community/</loc>'));++ assert.ok(r.text.includes('<loc>https://openvibe.community/pastes</loc>'));++ assert.ok(r.text.includes('<loc>https://openvibe.community/p/amber-fox-42</loc>'));++ assert.ok(r.text.includes('<lastmod>2026-09-15</lastmod>'));++ assert.ok(r.text.includes('/p/fair-moon-23'));++ assert.ok(!r.text.includes('dark-owl-55') && !r.text.includes('grim-vale-61'));++ });++++ await check('feed.xml is a valid-looking RSS of the latest pastes', async () => {++ const r = await t.get('/feed.xml');++ assert.strictEqual(r.status, 200);++ assert.ok(r.text.includes('<rss version="2.0"'));++ assert.ok(r.text.includes('<link>https://openvibe.community/p/amber-fox-42</link>'));++ assert.ok(r.text.includes('<dc:creator'));++ });++++ await check('assets: hashed ?v= is immutable, everything else revalidates; the page references the hashed URLs', async () => {++ const home = await t.get('/');++ const m = home.text.match(/\/css\/community\.css\?v=([0-9a-f]{10})/);++ assert.ok(m, 'hashed css url in page');++ const hashed = await t.get(`/css/community.css?v=${m[1]}`);++ assert.strictEqual(hashed.headers.get('cache-control'), 'public, max-age=31536000, immutable');++ const plain = await t.get('/css/community.css');++ assert.strictEqual(plain.headers.get('cache-control'), 'no-cache');++ const fav = await t.get('/favicon.svg');++ assert.strictEqual(fav.status, 200);++ const og = await t.get('/og-default.png');++ assert.strictEqual(og.status, 200);++ });++++ await check('security headers are present on pages', async () => {++ const r = await t.get('/');++ assert.ok(r.headers.get('content-security-policy').includes("script-src 'self' 'unsafe-inline' https://openvibe.network"));++ assert.strictEqual(r.headers.get('x-content-type-options'), 'nosniff');++ assert.strictEqual(r.headers.get('x-powered-by'), null);++ });++++ await t.close();++ done();++})();nnew file mode 100644@@@ -0,0 +1,190 @@++'use strict';++/** Server-rendered pages: home, browse, paste (SEO head + body), my, new, errors. */++const assert = require('assert');++const { boot, check, done } = require('./helpers/app');++++(async () => {++ const t = await boot();++ const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to find: ${s}`);++++ await check('home renders hero, latest + trending pastes, CTA and roadmap with full SEO head', async () => {++ const r = await t.get('/');++ assert.strictEqual(r.status, 200);++ has(r.text, '<html lang="en"');++ has(r.text, '<title>OpenVibe.Community — the people of OpenVibe</title>');++ has(r.text, '<link rel="canonical" href="https://openvibe.community/">');++ has(r.text, '<meta property="og:type" content="website">');++ has(r.text, '<meta name="twitter:card" content="summary">');++ has(r.text, '"@type":"WebSite"');++ has(r.text, 'https://openvibe.network/shared/theme-loader.js');++ has(r.text, 'https://openvibe.network/shared/navbar.js');++ has(r.text, 'https://openvibe.network/shared/footer.js');++ has(r.text, '<h1>The people of OpenVibe</h1>');++ has(r.text, '<h2>Latest pastes</h2>');++ has(r.text, '<h2>Most viewed</h2>');++ has(r.text, 'id="coming"');++ has(r.text, 'href="/p/amber-fox-42"');++ // Most-viewed puts the 900-view nginx paste first.++ const trendingAt = r.text.indexOf('id="trending"');++ assert.ok(r.text.indexOf('blue-lake-17', trendingAt) < r.text.indexOf('amber-fox-42', trendingAt));++ // Unlisted/private fixtures never show on shared pages.++ assert.ok(!r.text.includes('dark-owl-55') && !r.text.includes('grim-vale-61'));++ // Navbar/footer init payload matches the shared-chrome contract.++ const cfg = JSON.parse(r.text.match(/window\.__OV_PAGE = (.*);\n/)[1]);++ assert.strictEqual(cfg.navbar.service, 'community');++ assert.deepStrictEqual(cfg.navbar.links.map((l) => l.href), ['/pastes', '/new']);++ assert.strictEqual(cfg.navbar.menu.after[0].href, '/my');++ assert.strictEqual(cfg.navbar.history.type, 'page');++ assert.strictEqual(cfg.navbar.silentLogin, 'https://openvibe.community/auth/login?silent=1&next={url}');++ assert.strictEqual(cfg.footer.mount, '#ov-footer');++ assert.strictEqual(cfg.footer.variant, 'full');++ // Verbiage: never "free"/"no cost".++ assert.ok(!/\bfree\b(?! speech)/i.test(r.text.replace(/<script[\s\S]*?<\/script>/g, '')), 'no "free" claims outside "free speech"');++ });++++ await check('paste page: SEO head, Article JSON-LD with author/datePublished, highlighted body, actions, related', async () => {++ const r = await t.get('/p/amber-fox-42');++ assert.strictEqual(r.status, 200);++ has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');++ has(r.text, '<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">');++ has(r.text, '<meta name="robots" content="index,follow">');++ has(r.text, '<meta property="og:type" content="article">');++ has(r.text, 'article:published_time');++ const ld = r.text.match(/<script type="application\/ld\+json">(.*?)<\/script>/g).map((s) => JSON.parse(s.replace(/<script[^>]*>|<\/script>/g, '')));++ const article = ld.find((o) => o['@type'] === 'Article');++ assert.ok(article, 'Article JSON-LD present');++ assert.strictEqual(article.author.name, 'Alex');++ assert.strictEqual(article.datePublished, '2026-09-15T10:00:00.000Z');++ assert.strictEqual(article.hasPart.programmingLanguage, 'javascript');++ assert.ok(ld.some((o) => o['@type'] === 'BreadcrumbList'));++ has(r.text, 'class="hljs-keyword">const</span>', 'server-side highlighted');++ has(r.text, 'href="/p/amber-fox-42/raw"');++ has(r.text, 'href="/p/amber-fox-42/download"');++ has(r.text, 'data-copy-content="amber-fox-42"');++ has(r.text, 'twitter.com/intent/tweet');++ has(r.text, '<h2>More like this</h2>');++ has(r.text, 'https://openvibe.live/data/avatars/alex.png', 'relative Live avatar made absolute');++ const cfg = JSON.parse(r.text.match(/window\.__OV_PAGE = (.*);\n/)[1]);++ assert.strictEqual(cfg.navbar.history.type, 'paste');++ assert.strictEqual(cfg.navbar.history.title, 'Hello world in JavaScript');++ assert.strictEqual(cfg.footer.variant, 'compact');++ // The view was counted by Live (the SSR fetch had no no_view flag).++ const call = t.live.calls.find((c) => c.path === '/api/pastes/amber-fox-42');++ assert.strictEqual(call.query.no_view, undefined);++ });++++ await check('unlisted paste is noindex and its content is escaped', async () => {++ const r = await t.get('/p/dark-owl-55');++ assert.strictEqual(r.status, 200);++ has(r.text, '<meta name="robots" content="noindex,follow">');++ has(r.text, '<script>alert(1)</script>');++ assert.ok(!r.text.includes('<script>alert(1)</script>'));++ });++++ await check('screenshot paste uses the Media image as og:image and ImageObject JSON-LD', async () => {++ const r = await t.get('/p/fair-moon-23');++ has(r.text, '<meta property="og:image" content="https://openvibe.media/p/fair-moon-23/screenshot">');++ has(r.text, '<meta name="twitter:card" content="summary_large_image">');++ has(r.text, '"@type":"ImageObject"');++ has(r.text, '<img src="https://openvibe.media/p/fair-moon-23/screenshot"');++ });++++ await check('private paste 404s for anonymous visitors and renders for its owner', async () => {++ const anon = await t.get('/p/grim-vale-61');++ assert.strictEqual(anon.status, 404);++ has(anon.text, 'Paste not found');++ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });++ const owner = await t.get('/p/grim-vale-61', { cookies: [`ov_token=${token}`] });++ assert.strictEqual(owner.status, 200);++ has(owner.text, 'data-delete="grim-vale-61"', 'owner sees delete');++ const call = t.live.calls.filter((c) => c.path === '/api/pastes/grim-vale-61').pop();++ assert.strictEqual(call.headers.authorization, `Bearer ${token}`, 'visitor token forwarded to Live');++ });++++ await check('raw / screenshot bounce to Media; download serves the text as an attachment without a view', async () => {++ const raw = await t.get('/p/amber-fox-42/raw');++ assert.strictEqual(raw.status, 302);++ assert.strictEqual(raw.headers.get('location'), 'https://openvibe.media/p/amber-fox-42/raw');++ const shot = await t.get('/p/fair-moon-23/screenshot');++ assert.strictEqual(shot.headers.get('location'), 'https://openvibe.media/p/fair-moon-23/screenshot');++ const dl = await t.get('/p/amber-fox-42/download');++ assert.strictEqual(dl.status, 200);++ assert.strictEqual(dl.headers.get('content-disposition'), 'attachment; filename="amber-fox-42.js"');++ assert.ok(dl.text.startsWith('const greet'));++ const call = t.live.calls.filter((c) => c.path === '/api/pastes/amber-fox-42').pop();++ assert.strictEqual(call.query.no_view, '1');++ });++++ await check('browse: search passes through to Live, views sort and language filter work, search pages are noindex', async () => {++ const all = await t.get('/pastes');++ assert.strictEqual(all.status, 200);++ has(all.text, '<link rel="canonical" href="https://openvibe.community/pastes">');++ has(all.text, 'href="/p/blue-lake-17"');++ const q = await t.get('/pastes?q=nginx');++ has(q.text, '<meta name="robots" content="noindex,follow">');++ has(q.text, 'href="/p/blue-lake-17"');++ assert.ok(!q.text.includes('href="/p/amber-fox-42"'));++ const call = t.live.calls.filter((c) => c.path === '/api/pastes' && c.query.search).pop();++ assert.strictEqual(call.query.search, 'nginx');++ const views = await t.get('/pastes?sort=views');++ const res = views.text.slice(views.text.indexOf('id="results"'));++ assert.ok(res.indexOf('blue-lake-17') < res.indexOf('amber-fox-42') && res.indexOf('amber-fox-42') < res.indexOf('cold-ridge-88'));++ const py = await t.get('/pastes?lang=python');++ has(py.text, 'href="/p/cold-ridge-88"');++ assert.ok(!py.text.includes('href="/p/amber-fox-42"'));++ has(py.text, '<option value="python" selected>');++ const old = await t.get('/pastes/amber-fox-42');++ assert.strictEqual(old.status, 301);++ });++++ await check('/my redirects anonymous visitors to sign-in and lists the signed-in user\'s pastes (incl. unlisted/private)', async () => {++ const anon = await t.get('/my');++ assert.strictEqual(anon.status, 302);++ assert.strictEqual(anon.headers.get('location'), '/auth/login?next=%2Fmy');++ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });++ const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });++ assert.strictEqual(mine.status, 200);++ has(mine.text, '<meta name="robots" content="noindex,nofollow">');++ for (const slug of ['amber-fox-42', 'cold-ridge-88', 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);++ assert.ok(!mine.text.includes('blue-lake-17'));++ const call = t.live.calls.filter((c) => c.path === '/api/pastes/by-user/alex').pop();++ assert.strictEqual(call.headers.authorization, `Bearer ${token}`);++ });++++ await check('/new renders the form (private option only when signed in) and the no-JS post creates through Live', async () => {++ const anon = await t.get('/new');++ assert.strictEqual(anon.status, 200);++ has(anon.text, 'name="content"');++ assert.ok(!anon.text.includes('value="private"'));++ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });++ const signed = await t.get('/new?fork=amber-fox-42', { cookies: [`ov_token=${token}`] });++ has(signed.text, 'value="private"');++ has(signed.text, 'Fork of Hello world in JavaScript');++ const body = new URLSearchParams({ title: 'via form', language: 'python', content: 'print(1)', visibility: 'public' }).toString();++ const post = await t.get('/new', { method: 'POST', body, headers: { 'content-type': 'application/x-www-form-urlencoded' }, cookies: [`ov_token=${token}`] });++ assert.strictEqual(post.status, 303);++ assert.strictEqual(post.headers.get('location'), '/p/new-paste-99');++ const call = t.live.calls.filter((c) => c.method === 'POST' && c.path === '/api/pastes').pop();++ assert.strictEqual(call.headers.authorization, `Bearer ${token}`);++ assert.deepStrictEqual(JSON.parse(call.body.toString()).content, 'print(1)');++ const empty = await t.get('/new', { method: 'POST', body: 'title=x', headers: { 'content-type': 'application/x-www-form-urlencoded' } });++ assert.strictEqual(empty.status, 400);++ has(empty.text, 'content is empty');++ const limited = await t.get('/new', { method: 'POST', body: new URLSearchParams({ title: 'slow down', content: 'x' }).toString(), headers: { 'content-type': 'application/x-www-form-urlencoded' } });++ assert.strictEqual(limited.status, 429);++ has(limited.text, 'Please wait 30s');++ });++++ await check('404 page and API 404 JSON; health', async () => {++ const r = await t.get('/nope');++ assert.strictEqual(r.status, 404);++ has(r.text, 'Page not found');++ const a = await t.get('/api/nope');++ assert.strictEqual(a.status, 404);++ assert.deepStrictEqual(a.json(), { error: 'Not found' });++ assert.strictEqual((await t.get('/api/health')).json().service, 'openvibe-community');++ });++++ await t.close();++ done();++})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,112 @@
@@ -0,0 +1,112 @@
+'use strict';
+/** The OAuth client: login (incl. silent), callback, logout, me, refresh, and next sanitising. */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ const cookieNamed = (r, name) => r.setCookies.find((c) => c.startsWith(`${name}=`));
+
+ await check('GET /auth/login builds the Network authorize URL and sets the state cookie', async () => {
+ const r = await t.get('/auth/login?next=/pastes%3Fsort%3Dviews');
+ assert.strictEqual(r.status, 302);
+ const u = new URL(r.headers.get('location'));
+ assert.strictEqual(u.origin, t.network.url);
+ assert.strictEqual(u.pathname, '/oauth/authorize');
+ assert.strictEqual(u.searchParams.get('client_id'), 'community');
+ assert.strictEqual(u.searchParams.get('redirect_uri'), 'https://openvibe.community/auth/callback');
+ assert.strictEqual(u.searchParams.get('response_type'), 'code');
+ assert.strictEqual(u.searchParams.get('scope'), 'profile theme');
+ assert.strictEqual(u.searchParams.get('prompt'), null);
+ assert.ok(/^[0-9a-f]{32}$/.test(u.searchParams.get('state')));
+ const state = cookieNamed(r, 'ov_oauth_state');
+ assert.ok(state && /HttpOnly/i.test(state) && /Path=\/auth/.test(state) && /Secure/.test(state) && !/Domain=/i.test(state), 'state cookie: httpOnly, /auth, secure, host-only');
+ assert.ok(state.includes(u.searchParams.get('state')));
+ assert.strictEqual(decodeURIComponent(cookieNamed(r, 'ov_oauth_next').split(';')[0].split('=').slice(1).join('=')), '/pastes?sort=views');
+ });
+
+ await check('silent=1 adds prompt=none; login_required bounces back to next + ?sso=none', async () => {
+ const r = await t.get('/auth/login?silent=1&next=/p/amber-fox-42');
+ const u = new URL(r.headers.get('location'));
+ assert.strictEqual(u.searchParams.get('prompt'), 'none');
+ assert.ok(cookieNamed(r, 'ov_oauth_silent'));
+ const back = await t.get(`/auth/callback?error=login_required&state=${u.searchParams.get('state')}`);
+ assert.strictEqual(back.status, 302);
+ assert.strictEqual(back.headers.get('location'), '/p/amber-fox-42?sso=none');
+ assert.ok(!t.jar.has('ov_token'), 'no session was created');
+ });
+
+ await check('?sso=none is appended correctly to a next that already has a query or a Network URL', async () => {
+ const { withParam, sanitizeNext } = require('../server/auth/routes');
+ const cfg = require('../server/config');
+ assert.strictEqual(withParam('/pastes?sort=views', 'sso', 'none'), '/pastes?sort=views&sso=none');
+ assert.strictEqual(withParam('/pastes#x', 'sso', 'none'), '/pastes?sso=none#x');
+ assert.strictEqual(sanitizeNext('/my', cfg), '/my');
+ assert.strictEqual(sanitizeNext('//evil.example/x', cfg), '/');
+ assert.strictEqual(sanitizeNext('https://evil.example/x', cfg), '/');
+ assert.strictEqual(sanitizeNext('https://openvibe.community/pastes', cfg), 'https://openvibe.community/pastes');
+ assert.strictEqual(sanitizeNext(`${t.network.url}/sso/next?step=2`.replace(/^http:/, 'https:'), cfg), `${t.network.url}/sso/next?step=2`.replace(/^http:/, 'https:'));
+ assert.strictEqual(sanitizeNext('javascript:alert(1)', cfg), '/');
+ });
+
+ await check('callback exchanges the code, sets host-only ov_token (JS-readable) + ov_refresh + ov_sso_hint=account, redirects to next', async () => {
+ const login = await t.get('/auth/login?next=/my');
+ const state = new URL(login.headers.get('location')).searchParams.get('state');
+ const cb = await t.get(`/auth/callback?code=good-code&state=${state}`);
+ assert.strictEqual(cb.status, 302, cb.text);
+ assert.strictEqual(cb.headers.get('location'), '/my');
+ const tok = cookieNamed(cb, 'ov_token');
+ assert.ok(tok && !/HttpOnly/i.test(tok) && /SameSite=Lax/i.test(tok) && /Secure/.test(tok) && !/Domain=/i.test(tok) && /Path=\//.test(tok));
+ const ref = cookieNamed(cb, 'ov_refresh');
+ assert.ok(ref && /HttpOnly/i.test(ref) && /Path=\/auth/.test(ref));
+ const hint = cookieNamed(cb, 'ov_sso_hint');
+ assert.ok(hint && hint.startsWith('ov_sso_hint=account') && !/HttpOnly/i.test(hint) && /Max-Age=31536000/.test(hint));
+ const grant = t.network.grants.pop();
+ assert.strictEqual(grant.grant_type, 'authorization_code');
+ assert.strictEqual(grant.client_id, 'community');
+ assert.strictEqual(grant.redirect_uri, 'https://openvibe.community/auth/callback');
+ });
+
+ await check('/auth/me verifies the cookie offline via the Network JWKS', async () => {
+ const me = await t.get('/auth/me');
+ assert.strictEqual(me.status, 200);
+ const body = me.json();
+ assert.strictEqual(body.user.username, 'alex');
+ assert.strictEqual(body.user.exp, undefined);
+ assert.ok(body.expires_at > Date.now());
+ const bad = await t.get('/auth/me', { cookies: ['ov_token=garbage'], headers: { authorization: 'Bearer garbage' } });
+ assert.strictEqual(bad.status, 401);
+ });
+
+ await check('callback rejects a missing or mismatched state', async () => {
+ const r = await t.get('/auth/callback?code=good-code&state=deadbeef');
+ assert.strictEqual(r.status, 400);
+ });
+
+ await check('POST /auth/refresh rotates tokens; a rejected refresh clears the session', async () => {
+ t.jar.set('ov_refresh', 'refresh-1');
+ const ok = await t.get('/auth/refresh', { method: 'POST' });
+ assert.strictEqual(ok.status, 200);
+ assert.strictEqual(ok.json().user.username, 'alex');
+ assert.strictEqual(t.jar.get('ov_refresh'), 'refresh-2');
+ t.jar.set('ov_refresh', 'stale');
+ const no = await t.get('/auth/refresh', { method: 'POST' });
+ assert.strictEqual(no.status, 401);
+ assert.ok(!t.jar.has('ov_token'));
+ });
+
+ await check('logout clears the session, sets ov_sso_hint=guest and honours a Network next', async () => {
+ t.jar.set('ov_token', 'x'); t.jar.set('ov_refresh', 'refresh-2');
+ const next = `${t.network.url}/logout?everywhere=1`.replace(/^http:/, 'https:');
+ const r = await t.get(`/auth/logout?next=${encodeURIComponent(next)}`);
+ assert.strictEqual(r.status, 302);
+ assert.strictEqual(r.headers.get('location'), next);
+ assert.ok(cookieNamed(r, 'ov_sso_hint').startsWith('ov_sso_hint=guest'));
+ assert.ok(!t.jar.has('ov_token') && !t.jar.has('ov_refresh'));
+ const evil = await t.get('/auth/logout?next=https://evil.example/');
+ assert.strictEqual(evil.headers.get('location'), '/');
+ });
+
+ await t.close();
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+'use strict';
+/** Boots the mocks, points the app at them, and returns a tiny HTTP client that keeps cookies. */
+const http = require('http');
+const mockLive = require('./mock-live');
+const mockNetwork = require('./mock-network');
+
+async function boot() {
+ const liveSrv = await mockLive.start();
+ const netSrv = await mockNetwork.start();
+ process.env.NODE_ENV = 'test';
+ process.env.BASE_URL = 'https://openvibe.community';
+ process.env.OV_LIVE_INTERNAL_URL = liveSrv.url;
+ process.env.OV_LIVE_URL = 'https://openvibe.live';
+ process.env.OV_MEDIA_URL = 'https://openvibe.media';
+ process.env.OV_NETWORK_URL = netSrv.url;
+ process.env.OV_NETWORK_INTERNAL_URL = netSrv.url;
+ process.env.OV_OAUTH_CLIENT_ID = 'community';
+ process.env.OV_OAUTH_CLIENT_SECRET = 'shh';
+ process.env.OV_OAUTH_REDIRECT_URI = 'https://openvibe.community/auth/callback';
+ process.env.COOKIE_SECURE = 'true';
+ process.env.TRUST_PROXY = '1';
+ for (const k of Object.keys(require.cache)) if (k.includes('/server/')) delete require.cache[k];
+ const { createApp } = require('../../server/app');
+ const app = createApp();
+ const server = await new Promise((resolve) => { const s = http.createServer(app); s.listen(0, '127.0.0.1', () => resolve(s)); });
+ const base = `http://127.0.0.1:${server.address().port}`;
+
+ const jar = new Map();
+ async function get(path, opts = {}) {
+ const headers = { ...(opts.headers || {}) };
+ const cookies = [...jar.entries()].map(([k, v]) => `${k}=${v}`).concat(opts.cookies || []);
+ if (cookies.length) headers.cookie = cookies.join('; ');
+ const res = await fetch(base + path, { method: opts.method || 'GET', headers, body: opts.body, redirect: 'manual', duplex: opts.body && typeof opts.body.pipe === 'function' ? 'half' : undefined });
+ for (const sc of res.headers.getSetCookie ? res.headers.getSetCookie() : []) {
+ const [pair, ...attrs] = sc.split(';');
+ const [k, v] = pair.split('=');
+ const expired = attrs.some((a) => /max-age=0|expires=thu, 01 jan 1970/i.test(a.trim()));
+ if (expired) jar.delete(k.trim()); else jar.set(k.trim(), v);
+ }
+ const text = await res.text();
+ return { status: res.status, headers: res.headers, text, setCookies: res.headers.getSetCookie ? res.headers.getSetCookie() : [], json() { return JSON.parse(text); } };
+ }
+ return {
+ app, base, get, jar, live: liveSrv, network: netSrv,
+ close: async () => { await new Promise((r) => server.close(r)); await liveSrv.close(); await netSrv.close(); },
+ };
+}
+
+let failures = 0;
+async function check(name, fn) {
+ try { await fn(); console.log(' ✓', name); }
+ catch (e) { failures++; console.log(' ✗', name, '\n ', (e.stack || String(e)).split('\n').slice(0, 4).join('\n ')); }
+}
+function done() { console.log(failures ? `\n${failures} failed` : '\nall passed'); process.exit(failures ? 1 : 0); }
+
+module.exports = { boot, check, done };
new file mode 100644
new file mode 100644
@@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
+'use strict';
+/**
+ * A stand-in for OpenVibe.Live's /api/pastes surface: enough of the list / get / by-user /
+ * create endpoints to render every page, plus an echo of whatever else arrives so the proxy
+ * tests can see exactly what was forwarded.
+ */
+const http = require('http');
+
+const FIXTURES = [
+ { id: 1, slug: 'amber-fox-42', user_id: 7, username: 'alex', display_name: 'Alex', avatar_url: '/data/avatars/alex.png', type: 'paste', title: 'Hello world in JavaScript', content: 'const greet = (name) => {\n return `Hello, ${name}!`;\n};\nconsole.log(greet("OpenVibe"));\n', language: 'javascript', visibility: 'public', views: 120, likes: 3, created_at: '2026-09-15 10:00:00', updated_at: '2026-09-15 10:00:00' },
+ { id: 2, slug: 'blue-lake-17', user_id: 9, username: 'sam', display_name: 'Sam', type: 'paste', title: 'nginx snippet', content: 'server {\n listen 80;\n}\n', language: 'nginx', visibility: 'public', views: 900, likes: 0, created_at: '2026-09-14 09:00:00', updated_at: '2026-09-14 09:00:00' },
+ { id: 3, slug: 'cold-ridge-88', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'python helper', content: 'def add(a, b):\n return a + b\n', language: 'python', visibility: 'public', views: 15, likes: 1, created_at: '2026-09-13 08:00:00', updated_at: '2026-09-13 08:00:00' },
+ { id: 4, slug: 'dark-owl-55', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'secret notes', content: 'unlisted <script>alert(1)</script>', language: 'text', visibility: 'unlisted', views: 2, likes: 0, created_at: '2026-09-12 08:00:00', updated_at: '2026-09-12 08:00:00' },
+ { id: 5, slug: 'fair-moon-23', user_id: 9, username: 'sam', display_name: 'Sam', type: 'screenshot', title: 'Desktop shot', content: 'my desktop', language: 'text', visibility: 'public', screenshot_url: '/p/fair-moon-23/screenshot', views: 40, likes: 0, created_at: '2026-09-11 08:00:00', updated_at: '2026-09-11 08:00:00' },
+ { id: 6, slug: 'grim-vale-61', user_id: 7, username: 'alex', display_name: 'Alex', type: 'paste', title: 'private thing', content: 'private', language: 'text', visibility: 'private', views: 0, likes: 0, created_at: '2026-09-10 08:00:00', updated_at: '2026-09-10 08:00:00' },
+];
+
+function start() {
+ const calls = [];
+ const server = http.createServer((req, res) => {
+ const chunks = [];
+ req.on('data', (c) => chunks.push(c));
+ req.on('end', () => {
+ const body = Buffer.concat(chunks);
+ const url = new URL(req.url, 'http://live');
+ const call = { method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams), headers: req.headers, body };
+ calls.push(call);
+ const auth = req.headers.authorization || '';
+ const signedIn = auth === 'Bearer good-token' || auth.startsWith('Bearer eyJ');
+ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
+ const publicOnly = FIXTURES.filter((p) => p.visibility === 'public');
+
+ if (req.method === 'GET' && url.pathname === '/api/pastes') {
+ let rows = publicOnly;
+ if (url.searchParams.get('type')) rows = rows.filter((p) => p.type === url.searchParams.get('type'));
+ const q = url.searchParams.get('search');
+ if (q) rows = rows.filter((p) => (p.title + p.content).toLowerCase().includes(q.toLowerCase()));
+ const limit = Number(url.searchParams.get('limit') || 50), offset = Number(url.searchParams.get('offset') || 0);
+ return json(200, { pastes: rows.slice(offset, offset + limit).map((p) => ({ ...p, content: p.type === 'paste' ? p.content.slice(0, 300) : null })), total: rows.length, limit, offset });
+ }
+ let m;
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/pastes\/by-user\/([^/]+)$/))) {
+ const username = decodeURIComponent(m[1]);
+ if (!FIXTURES.some((p) => p.username === username)) return json(404, { error: 'User not found' });
+ const rows = FIXTURES.filter((p) => p.username === username && (signedIn || p.visibility === 'public'));
+ return json(200, { pastes: rows, total: rows.length, username });
+ }
+ if (req.method === 'GET' && (m = url.pathname.match(/^\/api\/pastes\/([^/]+)$/))) {
+ const p = FIXTURES.find((x) => x.slug === decodeURIComponent(m[1]));
+ if (!p || (p.visibility === 'private' && !signedIn)) return json(404, { error: 'Paste not found' });
+ if (p.slug === 'burned') return json(410, { error: 'burned' });
+ return json(200, { paste: { ...p, views: p.views + (url.searchParams.get('no_view') === '1' ? 0 : 1) } });
+ }
+ if (req.method === 'POST' && url.pathname === '/api/pastes') {
+ let parsed = {}; try { parsed = JSON.parse(body.toString('utf8')); } catch { /* */ }
+ if (!parsed.content) return json(400, { error: 'Content is required' });
+ if (parsed.title === 'slow down') return json(429, { error: 'Please wait 30s', cooldown: 30 });
+ return json(201, { id: 99, slug: 'new-paste-99', url: '/p/new-paste-99', paste: { slug: 'new-paste-99', ...parsed, user_id: signedIn ? 7 : null } });
+ }
+ if (req.method === 'POST' && url.pathname === '/api/pastes/screenshot') {
+ return json(201, { paste: { slug: 'shot-100' }, url: '/p/shot-100', received: body.length, contentType: req.headers['content-type'] || null });
+ }
+ // Everything else: echo, so proxy tests can inspect the forwarded request.
+ json(200, { echo: true, method: req.method, path: url.pathname, query: call.query, headers: req.headers, body: body.toString('utf8') });
+ });
+ });
+ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => resolve({
+ url: `http://127.0.0.1:${server.address().port}`,
+ calls,
+ fixtures: FIXTURES,
+ close: () => new Promise((r) => server.close(r)),
+ })));
+}
+
+module.exports = { start, FIXTURES };
new file mode 100644
new file mode 100644
@@ -0,0 +1,41 @@
@@ -0,0 +1,41 @@
+'use strict';
+/** A stand-in for OpenVibe.Network: JWKS + the token endpoint, with a real RS256 key pair. */
+const http = require('http');
+const crypto = require('crypto');
+const jwt = require('jsonwebtoken');
+
+function start() {
+ const { publicKey, privateKey } = crypto.generateKeyPairSync('rsa', { modulusLength: 2048 });
+ const publicPem = publicKey.export({ type: 'spki', format: 'pem' });
+ const privatePem = privateKey.export({ type: 'pkcs8', format: 'pem' });
+ const grants = [];
+ let issuer = 'http://network.test';
+ const server = http.createServer((req, res) => {
+ const chunks = [];
+ req.on('data', (c) => chunks.push(c));
+ req.on('end', () => {
+ const json = (status, obj) => { res.writeHead(status, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(obj)); };
+ if (req.url === '/api/.well-known/jwks') return json(200, { public_key: publicPem, algorithm: 'RS256' });
+ if (req.url === '/oauth/token' && req.method === 'POST') {
+ let body = {}; try { body = JSON.parse(Buffer.concat(chunks).toString('utf8')); } catch { /* */ }
+ grants.push(body);
+ if (body.client_secret !== 'shh') return json(401, { error: 'invalid_client' });
+ if (body.grant_type === 'authorization_code' && body.code !== 'good-code') return json(400, { error: 'invalid_grant' });
+ if (body.grant_type === 'refresh_token' && body.refresh_token !== 'refresh-1') return json(400, { error: 'invalid_grant' });
+ return json(200, { access_token: sign({ id: 7, username: 'alex', display_name: 'Alex', role: 'user' }), refresh_token: 'refresh-2', token_type: 'Bearer', expires_in: 86400 });
+ }
+ if (req.url === '/oauth/revoke') return json(200, { ok: true });
+ json(404, { error: 'not found' });
+ });
+ });
+ function sign(claims, opts = {}) {
+ return jwt.sign({ sub: claims.id, ...claims }, privatePem, { algorithm: 'RS256', issuer, expiresIn: '1h', ...opts });
+ }
+ return new Promise((resolve) => server.listen(0, '127.0.0.1', () => {
+ const url = `http://127.0.0.1:${server.address().port}`;
+ issuer = url;
+ resolve({ url, grants, sign, publicPem, close: () => new Promise((r) => server.close(r)) });
+ }));
+}
+
+module.exports = { start };
new file mode 100644
new file mode 100644
@@ -0,0 +1,83 @@
@@ -0,0 +1,83 @@
+'use strict';
+/** /api/pastes/* is a transparent proxy to Live that forwards the visitor's identity and address. */
+const assert = require('assert');
+const { Readable } = require('stream');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+
+ await check('GET forwards path, query, the ov_token cookie as Bearer and the client address', async () => {
+ const r = await t.get('/api/pastes/echo/thing?limit=5&x=y', { cookies: ['ov_token=cookie-jwt'], headers: { 'x-forwarded-for': '203.0.113.9' } });
+ assert.strictEqual(r.status, 200);
+ const e = r.json();
+ assert.strictEqual(e.path, '/api/pastes/echo/thing');
+ assert.deepStrictEqual(e.query, { limit: '5', x: 'y' });
+ assert.strictEqual(e.headers.authorization, 'Bearer cookie-jwt');
+ assert.strictEqual(e.headers['x-forwarded-for'], '203.0.113.9');
+ assert.strictEqual(e.headers.cookie, undefined, 'cookies never leave this site');
+ });
+
+ await check('Authorization header wins over the cookie; anonymous calls carry no Authorization', async () => {
+ const r = await t.get('/api/pastes/echo/x', { cookies: ['ov_token=cookie-jwt'], headers: { authorization: 'Bearer header-jwt' } });
+ assert.strictEqual(r.json().headers.authorization, 'Bearer header-jwt');
+ const anon = await t.get('/api/pastes/echo/x');
+ assert.strictEqual(anon.json().headers.authorization, undefined);
+ });
+
+ await check('JSON POST body reaches Live byte-for-byte with its content-type', async () => {
+ const body = JSON.stringify({ title: 't', content: 'hello', language: 'text' });
+ const r = await t.get('/api/pastes', { method: 'POST', body, headers: { 'content-type': 'application/json' }, cookies: ['ov_token=good-token'] });
+ assert.strictEqual(r.status, 201);
+ assert.strictEqual(r.json().slug, 'new-paste-99');
+ const call = t.live.calls.filter((c) => c.method === 'POST' && c.path === '/api/pastes').pop();
+ assert.strictEqual(call.body.toString(), body);
+ assert.strictEqual(call.headers['content-type'], 'application/json');
+ assert.strictEqual(call.headers.authorization, 'Bearer good-token');
+ });
+
+ await check('multipart screenshot upload streams through untouched', async () => {
+ const boundary = 'xxBOUNDARYxx';
+ const png = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0, 1, 2, 3, 255, 254]);
+ const body = Buffer.concat([
+ Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="title"\r\n\r\nShot\r\n--${boundary}\r\nContent-Disposition: form-data; name="screenshot"; filename="a.png"\r\nContent-Type: image/png\r\n\r\n`),
+ png, Buffer.from(`\r\n--${boundary}--\r\n`),
+ ]);
+ const r = await t.get('/api/pastes/screenshot', { method: 'POST', body: Readable.from([body]), headers: { 'content-type': `multipart/form-data; boundary=${boundary}`, 'content-length': String(body.length) } });
+ assert.strictEqual(r.status, 201);
+ assert.strictEqual(r.json().received, body.length);
+ const call = t.live.calls.filter((c) => c.path === '/api/pastes/screenshot').pop();
+ assert.ok(call.body.equals(body), 'body identical');
+ assert.strictEqual(call.headers['content-type'], `multipart/form-data; boundary=${boundary}`);
+ });
+
+ await check('upstream status codes and error bodies pass through (404, 429)', async () => {
+ const nf = await t.get('/api/pastes/does-not-exist');
+ assert.strictEqual(nf.status, 404);
+ assert.deepStrictEqual(nf.json(), { error: 'Paste not found' });
+ const rl = await t.get('/api/pastes', { method: 'POST', body: JSON.stringify({ title: 'slow down', content: 'x' }), headers: { 'content-type': 'application/json' } });
+ assert.strictEqual(rl.status, 429);
+ assert.strictEqual(rl.json().cooldown, 30);
+ });
+
+ await check('DELETE and the copy/like sub-routes are forwarded with the method intact', async () => {
+ const del = await t.get('/api/pastes/amber-fox-42', { method: 'DELETE', cookies: ['ov_token=good-token'] });
+ assert.strictEqual(del.json().method, 'DELETE');
+ const copy = await t.get('/api/pastes/amber-fox-42/copy', { method: 'POST', body: '{}', headers: { 'content-type': 'application/json' } });
+ assert.strictEqual(copy.json().path, '/api/pastes/amber-fox-42/copy');
+ });
+
+ await check('an unreachable Live answers 502 JSON instead of hanging', async () => {
+ const { createApp } = require('../server/app');
+ const http = require('http');
+ const app = createApp({ liveUrl: 'http://127.0.0.1:1' });
+ const srv = await new Promise((resolve) => { const s = http.createServer(app); s.listen(0, '127.0.0.1', () => resolve(s)); });
+ const res = await fetch(`http://127.0.0.1:${srv.address().port}/api/pastes`);
+ assert.strictEqual(res.status, 502);
+ assert.deepStrictEqual(await res.json(), { error: 'Could not reach the paste service' });
+ await new Promise((r) => srv.close(r));
+ });
+
+ await t.close();
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
+#!/usr/bin/env node
+/**
+ * Runs every test in test/ — the files named *.test.js — each in its own process, and fails
+ * if any of them fails. They use in-process mocks of OpenVibe.Live and OpenVibe.Network only;
+ * none of them needs the network or a running site.
+ *
+ * npm test # everything
+ * npm test -- seo proxy # only files whose name contains one of the words
+ */
+'use strict';
+const { spawn } = require('child_process');
+const fs = require('fs');
+const path = require('path');
+
+const filters = process.argv.slice(2).filter((a) => !a.startsWith('-'));
+const files = fs.readdirSync(__dirname)
+ .filter((f) => f.endsWith('.test.js'))
+ .filter((f) => !filters.length || filters.some((w) => f.includes(w)))
+ .sort();
+const TIMEOUT_MS = 60000;
+
+function runOne(file) {
+ return new Promise((resolve) => {
+ const started = Date.now();
+ const child = spawn(process.execPath, [path.join(__dirname, file)], {
+ cwd: path.join(__dirname, '..'),
+ env: { ...process.env, NODE_ENV: 'test' },
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+ let output = '';
+ child.stdout.on('data', (c) => { output += c; });
+ child.stderr.on('data', (c) => { output += c; });
+ const timer = setTimeout(() => { output += `\n[run] timed out after ${TIMEOUT_MS}ms`; child.kill('SIGKILL'); }, TIMEOUT_MS);
+ child.on('close', (code, signal) => {
+ clearTimeout(timer);
+ resolve({ file, ok: code === 0, code: code ?? signal, ms: Date.now() - started, output });
+ });
+ });
+}
+
+(async () => {
+ if (!files.length) { console.error('no test files matched'); process.exit(1); }
+ const results = [];
+ for (const f of files) {
+ const r = await runOne(f);
+ results.push(r);
+ console.log(`${r.ok ? '✓' : '✗'} ${r.file.padEnd(32)} ${String(r.ms).padStart(6)}ms`);
+ }
+ const failed = results.filter((r) => !r.ok);
+ for (const r of failed) {
+ console.log(`\n── ${r.file} (exit ${r.code}) ──`);
+ console.log(r.output.split('\n').slice(-60).join('\n'));
+ }
+ console.log(`\n${results.length - failed.length}/${results.length} test files passed`);
+ process.exit(failed.length ? 1 : 0);
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
+'use strict';
+/** robots.txt, sitemap.xml, feed.xml, and the static-asset cache policy. */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+
+ await check('robots.txt allows crawling, blocks the private routes, points at the sitemap', async () => {
+ const r = await t.get('/robots.txt');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.headers.get('content-type').startsWith('text/plain'));
+ assert.ok(r.text.includes('User-agent: *\nAllow: /'));
+ for (const d of ['/api/', '/auth/', '/my', '/new']) assert.ok(r.text.includes(`Disallow: ${d}`));
+ assert.ok(r.text.includes('Sitemap: https://openvibe.community/sitemap.xml'));
+ });
+
+ await check('sitemap.xml lists home, /pastes and every recent public paste — never unlisted/private', async () => {
+ const r = await t.get('/sitemap.xml');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.headers.get('content-type').includes('xml'));
+ assert.ok(r.text.startsWith('<?xml version="1.0" encoding="UTF-8"?>'));
+ assert.ok(r.text.includes('<loc>https://openvibe.community/</loc>'));
+ assert.ok(r.text.includes('<loc>https://openvibe.community/pastes</loc>'));
+ assert.ok(r.text.includes('<loc>https://openvibe.community/p/amber-fox-42</loc>'));
+ assert.ok(r.text.includes('<lastmod>2026-09-15</lastmod>'));
+ assert.ok(r.text.includes('/p/fair-moon-23'));
+ assert.ok(!r.text.includes('dark-owl-55') && !r.text.includes('grim-vale-61'));
+ });
+
+ await check('feed.xml is a valid-looking RSS of the latest pastes', async () => {
+ const r = await t.get('/feed.xml');
+ assert.strictEqual(r.status, 200);
+ assert.ok(r.text.includes('<rss version="2.0"'));
+ assert.ok(r.text.includes('<link>https://openvibe.community/p/amber-fox-42</link>'));
+ assert.ok(r.text.includes('<dc:creator'));
+ });
+
+ await check('assets: hashed ?v= is immutable, everything else revalidates; the page references the hashed URLs', async () => {
+ const home = await t.get('/');
+ const m = home.text.match(/\/css\/community\.css\?v=([0-9a-f]{10})/);
+ assert.ok(m, 'hashed css url in page');
+ const hashed = await t.get(`/css/community.css?v=${m[1]}`);
+ assert.strictEqual(hashed.headers.get('cache-control'), 'public, max-age=31536000, immutable');
+ const plain = await t.get('/css/community.css');
+ assert.strictEqual(plain.headers.get('cache-control'), 'no-cache');
+ const fav = await t.get('/favicon.svg');
+ assert.strictEqual(fav.status, 200);
+ const og = await t.get('/og-default.png');
+ assert.strictEqual(og.status, 200);
+ });
+
+ await check('security headers are present on pages', async () => {
+ const r = await t.get('/');
+ assert.ok(r.headers.get('content-security-policy').includes("script-src 'self' 'unsafe-inline' https://openvibe.network"));
+ assert.strictEqual(r.headers.get('x-content-type-options'), 'nosniff');
+ assert.strictEqual(r.headers.get('x-powered-by'), null);
+ });
+
+ await t.close();
+ done();
+})();
new file mode 100644
new file mode 100644
@@ -0,0 +1,190 @@
@@ -0,0 +1,190 @@
+'use strict';
+/** Server-rendered pages: home, browse, paste (SEO head + body), my, new, errors. */
+const assert = require('assert');
+const { boot, check, done } = require('./helpers/app');
+
+(async () => {
+ const t = await boot();
+ const has = (html, s, msg) => assert.ok(html.includes(s), msg || `expected to find: ${s}`);
+
+ await check('home renders hero, latest + trending pastes, CTA and roadmap with full SEO head', async () => {
+ const r = await t.get('/');
+ assert.strictEqual(r.status, 200);
+ has(r.text, '<html lang="en"');
+ has(r.text, '<title>OpenVibe.Community — the people of OpenVibe</title>');
+ has(r.text, '<link rel="canonical" href="https://openvibe.community/">');
+ has(r.text, '<meta property="og:type" content="website">');
+ has(r.text, '<meta name="twitter:card" content="summary">');
+ has(r.text, '"@type":"WebSite"');
+ has(r.text, 'https://openvibe.network/shared/theme-loader.js');
+ has(r.text, 'https://openvibe.network/shared/navbar.js');
+ has(r.text, 'https://openvibe.network/shared/footer.js');
+ has(r.text, '<h1>The people of OpenVibe</h1>');
+ has(r.text, '<h2>Latest pastes</h2>');
+ has(r.text, '<h2>Most viewed</h2>');
+ has(r.text, 'id="coming"');
+ has(r.text, 'href="/p/amber-fox-42"');
+ // Most-viewed puts the 900-view nginx paste first.
+ const trendingAt = r.text.indexOf('id="trending"');
+ assert.ok(r.text.indexOf('blue-lake-17', trendingAt) < r.text.indexOf('amber-fox-42', trendingAt));
+ // Unlisted/private fixtures never show on shared pages.
+ assert.ok(!r.text.includes('dark-owl-55') && !r.text.includes('grim-vale-61'));
+ // Navbar/footer init payload matches the shared-chrome contract.
+ const cfg = JSON.parse(r.text.match(/window\.__OV_PAGE = (.*);\n/)[1]);
+ assert.strictEqual(cfg.navbar.service, 'community');
+ assert.deepStrictEqual(cfg.navbar.links.map((l) => l.href), ['/pastes', '/new']);
+ assert.strictEqual(cfg.navbar.menu.after[0].href, '/my');
+ assert.strictEqual(cfg.navbar.history.type, 'page');
+ assert.strictEqual(cfg.navbar.silentLogin, 'https://openvibe.community/auth/login?silent=1&next={url}');
+ assert.strictEqual(cfg.footer.mount, '#ov-footer');
+ assert.strictEqual(cfg.footer.variant, 'full');
+ // Verbiage: never "free"/"no cost".
+ assert.ok(!/\bfree\b(?! speech)/i.test(r.text.replace(/<script[\s\S]*?<\/script>/g, '')), 'no "free" claims outside "free speech"');
+ });
+
+ await check('paste page: SEO head, Article JSON-LD with author/datePublished, highlighted body, actions, related', async () => {
+ const r = await t.get('/p/amber-fox-42');
+ assert.strictEqual(r.status, 200);
+ has(r.text, '<title>Hello world in JavaScript · OpenVibe.Community</title>');
+ has(r.text, '<link rel="canonical" href="https://openvibe.community/p/amber-fox-42">');
+ has(r.text, '<meta name="robots" content="index,follow">');
+ has(r.text, '<meta property="og:type" content="article">');
+ has(r.text, 'article:published_time');
+ const ld = r.text.match(/<script type="application\/ld\+json">(.*?)<\/script>/g).map((s) => JSON.parse(s.replace(/<script[^>]*>|<\/script>/g, '')));
+ const article = ld.find((o) => o['@type'] === 'Article');
+ assert.ok(article, 'Article JSON-LD present');
+ assert.strictEqual(article.author.name, 'Alex');
+ assert.strictEqual(article.datePublished, '2026-09-15T10:00:00.000Z');
+ assert.strictEqual(article.hasPart.programmingLanguage, 'javascript');
+ assert.ok(ld.some((o) => o['@type'] === 'BreadcrumbList'));
+ has(r.text, 'class="hljs-keyword">const</span>', 'server-side highlighted');
+ has(r.text, 'href="/p/amber-fox-42/raw"');
+ has(r.text, 'href="/p/amber-fox-42/download"');
+ has(r.text, 'data-copy-content="amber-fox-42"');
+ has(r.text, 'twitter.com/intent/tweet');
+ has(r.text, '<h2>More like this</h2>');
+ has(r.text, 'https://openvibe.live/data/avatars/alex.png', 'relative Live avatar made absolute');
+ const cfg = JSON.parse(r.text.match(/window\.__OV_PAGE = (.*);\n/)[1]);
+ assert.strictEqual(cfg.navbar.history.type, 'paste');
+ assert.strictEqual(cfg.navbar.history.title, 'Hello world in JavaScript');
+ assert.strictEqual(cfg.footer.variant, 'compact');
+ // The view was counted by Live (the SSR fetch had no no_view flag).
+ const call = t.live.calls.find((c) => c.path === '/api/pastes/amber-fox-42');
+ assert.strictEqual(call.query.no_view, undefined);
+ });
+
+ await check('unlisted paste is noindex and its content is escaped', async () => {
+ const r = await t.get('/p/dark-owl-55');
+ assert.strictEqual(r.status, 200);
+ has(r.text, '<meta name="robots" content="noindex,follow">');
+ has(r.text, '<script>alert(1)</script>');
+ assert.ok(!r.text.includes('<script>alert(1)</script>'));
+ });
+
+ await check('screenshot paste uses the Media image as og:image and ImageObject JSON-LD', async () => {
+ const r = await t.get('/p/fair-moon-23');
+ has(r.text, '<meta property="og:image" content="https://openvibe.media/p/fair-moon-23/screenshot">');
+ has(r.text, '<meta name="twitter:card" content="summary_large_image">');
+ has(r.text, '"@type":"ImageObject"');
+ has(r.text, '<img src="https://openvibe.media/p/fair-moon-23/screenshot"');
+ });
+
+ await check('private paste 404s for anonymous visitors and renders for its owner', async () => {
+ const anon = await t.get('/p/grim-vale-61');
+ assert.strictEqual(anon.status, 404);
+ has(anon.text, 'Paste not found');
+ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });
+ const owner = await t.get('/p/grim-vale-61', { cookies: [`ov_token=${token}`] });
+ assert.strictEqual(owner.status, 200);
+ has(owner.text, 'data-delete="grim-vale-61"', 'owner sees delete');
+ const call = t.live.calls.filter((c) => c.path === '/api/pastes/grim-vale-61').pop();
+ assert.strictEqual(call.headers.authorization, `Bearer ${token}`, 'visitor token forwarded to Live');
+ });
+
+ await check('raw / screenshot bounce to Media; download serves the text as an attachment without a view', async () => {
+ const raw = await t.get('/p/amber-fox-42/raw');
+ assert.strictEqual(raw.status, 302);
+ assert.strictEqual(raw.headers.get('location'), 'https://openvibe.media/p/amber-fox-42/raw');
+ const shot = await t.get('/p/fair-moon-23/screenshot');
+ assert.strictEqual(shot.headers.get('location'), 'https://openvibe.media/p/fair-moon-23/screenshot');
+ const dl = await t.get('/p/amber-fox-42/download');
+ assert.strictEqual(dl.status, 200);
+ assert.strictEqual(dl.headers.get('content-disposition'), 'attachment; filename="amber-fox-42.js"');
+ assert.ok(dl.text.startsWith('const greet'));
+ const call = t.live.calls.filter((c) => c.path === '/api/pastes/amber-fox-42').pop();
+ assert.strictEqual(call.query.no_view, '1');
+ });
+
+ await check('browse: search passes through to Live, views sort and language filter work, search pages are noindex', async () => {
+ const all = await t.get('/pastes');
+ assert.strictEqual(all.status, 200);
+ has(all.text, '<link rel="canonical" href="https://openvibe.community/pastes">');
+ has(all.text, 'href="/p/blue-lake-17"');
+ const q = await t.get('/pastes?q=nginx');
+ has(q.text, '<meta name="robots" content="noindex,follow">');
+ has(q.text, 'href="/p/blue-lake-17"');
+ assert.ok(!q.text.includes('href="/p/amber-fox-42"'));
+ const call = t.live.calls.filter((c) => c.path === '/api/pastes' && c.query.search).pop();
+ assert.strictEqual(call.query.search, 'nginx');
+ const views = await t.get('/pastes?sort=views');
+ const res = views.text.slice(views.text.indexOf('id="results"'));
+ assert.ok(res.indexOf('blue-lake-17') < res.indexOf('amber-fox-42') && res.indexOf('amber-fox-42') < res.indexOf('cold-ridge-88'));
+ const py = await t.get('/pastes?lang=python');
+ has(py.text, 'href="/p/cold-ridge-88"');
+ assert.ok(!py.text.includes('href="/p/amber-fox-42"'));
+ has(py.text, '<option value="python" selected>');
+ const old = await t.get('/pastes/amber-fox-42');
+ assert.strictEqual(old.status, 301);
+ });
+
+ await check('/my redirects anonymous visitors to sign-in and lists the signed-in user\'s pastes (incl. unlisted/private)', async () => {
+ const anon = await t.get('/my');
+ assert.strictEqual(anon.status, 302);
+ assert.strictEqual(anon.headers.get('location'), '/auth/login?next=%2Fmy');
+ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });
+ const mine = await t.get('/my', { cookies: [`ov_token=${token}`] });
+ assert.strictEqual(mine.status, 200);
+ has(mine.text, '<meta name="robots" content="noindex,nofollow">');
+ for (const slug of ['amber-fox-42', 'cold-ridge-88', 'dark-owl-55', 'grim-vale-61']) has(mine.text, `href="/p/${slug}"`);
+ assert.ok(!mine.text.includes('blue-lake-17'));
+ const call = t.live.calls.filter((c) => c.path === '/api/pastes/by-user/alex').pop();
+ assert.strictEqual(call.headers.authorization, `Bearer ${token}`);
+ });
+
+ await check('/new renders the form (private option only when signed in) and the no-JS post creates through Live', async () => {
+ const anon = await t.get('/new');
+ assert.strictEqual(anon.status, 200);
+ has(anon.text, 'name="content"');
+ assert.ok(!anon.text.includes('value="private"'));
+ const token = t.network.sign({ id: 7, username: 'alex', display_name: 'Alex' });
+ const signed = await t.get('/new?fork=amber-fox-42', { cookies: [`ov_token=${token}`] });
+ has(signed.text, 'value="private"');
+ has(signed.text, 'Fork of Hello world in JavaScript');
+ const body = new URLSearchParams({ title: 'via form', language: 'python', content: 'print(1)', visibility: 'public' }).toString();
+ const post = await t.get('/new', { method: 'POST', body, headers: { 'content-type': 'application/x-www-form-urlencoded' }, cookies: [`ov_token=${token}`] });
+ assert.strictEqual(post.status, 303);
+ assert.strictEqual(post.headers.get('location'), '/p/new-paste-99');
+ const call = t.live.calls.filter((c) => c.method === 'POST' && c.path === '/api/pastes').pop();
+ assert.strictEqual(call.headers.authorization, `Bearer ${token}`);
+ assert.deepStrictEqual(JSON.parse(call.body.toString()).content, 'print(1)');
+ const empty = await t.get('/new', { method: 'POST', body: 'title=x', headers: { 'content-type': 'application/x-www-form-urlencoded' } });
+ assert.strictEqual(empty.status, 400);
+ has(empty.text, 'content is empty');
+ const limited = await t.get('/new', { method: 'POST', body: new URLSearchParams({ title: 'slow down', content: 'x' }).toString(), headers: { 'content-type': 'application/x-www-form-urlencoded' } });
+ assert.strictEqual(limited.status, 429);
+ has(limited.text, 'Please wait 30s');
+ });
+
+ await check('404 page and API 404 JSON; health', async () => {
+ const r = await t.get('/nope');
+ assert.strictEqual(r.status, 404);
+ has(r.text, 'Page not found');
+ const a = await t.get('/api/nope');
+ assert.strictEqual(a.status, 404);
+ assert.deepStrictEqual(a.json(), { error: 'Not found' });
+ assert.strictEqual((await t.get('/api/health')).json().service, 'openvibe-community');
+ });
+
+ await t.close();
+ done();
+})();