Files
iofficeai--aionui/tests/unit/web-cli/ensureAdminPassword.test.ts
2026-07-13 13:20:22 +08:00

236 lines
8.2 KiB
TypeScript

/**
* @license
* Copyright 2025 AionUi (aionui.com)
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect } from 'vitest';
import {
ensureAdminPassword,
type EnsureAdminPasswordDeps,
} from '../../../packages/web-cli/src/ensureAdminPassword.js';
type FetchCall = { url: string; init?: RequestInit };
function mockResponse(status: number, body: unknown): Response {
return new Response(typeof body === 'string' ? body : JSON.stringify(body), {
status,
headers: { 'content-type': 'application/json' },
});
}
function makeDeps(opts: { handlers: Array<(url: string, init?: RequestInit) => Response | Promise<Response>> }): {
deps: EnsureAdminPasswordDeps;
calls: FetchCall[];
logs: string[];
warns: string[];
sleeps: number[];
} {
const calls: FetchCall[] = [];
const logs: string[] = [];
const warns: string[] = [];
const sleeps: number[] = [];
let nowVal = 0;
let idx = 0;
const deps: EnsureAdminPasswordDeps = {
fetch: (async (input: RequestInfo | URL, init?: RequestInit) => {
const url = typeof input === 'string' ? input : input.toString();
calls.push({ url, init });
const handler = opts.handlers[idx] ?? opts.handlers[opts.handlers.length - 1];
idx++;
return handler(url, init);
}) as typeof fetch,
log: (msg) => logs.push(msg),
warn: (msg) => warns.push(msg),
sleep: async (ms) => {
sleeps.push(ms);
nowVal += ms;
},
now: () => nowVal,
};
return { deps, calls, logs, warns, sleeps };
}
describe('ensureAdminPassword', () => {
it('seeds password on fresh install (needs_setup=true)', async () => {
const { deps, calls, logs, warns } = makeDeps({
handlers: [
// /api/auth/status
() => mockResponse(200, { needs_setup: true }),
// POST /api/webui/reset-password
() => mockResponse(200, { data: { new_password: 'SuperSecret123' } }),
// /api/auth/internal/users/system
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(calls[0].url).toBe('http://127.0.0.1:25808/api/auth/status');
expect(calls[1].url).toBe('http://127.0.0.1:25808/api/webui/reset-password');
expect(calls[1].init?.method).toBe('POST');
expect(logs).toContain('[aionui-web] Generated initial admin password: SuperSecret123');
expect(logs.some((m) => m.includes('Log in with username "admin"'))).toBe(true);
expect(warns).toEqual([]);
});
it('accepts top-level new_password in reset-password response', async () => {
const { deps, logs } = makeDeps({
handlers: [
() => mockResponse(200, { needs_setup: true }),
() => mockResponse(200, { new_password: 'FromTopLevel' }),
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(logs).toContain('[aionui-web] Generated initial admin password: FromTopLevel');
});
it('reads needs_setup from nested data field', async () => {
const { deps, calls, logs } = makeDeps({
handlers: [
() => mockResponse(200, { data: { needs_setup: true } }),
() => mockResponse(200, { data: { new_password: 'Nested' } }),
() => mockResponse(200, { data: { username: 'custom-admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(calls).toHaveLength(3);
expect(logs.some((m) => m.includes('custom-admin'))).toBe(true);
});
it('no-op when admin already provisioned (needs_setup=false)', async () => {
const { deps, calls, logs, warns } = makeDeps({
handlers: [
() => mockResponse(200, { needs_setup: false }),
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(calls).toHaveLength(2);
expect(calls[1].url).toBe('http://127.0.0.1:25808/api/auth/internal/users/system');
expect(logs.some((m) => m.includes('resetpass'))).toBe(true);
expect(logs.every((m) => !m.includes('Generated initial admin password'))).toBe(true);
expect(warns).toEqual([]);
});
it('polls /api/auth/status until backend is ready', async () => {
let statusAttempts = 0;
const { deps, sleeps, logs } = makeDeps({
handlers: [
// First three fetches are all /api/auth/status: 2 failures then success.
() => {
statusAttempts++;
throw new Error('connect ECONNREFUSED');
},
() => {
statusAttempts++;
throw new Error('connect ECONNREFUSED');
},
() => {
statusAttempts++;
return mockResponse(200, { needs_setup: true });
},
() => mockResponse(200, { data: { new_password: 'Pw' } }),
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808, statusTimeoutMs: 10_000, statusPollIntervalMs: 100 }, deps);
expect(statusAttempts).toBe(3);
expect(sleeps.length).toBeGreaterThanOrEqual(2);
expect(logs.some((m) => m.includes('Generated initial admin password'))).toBe(true);
});
it('warns (not throws) when status never comes up within budget', async () => {
const { deps, warns, logs } = makeDeps({
handlers: [() => mockResponse(500, 'oops')],
});
await ensureAdminPassword({ backendPort: 25808, statusTimeoutMs: 1_000, statusPollIntervalMs: 250 }, deps);
expect(warns.some((w) => w.includes('could not verify admin credentials'))).toBe(true);
expect(logs).toEqual([]);
});
it('warns (not throws) when reset-password fails', async () => {
const { deps, warns, logs } = makeDeps({
handlers: [() => mockResponse(200, { needs_setup: true }), () => mockResponse(500, 'boom')],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(warns.some((w) => w.includes('/api/webui/reset-password returned 500'))).toBe(true);
expect(logs).toEqual([]);
});
it('warns (not throws) when reset-password returns no password', async () => {
const { deps, warns, logs } = makeDeps({
handlers: [() => mockResponse(200, { needs_setup: true }), () => mockResponse(200, { data: {} })],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(warns.some((w) => w.includes('returned no new_password'))).toBe(true);
expect(logs).toEqual([]);
});
it('falls back to "admin" username when system user lookup fails', async () => {
const { deps, logs } = makeDeps({
handlers: [
() => mockResponse(200, { needs_setup: true }),
() => mockResponse(200, { data: { new_password: 'Pw' } }),
() => mockResponse(500, 'broken'),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(logs.some((m) => m.includes('Log in with username "admin"'))).toBe(true);
});
it('uses caller-supplied resetCommand in the "Forgot the password" hint', async () => {
const { deps, logs } = makeDeps({
handlers: [
() => mockResponse(200, { needs_setup: false }),
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808, resetCommand: 'bun run resetpass' }, deps);
expect(logs.some((m) => m.includes('bun run resetpass'))).toBe(true);
expect(logs.every((m) => !m.includes('aionui-web resetpass'))).toBe(true);
});
it('defaults to `aionui-web resetpass` when resetCommand is not provided', async () => {
const { deps, logs } = makeDeps({
handlers: [
() => mockResponse(200, { needs_setup: false }),
() => mockResponse(200, { data: { username: 'admin' } }),
],
});
await ensureAdminPassword({ backendPort: 25808 }, deps);
expect(logs.some((m) => m.includes('aionui-web resetpass'))).toBe(true);
});
it('propagates resetCommand into warn messages when reset-password fails', async () => {
const { deps, warns } = makeDeps({
handlers: [() => mockResponse(200, { needs_setup: true }), () => mockResponse(500, 'boom')],
});
await ensureAdminPassword({ backendPort: 25808, resetCommand: 'bun run resetpass' }, deps);
expect(warns.some((w) => w.includes('bun run resetpass'))).toBe(true);
});
});