cac7bd5727
Dev — Build & local smoke test / build-smoke (pull_request) Successful in 1m52s
159 tests across three Playwright projects (api, chromium, mobile): - tests/api/security.test.ts: security headers, CORS on /api/supabase-config (origin allowlist enforced), auth gate on /api/demo, Stripe webhook signature enforcement, source file leakage, path traversal, input validation (8000-char message cap) - tests/api/endpoints.test.ts: /api/health, /api/founding-count shape invariants, /api/supabase-config JWT shape, sitemap.xml, robots.txt, /llms.txt, /api/soul-health internal gate, 404 for unknown routes - tests/e2e/landing.spec.ts: title, h1 count, meta description, OG tags, canonical (no stage leak), JSON-LD schema, demo widget DOM presence, JS error filtering (known GTM/CSP noise excluded) - tests/e2e/seo.spec.ts: per-page title patterns, noindex on checkout, canonical URLs, sitemap production-URL enforcement - tests/e2e/checkout.spec.ts: all three plan variants, auth section, payment element, canonical - tests/e2e/chat.spec.ts: widget DOM structure, auth gate (send button disabled without session), API-level auth rejection - tests/e2e/navigation.spec.ts: all public routes return 200, 404s for removed/old paths (/terms, /enterprise-terms, /gallery), static files All 159 pass against stage. CI step added to stage.yaml after smoke test.
151 lines
6.8 KiB
TypeScript
151 lines
6.8 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
const BASE = process.env.BASE_URL || 'https://marketing-stage-r4tfklscwq-uc.a.run.app';
|
|
|
|
const get = (path: string, headers: Record<string, string> = {}) =>
|
|
fetch(`${BASE}${path}`, { headers });
|
|
|
|
// ── /api/health ───────────────────────────────────────────────────────────────
|
|
|
|
test('/api/health — returns 200 with status:ok', async () => {
|
|
const r = await get('/api/health');
|
|
expect(r.status).toBe(200);
|
|
const body = await r.json() as Record<string, string>;
|
|
expect(body.status).toBe('ok');
|
|
expect(body.service).toBe('neuron-web');
|
|
});
|
|
|
|
test('/api/health — content-type is application/json', async () => {
|
|
const r = await get('/api/health');
|
|
expect(r.headers.get('content-type')).toContain('application/json');
|
|
});
|
|
|
|
// ── /api/founding-count ───────────────────────────────────────────────────────
|
|
|
|
test('/api/founding-count — returns numeric fields', async () => {
|
|
const r = await get('/api/founding-count');
|
|
expect(r.status).toBe(200);
|
|
const body = await r.json() as Record<string, number>;
|
|
expect(typeof body.sold).toBe('number');
|
|
expect(typeof body.total).toBe('number');
|
|
expect(typeof body.remaining).toBe('number');
|
|
// Invariants
|
|
expect(body.total).toBe(1000);
|
|
expect(body.sold).toBeGreaterThanOrEqual(0);
|
|
expect(body.remaining).toBe(body.total - body.sold);
|
|
});
|
|
|
|
// ── /api/supabase-config ──────────────────────────────────────────────────────
|
|
// Requires a permitted Origin. See security.test.ts for CORS tests.
|
|
|
|
test('/api/supabase-config — returns url and anon_key for allowed origin', async () => {
|
|
const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
|
|
expect(r.status).toBe(200);
|
|
const body = await r.json() as Record<string, string>;
|
|
expect(body.url).toMatch(/supabase\.co/);
|
|
expect(typeof body.anon_key).toBe('string');
|
|
expect(body.anon_key.length).toBeGreaterThan(20);
|
|
});
|
|
|
|
test('/api/supabase-config — anon_key is a valid JWT shape', async () => {
|
|
const r = await get('/api/supabase-config', { Origin: 'https://neurontechnologies.ai' });
|
|
const body = await r.json() as Record<string, string>;
|
|
// Supabase anon key is a JWT: three base64 segments separated by dots
|
|
const parts = body.anon_key.split('.');
|
|
expect(parts).toHaveLength(3);
|
|
});
|
|
|
|
// ── /sitemap.xml ──────────────────────────────────────────────────────────────
|
|
|
|
test('/sitemap.xml — returns valid XML with production URLs', async () => {
|
|
const r = await get('/sitemap.xml');
|
|
expect(r.status).toBe(200);
|
|
expect(r.headers.get('content-type')).toContain('xml');
|
|
const text = await r.text();
|
|
expect(text).toContain('<urlset');
|
|
expect(text).toContain('neurontechnologies.ai');
|
|
// Must not leak stage URL
|
|
expect(text).not.toContain('run.app');
|
|
expect(text).not.toContain('stage');
|
|
});
|
|
|
|
test('/sitemap.xml — includes all major pages', async () => {
|
|
const r = await get('/sitemap.xml');
|
|
const text = await r.text();
|
|
expect(text).toContain('neurontechnologies.ai/');
|
|
expect(text).toContain('neurontechnologies.ai/about');
|
|
expect(text).toContain('neurontechnologies.ai/legal/terms');
|
|
expect(text).toContain('neurontechnologies.ai/legal/enterprise-terms');
|
|
});
|
|
|
|
// ── /robots.txt ───────────────────────────────────────────────────────────────
|
|
|
|
test('/robots.txt — accessible with correct directives', async () => {
|
|
const r = await get('/robots.txt');
|
|
expect(r.status).toBe(200);
|
|
const text = await r.text();
|
|
expect(text).toContain('User-agent');
|
|
// Private paths are disallowed
|
|
expect(text).toContain('Disallow: /checkout');
|
|
expect(text).toContain('Disallow: /account');
|
|
expect(text).toContain('Disallow: /api/');
|
|
// Sitemap link points to production
|
|
expect(text).toContain('Sitemap: https://neurontechnologies.ai/sitemap.xml');
|
|
});
|
|
|
|
// ── /llms.txt ─────────────────────────────────────────────────────────────────
|
|
|
|
test('/llms.txt — accessible', async () => {
|
|
const r = await get('/llms.txt');
|
|
expect(r.status).toBe(200);
|
|
const text = await r.text();
|
|
expect(text.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
// ── 404 handling ─────────────────────────────────────────────────────────────
|
|
|
|
test('Unknown route returns 404', async () => {
|
|
const r = await get('/this-route-xyz-does-not-exist-abc123');
|
|
expect(r.status).toBe(404);
|
|
});
|
|
|
|
// ── /api/webhooks/stripe — POST-only, requires valid signature ────────────────
|
|
|
|
test('/api/webhooks/stripe — rejects missing Stripe-Signature with 400', async () => {
|
|
const r = await fetch(`${BASE}/api/webhooks/stripe`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ type: 'payment_intent.succeeded' }),
|
|
});
|
|
expect(r.status).toBe(400);
|
|
});
|
|
|
|
// ── /api/demo — POST only, auth-gated ────────────────────────────────────────
|
|
|
|
test('/api/demo — missing access_token returns auth_required', async () => {
|
|
const r = await fetch(`${BASE}/api/demo`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ message: 'hello' }),
|
|
});
|
|
const body = await r.json() as Record<string, unknown>;
|
|
expect(body.auth_required).toBe(true);
|
|
});
|
|
|
|
// ── /api/soul-health — internal gate ─────────────────────────────────────────
|
|
// The probe responses embedded in the JSON body may contain literal newlines
|
|
// (control characters), so we test via text matching, not JSON.parse.
|
|
|
|
test('/api/soul-health — 404 without X-Internal header', async () => {
|
|
const r = await get('/api/soul-health');
|
|
expect(r.status).toBe(404);
|
|
});
|
|
|
|
test('/api/soul-health — 200 with X-Internal: true, body contains soul_url', async () => {
|
|
const r = await get('/api/soul-health', { 'X-Internal': 'true' });
|
|
expect(r.status).toBe(200);
|
|
const text = await r.text();
|
|
expect(text).toContain('"soul_url"');
|
|
expect(text).toMatch(/soul_url.*https?:\/\//);
|
|
});
|