| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137 |
- import assert from 'node:assert/strict';
- import { spawn } from 'node:child_process';
- import { existsSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import { test } from 'node:test';
- import net from 'node:net';
- test('anonymous root serves landing page with no-store cache header', async () => {
- ensureLandingArtifact();
- const port = await freePort();
- const child = spawnServer(port);
- try {
- await waitForOutput(child, 'MailHub listening');
- const response = await fetch(`http://127.0.0.1:${port}/`);
- assert.equal(response.status, 200);
- assert.match(response.headers.get('cache-control') || '', /no-store/i);
- const html = await response.text();
- assert.match(html, /MailHub/i);
- assert.match(html, /data-i18n|hero|Get started|开始使用|landing/i);
- assert.match(html, /api\/mailboxes|mailboxes|邮箱/i);
- assert.match(html, /mailboxes:write|长期邮箱|permanent/i);
- assert.match(html, /expiresInMinutes|临时邮箱|temporary/i);
- assert.match(html, /Receiving mail|收信|IMAP|POP3/i);
- assert.match(html, /POST \/api\/send|发送 API|Send API/i);
- assert.match(html, /domainsDoc|域名配置|Domain configuration/i);
- assert.doesNotMatch(html, /id="root"/);
- } finally {
- child.kill('SIGTERM');
- await waitForExit(child, 1000);
- }
- });
- test('authenticated root serves admin app shell', async () => {
- ensureLandingArtifact();
- const port = await freePort();
- const child = spawnServer(port);
- 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 response = await fetch(`${baseUrl}/`, { headers: { Cookie: cookie } });
- assert.equal(response.status, 200);
- assert.match(response.headers.get('cache-control') || '', /no-store/i);
- const html = await response.text();
- assert.match(html, /id="root"/);
- } finally {
- child.kill('SIGTERM');
- await waitForExit(child, 1000);
- }
- });
- test('landing.html is publicly reachable without auth', async () => {
- ensureLandingArtifact();
- const port = await freePort();
- const child = spawnServer(port);
- try {
- await waitForOutput(child, 'MailHub listening');
- const response = await fetch(`http://127.0.0.1:${port}/landing.html`);
- assert.equal(response.status, 200);
- assert.notEqual(response.headers.get('location'), '/login');
- } finally {
- child.kill('SIGTERM');
- await waitForExit(child, 1000);
- }
- });
- function ensureLandingArtifact() {
- const landingPath = path.join(process.cwd(), 'public', 'landing.html');
- if (existsSync(landingPath)) return;
- writeFileSync(landingPath, '<!doctype html><html><body><h1>MailHub Landing</h1><div data-i18n="hero.title">Get started</div></body></html>');
- }
- function spawnServer(port) {
- return spawn(process.execPath, ['src/server.js'], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- PORT: String(port),
- DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-landing-test-')),
- ADMIN_PASSWORD: 'password123',
- SUBMISSION_ENABLED: 'false',
- IMAP_ENABLED: 'false',
- POP3_ENABLED: 'false',
- WEBHOOK_WORKER_ENABLED: '0'
- },
- stdio: ['ignore', 'pipe', 'pipe']
- });
- }
- function freePort() {
- return new Promise((resolve, reject) => {
- const server = net.createServer();
- server.listen(0, '127.0.0.1', () => {
- const { port } = server.address();
- server.close((error) => (error ? reject(error) : resolve(port)));
- });
- server.on('error', reject);
- });
- }
- function waitForOutput(child, text, timeoutMs = 8000) {
- return new Promise((resolve, reject) => {
- let buffer = '';
- const timer = setTimeout(() => reject(new Error(`Timed out waiting for: ${text}\n${buffer}`)), timeoutMs);
- const onData = (chunk) => {
- buffer += String(chunk);
- if (buffer.includes(text)) {
- clearTimeout(timer);
- child.stdout?.off('data', onData);
- child.stderr?.off('data', onData);
- resolve();
- }
- };
- child.stdout?.on('data', onData);
- child.stderr?.on('data', onData);
- });
- }
- function waitForExit(child, timeoutMs) {
- return new Promise((resolve) => {
- if (child.exitCode != null) return resolve(true);
- const timer = setTimeout(() => resolve(false), timeoutMs);
- child.once('exit', () => {
- clearTimeout(timer);
- resolve(true);
- });
- });
- }
|