Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528/** * Admin Proxy API Tests */import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';import express from 'express';import request from 'supertest';import crypto from 'crypto';import { createAdminRoutes } from './admin.js';import { DatabaseService } from '../services/database.js';
import { createAdminCookie, ADMIN_COOKIE_NAME } from '../utils/proxy-auth.js';
const ADMIN_TOKEN = 'test-admin-token-secret';const SESSION_SECRET = 'test-session-secret-for-admin-32!';
function createTestApp(db: DatabaseService, sessionSecret?: string) { const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true }));
const router = createAdminRoutes(db, ADMIN_TOKEN, null, null, null, sessionSecret); app.use('/admin', router); return app;}
function authHeader() { return { Authorization: `Bearer ${ADMIN_TOKEN}` };}
describe('Admin Proxy Origins', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should require admin auth', async () => { const res = await request(app).get('/admin/proxy/origins'); expect(res.status).toBe(401); });
it('should reject invalid admin token', async () => { const res = await request(app) .get('/admin/proxy/origins') .set('Authorization', 'Bearer wrong-token'); expect(res.status).toBe(403); });
it('should list empty origins initially', async () => { const res = await request(app) .get('/admin/proxy/origins') .set(authHeader());
expect(res.status).toBe(200); expect(res.body.origins).toEqual([]); });
it('should add an allowed origin', async () => { const res = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
expect(res.status).toBe(201); expect(res.body.origin).toBe('https://search.arcnode.xyz'); expect(res.body.name).toBe('SearXNG'); expect(res.body.id).toBeGreaterThan(0); });
it('should reject origin without name', async () => { const res = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://test.example.com' });
expect(res.status).toBe(400); });
it('should reject invalid origin format', async () => { const res = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'not-a-url', name: 'Bad' });
expect(res.status).toBe(400); });
it('should reject origin with trailing path', async () => { const res = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz/path', name: 'Bad' });
expect(res.status).toBe(400); });
it('should reject duplicate origin', async () => { await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
const res = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'Duplicate' });
expect(res.status).toBe(409); });
it('should list origins after adding', async () => { await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' }); await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://element.arcnode.xyz', name: 'Element' });
const res = await request(app) .get('/admin/proxy/origins') .set(authHeader());
expect(res.status).toBe(200); expect(res.body.origins).toHaveLength(2); });
it('should delete an origin', async () => { const created = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
const res = await request(app) .delete(`/admin/proxy/origins/${created.body.id}`) .set(authHeader());
expect(res.status).toBe(200);
const list = await request(app) .get('/admin/proxy/origins') .set(authHeader()); expect(list.body.origins).toHaveLength(0); });});
describe('Admin Proxy Sessions', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
function createSession(overrides: Partial<{ id: string; did: string; handle: string }> = {}) { const now = Math.floor(Date.now() / 1000); const session = { id: overrides.id || crypto.randomBytes(16).toString('base64url'), did: overrides.did || 'did:plc:test123', handle: overrides.handle || 'test.bsky.social', created_at: now, expires_at: now + 604800, last_activity: now, }; db.createProxySession(session); return session; }
it('should list proxy sessions', async () => { createSession({ id: 's1', did: 'did:plc:a' }); createSession({ id: 's2', did: 'did:plc:b' });
const res = await request(app) .get('/admin/proxy/sessions') .set(authHeader());
expect(res.status).toBe(200); expect(res.body.sessions).toHaveLength(2); });
it('should filter sessions by DID', async () => { createSession({ id: 's1', did: 'did:plc:a' }); createSession({ id: 's2', did: 'did:plc:b' });
const res = await request(app) .get('/admin/proxy/sessions') .query({ did: 'did:plc:a' }) .set(authHeader());
expect(res.status).toBe(200); expect(res.body.sessions).toHaveLength(1); expect(res.body.sessions[0].did).toBe('did:plc:a'); });
it('should delete a proxy session', async () => { const session = createSession();
const res = await request(app) .delete(`/admin/proxy/sessions/${session.id}`) .set(authHeader());
expect(res.status).toBe(200); expect(db.getProxySession(session.id)).toBeNull(); });});
describe('Admin Access Rules API', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); });
afterEach(() => { db.close(); });
it('should list empty rules', async () => { const res = await request(app) .get('/admin/proxy/access') .set(authHeader());
expect(res.status).toBe(200); expect(res.body.rules).toEqual([]); });
it('should create an allow rule', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', description: 'PDS users', });
expect(res.status).toBe(201); expect(res.body.rule_type).toBe('allow'); expect(res.body.subject_value).toBe('*.arcnode.xyz'); });
it('should create a deny rule', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: null, rule_type: 'deny', subject_type: 'did', subject_value: 'did:plc:banned', });
expect(res.status).toBe(201); expect(res.body.rule_type).toBe('deny'); });
it('should reject invalid rule_type', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ rule_type: 'invalid', subject_type: 'did', subject_value: 'did:plc:test', });
expect(res.status).toBe(400); });
it('should reject invalid subject_type', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ rule_type: 'allow', subject_type: 'email', subject_value: 'test@test.com', });
expect(res.status).toBe(400); });
it('should reject DID without did: prefix', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ rule_type: 'allow', subject_type: 'did', subject_value: 'plc:test123', });
expect(res.status).toBe(400); });
it('should reject invalid handle pattern', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '***', });
expect(res.status).toBe(400); });
it('should reject non-existent origin_id', async () => { const res = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: 9999, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*', });
expect(res.status).toBe(404); });
it('should delete a rule', async () => { const created = await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*', });
const res = await request(app) .delete(`/admin/proxy/access/${created.body.id}`) .set(authHeader());
expect(res.status).toBe(200);
const list = await request(app) .get('/admin/proxy/access') .set(authHeader()); expect(list.body.rules).toHaveLength(0); });
it('should filter rules by origin_id', async () => { const origin = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: origin.body.id, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*', }); await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', });
const res = await request(app) .get('/admin/proxy/access') .query({ origin_id: origin.body.id }) .set(authHeader());
expect(res.status).toBe(200); // Should include origin-specific + global (NULL) rules expect(res.body.rules).toHaveLength(2); });
it('should run a dry-run access check', async () => { const origin = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
await request(app) .post('/admin/proxy/access') .set(authHeader()) .send({ origin_id: null, rule_type: 'allow', subject_type: 'handle_pattern', subject_value: '*.arcnode.xyz', });
const allowed = await request(app) .post('/admin/proxy/access/check') .set(authHeader()) .send({ did: 'did:plc:test', handle: 'bkb.arcnode.xyz', origin_id: origin.body.id });
expect(allowed.status).toBe(200); expect(allowed.body.allowed).toBe(true);
const denied = await request(app) .post('/admin/proxy/access/check') .set(authHeader()) .send({ did: 'did:plc:test', handle: 'random.bsky.social', origin_id: origin.body.id });
expect(denied.status).toBe(200); expect(denied.body.allowed).toBe(false); });
it('should return open access when no rules exist', async () => { const origin = await request(app) .post('/admin/proxy/origins') .set(authHeader()) .send({ origin: 'https://search.arcnode.xyz', name: 'SearXNG' });
const res = await request(app) .post('/admin/proxy/access/check') .set(authHeader()) .send({ did: 'did:plc:anyone', handle: 'anyone.bsky.social', origin_id: origin.body.id });
expect(res.status).toBe(200); expect(res.body.allowed).toBe(true); expect(res.body.reason).toContain('open access'); });});
describe('Admin Cookie Auth', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db, SESSION_SECRET); });
afterEach(() => { db.close(); });
it('should render login page', async () => { const res = await request(app).get('/admin/login'); expect(res.status).toBe(200); expect(res.text).toContain('Admin Token'); expect(res.text).toContain('form'); });
it('should reject invalid login token', async () => { const res = await request(app) .post('/admin/login') .type('form') .send({ token: 'wrong-token' });
expect(res.status).toBe(401); expect(res.text).toContain('Invalid admin token'); });
it('should set cookie on valid login', async () => { const res = await request(app) .post('/admin/login') .type('form') .send({ token: ADMIN_TOKEN });
expect(res.status).toBe(302); expect(res.headers.location).toBe('/admin/dashboard'); expect(res.headers['set-cookie']).toBeDefined(); expect(res.headers['set-cookie'][0]).toContain(ADMIN_COOKIE_NAME); expect(res.headers['set-cookie'][0]).toContain('HttpOnly'); expect(res.headers['set-cookie'][0]).toContain('SameSite=Strict'); });
it('should clear cookie on logout', async () => { const res = await request(app).get('/admin/logout'); expect(res.status).toBe(302); expect(res.headers.location).toBe('/admin/login'); expect(res.headers['set-cookie'][0]).toContain('Max-Age=0'); });
it('should accept admin cookie for protected endpoints', async () => { const cookie = createAdminCookie(SESSION_SECRET, 86400);
const res = await request(app) .get('/admin/proxy/origins') .set('Cookie', `${ADMIN_COOKIE_NAME}=${cookie}`);
expect(res.status).toBe(200); });
it('should reject invalid admin cookie', async () => { const res = await request(app) .get('/admin/proxy/origins') .set('Cookie', `${ADMIN_COOKIE_NAME}=invalid.cookie`) .set('Accept', 'application/json');
expect(res.status).toBe(401); });
it('should reject expired admin cookie', async () => { // Create cookie with 0s TTL (already expired) const cookie = createAdminCookie(SESSION_SECRET, -1);
const res = await request(app) .get('/admin/proxy/origins') .set('Cookie', `${ADMIN_COOKIE_NAME}=${cookie}`) .set('Accept', 'application/json');
expect(res.status).toBe(401); });});