e4b97368fb
POST /api/v1/auth/setup — unauthenticated, only succeeds when the users table is empty. Enables first-admin creation via HTTP for Docker deployments without shell access to the container volume. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
88 lines
3.2 KiB
JavaScript
88 lines
3.2 KiB
JavaScript
import { test, after } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { mkdtempSync, rmSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const tmpDir = mkdtempSync(join(tmpdir(), 'oikos-setup-test-'));
|
|
|
|
process.env.SESSION_SECRET = 'test-setup-secret-minimum-32-chars-x';
|
|
process.env.DB_PATH = join(tmpDir, 'test.db');
|
|
process.env.SESSION_SECURE = 'false';
|
|
process.env.PORT = '13099';
|
|
|
|
// Dynamic import so env vars are set before module initialization
|
|
const { default: app } = await import('./server/index.js');
|
|
await new Promise(r => setTimeout(r, 400));
|
|
|
|
const BASE = 'http://localhost:13099';
|
|
|
|
after(() => {
|
|
rmSync(tmpDir, { recursive: true, force: true });
|
|
process.exit(0);
|
|
});
|
|
|
|
// Validation tests run first (DB is empty at this point)
|
|
|
|
test('POST /api/v1/auth/setup: 400 when required fields missing', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin' }),
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('POST /api/v1/auth/setup: 400 when username invalid', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'a!b', display_name: 'Test', password: 'password123' }),
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('POST /api/v1/auth/setup: 400 when password too short', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', display_name: 'Test', password: 'short' }),
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('POST /api/v1/auth/setup: 400 when display_name too long', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', display_name: 'x'.repeat(129), password: 'password123' }),
|
|
});
|
|
assert.equal(res.status, 400);
|
|
});
|
|
|
|
test('POST /api/v1/auth/setup: 201 creates first admin', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin', display_name: 'Test Admin', password: 'password123' }),
|
|
});
|
|
assert.equal(res.status, 201);
|
|
const data = await res.json();
|
|
assert.equal(data.user.username, 'admin');
|
|
assert.equal(data.user.display_name, 'Test Admin');
|
|
assert.equal(data.user.role, 'admin');
|
|
assert.ok(typeof data.user.id === 'number');
|
|
assert.ok(typeof data.user.avatar_color === 'string' && data.user.avatar_color.startsWith('#'));
|
|
});
|
|
|
|
test('POST /api/v1/auth/setup: 403 when users already exist', async () => {
|
|
const res = await fetch(`${BASE}/api/v1/auth/setup`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username: 'admin2', display_name: 'Another', password: 'password123' }),
|
|
});
|
|
assert.equal(res.status, 403);
|
|
const data = await res.json();
|
|
assert.equal(data.code, 403);
|
|
});
|