| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- import assert from 'node:assert/strict';
- import { spawn } from 'node:child_process';
- import { mkdtempSync, readdirSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import process from 'node:process';
- import { test } from 'node:test';
- import net from 'node:net';
- test('admin API routes respond once and keep the server alive', async () => {
- const port = await freePort();
- const child = spawn(process.execPath, ['src/server.js'], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- PORT: String(port),
- DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-server-test-')),
- ADMIN_PASSWORD: 'password123',
- SUBMISSION_ENABLED: 'false'
- },
- stdio: ['ignore', 'pipe', 'pipe']
- });
- try {
- await waitForOutput(child, 'MailHub listening');
- const baseUrl = `http://127.0.0.1:${port}`;
- const login = await fetch(`${baseUrl}/api/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username: 'admin', password: 'password123' })
- });
- assert.equal(login.status, 200);
- const cookie = login.headers.get('set-cookie')?.split(';')[0] || '';
- assert.ok(cookie);
- const settings = await fetch(`${baseUrl}/api/admin/settings`, {
- headers: { Cookie: cookie }
- });
- assert.equal(settings.status, 200);
- assert.equal((await settings.json()).settings.mailHostname, 'ali.ss5.xyz');
- const exited = await waitForExit(child, 300);
- assert.equal(exited, false);
- } finally {
- child.kill('SIGTERM');
- await waitForExit(child, 1000);
- }
- });
- test('built auth assets are served before authentication', async () => {
- const assetName = readdirSync(path.join(process.cwd(), 'public', 'assets')).find((name) => /\.(js|css)$/.test(name));
- assert.ok(assetName, 'expected at least one built frontend asset');
- const port = await freePort();
- const child = spawn(process.execPath, ['src/server.js'], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- PORT: String(port),
- DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-server-test-')),
- ADMIN_PASSWORD: 'password123',
- SUBMISSION_ENABLED: 'false'
- },
- stdio: ['ignore', 'pipe', 'pipe']
- });
- try {
- await waitForOutput(child, 'MailHub listening');
- const baseUrl = `http://127.0.0.1:${port}`;
- const login = await fetch(`${baseUrl}/login`);
- assert.equal(login.status, 200);
- const asset = await fetch(`${baseUrl}/assets/${assetName}`, { redirect: 'manual' });
- assert.equal(asset.status, 200);
- assert.notEqual(asset.headers.get('location'), '/login');
- } finally {
- child.kill('SIGTERM');
- await waitForExit(child, 1000);
- }
- });
- function freePort() {
- return new Promise((resolve, reject) => {
- const server = net.createServer();
- server.listen(0, '127.0.0.1', () => {
- const address = server.address();
- server.close(() => {
- if (address && typeof address === 'object') resolve(address.port);
- else reject(new Error('Unable to allocate a test port.'));
- });
- });
- });
- }
- function waitForOutput(child, text) {
- return new Promise((resolve, reject) => {
- const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${text}`)), 5000);
- const chunks = [];
- const onData = (chunk) => {
- chunks.push(String(chunk));
- if (chunks.join('').includes(text)) {
- clearTimeout(timeout);
- child.stdout.off('data', onData);
- child.stderr.off('data', onData);
- resolve();
- }
- };
- child.stdout.on('data', onData);
- child.stderr.on('data', onData);
- child.once('exit', (code) => {
- clearTimeout(timeout);
- reject(new Error(`Server exited early with code ${code}: ${chunks.join('')}`));
- });
- });
- }
- function waitForExit(child, timeoutMs) {
- if (child.exitCode !== null) return Promise.resolve(true);
- return new Promise((resolve) => {
- const timeout = setTimeout(() => {
- child.off('exit', onExit);
- resolve(false);
- }, timeoutMs);
- const onExit = () => {
- clearTimeout(timeout);
- resolve(true);
- };
- child.once('exit', onExit);
- });
- }
|