Files
anwar-pi-website/web/tests/free-launch.test.cjs
T

144 lines
9.8 KiB
JavaScript

const { test } = require('node:test')
const assert = require('node:assert/strict')
const fs = require('node:fs')
const path = require('node:path')
const vm = require('node:vm')
const ts = require(process.env.TEST_TYPESCRIPT || 'typescript')
const root = path.join(__dirname, '..')
function load(file, mocks = {}, globals = {}) {
const source = fs.readFileSync(path.join(root, file), 'utf8')
const output = ts.transpileModule(source, { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } }).outputText
const module = { exports: {} }
const jsx = (type, props) => ({ type: typeof type === 'string' ? type : 'component', ...props })
const requireMock = id => {
if (id in mocks) return mocks[id]
if (id === 'react/jsx-runtime') return { jsx, jsxs: jsx, Fragment: 'fragment' }
if (id === '@/lib/viewer') return load('src/lib/viewer.ts', mocks)
if (id === '@/lib/launch') return load('src/lib/launch.ts')
if (id.startsWith('node:') || ['crypto', 'util'].includes(id)) return require(id)
if (id.endsWith('.css')) return {}
return { __esModule: true, default: () => null }
}
vm.runInNewContext(output, { module, exports: module.exports, require: requireMock, process, console, ...globals }, { filename: file })
return module.exports
}
const response = { NextResponse: { json: (body, options = {}) => ({ body, status: options.status || 200 }) } }
const noIO = { getPayload: () => { throw new Error('Unexpected database access') } }
test('free launch cannot be switched to billing by Stripe environment or date', async () => {
const old = { ...process.env }
try {
for (const mode of [undefined, 'free', 'paid']) {
if (mode) process.env.LAUNCH_MODE = mode; else delete process.env.LAUNCH_MODE
process.env.STRIPE_SECRET_KEY = 'test-present'; process.env.STRIPE_WEBHOOK_SECRET = 'test-present'; process.env.STRIPE_PRICE_ID = 'test-present'
const policy = load('src/lib/launch.ts')
assert.equal(policy.billingEnabled(), false, `billing must stay disabled for env mode ${mode}`)
assert.equal(policy.freeEarlyAccess(), true)
const result = await load('src/app/api/subscribe/route.ts', { 'next/server': response, payload: noIO }).POST()
assert.equal(result.status, 503); assert.ok(result.body.error); assert.equal(result.body.url, undefined)
}
} finally { process.env = old }
})
test('unverified notification signup rejects without storing email or sending mail', async () => {
const result = await load('src/app/api/notify/route.ts', { 'next/server': response, payload: noIO }).POST(new Request('https://example.test/api/notify', { method: 'POST', body: JSON.stringify({ email: 'reader@example.test' }) }))
assert.equal(result.status, 503); assert.ok(result.body.error); assert.equal(result.body.ok, undefined)
})
test('policies and public entry points describe current free reading without signup or notification form', async () => {
for (const name of ['terms', 'privacy']) {
const page = load(`src/app/(frontend)/${name}/page.tsx`).default()
const text = JSON.stringify(page)
assert.doesNotMatch(text, /مسودة|يجب إضافة|قبل الإطلاق يجب|تجريبية للواجهة/)
assert.match(text, /2026-10-10/); assert.match(text, /Anwarpy/); assert.match(text, /hello@anwarpy.ai/)
}
const privacy = JSON.stringify(load('src/app/(frontend)/privacy/page.tsx').default())
assert.match(privacy, /Cloudflare/); assert.match(privacy, /سنة واحدة/); assert.match(privacy, /تلقائي/)
const subscription = await load('src/app/(frontend)/subscription/page.tsx', {
payload: { getPayload: async () => ({ auth: async () => ({ user: null }), find: async () => ({ docs: [] }) }) },
'next/headers': { headers: async () => ({}) },
}).default()
const rendered = JSON.stringify(subscription)
assert.doesNotMatch(rendered, /المقدمة فقط|حساب مجاني|شهرياً|تُضاف أدلة جديدة كل شهر/)
assert.match(rendered, /دون حساب/); assert.match(rendered, /مستقبلاً/)
const source = fs.readFileSync(path.join(root, 'src/app/(frontend)/subscription/page.tsx'), 'utf8')
assert.doesNotMatch(source, /NotifyForm|SubscribeButton/)
const footer = JSON.stringify(load('src/components/footer.tsx').default())
assert.doesNotMatch(footer, /\/signup/)
})
test('frontend layout displays the exact trial banner on every frontend route', () => {
const layout = 'src/app/(frontend)/layout.tsx'
assert.ok(fs.existsSync(path.join(root, layout)), 'shared frontend layout is required')
const tree = JSON.stringify(load(layout).default({ children: 'page content' }))
assert.match(tree, /اطلاق تجريبي/); assert.match(tree, /دون حساب/); assert.match(tree, /تلقائياً/)
assert.match(tree, /aria-label/)
})
test('missing download payload never reports success and valid payload creates a file', () => {
for (const key of ['missing.md', 'agent-state.md']) {
const button = { dataset: { dl: key }, textContent: 'تنزيل', addEventListener: (event, fn) => { button.click = fn } }
const blobs = []; let clicked = 0
const document = { getElementById: () => null, querySelectorAll: () => [button], body: { append: () => {} }, createElement: () => ({ click: () => clicked++, remove: () => {} }) }
const react = { useEffect: fn => fn(), useState: value => [value, () => {}] }
load('src/app/(frontend)/posts/[slug]/post-extras.tsx', { react }, {
document, Blob: class { constructor(body) { blobs.push(body) } }, URL: { createObjectURL: () => 'blob:test', revokeObjectURL: () => {} }, setTimeout: () => {},
}).default({ slug: 'fixture', loggedIn: false })
button.click({ currentTarget: button })
if (key === 'missing.md') { assert.notEqual(button.textContent, 'تم التنزيل ✓'); assert.equal(clicked, 0); assert.equal(blobs.length, 0) }
else { assert.equal(clicked, 1); assert.ok(blobs[0][0].length > 20); assert.equal(button.textContent, 'تم التنزيل ✓') }
}
})
test('home and library entry points label intended membership as future, never require signup for reading', () => {
for (const file of ['src/app/(frontend)/page.tsx', 'src/app/(frontend)/articles/page.tsx', 'src/app/(frontend)/topics/[topic]/page.tsx']) {
const source = fs.readFileSync(path.join(root, file), 'utf8')
assert.match(source, /accessLabel/)
assert.doesNotMatch(source, /العضوية تفتح الأدلة|للأعضاء، وتُضاف/)
}
assert.match(fs.readFileSync(path.join(root, 'src/components/site-links.ts'), 'utf8'), /الوصول المبكر/)
assert.doesNotMatch(fs.readFileSync(path.join(root, 'src/app/(frontend)/login/page.tsx'), 'utf8'), /\/signup/)
})
test('private VIP files remain protected during free launch', async () => {
let diskRead = false
const result = await load('src/app/api/files/[id]/route.ts', {
'next/server': response,
payload: { getPayload: async () => ({ findByID: async () => ({ id: 1, access: 'vip', filename: 'private.md', mimeType: 'text/plain' }) }) },
'@/lib/viewer': { getViewer: async () => null },
'@/collections/Files': { FILES_DIR: '/not-used' },
'node:fs/promises': { readFile: async () => { diskRead = true; return Buffer.from('private') } },
}).GET({}, { params: Promise.resolve({ id: '1' }) })
assert.equal(result.status, 401); assert.equal(diskRead, false)
})
test('source traceability is never readable and users remain admin-created only', () => {
const posts = load('src/collections/Posts.ts').default
const sourceId = posts.fields.find(field => field.name === 'sourceId')
assert.equal(typeof sourceId.access?.read, 'function')
assert.equal(sourceId.access.read({ req: { user: null } }), false)
assert.equal(sourceId.access.read({ req: { user: { role: 'admin' } } }), false)
assert.equal(posts.access.read({ req: { user: null } }).status.equals, 'published')
const users = load('src/collections/Users.ts').default
assert.equal(users.access.create({ req: { user: null } }), false)
assert.equal(users.access.create({ req: { user: { role: 'free' } } }), false)
assert.equal(users.access.create({ req: { user: { role: 'admin' } } }), true)
})
test('unused notification and checkout controls also fail closed if reused', () => {
const react = { useState: value => [value, () => {}] }
const notify = JSON.stringify(load('src/app/(frontend)/subscription/notify-form.tsx', { react }).default())
assert.doesNotMatch(notify, /\"type\":\"input\"|\"type\":\"form\"/)
const checkout = JSON.stringify(load('src/app/(frontend)/subscription/subscribe-button.tsx', { react }).default({ loggedIn: false }))
assert.doesNotMatch(checkout, /\/signup|اشترك/); assert.match(checkout, /\/articles/)
assert.doesNotMatch(fs.readFileSync(path.join(root, 'src/app/(frontend)/account-settings/page.tsx'), 'utf8'), /\/signup/)
})
test('all source TypeScript and TSX files transpile without syntax diagnostics', () => {
let count = 0
function walk(dir) {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name)
if (entry.isDirectory()) walk(full)
else if (/\.tsx?$/.test(entry.name) && !entry.name.endsWith('.d.ts')) {
const result = ts.transpileModule(fs.readFileSync(full, 'utf8'), { fileName: full, reportDiagnostics: true, compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022, jsx: ts.JsxEmit.ReactJSX } })
const errors = (result.diagnostics || []).filter(d => d.category === ts.DiagnosticCategory.Error)
assert.equal(errors.length, 0, `${full}: ${errors.map(d => ts.flattenDiagnosticMessageText(d.messageText, '\n')).join('; ')}`)
count++
}
}
}
walk(path.join(root, 'src')); assert.ok(count > 50); console.log(`Syntax transpile checked ${count} source files (not full type/build validation)`)
})
module.exports = { load, response, noIO }