server-admin-api.test.js 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. import assert from 'node:assert/strict';
  2. import { spawn } from 'node:child_process';
  3. import { mkdtempSync, readdirSync } from 'node:fs';
  4. import { tmpdir } from 'node:os';
  5. import path from 'node:path';
  6. import process from 'node:process';
  7. import { test } from 'node:test';
  8. import net from 'node:net';
  9. test('admin API routes respond once and keep the server alive', async () => {
  10. const port = await freePort();
  11. const child = spawn(process.execPath, ['src/server.js'], {
  12. cwd: process.cwd(),
  13. env: {
  14. ...process.env,
  15. PORT: String(port),
  16. DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-server-test-')),
  17. ADMIN_PASSWORD: 'password123',
  18. SUBMISSION_ENABLED: 'false'
  19. },
  20. stdio: ['ignore', 'pipe', 'pipe']
  21. });
  22. try {
  23. await waitForOutput(child, 'MailHub listening');
  24. const baseUrl = `http://127.0.0.1:${port}`;
  25. const login = await fetch(`${baseUrl}/api/login`, {
  26. method: 'POST',
  27. headers: { 'Content-Type': 'application/json' },
  28. body: JSON.stringify({ username: 'admin', password: 'password123' })
  29. });
  30. assert.equal(login.status, 200);
  31. const cookie = login.headers.get('set-cookie')?.split(';')[0] || '';
  32. assert.ok(cookie);
  33. const settings = await fetch(`${baseUrl}/api/admin/settings`, {
  34. headers: { Cookie: cookie }
  35. });
  36. assert.equal(settings.status, 200);
  37. assert.equal((await settings.json()).settings.mailHostname, 'ali.ss5.xyz');
  38. const exited = await waitForExit(child, 300);
  39. assert.equal(exited, false);
  40. } finally {
  41. child.kill('SIGTERM');
  42. await waitForExit(child, 1000);
  43. }
  44. });
  45. test('built auth assets are served before authentication', async () => {
  46. const assetName = readdirSync(path.join(process.cwd(), 'public', 'assets')).find((name) => /\.(js|css)$/.test(name));
  47. assert.ok(assetName, 'expected at least one built frontend asset');
  48. const port = await freePort();
  49. const child = spawn(process.execPath, ['src/server.js'], {
  50. cwd: process.cwd(),
  51. env: {
  52. ...process.env,
  53. PORT: String(port),
  54. DATA_DIR: mkdtempSync(path.join(tmpdir(), 'mailhub-server-test-')),
  55. ADMIN_PASSWORD: 'password123',
  56. SUBMISSION_ENABLED: 'false'
  57. },
  58. stdio: ['ignore', 'pipe', 'pipe']
  59. });
  60. try {
  61. await waitForOutput(child, 'MailHub listening');
  62. const baseUrl = `http://127.0.0.1:${port}`;
  63. const login = await fetch(`${baseUrl}/login`);
  64. assert.equal(login.status, 200);
  65. const asset = await fetch(`${baseUrl}/assets/${assetName}`, { redirect: 'manual' });
  66. assert.equal(asset.status, 200);
  67. assert.notEqual(asset.headers.get('location'), '/login');
  68. } finally {
  69. child.kill('SIGTERM');
  70. await waitForExit(child, 1000);
  71. }
  72. });
  73. function freePort() {
  74. return new Promise((resolve, reject) => {
  75. const server = net.createServer();
  76. server.listen(0, '127.0.0.1', () => {
  77. const address = server.address();
  78. server.close(() => {
  79. if (address && typeof address === 'object') resolve(address.port);
  80. else reject(new Error('Unable to allocate a test port.'));
  81. });
  82. });
  83. });
  84. }
  85. function waitForOutput(child, text) {
  86. return new Promise((resolve, reject) => {
  87. const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${text}`)), 5000);
  88. const chunks = [];
  89. const onData = (chunk) => {
  90. chunks.push(String(chunk));
  91. if (chunks.join('').includes(text)) {
  92. clearTimeout(timeout);
  93. child.stdout.off('data', onData);
  94. child.stderr.off('data', onData);
  95. resolve();
  96. }
  97. };
  98. child.stdout.on('data', onData);
  99. child.stderr.on('data', onData);
  100. child.once('exit', (code) => {
  101. clearTimeout(timeout);
  102. reject(new Error(`Server exited early with code ${code}: ${chunks.join('')}`));
  103. });
  104. });
  105. }
  106. function waitForExit(child, timeoutMs) {
  107. if (child.exitCode !== null) return Promise.resolve(true);
  108. return new Promise((resolve) => {
  109. const timeout = setTimeout(() => {
  110. child.off('exit', onExit);
  111. resolve(false);
  112. }, timeoutMs);
  113. const onExit = () => {
  114. clearTimeout(timeout);
  115. resolve(true);
  116. };
  117. child.once('exit', onExit);
  118. });
  119. }