Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(tests): Add network fixture support for functional tests #6203

Merged
1 commit merged into from Dec 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions package.json
Expand Up @@ -120,6 +120,7 @@
"@types/react-sortable-hoc": "^0.6.2",
"@types/react-virtualized": "9.7.12",
"@types/react-virtualized-select": "^3.0.3",
"@types/request-promise-native": "^1.0.15",
"@types/webdriverio": "^4.13.0",
"@types/webpack": "4.1.0",
"@types/webpack-env": "^1.13.5",
Expand Down Expand Up @@ -163,6 +164,7 @@
"loader-utils": "^1.1.0",
"md5": "^2.2.1",
"minimist": "^1.2.0",
"mountebank": "^1.15.0",
"ngtemplate-loader": "^1.3.1",
"node-libs-browser": "^2.0.0",
"physical-cpu-count": "^2.0.0",
Expand All @@ -172,6 +174,7 @@
"pretty-quick": "^1.4.1",
"react-addons-test-utils": "15.6.2",
"react-test-renderer": "16.3.2",
"request-promise-native": "^1.0.5",
"rimraf": "^2.5.4",
"style-loader": "^0.20.3",
"thread-loader": "^1.1.5",
Expand Down
80 changes: 80 additions & 0 deletions test/functional/README.md
@@ -0,0 +1,80 @@
# Deck Functional Tests

## Recording Network Fixtures

Usage of fixtures goes as follows:

1. Create a mountebank control server. For now this is managed manually but will soon be coordinated by a script:

```
$ node
> require('ts-node/register');
{}
> const { MountebankService } = require('./test/functional/tools/MountebankService.ts');
undefined
> MountebankService.builder().
... mountebankPath(process.cwd() + '/node_modules/.bin/mb').
... onStdOut(data => { console.log('mb stdout: ' + String(data)); }).
... onStdErr(data => { console.log('mb stderr: ' + String(data)); }).
... build().launchServer();
Promise {
<pending>,
domain:
Domain {
domain: null,
_events: { error: [Function: debugDomainError] },
_eventsCount: 1,
_maxListeners: undefined,
members: [] } }
> mb stdout: info: [mb:2525] mountebank v1.15.0 now taking orders - point your browser to http://localhost:2525 for help
```

2. Launch Gate on a different port. We want 8084 to be free for the mitm proxy that will record the network traffic. Open `~/.hal/default/service-settings/gate.yml` and add these contents:

```
port: 18084
```

3. Restart Gate:

```
hal deploy apply --service-names gate
```

4. Record a fixture for a specific test:

```
$ ./node_modules/.bin/wdio wdio.conf.js --record-fixtures --spec test/functional/tests/core/home.spec.ts

DEPRECATION: Setting specFilter directly on Env is deprecated, please use the specFilter option in `configure`
DEPRECATION: Setting stopOnSpecFailure directly is deprecated, please use the failFast option in `configure`
․wrote fixture to ~/dev/spinnaker/deck/test/functional/tests/core/home.spec.ts.mountebank_fixture.json


1 passing (5.60s)
```

5. Kill the Gate process. On Mac this would go something like:

```
kill -15 $(lsof -t -i tcp:18084)
```

6. Run the test again without Gate running, instructing the test runner to create a network imposter:

```
$ ./node_modules/.bin/wdio wdio.conf.js --replay-fixtures --spec test/functional/tests/core/home.spec.ts

DEPRECATION: Setting specFilter directly on Env is deprecated, please use the specFilter option in `configure`
DEPRECATION: Setting stopOnSpecFailure directly is deprecated, please use the failFast option in `configure`
Creating imposter from fixture file ~/dev/spinnaker/deck/test/functional/tests/core/home.spec.ts.mountebank_fixture.json

1 passing (6.00s)
```

The mountebank server will still be running on port 2525 but can easily be exited by calling:

```
kill -15 $(lsof -t -i tcp:2525)
```
14 changes: 14 additions & 0 deletions test/functional/tools/FixtureService.ts
@@ -0,0 +1,14 @@
import * as path from 'path';

export class FixtureService {
constructor() {}

public fixtureNameForTestPath(testpath: string) {
const basename = path.basename(testpath);
return basename + '.mountebank_fixture.json';
}

public fixturePathForTestPath(testpath: string) {
return path.join(path.dirname(testpath), this.fixtureNameForTestPath(testpath));
}
}
191 changes: 191 additions & 0 deletions test/functional/tools/MountebankService.ts
@@ -0,0 +1,191 @@
import * as fs from 'fs';
import { spawn, ChildProcess } from 'child_process';
import * as request from 'request-promise-native';

const STARTUP_TIMEOUT_MS = 5000;

export class MountebankService {
private process: ChildProcess;

public static builder(): MountebankServiceBuilder {
return new MountebankServiceBuilder();
}

constructor(private options: MountebankServiceOptions) {}

public launchServer(): Promise<any | Error> {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('mountebank server took too long to start');
}, STARTUP_TIMEOUT_MS);
if (this.process == null) {
this.process = spawn(this.options.mountebankPath, ['--port', String(this.options.mountebankPort)]);
this.process.stdout.on('data', data => {
const str = String(data);
if (str.includes('now taking orders')) {
resolve();
}
this.options.onStdOut(str);
});
this.process.stderr.on('data', data => {
const str = String(data);
reject(str);
this.options.onStdErr(str);
});
this.process.on('close', code => {
this.options.onClose(code);
});
}
});
}

public kill() {
if (this.process) {
this.process.kill();
this.process = null;
}
}

