Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410import { describe, it, expect, beforeEach, afterEach } from 'vitest';import express from 'express';import request from 'supertest';import crypto from 'crypto';import { createSessionRoutes } from './session.js';import { DatabaseService } from '../services/database.js';import { createGatewayToken } from '../utils/hmac.js';
const TEST_SECRET = crypto.randomBytes(32).toString('hex');
function createTestApp(db: DatabaseService) { const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true }));
const router = createSessionRoutes(db); app.use('/session', router);
app.use((err: any, _req: any, res: any, _next: any) => { const status = err.status || err.statusCode || 500; res.status(status).json({ error: err.code || 'server_error', message: err.message, }); });
return app;}
function registerApp(db: DatabaseService, id = 'test-app') { db.upsertApp({ id, name: 'Test App', hmac_secret: TEST_SECRET, token_ttl_seconds: 3600, callback_url: 'https://app.example.com/callback', });}
function makeGatewayToken(did = 'did:plc:testuser', handle = 'test.bsky.social') { return createGatewayToken({ did, handle, app_id: 'test-app', user_id: -1 }, TEST_SECRET);}
function createSession(db: DatabaseService, overrides: Partial<{ id: string; did: string; handle: string; user_id: number | null; app_id: string;}> = {}) { const session = { id: overrides.id ?? crypto.randomUUID(), did: overrides.did ?? 'did:plc:testuser', handle: overrides.handle ?? 'test.bsky.social', user_id: overrides.user_id ?? null, app_id: overrides.app_id ?? 'test-app', expires_at: new Date(Date.now() + 3600 * 1000), }; db.createSession(session); return session;}
describe('POST /session/check-conflict', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerApp(db); });
afterEach(() => db.close());
it('should return has_conflict: false when no other sessions exist', async () => { const session = createSession(db);
const res = await request(app) .post('/session/check-conflict') .send({ session_id: session.id, app_id: 'test-app' });
expect(res.status).toBe(200); expect(res.body.has_conflict).toBe(false); expect(res.body.existing_sessions).toHaveLength(0); expect(res.body.pending_session_id).toBe(session.id); });
it('should return has_conflict: true when connected session exists', async () => { const existing = createSession(db, { id: 'existing-1' }); db.updateSessionConnectionState('existing-1', 'connected');
const pending = createSession(db, { id: 'pending-1' });
const res = await request(app) .post('/session/check-conflict') .send({ session_id: pending.id, app_id: 'test-app' });
expect(res.status).toBe(200); expect(res.body.has_conflict).toBe(true); expect(res.body.existing_sessions.length).toBeGreaterThan(0); expect(res.body.existing_sessions[0].session_id).toBe(existing.id); });
it('should not flag disconnected old sessions as conflicts', async () => { createSession(db, { id: 'old-1' }); // Default state is 'pending' and last_activity is at creation time // We need to make this session old enough (> 5 min) to not conflict // Since we can't easily backdate in SQLite in-memory, we just verify // that a fresh pending session IS detected (last_activity is recent) const pending = createSession(db, { id: 'pending-1' });
const res = await request(app) .post('/session/check-conflict') .send({ session_id: pending.id, app_id: 'test-app' });
// A fresh pending session has recent last_activity so it IS a conflict expect(res.status).toBe(200); });
it('should return 400 for missing session_id', async () => { const res = await request(app) .post('/session/check-conflict') .send({ app_id: 'test-app' });
expect(res.status).toBe(400); expect(res.body.error).toBe('missing_session_id'); });
it('should return 400 for missing app_id', async () => { const res = await request(app) .post('/session/check-conflict') .send({ session_id: 'some-id' });
expect(res.status).toBe(400); expect(res.body.error).toBe('missing_app_id'); });
it('should return 404 for nonexistent session', async () => { const res = await request(app) .post('/session/check-conflict') .send({ session_id: 'nonexistent', app_id: 'test-app' });
expect(res.status).toBe(404); expect(res.body.error).toBe('session_not_found'); });});
describe('POST /session/resolve-conflict', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerApp(db); });
afterEach(() => db.close());
it('should cancel a session (resolution: cancel)', async () => { const session = createSession(db);
const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: session.id, app_id: 'test-app', resolution: 'cancel' });
expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.action).toBe('cancelled');
// Session should be deleted expect(db.getSession(session.id)).toBeNull(); });
it('should transfer and issue token (resolution: transfer)', async () => { const existing = createSession(db, { id: 'existing-1' }); const pending = createSession(db, { id: 'pending-1' });
const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: pending.id, app_id: 'test-app', resolution: 'transfer' });
expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.action).toBe('transferred'); expect(res.body.token).toBeTypeOf('string'); expect(res.body.token).toContain('.'); expect(res.body.did).toBe('did:plc:testuser');
// Existing session should be deleted expect(db.getSession(existing.id)).toBeNull(); });
it('should close others and issue token (resolution: close_others)', async () => { createSession(db, { id: 'existing-1' }); createSession(db, { id: 'existing-2' }); const pending = createSession(db, { id: 'pending-1' });
const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: pending.id, app_id: 'test-app', resolution: 'close_others' });
expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.action).toBe('closed_others'); expect(res.body.closed_count).toBe(2); expect(res.body.token).toBeTypeOf('string'); });
it('should return 400 for invalid resolution', async () => { const session = createSession(db);
const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: session.id, app_id: 'test-app', resolution: 'invalid' });
expect(res.status).toBe(400); expect(res.body.error).toBe('invalid_resolution'); });
it('should return 400 for missing params', async () => { const res = await request(app) .post('/session/resolve-conflict') .send({ resolution: 'cancel' });
expect(res.status).toBe(400); expect(res.body.error).toBe('missing_params'); });
it('should return 404 for nonexistent session', async () => { const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: 'nonexistent', app_id: 'test-app', resolution: 'cancel' });
expect(res.status).toBe(404); expect(res.body.error).toBe('session_not_found'); });
it('should return 404 for nonexistent app', async () => { const session = createSession(db);
const res = await request(app) .post('/session/resolve-conflict') .send({ session_id: session.id, app_id: 'nonexistent', resolution: 'transfer' });
expect(res.status).toBe(404); expect(res.body.error).toBe('app_not_found'); });});
describe('POST /session/update-state', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerApp(db); });
afterEach(() => db.close());
it('should update session state to connected', async () => { const session = createSession(db); const token = makeGatewayToken(session.did);
const res = await request(app) .post('/session/update-state') .set('Authorization', `Bearer ${token}`) .send({ session_id: session.id, state: 'connected', client_info: 'Firefox 120' });
expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.state).toBe('connected'); });
it('should accept disconnected state', async () => { const session = createSession(db); const token = makeGatewayToken(session.did);
const res = await request(app) .post('/session/update-state') .set('Authorization', `Bearer ${token}`) .send({ session_id: session.id, state: 'disconnected' });
expect(res.status).toBe(200); expect(res.body.state).toBe('disconnected'); });
it('should return 400 for invalid state', async () => { const session = createSession(db);
const res = await request(app) .post('/session/update-state') .send({ session_id: session.id, state: 'invalid_state' });
expect(res.status).toBe(400); expect(res.body.error).toBe('invalid_state'); });
it('should return 400 for missing session_id', async () => { const res = await request(app) .post('/session/update-state') .send({ state: 'connected' });
expect(res.status).toBe(400); expect(res.body.error).toBe('missing_session_id'); });
it('should return 404 for nonexistent session', async () => { const res = await request(app) .post('/session/update-state') .send({ session_id: 'nonexistent', state: 'connected' });
expect(res.status).toBe(404); expect(res.body.error).toBe('session_not_found'); });});
describe('POST /session/heartbeat', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerApp(db); });
afterEach(() => db.close());
it('should update session activity', async () => { const session = createSession(db); const token = makeGatewayToken(session.did);
const res = await request(app) .post('/session/heartbeat') .set('Authorization', `Bearer ${token}`) .send({ session_id: session.id });
expect(res.status).toBe(200); expect(res.body.success).toBe(true); expect(res.body.session_id).toBe(session.id); });
it('should return 400 for missing session_id', async () => { const res = await request(app) .post('/session/heartbeat') .send({});
expect(res.status).toBe(400); expect(res.body.error).toBe('missing_session_id'); });
it('should return 404 for nonexistent session', async () => { const res = await request(app) .post('/session/heartbeat') .send({ session_id: 'nonexistent' });
expect(res.status).toBe(404); expect(res.body.error).toBe('session_not_found'); });});
describe('GET /session/active', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerApp(db); });
afterEach(() => db.close());
it('should list active sessions and mark current', async () => { const s1 = createSession(db, { id: 'session-1' }); createSession(db, { id: 'session-2' }); const token = makeGatewayToken(s1.did);
const res = await request(app) .get('/session/active') .set('Authorization', `Bearer ${token}`) .query({ session_id: s1.id, app_id: 'test-app' });
expect(res.status).toBe(200); expect(res.body.sessions).toHaveLength(2);
const current = res.body.sessions.find((s: any) => s.session_id === s1.id); const other = res.body.sessions.find((s: any) => s.session_id === 'session-2'); expect(current.is_current).toBe(true); expect(other.is_current).toBe(false); });
it('should return 400 for missing query params', async () => { const res = await request(app).get('/session/active'); expect(res.status).toBe(400); });
it('should return 404 for nonexistent session', async () => { const res = await request(app) .get('/session/active') .query({ session_id: 'nonexistent', app_id: 'test-app' });
expect(res.status).toBe(404); expect(res.body.error).toBe('session_not_found'); });});