Skip to content
Permalink
Browse files
fix(local-server): corrected issue where local server could come back…
… on a random port after restart

Replaced get-port with portfinder to avoid issue sindresorhus/get-port#43
  • Loading branch information
jan-molak committed Jul 4, 2020
1 parent bb3b027 commit 32f18b9dbd2278e0874635f7be9727aa0b90a6ae
Show file tree
Hide file tree
Showing 8 changed files with 304 additions and 58 deletions.
@@ -50,7 +50,7 @@
"npm": ">= 6"
},
"dependencies": {
"get-port": "^5.1.1",
"portfinder": "^1.0.26",
"http-shutdown": "^1.2.2"
},
"peerDependencies": {
@@ -70,7 +70,8 @@
"express": "^4.17.1",
"hapi": "^18.1.0",
"koa": "^2.12.0",
"restify": "^8.5.1"
"restify": "^8.5.1",
"semver": "^7.3.2"
},
"nyc": {
"include": [
@@ -2,9 +2,8 @@ import 'mocha';

import { EventRecorder, expect, PickEvent } from '@integration/testing-tools';
import { endsWith, Ensure, equals, not, startsWith } from '@serenity-js/assertions';
import { Actor, actorCalled, Cast, ConfigurationError, configure, Log } from '@serenity-js/core';
import { Actor, actorCalled, Cast, configure } from '@serenity-js/core';
import { ActivityFinished, ActivityStarts } from '@serenity-js/core/lib/events';
import { Photo } from '@serenity-js/core/lib/model';
import { CallAnApi, GetRequest, LastResponse, Send } from '@serenity-js/rest';
import axios from 'axios';

@@ -53,8 +52,8 @@ describe('@serenity-js/local-server', () => {
)).to.be.fulfilled.then(() => {

PickEvent.from(recorder.events)
.next(ActivityStarts, hasName(`Nadia starts the local server`))
.next(ActivityFinished, hasName(`Nadia starts the local server`))
.next(ActivityStarts, hasName(`Nadia starts local server on a random port`))
.next(ActivityFinished, hasName(`Nadia starts local server on a random port`))
.next(ActivityFinished, hasName(`Nadia ensures that the URL of the local server does start with 'http://127.0.0.1'`))
.next(ActivityFinished, hasName(`Nadia sends a GET request to the URL of the local server`))
.next(ActivityFinished, hasName(`Nadia ensures that the status of the last response does equal 200`))
@@ -0,0 +1,51 @@
import 'mocha';

import { expect } from '@integration/testing-tools';
import { Ensure, equals } from '@serenity-js/assertions';
import { Actor, actorCalled, actorInTheSpotlight, Cast, configure, Log } from '@serenity-js/core';
import { CallAnApi } from '@serenity-js/rest';
import axios from 'axios';
import { given } from 'mocha-testdata';
import { satisfies } from 'semver';
import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '../src';
import servers = require('./servers');

describe('ManageALocalServer', () => {

/** @test {ManageALocalServer} */
describe('restarting', () => {

given(servers).
it('allows the Actor to restart a server on the same port multiple times', function ({ handler, node }) {
if (! satisfies(process.versions.node, node)) {
return this.skip();
}

class Actors implements Cast {
prepare(actor: Actor): Actor {
return actor.whoCan(
ManageALocalServer.runningAHttpListener(handler()),
CallAnApi.using(axios.create()),
);
}
}

configure({
actors: new Actors(),
});

return expect(actorCalled('Nadia').attemptsTo(
StartLocalServer.onPort(30000),
Ensure.that(LocalServer.url(), equals('http://127.0.0.1:30000')),
StopLocalServer.ifRunning(),
StartLocalServer.onPort(30000),
Log.the(LocalServer.url()),
Ensure.that(LocalServer.url(), equals('http://127.0.0.1:30000')),
)).to.be.fulfilled; // tslint:disable-line:no-unused-expression
});

afterEach(() => actorInTheSpotlight().attemptsTo(
StopLocalServer.ifRunning(),
));
});
});
@@ -0,0 +1,73 @@
import 'mocha';

import { expect } from '@integration/testing-tools';
import { and, Ensure, equals, isGreaterThan, isLessThan, or } from '@serenity-js/assertions';
import { Actor, actorCalled, actorInTheSpotlight, Cast, configure } from '@serenity-js/core';
import { given } from 'mocha-testdata';
import { Server } from 'net';
import { satisfies } from 'semver';
import { RequestListener } from '../../../src/screenplay/abilities';

import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '../../../src';
import servers = require('../../servers');

/** @test {StartALocalServer} */
/** @test {LocalServer.url} */
/** @test {LocalServer.port} */
describe('StartALocalServer', () => {

class Actors implements Cast {
constructor(private readonly listener: () => RequestListener | Server) {
}

prepare(actor: Actor): Actor {
return actor.whoCan(ManageALocalServer.runningAHttpListener(this.listener()));
}
}

given(servers).
it('allows the Actor to start the server on a preferred port', function ({ handler, node }) {
if (! satisfies(process.versions.node, node)) {
return this.skip();
}

configure({ actors: new Actors(handler) });

return expect(actorCalled('Nadia').attemptsTo(
StartLocalServer.onPort(30000),
Ensure.that(LocalServer.port(), equals(30000)),
)).to.be.fulfilled; // tslint:disable-line:no-unused-expression
});

given(servers).
it('allows the Actor to start the server on a random port', function ({ handler, node }) {
if (! satisfies(process.versions.node, node)) {
return this.skip();
}

configure({ actors: new Actors(handler) });

return expect(actorCalled('Nadia').attemptsTo(
StartLocalServer.onRandomPort(),
Ensure.that(LocalServer.port(), and(or(equals(8000), isGreaterThan(8000)), or(isLessThan(65535), equals(65535)))),
)).to.be.fulfilled; // tslint:disable-line:no-unused-expression
});

given(servers).
it('allows the Actor to start the server on a random port within a range', function ({ handler, node }) {
if (! satisfies(process.versions.node, node)) {
return this.skip();
}

configure({ actors: new Actors(handler) });

return expect(actorCalled('Nadia').attemptsTo(
StartLocalServer.onRandomPortBetween(8080, 9090),
Ensure.that(LocalServer.port(), and(or(equals(8080), isGreaterThan(8080)), or(isLessThan(9090), equals(9090)))),
)).to.be.fulfilled; // tslint:disable-line:no-unused-expression
});

afterEach(() => actorInTheSpotlight().attemptsTo(
StopLocalServer.ifRunning(),
));
});
@@ -12,9 +12,9 @@ import { satisfies } from 'semver'; // tslint:disable-line:no-implicit-dependenc
import { LocalServer, ManageALocalServer, StartLocalServer, StopLocalServer } from '../src';
import servers = require('./servers');

/** @test {ManageALocalServer} */
describe('ManageALocalServer', () => {

/** @test {ManageALocalServer} */
describe('when working with HTTP', () => {

given(servers).
@@ -71,6 +71,7 @@ describe('ManageALocalServer', () => {

// ---

/** @test {ManageALocalServer} */
describe('when working with HTTPS', () => {

const testHttpsServer = [
@@ -1,9 +1,9 @@
import { Ability, ConfigurationError, UsesAbilities } from '@serenity-js/core';
import getPort = require('get-port');
import * as http from 'http';
import withShutdownSupport = require('http-shutdown');
import * as https from 'https';
import * as net from 'net';
import { getPortPromise } from 'portfinder';

/**
* @desc
@@ -65,7 +65,7 @@ export class ManageALocalServer implements Ability {
* @desc
* {@link @serenity-js/core/lib/screenplay~Ability} to manage a Node.js HTTPS server using the provided server `requestListener`.
*
* @param {RequestListener | net.Server} listener
* @param {RequestListener | https~Server} listener
* @param {https~ServerOptions} options - Accepts options from `tls.createServer()`, `tls.createSecureContext()` and `http.createServer()`.
* @returns {ManageALocalServer}
*
@@ -105,29 +105,35 @@ export class ManageALocalServer implements Ability {

/**
* @desc
* Starts the server on the first available of the `preferredPorts`.
* Starts the server on the first free port between `preferredPort` and `highestPort`, inclusive.
*
* @param {number} [preferredPort=8000]
* Lower bound of the preferred port range
*
* @param {number} [highestPort=65535] highestPort
* Upper bound of the preferred port range
*
* @param {number[]} preferredPorts - If the provided list is empty the server will be started on a random port
* @returns {Promise<void>}
*/
listen(preferredPorts: number[]): Promise<void> {
return getPort({ port: preferredPorts }).then(port => new Promise<void>((resolve, reject) => {
function errorHandler(error: Error & {code: string}) {
if (error.code === 'EADDRINUSE') {
return reject(new ConfigurationError(`Server address is in use. Is there another server running on port ${ port }?`, error));
listen(preferredPort: number = 8000, highestPort: number = 65535): Promise<void> {
return getPortPromise({ port: preferredPort, stopPort: highestPort })
.then(port => new Promise<void>((resolve, reject) => {
function errorHandler(error: Error & {code: string}) {
if (error.code === 'EADDRINUSE') {
return reject(new ConfigurationError(`Server address is in use. Is there another server running on port ${ port }?`, error));
}

return reject(error);
}

return reject(error);
}

this.server.once('error', errorHandler);
this.server.once('error', errorHandler);

this.server.listen(port, '127.0.0.1', () => {
this.server.removeListener('error', errorHandler);
this.server.listen(port, '127.0.0.1', () => {
this.server.removeListener('error', errorHandler);

resolve();
});
}));
resolve();
});
}));
}

/**
@@ -165,7 +171,10 @@ export type RequestListener = (request: http.IncomingMessage, response: http.Ser
*
* @typedef {net~Server & { shutdown: (callback: (error?: Error) => void) => void }} ServerWithShutdown
*/
export type ServerWithShutdown = net.Server & { shutdown: (callback: (error?: Error) => void) => void };
export type ServerWithShutdown = net.Server & {
shutdown: (callback: (error?: Error) => void) => void,
forceShutdown: (callback: (error?: Error) => void) => void,
};

/**
* @desc

0 comments on commit 32f18b9

Please sign in to comment.