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 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 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 user creation against a non-assignable service', async () => { const app = await createTestApp(); const response = await request(app).post('/api/users').send({ username: 'bad-admin-user', password: 'secret123', serviceId: 'admin', 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 initial = await request(app).get('/api/state'); const userId = initial.body.userRecords[0].id; const username = initial.body.userRecords[0].username; const paused = await request(app).post(`/api/users/${userId}/pause`); 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}`); expect(removed.status).toBe(200); expect(removed.body.userRecords.some((entry: { username: string }) => entry.username === username)).toBe( false, ); }); it('rejects system updates when two services reuse the same port', async () => { const app = await createTestApp(); const initial = await request(app).get('/api/state'); const system = createSystemPayload(initial.body); system.services[1].port = system.services[0].port; const response = await request(app).put('/api/system').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 initial = await request(app).get('/api/state'); 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').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('updates system settings and regenerates the rendered config', async () => { const app = await createTestApp(); const initial = await request(app).get('/api/state'); 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').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')); return createApp({ store, runtime, runtimeRootDir: dir, }); } function createSystemPayload(body: { system: Record }): UpdateSystemInput { const { previewConfig: _previewConfig, ...system } = body.system; return structuredClone(system) as unknown as UpdateSystemInput; }