mirror of
https://github.com/balena-io/balena-cli.git
synced 2025-02-20 17:33:18 +00:00
Add tests for standalone executable via proxy server
Change-type: patch
This commit is contained in:
parent
2c01f8adee
commit
7e1d58546c
@ -12,7 +12,8 @@ The balena CLI is an open source project and your contribution is welcome!
|
||||
In order to ease development:
|
||||
|
||||
* `npm run build:fast` skips some of the build steps for interactive testing, or
|
||||
* `./bin/balena-dev` uses `ts-node/register` and `coffeescript/register` to transpile on the fly.
|
||||
* `npm run test:source` skips testing the standalone zip packages (which is rather slow)
|
||||
* `./bin/balena-dev` uses `ts-node/register` to transpile on the fly.
|
||||
|
||||
Before opening a PR, test your changes with `npm test`. Keep compatibility in mind, as the CLI is
|
||||
meant to run on Linux, macOS and Windows. balena CI will run test code on all three platforms, but
|
||||
@ -121,22 +122,15 @@ $ npm install # "cleanly" update the npm-shrinkwrap.json file
|
||||
$ git add npm-shrinkwrap.json # add it for committing (solve merge errors)
|
||||
```
|
||||
|
||||
## TypeScript vs CoffeeScript, and Capitano vs oclif
|
||||
## TypeScript and oclif
|
||||
|
||||
The CLI was originally written in [CoffeeScript](https://coffeescript.org), but we decided to
|
||||
migrate to [TypeScript](https://www.typescriptlang.org/) in order to take advantage of static
|
||||
typing and formal programming interfaces. The migration is taking place gradually, as part of
|
||||
maintenance work or the implementation of new features. The recommended way of making the
|
||||
conversion is to first generate plain Javascript, for example using the command:
|
||||
|
||||
```
|
||||
npx decaffeinate --use-js-modules file.coffee
|
||||
```
|
||||
|
||||
Then manually convert plain Javascript to Typescript. There is also a ["Coffeescript Preview"
|
||||
Visual Studio Code
|
||||
extension](https://marketplace.visualstudio.com/items?itemName=drewbarrett.vscode-coffeescript-preview)
|
||||
that you may find handy.
|
||||
The CLI currently contains a mix of plain JavaScript and
|
||||
[TypeScript](https://www.typescriptlang.org/) code. The goal is to have all code written in
|
||||
Typescript, in order to take advantage of static typing and formal programming interfaces.
|
||||
The migration towards Typescript is taking place gradually, as part of maintenance work or
|
||||
the implementation of new features. Historically, the CLI was originally written in
|
||||
[CoffeeScript](https://coffeescript.org), but all CoffeeScript code was migrated to either
|
||||
Javascript or Typescript.
|
||||
|
||||
Similarly, [Capitano](https://github.com/balena-io/capitano) was originally adopted as the CLI's
|
||||
framework, but later we decided to take advantage of [oclif](https://oclif.io/)'s features such
|
||||
@ -230,3 +224,4 @@ gotchas to bear in mind:
|
||||
`node_modules/balena-sdk/node_modules/balena-errors`
|
||||
In the case of subclasses of `TypedError`, a string comparison may be used instead:
|
||||
`error.name === 'BalenaApplicationNotFound'`
|
||||
|
||||
|
@ -162,12 +162,21 @@ async function setupGlobalAgentProxy(
|
||||
privateNoProxy.push(`172.${i}.*`);
|
||||
}
|
||||
|
||||
// BALENARC_DO_PROXY is a list of entries to exclude from BALENARC_NO_PROXY
|
||||
// (so "do proxy" takes precedence over "no proxy"). It is an undocumented
|
||||
// feature/hack added to facilitate testing of the CLI's standalone executable
|
||||
// through a local proxy server, by setting BALENARC_DO_PROXY="localhost,127.0.0.1"
|
||||
// See also runCommandInSubprocess() function in `tests/helpers.ts`.
|
||||
const doProxy = (process.env.BALENARC_DO_PROXY || '').split(',');
|
||||
|
||||
const env = process.env;
|
||||
env.GLOBAL_AGENT_ENVIRONMENT_VARIABLE_NAMESPACE = '';
|
||||
env.NO_PROXY = [
|
||||
...requiredNoProxy,
|
||||
...(noProxy ? noProxy.split(',').filter(v => v) : privateNoProxy),
|
||||
].join(',');
|
||||
]
|
||||
.filter(i => !doProxy.includes(i))
|
||||
.join(',');
|
||||
|
||||
if (proxy) {
|
||||
const proxyUrl: string =
|
||||
|
@ -208,6 +208,7 @@ export class DeviceAPI {
|
||||
'Non-200 response from log streaming endpoint',
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
res.socket.setKeepAlive(true, 1000);
|
||||
if (os.platform() !== 'win32') {
|
||||
|
@ -101,7 +101,12 @@ async function getBuilderEndpoint(
|
||||
});
|
||||
// Note that using https (rather than http) is a requirement when using the
|
||||
// --registry-secrets feature, as the secrets are not otherwise encrypted.
|
||||
return `https://builder.${baseUrl}/v3/build?${args}`;
|
||||
let builderUrl =
|
||||
process.env.BALENARC_BUILDER_URL || `https://builder.${baseUrl}`;
|
||||
if (builderUrl.endsWith('/')) {
|
||||
builderUrl = builderUrl.slice(0, -1);
|
||||
}
|
||||
return `${builderUrl}/v3/build?${args}`;
|
||||
}
|
||||
|
||||
export async function startRemoteBuild(build: RemoteBuild): Promise<void> {
|
||||
|
83
npm-shrinkwrap.json
generated
83
npm-shrinkwrap.json
generated
@ -1050,6 +1050,15 @@
|
||||
"integrity": "sha512-JPhCJHHu5/5HuQ78rmbrnCk+XlyxKuAopk+/8rP5MfAeL7KwiCH/gFFNtAqIr0/JFqWFk+jXWZjEWSP8dzGpMg==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/http-proxy": {
|
||||
"version": "1.17.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.4.tgz",
|
||||
"integrity": "sha512-IrSHl2u6AWXduUaDLqYpt45tLVCtYv7o4Z0s1KghBCDgIIS9oW5K1H8mZG/A2CfeLdEa7rTd1ACOiHBc1EMT2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/inquirer": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-6.5.0.tgz",
|
||||
@ -4153,31 +4162,45 @@
|
||||
}
|
||||
},
|
||||
"cross-env": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-3.2.4.tgz",
|
||||
"integrity": "sha1-ngWF8neGTtQhznVvgamA/w1piro=",
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.2.tgz",
|
||||
"integrity": "sha512-KZP/bMEOJEDCkDQAyRhu3RL2ZO/SUVrxQVI0G3YEQ+OLbRA3c6zgixe8Mq8a/z7+HKlNEjo8oiLUs8iRijY2Rw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cross-spawn": "^5.1.0",
|
||||
"is-windows": "^1.0.0"
|
||||
"cross-spawn": "^7.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-spawn": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
|
||||
"integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"lru-cache": "^4.0.1",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"dev": true
|
||||
},
|
||||
"shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"isexe": "^2.0.0"
|
||||
"shebang-regex": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -5863,6 +5886,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz",
|
||||
"integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==",
|
||||
"dev": true
|
||||
},
|
||||
"execa": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz",
|
||||
@ -7581,6 +7610,17 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"http-signature": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
|
||||
@ -8974,6 +9014,7 @@
|
||||
"version": "4.1.5",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz",
|
||||
"integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"pseudomap": "^1.0.2",
|
||||
"yallist": "^2.1.2"
|
||||
@ -8982,7 +9023,8 @@
|
||||
"yallist": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
|
||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
|
||||
"integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -13621,7 +13663,6 @@
|
||||
"common-tags": "^1.4.0",
|
||||
"concurrently": "^3.4.0",
|
||||
"cpy-cli": "^1.0.1",
|
||||
"cross-env": "^3.1.4",
|
||||
"is-windows": "^1.0.0",
|
||||
"mkdirp": "^0.5.1",
|
||||
"ncp": "2.0.0",
|
||||
@ -15782,6 +15823,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resin-bundle-resolve": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/resin-bundle-resolve/-/resin-bundle-resolve-4.3.0.tgz",
|
||||
|
@ -56,7 +56,10 @@
|
||||
"package": "npm run build:fast && npm run build:standalone && npm run build:installer",
|
||||
"release": "ts-node --transpile-only automation/run.ts release",
|
||||
"pretest": "npm run build",
|
||||
"test": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
||||
"test": "npm run test:source && npm run test:standalone",
|
||||
"test:source": "mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
||||
"test:standalone": "npm run build:standalone && cross-env BALENA_CLI_TEST_TYPE=standalone npm run test:source",
|
||||
"test:standalone:fast": "cross-env BALENA_CLI_TEST_TYPE=standalone npm run test:source",
|
||||
"test:fast": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/*.spec.ts\"",
|
||||
"test:only": "npm run build:fast && mocha --timeout 6000 -r ts-node/register/transpile-only \"tests/**/${npm_config_test}.spec.ts\"",
|
||||
"catch-uncommitted": "ts-node --transpile-only automation/run.ts catch-uncommitted",
|
||||
@ -117,6 +120,7 @@
|
||||
"@types/fs-extra": "^8.1.0",
|
||||
"@types/global-agent": "^2.1.0",
|
||||
"@types/global-tunnel-ng": "^2.1.0",
|
||||
"@types/http-proxy": "^1.17.4",
|
||||
"@types/intercept-stdout": "^0.1.0",
|
||||
"@types/is-root": "^2.1.2",
|
||||
"@types/js-yaml": "^3.12.3",
|
||||
@ -148,11 +152,13 @@
|
||||
"catch-uncommitted": "^1.5.0",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"cross-env": "^7.0.2",
|
||||
"ent": "^2.2.0",
|
||||
"filehound": "^1.17.4",
|
||||
"fs-extra": "^8.0.1",
|
||||
"gulp": "^4.0.1",
|
||||
"gulp-inline-source": "^2.1.0",
|
||||
"http-proxy": "^1.18.1",
|
||||
"husky": "^4.2.5",
|
||||
"intercept-stdout": "^0.1.2",
|
||||
"mocha": "^6.2.3",
|
||||
|
@ -28,7 +28,7 @@ const jHeader = { 'Content-Type': 'application/json' };
|
||||
|
||||
export class BalenaAPIMock extends NockMock {
|
||||
constructor() {
|
||||
super('https://api.balena-cloud.com');
|
||||
super(/api\.balena-cloud\.com/);
|
||||
}
|
||||
|
||||
public expectGetApplication(opts: ScopeOpts = {}) {
|
||||
|
@ -27,7 +27,7 @@ export const builderResponsePath = path.normalize(
|
||||
|
||||
export class BuilderMock extends NockMock {
|
||||
constructor() {
|
||||
super('https://builder.balena-cloud.com');
|
||||
super(/builder\.balena-cloud\.com/);
|
||||
}
|
||||
|
||||
public expectPostBuild(opts: {
|
||||
|
@ -62,6 +62,9 @@ const commonComposeQueryParams = [
|
||||
['labels', ''],
|
||||
];
|
||||
|
||||
// "itSS" means "it() Skip Standalone"
|
||||
const itSS = process.env.BALENA_CLI_TEST_TYPE === 'standalone' ? it.skip : it;
|
||||
|
||||
describe('balena build', function() {
|
||||
let api: BalenaAPIMock;
|
||||
let docker: DockerMock;
|
||||
@ -135,7 +138,7 @@ describe('balena build', function() {
|
||||
});
|
||||
});
|
||||
|
||||
it('should create the expected tar stream (--emulated)', async () => {
|
||||
itSS('should create the expected tar stream (--emulated)', async () => {
|
||||
const projectPath = path.join(projectsPath, 'no-docker-compose', 'basic');
|
||||
const isV12W = isWindows && isV12();
|
||||
const transposedDockerfile =
|
||||
@ -373,9 +376,7 @@ describe('balena build: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`build ${projectPath} -A amd64 -d nuc`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
@ -173,6 +173,9 @@ describe('balena deploy', function() {
|
||||
const expectedResponseLines = ['[Error] Deploy failed'];
|
||||
const errMsg = 'Patch Image Error';
|
||||
const expectedErrorLines = [errMsg];
|
||||
// The SDK should produce an "unexpected" BalenaRequestError, which
|
||||
// causes the CLI to call process.exit() with process.exitCode = 1
|
||||
const expectedExitCode = 1;
|
||||
|
||||
// Mock this patch HTTP request to return status code 500, in which case
|
||||
// the release status should be saved as "failed" rather than "success"
|
||||
@ -202,17 +205,13 @@ describe('balena deploy', function() {
|
||||
expectedFilesByService: { main: expectedFiles },
|
||||
expectedQueryParamsByService: { main: commonQueryParams },
|
||||
expectedErrorLines,
|
||||
expectedExitCode,
|
||||
expectedResponseLines,
|
||||
projectPath,
|
||||
responseBody,
|
||||
responseCode: 200,
|
||||
services: ['main'],
|
||||
});
|
||||
// The SDK should produce an "unexpected" BalenaRequestError, which
|
||||
// causes the CLI to call process.exit() with process.exitCode = 1
|
||||
// @ts-ignore
|
||||
sinon.assert.calledWith(process.exit);
|
||||
expect(process.exitCode).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -221,6 +220,7 @@ describe('balena deploy: project validation', function() {
|
||||
this.beforeEach(() => {
|
||||
api = new BalenaAPIMock();
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
});
|
||||
|
||||
this.afterEach(() => {
|
||||
@ -243,9 +243,7 @@ describe('balena deploy: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`deploy testApp --source ${projectPath}`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
@ -64,7 +64,7 @@ describe('balena devices', function() {
|
||||
});
|
||||
|
||||
it('should list devices from own and collaborator apps', async () => {
|
||||
api.expectGetWhoAmI({ optional: true });
|
||||
api.expectGetWhoAmI({ optional: true, persist: true });
|
||||
api.expectGetMixpanel({ optional: true });
|
||||
|
||||
api.scope
|
||||
|
@ -486,9 +486,7 @@ describe('balena push: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath} --nogitignore`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
|
||||
@ -507,9 +505,7 @@ describe('balena push: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath}`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
|
||||
@ -536,12 +532,8 @@ describe('balena push: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath} --nolive`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(
|
||||
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedOutputLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(out, true)).to.include.members(expectedOutputLines);
|
||||
});
|
||||
|
||||
it('should suppress a parent folder check with --noparent-check', async () => {
|
||||
@ -558,9 +550,7 @@ describe('balena push: project validation', function() {
|
||||
const { out, err } = await runCommand(
|
||||
`push testApp --source ${projectPath} --nolive --noparent-check`,
|
||||
);
|
||||
expect(
|
||||
cleanOutput(err).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
expect(out).to.be.empty;
|
||||
});
|
||||
});
|
||||
|
@ -1,3 +1,19 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2019-2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { expect } from 'chai';
|
||||
import * as fs from 'fs';
|
||||
import { runCommand } from '../helpers';
|
||||
@ -9,29 +25,35 @@ const nodeVersion = process.version.startsWith('v')
|
||||
|
||||
describe('balena version', function() {
|
||||
it('should print the installed version of the CLI', async () => {
|
||||
const { out } = await runCommand('version');
|
||||
|
||||
const { err, out } = await runCommand('version');
|
||||
expect(err).to.be.empty;
|
||||
expect(out.join('')).to.equal(`${packageJSON.version}\n`);
|
||||
});
|
||||
|
||||
it('should print additional version information with the -a flag', async () => {
|
||||
const { out } = await runCommand('version -a');
|
||||
|
||||
expect(out.join('')).to.equal(
|
||||
`balena-cli version "${packageJSON.version}"
|
||||
Node.js version "${nodeVersion}"
|
||||
`,
|
||||
const { err, out } = await runCommand('version -a');
|
||||
expect(err).to.be.empty;
|
||||
expect(out[0].trim()).to.equal(
|
||||
`balena-cli version "${packageJSON.version}"`,
|
||||
);
|
||||
|
||||
if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
|
||||
expect(out[1]).to.match(/Node.js version "\d+\.\d+.\d+"/);
|
||||
} else {
|
||||
expect(out[1].trim()).to.equal(`Node.js version "${nodeVersion}"`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should print version information as JSON with the the -j flag', async () => {
|
||||
const { out } = await runCommand('version -j');
|
||||
|
||||
const { err, out } = await runCommand('version -j');
|
||||
expect(err).to.be.empty;
|
||||
const json = JSON.parse(out.join(''));
|
||||
expect(json['balena-cli']).to.equal(packageJSON.version);
|
||||
|
||||
expect(json).to.deep.equal({
|
||||
'balena-cli': packageJSON.version,
|
||||
'Node.js': nodeVersion,
|
||||
});
|
||||
if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
|
||||
expect(json['Node.js']).to.match(/\d+\.\d+.\d+/);
|
||||
} else {
|
||||
expect(json['Node.js']).to.equal(nodeVersion);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -21,6 +21,7 @@ import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
import * as path from 'path';
|
||||
import { PathUtils } from 'resin-multibuild';
|
||||
import * as sinon from 'sinon';
|
||||
import { Readable } from 'stream';
|
||||
import * as tar from 'tar-stream';
|
||||
import { streamToBuffer } from 'tar-utils';
|
||||
@ -139,6 +140,7 @@ export async function testDockerBuildStream(o: {
|
||||
expectedFilesByService: ExpectedTarStreamFilesByService;
|
||||
expectedQueryParamsByService: { [service: string]: string[][] };
|
||||
expectedErrorLines?: string[];
|
||||
expectedExitCode?: number;
|
||||
expectedResponseLines: string[];
|
||||
projectPath: string;
|
||||
responseCode: number;
|
||||
@ -174,21 +176,25 @@ export async function testDockerBuildStream(o: {
|
||||
o.dockerMock.expectGetImages();
|
||||
}
|
||||
|
||||
const { out, err } = await runCommand(o.commandLine);
|
||||
|
||||
const cleanLines = (lines: string[]) =>
|
||||
cleanOutput(lines).map(line => line.replace(/\s{2,}/g, ' '));
|
||||
const { exitCode, out, err } = await runCommand(o.commandLine);
|
||||
|
||||
if (expectedErrorLines.length) {
|
||||
expect(cleanLines(err)).to.include.members(expectedErrorLines);
|
||||
expect(cleanOutput(err, true)).to.include.members(expectedErrorLines);
|
||||
} else {
|
||||
expect(err).to.be.empty;
|
||||
}
|
||||
if (expectedResponseLines.length) {
|
||||
expect(cleanLines(out)).to.include.members(expectedResponseLines);
|
||||
expect(cleanOutput(out, true)).to.include.members(expectedResponseLines);
|
||||
} else {
|
||||
expect(out).to.be.empty;
|
||||
}
|
||||
if (o.expectedExitCode != null) {
|
||||
if (process.env.BALENA_CLI_TEST_TYPE !== 'standalone') {
|
||||
// @ts-ignore
|
||||
sinon.assert.calledWith(process.exit);
|
||||
}
|
||||
expect(o.expectedExitCode).to.equal(exitCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,7 +227,5 @@ export async function testPushBuildStream(o: {
|
||||
const { out, err } = await runCommand(o.commandLine);
|
||||
|
||||
expect(err).to.be.empty;
|
||||
expect(
|
||||
cleanOutput(out).map(line => line.replace(/\s{2,}/g, ' ')),
|
||||
).to.include.members(expectedResponseLines);
|
||||
expect(cleanOutput(out, true)).to.include.members(expectedResponseLines);
|
||||
}
|
||||
|
188
tests/helpers.ts
188
tests/helpers.ts
@ -18,37 +18,63 @@
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
require('./config-tests'); // required for side effects
|
||||
|
||||
import { execFile } from 'child_process';
|
||||
import intercept = require('intercept-stdout');
|
||||
import * as _ from 'lodash';
|
||||
import { fs } from 'mz';
|
||||
import * as nock from 'nock';
|
||||
import * as path from 'path';
|
||||
|
||||
import * as balenaCLI from '../build/app';
|
||||
import { setupSentry } from '../build/app-common';
|
||||
|
||||
export const runCommand = async (cmd: string) => {
|
||||
const balenaExe = process.platform === 'win32' ? 'balena.exe' : 'balena';
|
||||
const standalonePath = path.resolve(__dirname, '..', 'build-bin', balenaExe);
|
||||
|
||||
interface TestOutput {
|
||||
err: string[]; // stderr
|
||||
out: string[]; // stdout
|
||||
exitCode?: number; // process.exitCode
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter stdout / stderr lines to remove lines that start with `[debug]` and
|
||||
* other lines that can be ignored for testing purposes.
|
||||
* @param testOutput
|
||||
*/
|
||||
function filterCliOutputForTests(testOutput: TestOutput): TestOutput {
|
||||
return {
|
||||
exitCode: testOutput.exitCode,
|
||||
err: testOutput.err.filter(
|
||||
(line: string) =>
|
||||
!line.match(/\[debug\]/i) &&
|
||||
// TODO stop this warning message from appearing when running
|
||||
// sdk.setSharedOptions multiple times in the same process
|
||||
!line.startsWith('Shared SDK options') &&
|
||||
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
|
||||
!line.includes('[DEP0066]'),
|
||||
),
|
||||
out: testOutput.out.filter((line: string) => !line.match(/\[debug\]/i)),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the CLI in this same process, by calling the run() function in `app.ts`.
|
||||
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
||||
*/
|
||||
async function runCommanInProcess(cmd: string): Promise<TestOutput> {
|
||||
const preArgs = [process.argv[0], path.join(process.cwd(), 'bin', 'balena')];
|
||||
|
||||
const err: string[] = [];
|
||||
const out: string[] = [];
|
||||
|
||||
const stdoutHook = (log: string | Buffer) => {
|
||||
// Skip over debug messages
|
||||
if (typeof log === 'string' && !log.startsWith('[debug]')) {
|
||||
if (typeof log === 'string') {
|
||||
out.push(log);
|
||||
}
|
||||
};
|
||||
const stderrHook = (log: string | Buffer) => {
|
||||
// Skip over debug messages
|
||||
if (
|
||||
typeof log === 'string' &&
|
||||
!log.match(/\[debug\]/i) &&
|
||||
// TODO stop this warning message from appearing when running
|
||||
// sdk.setSharedOptions multiple times in the same process
|
||||
!log.startsWith('Shared SDK options') &&
|
||||
// Node 12: '[DEP0066] DeprecationWarning: OutgoingMessage.prototype._headers is deprecated'
|
||||
!log.includes('[DEP0066]')
|
||||
) {
|
||||
if (typeof log === 'string') {
|
||||
err.push(log);
|
||||
}
|
||||
};
|
||||
@ -58,14 +84,123 @@ export const runCommand = async (cmd: string) => {
|
||||
await balenaCLI.run(preArgs.concat(cmd.split(' ')), {
|
||||
noFlush: true,
|
||||
});
|
||||
return {
|
||||
err,
|
||||
out,
|
||||
};
|
||||
} finally {
|
||||
unhookIntercept();
|
||||
}
|
||||
};
|
||||
return filterCliOutputForTests({
|
||||
err,
|
||||
out,
|
||||
// this makes sense if `process.exit()` was stubbed with sinon
|
||||
exitCode: process.exitCode,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the command (e.g. `balena xxx args`) in a child process, instead of
|
||||
* the same process as mocha. This is slow and does not allow mocking the
|
||||
* source code, but it is useful for testing the standalone zip package binary.
|
||||
* (Every now and then, bugs surface because of missing entries in the
|
||||
* `pkg.assets` section of `package.json`, usually because of updated
|
||||
* dependencies that don't clearly declare the have compatibility issues
|
||||
* with `pkg`.)
|
||||
*
|
||||
* `mocha` runs on the parent process, and many of the tests inspect network
|
||||
* traffic intercepted with `nock`. But this interception only works in the
|
||||
* parent process itself. To get around this, we run a HTTP proxy server on
|
||||
* the parent process, and get the child process to use it (the CLI already had
|
||||
* support for proxy servers as a product feature, and this testing arrangement
|
||||
* also exercises the proxy capabilities).
|
||||
*
|
||||
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
||||
* @param proxyPort TCP port number for the HTTP proxy server running on the
|
||||
* parent process
|
||||
*/
|
||||
async function runCommandInSubprocess(
|
||||
cmd: string,
|
||||
proxyPort: number,
|
||||
): Promise<TestOutput> {
|
||||
let exitCode = 0;
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
const addedEnvs = {
|
||||
// Use http instead of https, so we can intercept and test the data,
|
||||
// for example the contents of tar streams sent by the CLI to Docker
|
||||
BALENARC_API_URL: 'http://api.balena-cloud.com',
|
||||
BALENARC_BUILDER_URL: 'http://builder.balena-cloud.com',
|
||||
BALENARC_PROXY: `http://127.0.0.1:${proxyPort}`,
|
||||
// override default proxy exclusion to allow proxying of requests to 127.0.0.1
|
||||
BALENARC_DO_PROXY: '127.0.0.1,localhost',
|
||||
};
|
||||
await new Promise(resolve => {
|
||||
const child = execFile(
|
||||
standalonePath,
|
||||
cmd.split(' '),
|
||||
{ env: { ...process.env, ...addedEnvs } },
|
||||
($error, $stdout, $stderr) => {
|
||||
stderr = $stderr || '';
|
||||
stdout = $stdout || '';
|
||||
// $error will be set if the CLI child process exits with a
|
||||
// non-zero exit code. Usually this is harmless/expected, as
|
||||
// the CLI child process is tested for error conditions.
|
||||
if ($error && process.env.DEBUG) {
|
||||
console.error(`
|
||||
[debug] Error (possibly expected) executing child CLI process "${standalonePath}"
|
||||
------------------------------------------------------------------
|
||||
${$error}
|
||||
------------------------------------------------------------------`);
|
||||
}
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
child.on('exit', (code: number, signal: string) => {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(
|
||||
`CLI child process exited with code=${code} signal=${signal}`,
|
||||
);
|
||||
}
|
||||
exitCode = code;
|
||||
});
|
||||
});
|
||||
|
||||
const splitLines = (lines: string) =>
|
||||
lines
|
||||
.split(/[\r\n]/) // includes '\r' in isolation, used in progress bars
|
||||
.filter(l => l)
|
||||
.map(l => l + '\n');
|
||||
|
||||
return filterCliOutputForTests({
|
||||
exitCode,
|
||||
err: splitLines(stderr),
|
||||
out: splitLines(stdout),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a CLI command and capture its stdout, stderr and exit code for testing.
|
||||
* If the BALENA_CLI_TEST_TYPE env var is set to 'standalone', then the command
|
||||
* will be executed in a separate child process, and a proxy server will be
|
||||
* started in order to intercept and test HTTP requests.
|
||||
* Otherwise, simply call the CLI's run() entry point in this same process.
|
||||
* @param cmd Command to execute, e.g. `push myApp` (without 'balena' prefix)
|
||||
*/
|
||||
export async function runCommand(cmd: string): Promise<TestOutput> {
|
||||
if (process.env.BALENA_CLI_TEST_TYPE === 'standalone') {
|
||||
const semver = await import('semver');
|
||||
if (semver.lt(process.version, '10.16.0')) {
|
||||
throw new Error(
|
||||
`The standalone tests require Node.js >= v10.16.0 because of net/proxy features ('global-agent' npm package)`,
|
||||
);
|
||||
}
|
||||
if (!(await fs.exists(standalonePath))) {
|
||||
throw new Error(`Standalone executable not found: "${standalonePath}"`);
|
||||
}
|
||||
const proxy = await import('./proxy-server');
|
||||
const [proxyPort] = await proxy.createProxyServerOnce();
|
||||
return runCommandInSubprocess(cmd, proxyPort);
|
||||
} else {
|
||||
return runCommanInProcess(cmd);
|
||||
}
|
||||
}
|
||||
|
||||
export const balenaAPIMock = () => {
|
||||
if (!nock.isActive()) {
|
||||
@ -85,13 +220,16 @@ export const balenaAPIMock = () => {
|
||||
});
|
||||
};
|
||||
|
||||
export function cleanOutput(output: string[] | string): string[] {
|
||||
export function cleanOutput(
|
||||
output: string[] | string,
|
||||
collapseBlank = false,
|
||||
): string[] {
|
||||
const cleanLine = collapseBlank
|
||||
? (line: string) => monochrome(line.trim()).replace(/\s{2,}/g, ' ')
|
||||
: (line: string) => monochrome(line.trim());
|
||||
|
||||
return _(_.castArray(output))
|
||||
.map((log: string) => {
|
||||
return log.split('\n').map(line => {
|
||||
return monochrome(line.trim());
|
||||
});
|
||||
})
|
||||
.map((log: string) => log.split('\n').map(cleanLine))
|
||||
.flatten()
|
||||
.compact()
|
||||
.value();
|
||||
@ -106,7 +244,7 @@ export function cleanOutput(output: string[] | string): string[] {
|
||||
* coded from observation of a few samples only, and may not cover all cases.
|
||||
*/
|
||||
export function monochrome(text: string): string {
|
||||
return text.replace(/\u001b\[\??\d+?[a-zA-Z]\r?/g, '');
|
||||
return text.replace(/\u001b\[\??(\d+;)*\d+[a-zA-Z]\r?/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -133,11 +133,22 @@ export class NockMock {
|
||||
}
|
||||
|
||||
protected handleUnexpectedRequest(req: any) {
|
||||
const {
|
||||
interceptorServerPort,
|
||||
} = require('./proxy-server') as typeof import('./proxy-server');
|
||||
const o = req.options || {};
|
||||
const u = o.uri || {};
|
||||
const method = req.method;
|
||||
const proto = req.protocol || req.proto || o.proto || u.protocol;
|
||||
const host = req.host || req.headers?.host || o.host || u.host;
|
||||
const path = req.path || o.path || u.path;
|
||||
|
||||
// Requests made by the local proxy/interceptor server are OK
|
||||
if (host === `127.0.0.1:${interceptorServerPort}`) {
|
||||
return;
|
||||
}
|
||||
console.error(
|
||||
`Unexpected http request!: ${req.method} ${o.proto ||
|
||||
u.protocol}//${o.host || u.host}${req.path || o.path || u.path}`,
|
||||
`NockMock: Unexpected HTTP request: ${method} ${proto}//${host}${path}`,
|
||||
);
|
||||
// Errors thrown here are not causing the tests to fail for some reason.
|
||||
// Possibly due to CLI global error handlers? (error.js)
|
||||
|
217
tests/proxy-server.ts
Normal file
217
tests/proxy-server.ts
Normal file
@ -0,0 +1,217 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This module creates two HTTP servers listening on the local machine:
|
||||
* * The "proxy server" which is a standard HTTP proxy server that handles the
|
||||
* CONNECT HTTP verb, using the `http-proxy` dependency.
|
||||
* * The "interceptor server" which actually handles the proxied requests.
|
||||
*
|
||||
* The proxy server proxies the client request to the interceptor server. (This
|
||||
* two-server approach (proxy + interceptor) is mainly a result of accommodating
|
||||
* the typical setup documented by the `http-proxy` dependency.)
|
||||
*
|
||||
* The use case for these servers is to test the standalone executable (CLI's
|
||||
* standalone zip package) in a child process. Most of the CLI's automated tests
|
||||
* currently test HTTP requests using `nock`, but `nock` can only mock/test the
|
||||
* same process (Node's built-in `http` library). However, the CLI has support
|
||||
* for proxy servers as a product feature, so the idea was to proxy the child
|
||||
* process requests to the parent process, where the proxy / interceptor servers
|
||||
* run. The interceptor server then forwards the request (mostly unchanged) with
|
||||
* the expectation that `nock` will intercept the requests for testing (in the
|
||||
* parent process) as usual.
|
||||
*
|
||||
* 1. A `mocha` test case calls `runCommand('push test-rpi')`, with `nock` setup
|
||||
* to intercept HTTP requests (in the same process that runs `mocha`).
|
||||
* 2. The proxy and interceptor servers are started in the parent process (only
|
||||
* once: singleton) at free TCP port numbers randomly allocated by the OS.
|
||||
* 3. A CLI child process gets spawned to run the command (`balena push test-rpi`)
|
||||
* with environment variables including BALENARC_PROXY (set to
|
||||
* 'http://127.0.0.1:${proxyPort}'). (Additional env vars instruct the
|
||||
* child process to use HTTP instead of HTTPS for the balena API and builder.)
|
||||
* 4. The child process sends the HTTP requests to the proxy server.
|
||||
* 5. The proxy server forwards the request to the interceptor server.
|
||||
* 6. The interceptor server simply re-issues the HTTP request (unchange), with
|
||||
* the expectation that `nock` will intercept it.
|
||||
* 7. `nock` (running on the parent process, same process that runs `mocha`)
|
||||
* intercepts the HTTP request, test it and replies with a mocked response.
|
||||
* 8. `nocks` response is returned to the interceptor server, which returns it
|
||||
* to the proxy server, which returns it to the child process, which continues
|
||||
* CLI command execution.
|
||||
*/
|
||||
|
||||
import * as http from 'http';
|
||||
|
||||
const proxyServers: http.Server[] = [];
|
||||
|
||||
after(function() {
|
||||
if (proxyServers.length) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(
|
||||
`[debug] Closing proxy servers (count=${proxyServers.length})`,
|
||||
);
|
||||
}
|
||||
proxyServers.forEach(s => s.close());
|
||||
proxyServers.splice(0);
|
||||
}
|
||||
});
|
||||
|
||||
export let proxyServerPort = 0;
|
||||
export let interceptorServerPort = 0;
|
||||
|
||||
export async function createProxyServerOnce(): Promise<[number, number]> {
|
||||
if (proxyServerPort === 0) {
|
||||
[proxyServerPort, interceptorServerPort] = await createProxyServer();
|
||||
}
|
||||
return [proxyServerPort, interceptorServerPort];
|
||||
}
|
||||
|
||||
async function createProxyServer(): Promise<[number, number]> {
|
||||
const httpProxy = require('http-proxy') as typeof import('http-proxy');
|
||||
|
||||
const interceptorPort = await createInterceptorServer();
|
||||
|
||||
const proxy = httpProxy.createProxyServer();
|
||||
proxy.on('error', function(
|
||||
err: Error,
|
||||
_req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
) {
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
const msg = `Proxy server error: ${err}`;
|
||||
console.error(msg);
|
||||
res.end(msg);
|
||||
});
|
||||
|
||||
const server = http.createServer(function(
|
||||
req: http.IncomingMessage,
|
||||
res: http.ServerResponse,
|
||||
) {
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Proxy forwarding for ${req.url}`);
|
||||
}
|
||||
proxy.web(req, res, { target: `http://127.0.0.1:${interceptorPort}` });
|
||||
});
|
||||
proxyServers.push(server);
|
||||
|
||||
server.on('error', (err: Error) => {
|
||||
console.error(`Proxy server error (http.createServer):\n${err}`);
|
||||
});
|
||||
|
||||
let proxyPort = 0; // TCP port number, 0 means automatic allocation
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
|
||||
if (err) {
|
||||
console.error(`Error starting proxy server:\n${err}`);
|
||||
reject(err);
|
||||
} else {
|
||||
const info: any = listener.address();
|
||||
proxyPort = info.port;
|
||||
console.error(
|
||||
`[Info] Proxy server listening on ${info.address}:${proxyPort}`,
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return [proxyPort, interceptorPort];
|
||||
}
|
||||
|
||||
async function createInterceptorServer(): Promise<number> {
|
||||
const url = await import('url');
|
||||
|
||||
const server = http.createServer();
|
||||
proxyServers.push(server);
|
||||
|
||||
server
|
||||
.on('error', (err: Error) => {
|
||||
console.error(`Interceptor server error: ${err}`);
|
||||
})
|
||||
.on(
|
||||
'request',
|
||||
(cliReq: http.IncomingMessage, cliRes: http.ServerResponse) => {
|
||||
const proxiedFor = `http://${cliReq.headers.host}${cliReq.url}`;
|
||||
if (process.env.DEBUG) {
|
||||
console.error(`[debug] Interceptor forwarding for ${proxiedFor}`);
|
||||
}
|
||||
// tslint:disable-next-line:prefer-const
|
||||
let { protocol, hostname, port, path: urlPath, hash } = url.parse(
|
||||
proxiedFor,
|
||||
);
|
||||
protocol = (protocol || 'http:').toLowerCase();
|
||||
port = port || (protocol === 'https:' ? '443' : '80');
|
||||
const reqOpts = {
|
||||
protocol,
|
||||
port,
|
||||
host: hostname,
|
||||
path: `${urlPath || ''}${hash || ''}`,
|
||||
method: cliReq.method,
|
||||
headers: cliReq.headers,
|
||||
};
|
||||
const srvReq = http.request(reqOpts);
|
||||
srvReq
|
||||
.on('error', err => {
|
||||
console.error(
|
||||
`Interceptor server error in onward request:\n${err}`,
|
||||
);
|
||||
})
|
||||
.on('response', (srvRes: http.IncomingMessage) => {
|
||||
// Copy headers, status code and status message from interceptor to client
|
||||
for (const [key, val] of Object.entries(srvRes.headers)) {
|
||||
if (key && val) {
|
||||
cliRes.setHeader(key, val);
|
||||
}
|
||||
}
|
||||
cliRes.statusCode = srvRes.statusCode || cliRes.statusCode;
|
||||
cliRes.statusMessage = srvRes.statusMessage || cliRes.statusMessage;
|
||||
srvRes.pipe(cliRes).on('error', (err: Error) => {
|
||||
console.error(
|
||||
`Interceptor server error piping response to proxy server:\n${err}`,
|
||||
);
|
||||
cliRes.end();
|
||||
});
|
||||
});
|
||||
cliReq.pipe(srvReq).on('error', (err: Error) => {
|
||||
console.error(
|
||||
`Proxy server error piping client request onward:\n${err}`,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let interceptorPort = 0;
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const listener = server.listen(0, '127.0.0.1', (err: Error) => {
|
||||
if (err) {
|
||||
console.error(`Error starting interceptor server:\n${err}`);
|
||||
reject(err);
|
||||
} else {
|
||||
const info: any = listener.address();
|
||||
interceptorPort = info.port;
|
||||
console.error(
|
||||
`[Info] Interceptor server listening on ${info.address}:${interceptorPort}`,
|
||||
);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return interceptorPort;
|
||||
}
|
56
tests/supervisor-mock.ts
Normal file
56
tests/supervisor-mock.ts
Normal file
@ -0,0 +1,56 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2020 Balena Ltd.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import * as _ from 'lodash';
|
||||
import * as path from 'path';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
import { NockMock, ScopeOpts } from './nock-mock';
|
||||
|
||||
export const dockerResponsePath = path.normalize(
|
||||
path.join(__dirname, 'test-data', 'docker-response'),
|
||||
);
|
||||
|
||||
export class SupervisorMock extends NockMock {
|
||||
constructor() {
|
||||
super(/1\.2\.3\.4:48484/);
|
||||
}
|
||||
|
||||
public expectGetPing(opts: ScopeOpts = {}) {
|
||||
this.optGet('/ping', opts).reply(200, 'OK');
|
||||
}
|
||||
|
||||
public expectGetLogs(opts: ScopeOpts = {}) {
|
||||
const chunks = [
|
||||
'\n',
|
||||
'{"message":"Streaming logs","isSystem":true}\n',
|
||||
'{"serviceName":"bar","serviceId":1,"imageId":1,"isStdout":true,"timestamp":1591991625223,"message":"bar 8 (332) Linux 4e3f81149d71 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux"}\n',
|
||||
'{"serviceName":"foo","serviceId":2,"imageId":2,"isStdout":true,"timestamp":1591991628757,"message":"foo 8 (200) Linux cc5df60d89ee 4.19.75 #1 SMP PREEMPT Mon Mar 23 11:50:49 UTC 2020 aarch64 GNU/Linux"}\n',
|
||||
];
|
||||
let chunkCount = 0;
|
||||
const chunkedStream = new Readable({
|
||||
read(_size) {
|
||||
setTimeout(() => {
|
||||
this.push(chunkCount === chunks.length ? null : chunks[chunkCount++]);
|
||||
}, 10);
|
||||
},
|
||||
});
|
||||
this.optGet('/v2/local/logs', opts).reply((_uri, _reqBody, cb) => {
|
||||
cb(null, [200, chunkedStream]);
|
||||
});
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user