server-landing.test.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import assert from 'node:assert/strict';
  2. import { spawn } from 'node:child_process';
  3. import { existsSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
  4. import { tmpdir } from 'node:os';
  5. import path from 'node:path';
  6. import { test } from 'node:test';
  7. import net from 'node:net';
  8. test('anonymous root serves landing page with no-store cache header', async () => {
  9. ensureLandingArtifact();
  10. const port = await freePort();
  11. const child = spawnServer(port);
  12. try {
  13. await waitForOutput(child, 'MailHub listening');
  14. const response = await fetch(`http://127.0.0.1:${port}/`);
  15. assert.equal(response.status, 200);
  16. assert.match(response.headers.get('cache-control') || '', /no-store/i);
  17. const html = await response.text();
  18. assert.match(html, /MailHub/i);
  19. assert.match(html, /data-i18n|hero|Get started|开始使用|landing/i);
  20. assert.doesNotMatch(html, /id="root"/);
  21. } finally {
  22. child.kill('SIGTERM');
  23. await waitForExit(child, 1000);
  24. }
  25. });
  26. test('authenticated root serves admin app shell', async () => {
  27. ensureLandingArtifact();
  28. const port = await freePort();
  29. const child = spawnServer(port);
  30. try {
  31. await waitForOutput(child, 'MailHub listening');
  32. const baseUrl = `http://127.0.0.1:${port}`;
  33. const login = await fetch(`${baseUrl}/api/login`, {
  34. method: 'POST',
  35. headers: { 'Content-Type': 'application/json' },
  36. body: JSON.stringify({ username: 'admin', password: 'password123' })
  37. });
  38. assert.equal(login.status, 200);
  39. const cookie = login.headers.get('set-cookie')?.split(';')[0] || '';
  40. assert.ok(cookie);
  41. const response = await fetch(`${baseUrl}/`, { headers: { Cookie: cookie } });
  42. assert.equal(response.status, 200);
  43. assert.match(response.headers.get('cache-control') || '', /no-store/i);
  44. const html = await response.text();
  45. assert.match(html, /id="root"/);
  46. } finally {
  47. child.kill('SIGTERM');
  48. await waitForExit(child, 1000);
  49. }
  50. });
  51. test('landing.html is publicly reachable without auth', async () => {
  52. ensureLandingArtifact();
  53. const port = await freePort();
  54. const child = spawnServer(port);
  55. try {
  56. await waitForOutput(child, 'MailHub listening');
  57. const response = await fetch(`http://127.0.0.1:${port}/landing.html`);
  58. assert.equal(response.status, 200);
  59. assert.notEqual(response.headers.get('location'), '/login');
  60. } finally {
  61. child.kill('SIGTERM');
  62. await waitForExit(child, 1000);
  63. }
  64. });
  65. function ensureLandingArtifact() {
  66. const landingPath = path.join(process.cwd(), 'public', 'landing.html');
  67. if (existsSync(landingPath)) return;
  68. writeFileSync(landingPath, '<!doctype html><html><body><h1>MailHub Landing</h1><div data-i18n="hero.title">Get started</div></body></html>');
  69. }
  70. function spawnServer(port) {
  71. return spawn(process.execPath, ['src/server.js'], {
  72. cwd: process.cwd(),
  73. env: {
  74. ...process.env,
  75. PORT: String(port),
  76. DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-landing-test-')),
  77. ADMIN_PASSWORD: 'password123',
  78. SUBMISSION_ENABLED: 'false',
  79. WEBHOOK_WORKER_ENABLED: '0'
  80. },
  81. stdio: ['ignore', 'pipe', 'pipe']
  82. });
  83. }
  84. function freePort() {
  85. return new Promise((resolve, reject) => {
  86. const server = net.createServer();
  87. server.listen(0, '127.0.0.1', () => {
  88. const { port } = server.address();
  89. server.close((error) => (error ? reject(error) : resolve(port)));
  90. });
  91. server.on('error', reject);
  92. });
  93. }
  94. function waitForOutput(child, text, timeoutMs = 8000) {
  95. return new Promise((resolve, reject) => {
  96. let buffer = '';
  97. const timer = setTimeout(() => reject(new Error(`Timed out waiting for: ${text}\n${buffer}`)), timeoutMs);
  98. const onData = (chunk) => {
  99. buffer += String(chunk);
  100. if (buffer.includes(text)) {
  101. clearTimeout(timer);
  102. child.stdout?.off('data', onData);
  103. child.stderr?.off('data', onData);
  104. resolve();
  105. }
  106. };
  107. child.stdout?.on('data', onData);
  108. child.stderr?.on('data', onData);
  109. });
  110. }
  111. function waitForExit(child, timeoutMs) {
  112. return new Promise((resolve) => {
  113. if (child.exitCode != null) return resolve(true);
  114. const timer = setTimeout(() => resolve(false), timeoutMs);
  115. child.once('exit', () => {
  116. clearTimeout(timer);
  117. resolve(true);
  118. });
  119. });
  120. }