public createImposterFromFixtureFile(filepath: string): request.RequestPromise<any> | Promise<any> {
console.log('Creating imposter from fixture file', filepath);
try {
const rawFixture = fs.readFileSync(filepath, { encoding: 'utf8' });
const fixture = JSON.parse(rawFixture);
if (fixture) {
return request({
method: 'post',
json: true,
uri: `http://localhost:${this.options.mountebankPort}/imposters`,
body: fixture,
});
} else {
throw new Error(`no fixture found: ${filepath}`);
}
} catch (e) {
// Clean up on failure
return this.removeImposters().then(() => {
throw e;
});
}
}

public removeImposters(): request.RequestPromise<any> {
return request({
method: 'delete',
json: true,
uri: `http://localhost:${this.options.mountebankPort}/imposters`,
body: {
port: this.options.gatePort,
protocol: 'http',
stubs: [
{
responses: [
{
proxy: {
to: `http://localhost:${this.options.imposterPort}`,
mode: 'proxyTransparent',
predicateGenerators: [
{
matches: { method: true, path: true, query: true },
},
],
},
},
],
},
],
},
});
}

public beginRecording(): request.RequestPromise<any> {
return request.post({
method: 'post',
json: true,
uri: `http://localhost:${this.options.mountebankPort}/imposters`,
body: {
port: this.options.imposterPort,
protocol: 'http',
stubs: [
{
responses: [
{
proxy: {
to: `http://localhost:${this.options.gatePort}`,
predicateGenerators: [
{
matches: { method: true, path: true, query: true },
caseSensitive: true,
},
],
},
},
],
},
],
},
});
}

public saveRecording(filepath: string): Promise<any> {
const { mountebankPort, imposterPort } = this.options;
return request
.get(`http://localhost:${mountebankPort}/imposters/${imposterPort}?replayable=true&removeProxies=true`)
.then((res: any) => {
fs.writeFileSync(filepath, res);
});
}
}

export class MountebankServiceOptions {
public mountebankPath: string = 'node_modules/.bin/mb';
public mountebankPort: number = 2525; // Mountebank controller runs on this port
public gatePort: number = 18084; // Gate running on this port
public imposterPort: number = 8084; // port Deck will send requests to; Mountebank will insert an imposter here
public onStdOut = (_data: string) => {};
public onStdErr = (_data: string) => {};
public onClose = (_code: number) => {};
}

export class MountebankServiceBuilder {
private options: MountebankServiceOptions = new MountebankServiceOptions();

mountebankPath(p: string): MountebankServiceBuilder {
this.options.mountebankPath = p;
return this;
}

mountebankPort(p: number): MountebankServiceBuilder {
this.options.mountebankPort = p;
return this;
}

gatePort(p: number): MountebankServiceBuilder {
this.options.gatePort = p;
return this;
}

imposterPort(p: number): MountebankServiceBuilder {
this.options.imposterPort = p;
return this;
}

onStdOut(fn: (data: string) => void) {
this.options.onStdOut = fn;
return this;
}

onStdErr(fn: (data: string) => void) {
this.options.onStdErr = fn;
return this;
}

onClose(fn: (code: number) => void) {
this.options.onClose = fn;
return this;
}

build(): MountebankService {
return new MountebankService(this.options);
}
}
42 changes: 42 additions & 0 deletions wdio.conf.js
Expand Up @@ -4,9 +4,16 @@ const fs = require('fs');
const path = require('path');
const process = require('process');
const minimist = require('minimist');
const { MountebankService } = require('./test/functional/tools/MountebankService');
const { FixtureService } = require('./test/functional/tools/FixtureService');

const flags = minimist(process.argv.slice(2), {
default: {
'replay-fixtures': false,
'record-fixtures': false,
'mountebank-port': 2525,
'gate-port': 18084,
'imposter-port': 8084,
browser: 'chrome',
headless: false,
savelogs: false,
Expand All @@ -18,6 +25,15 @@ if (flags.savelogs && flags.browser !== 'chrome') {
flags.savelogs = false;
}

const mountebankService = MountebankService.builder()
.mountebankPath(path.resolve(__dirname, './node_modules/.bin/mb'))
.mountebankPort(flags['mountebank-port'])
.gatePort(flags['gate-port'])
.imposterPort(flags['imposter-port'])
.build();

let testRun = null;

const config = {
specs: ['test/functional/tests/**/*.spec.ts'],
maxInstances: 1,
Expand Down Expand Up @@ -50,9 +66,35 @@ const config = {

beforeTest: function(test) {
browser.windowHandleSize({ width: 1280, height: 1024 });
if (!flags['replay-fixtures'] && !flags['record-fixtures']) {
return;
}
const fixtureService = new FixtureService();
testRun = { fixtureFile: fixtureService.fixturePathForTestPath(test.file) };
return mountebankService.removeImposters().then(() => {
if (flags['record-fixtures']) {
return mountebankService.beginRecording();
} else {
return mountebankService.createImposterFromFixtureFile(testRun.fixtureFile);
}
});
},

afterTest: function(test) {
if (flags['record-fixtures']) {
if (test.passed) {
mountebankService
.saveRecording(testRun.fixtureFile)
.then(() => {
console.log(`wrote fixture to ${testRun.fixtureFile}`);
})
.catch(err => {
console.log(`error saving recording: ${err}`);
});
} else {
console.log(`test failed: "${test.fullName}"; network fixture will not be saved.`);
}
}
if (flags.savelogs && browser.sessionId) {
const outPath = path.resolve(__dirname, './' + browser.sessionId + '.browser.log');
fs.writeFileSync(outPath, JSON.stringify(browser.log('browser'), null, 4));
Expand Down