mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
Tests the full traffic path against live researchstack.info infrastructure: Edge Caddy (TLS) → Tailscale → Traefik Ingress → k3s services Coverage: - edge-tls-redirects: HTTPS reachability, cert validity, legacy subdomain 301s (status/dash/home/media/books/music/vault/pulse/apps), stable subdomains (auth, mail), wildcard fallback - path-routing: /apps/*, /server/*, /api/* routes; prefix stripping; SSO redirect vs token-auth isolation - auth-integration: Authentik login page, OIDC discovery, forward_auth gating on protected paths, /api/* bypass 19/40 tests pass against current live infrastructure (pre-deploy). The 21 failures are "not yet deployed" signals, not design errors. Run after each phase of the deployment plan to use as a regression gate. Run: cd 4-Infrastructure/k3s-flake/tests && npm test Generated with Devin (https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
161 lines
5.7 KiB
TypeScript
161 lines
5.7 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Path-based routing tests.
|
|
*
|
|
* Validates that Traefik Ingress correctly routes canonical paths to the
|
|
* right backend services. Tests check:
|
|
* 1. Path exists (not 404)
|
|
* 2. Response comes from the expected service (via content or headers)
|
|
* 3. Prefix stripping works (backend sees / not /apps/chat/)
|
|
*
|
|
* Note: Many paths are behind forward_auth (Authentik), so unauthenticated
|
|
* requests may get 302 → auth. That's still a valid "routing works" signal.
|
|
*/
|
|
|
|
test.describe('/apps/* routes', () => {
|
|
test('/apps/chat/ reaches Hermes (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/apps/chat/', {
|
|
maxRedirects: 0,
|
|
});
|
|
// Either serves the placeholder page (200) or redirects to auth (302)
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
if (response.status() === 200) {
|
|
const body = await response.text();
|
|
expect(body).toContain('Hermes');
|
|
}
|
|
if (response.status() === 302) {
|
|
const location = response.headers()['location'];
|
|
expect(location).toContain('auth.researchstack.info');
|
|
}
|
|
});
|
|
|
|
test('/apps/budget/ reaches Actual Budget (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/apps/budget/', {
|
|
maxRedirects: 0,
|
|
});
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
if (response.status() === 302) {
|
|
const location = response.headers()['location'];
|
|
expect(location).toContain('auth.researchstack.info');
|
|
}
|
|
});
|
|
|
|
test('/apps/chat/ strips prefix (backend sees /)', async ({ request }) => {
|
|
// Request a subpath — if prefix stripping works, the backend gets /health
|
|
// or /index.html, not /apps/chat/health
|
|
const response = await request.get('/apps/chat/', {
|
|
maxRedirects: 0,
|
|
});
|
|
// Should not get 404 from a misconfigured path
|
|
expect(response.status()).not.toBe(404);
|
|
});
|
|
});
|
|
|
|
test.describe('/server/* routes', () => {
|
|
test('/server/status/ reaches Uptime Kuma (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/server/status/', {
|
|
maxRedirects: 0,
|
|
});
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
if (response.status() === 302) {
|
|
const location = response.headers()['location'];
|
|
expect(location).toContain('auth.researchstack.info');
|
|
}
|
|
});
|
|
|
|
test('/server/dash/ reaches Homarr (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/server/dash/', {
|
|
maxRedirects: 0,
|
|
});
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
});
|
|
|
|
test('/server/vault/ reaches Vaultwarden (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/server/vault/', {
|
|
maxRedirects: 0,
|
|
});
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
});
|
|
});
|
|
|
|
test.describe('/api/* routes (no forward_auth, token-based)', () => {
|
|
test('/api/cred/ is reachable (no auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/api/cred/', {
|
|
maxRedirects: 0,
|
|
});
|
|
// API routes should NOT redirect to Authentik — they use token auth
|
|
// They might return 401/403 (no token), 200, or 404 (not deployed yet)
|
|
expect(response.status()).not.toBe(302);
|
|
expect([200, 401, 403, 404, 502, 503]).toContain(response.status());
|
|
});
|
|
|
|
test('/api/registry/health responds with service identity', async ({ request }) => {
|
|
const response = await request.get('/api/registry/health');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body.service).toBe('registry');
|
|
expect(body.status).toBe('ok');
|
|
} else {
|
|
// Service not deployed yet — 502/503 is acceptable
|
|
expect([502, 503, 404]).toContain(response.status());
|
|
}
|
|
});
|
|
|
|
test('/api/jobs/health responds with service identity', async ({ request }) => {
|
|
const response = await request.get('/api/jobs/health');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body.service).toBe('jobs');
|
|
expect(body.status).toBe('ok');
|
|
} else {
|
|
expect([502, 503, 404]).toContain(response.status());
|
|
}
|
|
});
|
|
|
|
test('/api/blobs/health responds with service identity', async ({ request }) => {
|
|
const response = await request.get('/api/blobs/health');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body.service).toBe('blobs');
|
|
expect(body.status).toBe('ok');
|
|
} else {
|
|
expect([502, 503, 404]).toContain(response.status());
|
|
}
|
|
});
|
|
|
|
test('/api/registry/nodes returns empty list or 502', async ({ request }) => {
|
|
const response = await request.get('/api/registry/nodes');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body).toHaveProperty('nodes');
|
|
expect(Array.isArray(body.nodes)).toBe(true);
|
|
} else {
|
|
expect([502, 503, 404]).toContain(response.status());
|
|
}
|
|
});
|
|
|
|
test('/api/jobs/ returns empty list or 502', async ({ request }) => {
|
|
const response = await request.get('/api/jobs/');
|
|
if (response.status() === 200) {
|
|
const body = await response.json();
|
|
expect(body).toHaveProperty('jobs');
|
|
} else {
|
|
expect([502, 503, 404]).toContain(response.status());
|
|
}
|
|
});
|
|
});
|
|
|
|
test.describe('Landing page', () => {
|
|
test('/ reaches Homer (or auth redirect)', async ({ request }) => {
|
|
const response = await request.get('/', {
|
|
maxRedirects: 0,
|
|
});
|
|
expect([200, 302, 303, 401]).toContain(response.status());
|
|
if (response.status() === 200) {
|
|
const body = await response.text();
|
|
// Homer dashboard should mention "Research Stack"
|
|
expect(body).toContain('Research Stack');
|
|
}
|
|
});
|
|
});
|