-
Notifications
You must be signed in to change notification settings - Fork 906
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(tests): Add network fixture support for functional tests (#6203)
Add mountebank library to record and replay network fixtures so that you dont need Gate running to exercise the UI. Network fixtures can be relatively large. For the two simple tests written so far the fixtures are several hundred kilobytes. To avoid ballooning Deck's repo size I plan to place fixtures in a publicly accessible bucket and only have them downloaded when the test runner needs them.
- Loading branch information
Scott
authored
Dec 14, 2018
1 parent
c8488c1
commit c92d463
Showing
6 changed files
with
947 additions
and
29 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.