| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703 |
- import assert from 'node:assert/strict';
- import { spawn, spawnSync } from 'node:child_process';
- import { mkdtempSync } from 'node:fs';
- import net from 'node:net';
- import { tmpdir } from 'node:os';
- import path from 'node:path';
- import process from 'node:process';
- import { test } from 'node:test';
- test('mailbox grants constrain admin, session, token, and send API access', async (t) => {
- const smtp = await startFakeSmtpServer();
- const readerSmtp = await startFakeSmtpServer();
- const fixture = await startTestServer(smtp.port);
- try {
- const seeded = seedMailboxAccessFixtures(fixture.dataDir, fixture.sessionSecret, readerSmtp.port);
- const adminCookie = await login(fixture.baseUrl, 'admin', 'password123');
- const ownerCookie = await login(fixture.baseUrl, 'access-owner', 'password123');
- const readerCookie = await login(fixture.baseUrl, 'access-reader', 'password123');
- const viewerCookie = await login(fixture.baseUrl, 'access-viewer', 'password123');
- await t.test('only administrators can list and replace mailbox grants', async () => {
- const ordinaryList = await requestJson(fixture.baseUrl, '/api/admin/mailbox-access', {
- cookie: readerCookie
- });
- assert.equal(ordinaryList.status, 403);
- const ordinaryReplace = await requestJson(
- fixture.baseUrl,
- `/api/admin/inbound-mailboxes/${seeded.sharedMailboxId}/access`,
- {
- method: 'PUT',
- cookie: readerCookie,
- body: { grants: [] }
- }
- );
- assert.equal(ordinaryReplace.status, 403);
- const malformed = await fetch(
- `${fixture.baseUrl}/api/admin/inbound-mailboxes/${seeded.sharedMailboxId}/access`,
- {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- Cookie: adminCookie
- },
- body: '{"grants":'
- }
- );
- assert.equal(malformed.status, 400);
- assert.equal(typeof (await malformed.json()).error, 'string');
- const initial = await requestJson(fixture.baseUrl, '/api/admin/mailbox-access', {
- cookie: adminCookie
- });
- assert.equal(initial.status, 200);
- const initialShared = findAdminMailbox(initial.body.mailboxes, seeded.sharedMailboxId);
- assert.equal(initialShared.owner.id, seeded.ownerUserId);
- assert.deepEqual(initialShared.grants, []);
- const replaced = await replaceGrants(
- fixture.baseUrl,
- adminCookie,
- seeded.sharedMailboxId,
- [
- { userId: seeded.readerUserId, permissions: ['receive', 'send'] },
- { userId: seeded.viewerUserId, permissions: ['view'] }
- ]
- );
- assert.equal(replaced.mailbox.id, seeded.sharedMailboxId);
- assert.equal(replaced.owner.id, seeded.ownerUserId);
- assert.deepEqual(grantPermissions(replaced, seeded.readerUserId), {
- view: true,
- receive: true,
- send: true
- });
- assert.deepEqual(grantPermissions(replaced, seeded.viewerUserId), {
- view: true,
- receive: false,
- send: false
- });
- const listed = await requestJson(fixture.baseUrl, '/api/admin/mailbox-access', {
- cookie: adminCookie
- });
- const listedShared = findAdminMailbox(listed.body.mailboxes, seeded.sharedMailboxId);
- assert.deepEqual(grantPermissions(listedShared, seeded.readerUserId), {
- view: true,
- receive: true,
- send: true
- });
- });
- await t.test('owned, effective, and all mailbox scopes stay distinct', async () => {
- assert.deepEqual(
- await mailboxAddresses(fixture.baseUrl, '/api/inbound-mailboxes?scope=owned', ownerCookie),
- ['other@access-owner.example', 'shared@access-owner.example']
- );
- assert.deepEqual(
- await mailboxAddresses(fixture.baseUrl, '/api/inbound-mailboxes?scope=owned', readerCookie),
- ['own@access-reader.example']
- );
- assert.deepEqual(
- await mailboxAddresses(fixture.baseUrl, '/api/inbound-mailboxes?scope=effective', readerCookie),
- ['own@access-reader.example', 'shared@access-owner.example']
- );
- assert.deepEqual(
- await mailboxAddresses(fixture.baseUrl, '/api/inbound-mailboxes?scope=effective', viewerCookie),
- ['shared@access-owner.example']
- );
- const ordinaryAll = await requestJson(fixture.baseUrl, '/api/inbound-mailboxes?scope=all', {
- cookie: readerCookie
- });
- assert.equal(ordinaryAll.status, 403);
- const adminAll = await requestJson(fixture.baseUrl, '/api/inbound-mailboxes?scope=all', {
- cookie: adminCookie
- });
- assert.equal(adminAll.status, 200);
- assert.deepEqual(
- adminAll.body.mailboxes.map((mailbox) => mailbox.address).sort(),
- [
- 'other@access-owner.example',
- 'own@access-reader.example',
- 'shared@access-owner.example'
- ]
- );
- const invalid = await requestJson(fixture.baseUrl, '/api/inbound-mailboxes?scope=unknown', {
- cookie: adminCookie
- });
- assert.equal(invalid.status, 400);
- });
- await t.test('receive grants allow message reads while view-only grants do not', async () => {
- const list = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.sharedMailboxId}&q=Shared%20grant`,
- { cookie: readerCookie }
- );
- assert.equal(list.status, 200);
- assert.equal(list.body.total, 1);
- assert.deepEqual(list.body.messages.map((message) => message.id), [seeded.sharedMessageId]);
- const detail = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- { cookie: readerCookie }
- );
- assert.equal(detail.status, 200);
- assert.equal(detail.body.message.subject, 'Shared grant message');
- const marked = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- {
- method: 'PATCH',
- cookie: readerCookie,
- body: { read: true }
- }
- );
- assert.equal(marked.status, 200);
- assert.equal(marked.body.message.read, true);
- const viewerList = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.sharedMailboxId}`,
- { cookie: viewerCookie }
- );
- assert.equal(viewerList.status, 200);
- assert.equal(viewerList.body.total, 0);
- const viewerDetail = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- { cookie: viewerCookie }
- );
- assert.equal(viewerDetail.status, 404);
- const viewerMark = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- {
- method: 'PATCH',
- cookie: viewerCookie,
- body: { read: false }
- }
- );
- assert.equal(viewerMark.status, 404);
- });
- await t.test('Bearer owner scope cannot expand into grants and selected scope is revoked immediately', async () => {
- const ownerToken = await createToken(fixture.baseUrl, readerCookie, {
- name: 'grant owner scope',
- scopes: ['mailboxes:read', 'messages:read'],
- mailboxAccess: 'owner'
- });
- const ownerMessages = await requestJson(fixture.baseUrl, '/api/inbound-messages?pageSize=100', {
- bearer: ownerToken.token
- });
- assert.equal(ownerMessages.status, 200);
- assert.deepEqual(ownerMessages.body.messages.map((message) => message.id), [seeded.readerMessageId]);
- const ownerMailboxes = await requestJson(fixture.baseUrl, '/api/mailboxes', {
- bearer: ownerToken.token
- });
- assert.deepEqual(ownerMailboxes.body.mailboxes.map((mailbox) => mailbox.id), [seeded.readerMailboxId]);
- const selectedToken = await createToken(fixture.baseUrl, readerCookie, {
- name: 'grant selected scope',
- scopes: ['mailboxes:read', 'messages:read'],
- mailboxAccess: 'selected',
- mailboxIds: [seeded.sharedMailboxId]
- });
- const selectedMessages = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.sharedMailboxId}`,
- { bearer: selectedToken.token }
- );
- assert.equal(selectedMessages.status, 200);
- assert.deepEqual(selectedMessages.body.messages.map((message) => message.id), [seeded.sharedMessageId]);
- const selectedDetail = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- { bearer: selectedToken.token }
- );
- assert.equal(selectedDetail.status, 200);
- await replaceGrants(
- fixture.baseUrl,
- adminCookie,
- seeded.sharedMailboxId,
- [{ userId: seeded.viewerUserId, permissions: ['view'] }]
- );
- const afterRevoke = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages?mailboxId=${seeded.sharedMailboxId}`,
- { bearer: selectedToken.token }
- );
- assert.equal(afterRevoke.status, 200);
- assert.equal(afterRevoke.body.total, 0);
- const detailAfterRevoke = await requestJson(
- fixture.baseUrl,
- `/api/inbound-messages/${seeded.sharedMessageId}`,
- { bearer: selectedToken.token }
- );
- assert.equal(detailAfterRevoke.status, 404);
- });
- await t.test('Bearer send scope is intersected with token mailbox selection and live send grants', async () => {
- await replaceGrants(
- fixture.baseUrl,
- adminCookie,
- seeded.sharedMailboxId,
- [{ userId: seeded.readerUserId, permissions: ['send'] }]
- );
- const ownerToken = await createToken(fixture.baseUrl, readerCookie, {
- name: 'send owner scope',
- scopes: ['send'],
- mailboxAccess: 'owner'
- });
- const selectedSharedToken = await createToken(fixture.baseUrl, readerCookie, {
- name: 'send selected shared mailbox',
- scopes: ['send'],
- mailboxAccess: 'selected',
- mailboxIds: [seeded.sharedMailboxId]
- });
- const selectedOwnToken = await createToken(fixture.baseUrl, readerCookie, {
- name: 'send selected owned mailbox',
- scopes: ['send'],
- mailboxAccess: 'selected',
- mailboxIds: [seeded.readerMailboxId]
- });
- const mailFromCount = smtpMailFromCommands(smtp).length;
- await assertSendDenied(fixture.baseUrl, {
- bearer: ownerToken.token,
- from: 'shared@access-owner.example'
- });
- await assertSendDenied(fixture.baseUrl, {
- bearer: selectedOwnToken.token,
- from: 'shared@access-owner.example'
- });
- const selectedSend = await sendApiMessage(fixture.baseUrl, {
- bearer: selectedSharedToken.token,
- from: 'shared@access-owner.example',
- subject: 'Selected token mailbox grant send'
- });
- assert.equal(selectedSend.status, 202, JSON.stringify(selectedSend.body));
- assert.equal(selectedSend.body.queued, true);
- assert.deepEqual(
- smtpMailFromCommands(smtp).slice(mailFromCount),
- ['MAIL FROM:<shared@access-owner.example>']
- );
- await replaceGrants(fixture.baseUrl, adminCookie, seeded.sharedMailboxId, []);
- await assertSendDenied(fixture.baseUrl, {
- bearer: selectedSharedToken.token,
- from: 'shared@access-owner.example'
- });
- assert.equal(smtpMailFromCommands(smtp).length, mailFromCount + 1);
- });
- await t.test('send grants authorize only the exact mailbox address and aliases', async () => {
- await replaceGrants(
- fixture.baseUrl,
- adminCookie,
- seeded.sharedMailboxId,
- [
- { userId: seeded.readerUserId, permissions: ['send'] },
- { userId: seeded.viewerUserId, permissions: ['view'] }
- ]
- );
- const mailFromCount = smtpMailFromCommands(smtp).length;
- const readerRelayMailFromCount = smtpMailFromCommands(readerSmtp).length;
- const forbiddenRelay = await sendApiMessage(fixture.baseUrl, {
- cookie: readerCookie,
- from: 'shared@access-owner.example',
- subject: 'Shared mailbox cannot use grantee relay',
- smtpRelayId: seeded.readerRelayId
- });
- assert.equal(forbiddenRelay.status, 403, JSON.stringify(forbiddenRelay.body));
- assert.match(forbiddenRelay.body.error, /域名所有者.*SMTP 出口/);
- assert.equal(smtpMailFromCommands(smtp).length, mailFromCount);
- assert.equal(smtpMailFromCommands(readerSmtp).length, readerRelayMailFromCount);
- for (const from of ['shared@access-owner.example', 'shared-alias@access-owner.example']) {
- const sent = await sendApiMessage(fixture.baseUrl, {
- cookie: readerCookie,
- from,
- subject: `Mailbox grant send from ${from}`
- });
- assert.equal(sent.status, 202, JSON.stringify(sent.body));
- assert.equal(sent.body.queued, true);
- }
- await assertSendDenied(fixture.baseUrl, {
- cookie: readerCookie,
- from: 'other@access-owner.example'
- });
- assert.deepEqual(
- smtpMailFromCommands(smtp).slice(mailFromCount),
- [
- 'MAIL FROM:<shared@access-owner.example>',
- 'MAIL FROM:<shared-alias@access-owner.example>'
- ]
- );
- });
- } finally {
- fixture.child.kill('SIGTERM');
- await waitForExit(fixture.child, 1000);
- await smtp.close();
- await readerSmtp.close();
- }
- });
- function seedMailboxAccessFixtures(dataDir, sessionSecret, readerRelayPort) {
- const script = `
- import {
- createDomain,
- createInboundMailbox,
- createInboundMessage,
- createUser,
- initDatabase,
- saveSmtpRelay
- } from './src/db.js';
- import { createDkimKeyPair } from './src/dkim.js';
- initDatabase(process.env.DATA_DIR, process.env.SESSION_SECRET);
- const owner = createUser({
- username: 'access-owner',
- email: 'access-owner@example.test',
- password: 'password123',
- status: 'active'
- });
- const reader = createUser({
- username: 'access-reader',
- email: 'access-reader@example.test',
- password: 'password123',
- status: 'active'
- });
- const viewer = createUser({
- username: 'access-viewer',
- email: 'access-viewer@example.test',
- password: 'password123',
- status: 'active'
- });
- const readerRelay = saveSmtpRelay(reader.id, {
- name: 'Reader private relay',
- host: '127.0.0.1',
- port: Number(process.env.READER_RELAY_PORT),
- secure: false,
- username: '',
- password: '',
- helo: 'mail.access-reader.example'
- });
- const keys = createDkimKeyPair();
- const createUserDomain = (user, name) => createDomain(user.id, {
- domain: name,
- selector: 'mh',
- verificationToken: 'verify-' + name,
- dkimPublic: keys.publicKey,
- dkimPrivate: keys.privateKey,
- senderHost: 'mail.' + name,
- sendingIp: '127.0.0.1',
- spfExtra: '',
- dmarcPolicy: 'none',
- dmarcRua: ''
- });
- createUserDomain(owner, 'access-owner.example');
- createUserDomain(reader, 'access-reader.example');
- const sharedMailbox = createInboundMailbox(owner.id, {
- address: 'shared@access-owner.example',
- password: 'mailbox-password',
- aliases: ['shared-alias']
- });
- const otherMailbox = createInboundMailbox(owner.id, {
- address: 'other@access-owner.example',
- password: 'mailbox-password'
- });
- const readerMailbox = createInboundMailbox(reader.id, {
- address: 'own@access-reader.example',
- password: 'mailbox-password'
- });
- const createMessage = (mailbox, subject, sequence) => createInboundMessage(mailbox, {
- sender: 'sender@example.net',
- recipients: [mailbox.address],
- subject,
- messageId: '<mailbox-access-' + sequence + '@example.net>',
- rawMessage: 'Subject: ' + subject + '\\r\\n\\r\\n' + subject,
- textBody: subject,
- receivedAt: '2026-07-18T0' + sequence + ':00:00.000Z'
- });
- const sharedMessage = createMessage(sharedMailbox, 'Shared grant message', 1);
- createMessage(otherMailbox, 'Other owner message', 2);
- const readerMessage = createMessage(readerMailbox, 'Reader owned message', 3);
- console.log(JSON.stringify({
- ownerUserId: owner.id,
- readerUserId: reader.id,
- viewerUserId: viewer.id,
- sharedMailboxId: sharedMailbox.id,
- otherMailboxId: otherMailbox.id,
- readerMailboxId: readerMailbox.id,
- readerRelayId: readerRelay.id,
- sharedMessageId: sharedMessage.id,
- readerMessageId: readerMessage.id
- }));
- `;
- const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- DATA_DIR: dataDir,
- SESSION_SECRET: sessionSecret,
- READER_RELAY_PORT: String(readerRelayPort)
- },
- encoding: 'utf8'
- });
- assert.equal(result.status, 0, result.stderr || result.stdout);
- return JSON.parse(result.stdout);
- }
- async function replaceGrants(baseUrl, adminCookie, mailboxId, grants) {
- const response = await requestJson(
- baseUrl,
- `/api/admin/inbound-mailboxes/${mailboxId}/access`,
- {
- method: 'PUT',
- cookie: adminCookie,
- body: { grants }
- }
- );
- assert.equal(response.status, 200, JSON.stringify(response.body));
- return response.body.mailbox;
- }
- function findAdminMailbox(mailboxes, mailboxId) {
- const mailbox = mailboxes.find((entry) => entry.mailbox.id === mailboxId);
- assert.ok(mailbox, `expected admin mailbox ${mailboxId}`);
- return mailbox;
- }
- function grantPermissions(mailboxAccess, userId) {
- const grant = mailboxAccess.grants.find((entry) => entry.user.id === userId);
- assert.ok(grant, `expected grant for user ${userId}`);
- return grant.permissions;
- }
- async function mailboxAddresses(baseUrl, pathname, cookie) {
- const response = await requestJson(baseUrl, pathname, { cookie });
- assert.equal(response.status, 200, JSON.stringify(response.body));
- return response.body.mailboxes.map((mailbox) => mailbox.address).sort();
- }
- async function createToken(baseUrl, cookie, input) {
- const response = await requestJson(baseUrl, '/api/api-tokens', {
- method: 'POST',
- cookie,
- body: input
- });
- assert.equal(response.status, 201, JSON.stringify(response.body));
- assert.ok(response.body.token.token);
- return response.body.token;
- }
- function sendApiMessage(baseUrl, {
- cookie = '',
- bearer = '',
- from,
- subject = 'Mailbox grant send',
- smtpRelayId = null
- }) {
- return requestJson(baseUrl, '/api/send', {
- method: 'POST',
- cookie,
- bearer,
- body: {
- from,
- to: 'recipient@example.net',
- subject,
- text: 'mailbox grant send test',
- ...(smtpRelayId ? { smtpRelayId } : {})
- }
- });
- }
- async function assertSendDenied(baseUrl, input) {
- const response = await sendApiMessage(baseUrl, {
- ...input,
- subject: 'Mailbox grant denial'
- });
- assert.equal(response.status, 403, JSON.stringify(response.body));
- assert.match(response.body.error, /未获得发信权限/);
- }
- function smtpMailFromCommands(smtp) {
- return smtp.commands.filter((command) => command.startsWith('MAIL FROM:'));
- }
- async function requestJson(baseUrl, pathname, {
- method = 'GET',
- cookie = '',
- bearer = '',
- body
- } = {}) {
- const headers = {};
- if (cookie) headers.Cookie = cookie;
- if (bearer) headers.Authorization = `Bearer ${bearer}`;
- if (body !== undefined) headers['Content-Type'] = 'application/json';
- const response = await fetch(`${baseUrl}${pathname}`, {
- method,
- headers,
- body: body === undefined ? undefined : JSON.stringify(body),
- redirect: 'manual'
- });
- const text = await response.text();
- return {
- status: response.status,
- body: text ? JSON.parse(text) : null
- };
- }
- async function login(baseUrl, username, password) {
- const response = await fetch(`${baseUrl}/api/login`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ username, password })
- });
- assert.equal(response.status, 200);
- const cookie = response.headers.get('set-cookie')?.split(';')[0] || '';
- assert.ok(cookie);
- return cookie;
- }
- async function startTestServer(smtpPort) {
- const port = await freePort();
- const dataDir = mkdtempSync(path.join(tmpdir(), 'mailhub-mailbox-access-'));
- const sessionSecret = 'mailbox-access-session-secret';
- const child = spawn(process.execPath, ['src/server.js'], {
- cwd: process.cwd(),
- env: {
- ...process.env,
- PORT: String(port),
- DATA_DIR: dataDir,
- SESSION_SECRET: sessionSecret,
- ADMIN_USER: 'admin',
- ADMIN_EMAIL: 'admin@example.test',
- ADMIN_PASSWORD: 'password123',
- DNS_AUTO_CHECK_ENABLED: 'false',
- DELIVERY_TRACKING_ENABLED: 'false',
- WEBHOOK_WORKER_ENABLED: 'false',
- SUBMISSION_ENABLED: 'false',
- IMAP_ENABLED: 'false',
- POP3_ENABLED: 'false',
- SMTP_HOST: '127.0.0.1',
- SMTP_PORT: String(smtpPort),
- SMTP_SECURE: 'false',
- SMTP_HELO: 'mailhub-access.test'
- },
- stdio: ['ignore', 'pipe', 'pipe']
- });
- await waitForOutput(child, 'MailHub listening');
- return { child, baseUrl: `http://127.0.0.1:${port}`, dataDir, sessionSecret };
- }
- function startFakeSmtpServer() {
- const commands = [];
- const server = net.createServer((socket) => {
- socket.setEncoding('utf8');
- socket.write('220 relay.test ESMTP ready\r\n');
- let buffer = '';
- let dataMode = false;
- socket.on('data', (chunk) => {
- buffer += chunk;
- let index;
- while ((index = buffer.indexOf('\n')) !== -1) {
- const line = buffer.slice(0, index).replace(/\r$/, '');
- buffer = buffer.slice(index + 1);
- if (dataMode) {
- if (line === '.') {
- dataMode = false;
- socket.write('250 2.0.0 queued as ACCESS123\r\n');
- }
- continue;
- }
- commands.push(line);
- if (line.startsWith('EHLO')) socket.write('250 relay.test\r\n');
- else if (line.startsWith('MAIL FROM:')) socket.write('250 2.1.0 ok\r\n');
- else if (line.startsWith('RCPT TO:')) socket.write('250 2.1.5 ok\r\n');
- else if (line === 'DATA') {
- dataMode = true;
- socket.write('354 end data\r\n');
- } else if (line === 'QUIT') {
- socket.write('221 bye\r\n');
- socket.end();
- }
- }
- });
- });
- return new Promise((resolve, reject) => {
- server.once('error', reject);
- server.listen(0, '127.0.0.1', () => {
- server.off('error', reject);
- resolve({
- port: server.address().port,
- commands,
- close: () => new Promise((closeResolve) => server.close(closeResolve))
- });
- });
- });
- }
- 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.'));
- });
- });
- server.on('error', reject);
- });
- }
- function waitForOutput(child, text, timeoutMs = 8000) {
- return new Promise((resolve, reject) => {
- let output = '';
- const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${text}\n${output}`)), timeoutMs);
- const onData = (chunk) => {
- output += chunk.toString();
- if (!output.includes(text)) return;
- 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 before startup with code ${code}\n${output}`));
- });
- });
- }
- function waitForExit(child, timeoutMs) {
- if (child.exitCode !== null) return Promise.resolve(child.exitCode);
- return new Promise((resolve) => {
- const timeout = setTimeout(() => resolve(null), timeoutMs);
- child.once('exit', (code) => {
- clearTimeout(timeout);
- resolve(code);
- });
- });
- }
|