File size: 3,715 Bytes
26abd99 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import http from 'http';
import { AddressInfo } from 'net';
import { assert, expect } from 'chai';
import * as sinon from 'sinon';
import { createHttpServerMock } from '../test/helpers/http-server';
import { LoopbackHandler, LoopbackServer } from './loopback-server';
const DEFAULT_ADDRESS: AddressInfo = {
address: '127.0.0.1',
family: 'IPv4',
port: 12345,
};
describe('LoopbackServer', () => {
let handler: sinon.SinonStubbedInstance<LoopbackHandler>;
let server: LoopbackServer;
let fakeServer: sinon.SinonStubbedInstance<http.Server>;
beforeEach(() => {
handler = {
handleRequest: sinon.stub(),
handleError: sinon.stub(),
handleClose: sinon.stub(),
};
fakeServer = createHttpServerMock(DEFAULT_ADDRESS);
sinon.stub(http, 'createServer').returns(fakeServer);
server = new LoopbackServer(handler);
});
afterEach(() => {
sinon.restore();
server.dispose();
});
describe('dispose', () => {
it('does nothing when no server is running', () => {
// Required since `listening` is a readonly getter.
Object.defineProperty(fakeServer, 'listening', { get: () => false });
server.dispose();
sinon.assert.notCalled(fakeServer.close);
});
describe('when a server is listening', () => {
beforeEach(async () => {
await server.start();
});
it('closes the server', () => {
server.dispose();
sinon.assert.calledOnce(fakeServer.close);
});
it('does nothing if called more than once', () => {
server.dispose();
server.dispose();
sinon.assert.calledOnce(fakeServer.close);
});
});
});
describe('start', () => {
it('rejects when a port cannot be obtained', async () => {
fakeServer.address.returns(null);
await expect(server.start()).to.be.rejectedWith(
'Failed to acquire server port',
);
});
it("rejects if a proxied address' port cannot be parsed", async () => {
fakeServer.address.returns('not-a-port');
await expect(server.start()).to.be.rejectedWith(
'Failed to acquire server port',
);
});
it('resolves a port number when started successfully', async () => {
await expect(server.start()).to.eventually.equal(DEFAULT_ADDRESS.port);
});
it('resolves the existing promise if start is called more than once', async () => {
const firstStart = server.start();
const secondStart = server.start();
await expect(firstStart).to.eventually.equal(DEFAULT_ADDRESS.port);
await expect(secondStart).to.eventually.equal(DEFAULT_ADDRESS.port);
// Verifies that the underlying server listen was not called a second
// time.
sinon.assert.calledOnce(fakeServer.listen);
});
});
describe('server started', () => {
beforeEach(async () => {
await server.start();
});
it('invokes the handler for incoming requests', () => {
const req = {} as http.IncomingMessage;
const res = {} as http.ServerResponse;
fakeServer.emit('request', req, res);
sinon.assert.calledOnceWithExactly(handler.handleRequest, req, res);
});
it('invokes the handler on error', () => {
const err = new Error('test error');
fakeServer.emit('error', err);
assert.isDefined(handler.handleError);
sinon.assert.calledOnceWithExactly(handler.handleError, err);
});
it('invokes the handler on close', () => {
fakeServer.emit('close');
assert.isDefined(handler.handleClose);
sinon.assert.calledOnce(handler.handleClose);
});
});
});
|