server-landing.test.js 4.4 KB

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