import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import request from 'supertest'; import { afterEach, describe, expect, it } from 'vitest'; import type { UpdateSystemInput } from '../src/shared/contracts'; import { createApp } from './app'; import { AuthService } from './lib/auth'; import type { RuntimeSnapshot } from './lib/config'; import type { RuntimeController } from './lib/runtime'; import { StateStore } from './lib/store'; class FakeRuntime implements RuntimeController { private status: RuntimeSnapshot = { status: 'idle' as const, pid: null, startedAt: null, lastError: null, }; getSnapshot() { return { ...this.status }; } async start() { this.status = { status: 'live', pid: 999, startedAt: new Date('2026-04-01T00:00:00.000Z').toISOString(), lastError: null, }; return this.getSnapshot(); } async restart() { return this.start(); } async stop() { this.status = { status: 'idle', pid: null, startedAt: null, lastError: null, }; return this.getSnapshot(); } async reload() { return this.getSnapshot(); } } const cleanupDirs: string[] = []; afterEach(async () => { await Promise.all(cleanupDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true }))); }); describe('panel api', () => { it('rejects protected api access without a bearer token', async () => { const app = await createTestApp(); const response = await request(app).get('/api/state'); expect(response.status).toBe(401); expect(response.body.error).toMatch(/authorization required/i); }); it('logs in and returns an expiring panel token', async () => { const app = await createTestApp(); const response = await request(app).post('/api/auth/login').send({ login: 'admin', password: 'proxy-ui-demo', }); expect(response.status).toBe(200); expect(response.body.token).toMatch(/\./); expect(response.body.ttlMs).toBe(24 * 60 * 60 * 1000); expect(typeof response.body.expiresAt).toBe('string'); }); it('rejects user creation against a disabled service', async () => { const app = await createTestApp(); const token = await authorize(app); const response = await request(app).post('/api/users').set('Authorization', `Bearer ${token}`).send({ username: 'bad-proxy-user', password: 'secret123', serviceId: 'proxy', quotaMb: 100, }); expect(response.status).toBe(400); expect(response.body.error).toMatch(/enabled assignable/i); }); it('pauses and deletes a user through the api', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const userId = initial.body.userRecords[0].id; const username = initial.body.userRecords[0].username; const paused = await request(app).post(`/api/users/${userId}/pause`).set('Authorization', `Bearer ${token}`); expect(paused.status).toBe(200); expect(paused.body.userRecords.find((entry: { id: string }) => entry.id === userId).paused).toBe(true); const removed = await request(app).delete(`/api/users/${userId}`).set('Authorization', `Bearer ${token}`); expect(removed.status).toBe(200); expect(removed.body.userRecords.some((entry: { username: string }) => entry.username === username)).toBe( false, ); }); it('updates a user through the api', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const userId = initial.body.userRecords[0].id; const updated = await request(app).put(`/api/users/${userId}`).set('Authorization', `Bearer ${token}`).send({ username: 'night-shift-updated', password: 'fresh-secret', serviceId: 'socks-lab', quotaMb: 512, }); expect(updated.status).toBe(200); expect(updated.body.userRecords.find((entry: { id: string }) => entry.id === userId)).toMatchObject({ username: 'night-shift-updated', password: 'fresh-secret', serviceId: 'socks-lab', quotaBytes: 512 * 1024 * 1024, }); }); it('stops the runtime through the api', async () => { const app = await createTestApp(); const token = await authorize(app); await request(app).post('/api/runtime/start').set('Authorization', `Bearer ${token}`); const stopped = await request(app).post('/api/runtime/stop').set('Authorization', `Bearer ${token}`); expect(stopped.status).toBe(200); expect(stopped.body.service.status).toBe('idle'); expect(stopped.body.service.pidLabel).toBe('pid -'); expect(stopped.body.service.lastEvent).toMatch(/stop requested/i); }); it('rejects system updates when two services reuse the same port', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const system = createSystemPayload(initial.body); system.services[1].port = system.services[0].port; const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system); expect(response.status).toBe(400); expect(response.body.error).toMatch(/cannot share port/i); }); it('rejects system updates that strand existing users on a disabled service', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const system = createSystemPayload(initial.body); system.services = system.services.map((service) => service.id === 'socks-main' ? { ...service, enabled: false } : service, ); const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system); expect(response.status).toBe(400); expect(response.body.error).toMatch(/enabled assignable service/i); expect(response.body.error).toMatch(/night-shift/i); }); it('removes linked users when a service is deleted from settings', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const system = createSystemPayload(initial.body); system.services = system.services.filter((service) => service.id !== 'socks-main'); const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system); expect(response.status).toBe(200); expect(response.body.userRecords.some((entry: { username: string }) => entry.username === 'night-shift')).toBe( false, ); expect(response.body.userRecords.some((entry: { username: string }) => entry.username === 'ops-east')).toBe( false, ); expect(response.body.service.lastEvent).toMatch(/removed 2 linked users/i); }); it('updates system settings and regenerates the rendered config', async () => { const app = await createTestApp(); const token = await authorize(app); const initial = await request(app).get('/api/state').set('Authorization', `Bearer ${token}`); const system = createSystemPayload(initial.body); system.publicHost = 'ops-gateway.example.net'; system.services = system.services.map((service) => service.id === 'socks-main' ? { ...service, port: 1180 } : service, ); const response = await request(app).put('/api/system').set('Authorization', `Bearer ${token}`).send(system); expect(response.status).toBe(200); expect(response.body.system.publicHost).toBe('ops-gateway.example.net'); expect(response.body.system.services.find((service: { id: string }) => service.id === 'socks-main').port).toBe( 1180, ); expect(response.body.system.previewConfig).toContain('socks -p1180 -u2'); expect(response.body.service.lastEvent).toBe('System configuration updated from panel'); }); }); async function createTestApp() { const dir = await fs.mkdtemp(path.join(os.tmpdir(), '3proxy-ui-')); cleanupDirs.push(dir); const runtime = new FakeRuntime(); const store = new StateStore(path.join(dir, 'state', 'panel-state.json')); const auth = new AuthService({ login: 'admin', password: 'proxy-ui-demo', ttlMs: 24 * 60 * 60 * 1000, }); return createApp({ store, runtime, runtimeRootDir: dir, auth, }); } function createSystemPayload(body: { system: Record }): UpdateSystemInput { const { previewConfig: _previewConfig, ...system } = body.system; return structuredClone(system) as unknown as UpdateSystemInput; } async function authorize(app: Awaited>) { const response = await request(app).post('/api/auth/login').send({ login: 'admin', password: 'proxy-ui-demo', }); return response.body.token as string; }