2020-01-15 19:41:47 +00:00
|
|
|
/**
|
|
|
|
* @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 nock from 'nock';
|
2021-05-19 22:12:49 +00:00
|
|
|
import * as fs from 'fs';
|
2020-01-15 19:41:47 +00:00
|
|
|
|
|
|
|
export interface ScopeOpts {
|
|
|
|
optional?: boolean;
|
|
|
|
persist?: boolean;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Base class for tests using nock to intercept HTTP requests.
|
|
|
|
* Subclasses include BalenaAPIMock, DockerMock and BuilderMock.
|
|
|
|
*/
|
|
|
|
export class NockMock {
|
|
|
|
public readonly scope: nock.Scope;
|
|
|
|
// Expose `scope` as `expect` to allow for better semantics in tests
|
2020-08-27 10:50:57 +00:00
|
|
|
public readonly expect;
|
2020-01-15 19:41:47 +00:00
|
|
|
protected static instanceCount = 0;
|
|
|
|
|
2021-07-28 13:19:57 +00:00
|
|
|
constructor(
|
|
|
|
public basePathPattern: string | RegExp,
|
|
|
|
public allowUnmocked: boolean = false,
|
|
|
|
) {
|
2020-01-15 19:41:47 +00:00
|
|
|
if (NockMock.instanceCount === 0) {
|
|
|
|
if (!nock.isActive()) {
|
|
|
|
nock.activate();
|
|
|
|
}
|
|
|
|
nock.emitter.on('no match', this.handleUnexpectedRequest);
|
|
|
|
} else if (process.env.DEBUG) {
|
|
|
|
console.error(
|
2020-01-20 21:21:05 +00:00
|
|
|
`[debug] NockMock.constructor() instance count is ${NockMock.instanceCount}`,
|
2020-01-15 19:41:47 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
NockMock.instanceCount += 1;
|
2021-07-28 13:19:57 +00:00
|
|
|
this.scope = nock(this.basePathPattern, { allowUnmocked });
|
2020-08-27 10:50:57 +00:00
|
|
|
this.expect = this.scope;
|
2020-01-15 19:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public optGet(
|
|
|
|
uri: string | RegExp | ((uri: string) => boolean),
|
2020-02-29 22:06:54 +00:00
|
|
|
{ optional = false, persist = false }: ScopeOpts,
|
2020-01-15 19:41:47 +00:00
|
|
|
): nock.Interceptor {
|
2020-02-29 22:06:54 +00:00
|
|
|
const get = (persist ? this.scope.persist() : this.scope).get(uri);
|
|
|
|
return optional ? get.optionally() : get;
|
2020-01-15 19:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public optDelete(
|
|
|
|
uri: string | RegExp | ((uri: string) => boolean),
|
2020-02-29 22:06:54 +00:00
|
|
|
{ optional = false, persist = false }: ScopeOpts,
|
2020-01-15 19:41:47 +00:00
|
|
|
) {
|
2020-02-29 22:06:54 +00:00
|
|
|
const del = (persist ? this.scope.persist() : this.scope).delete(uri);
|
|
|
|
return optional ? del.optionally() : del;
|
2020-01-15 19:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public optPatch(
|
|
|
|
uri: string | RegExp | ((uri: string) => boolean),
|
2020-02-29 22:06:54 +00:00
|
|
|
{ optional = false, persist = false }: ScopeOpts,
|
2020-01-15 19:41:47 +00:00
|
|
|
) {
|
2020-02-29 22:06:54 +00:00
|
|
|
const patch = (persist ? this.scope.persist() : this.scope).patch(uri);
|
|
|
|
return optional ? patch.optionally() : patch;
|
2020-01-15 19:41:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
public optPost(
|
|
|
|
uri: string | RegExp | ((uri: string) => boolean),
|
2020-02-29 22:06:54 +00:00
|
|
|
{ optional = false, persist = false }: ScopeOpts,
|
2020-01-15 19:41:47 +00:00
|
|
|
) {
|
2020-02-29 22:06:54 +00:00
|
|
|
const post = (persist ? this.scope.persist() : this.scope).post(uri);
|
|
|
|
return optional ? post.optionally() : post;
|
2020-01-15 19:41:47 +00:00
|
|
|
}
|
|
|
|
|
2020-05-21 13:51:10 +00:00
|
|
|
protected inspectNoOp(_uri: string, _requestBody: nock.Body): void {
|
|
|
|
return undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected getInspectedReplyBodyFunction(
|
|
|
|
inspectRequest: (uri: string, requestBody: nock.Body) => void,
|
|
|
|
replyBody: nock.ReplyBody,
|
|
|
|
) {
|
2020-06-15 22:53:07 +00:00
|
|
|
return function (
|
2020-05-21 13:51:10 +00:00
|
|
|
this: nock.ReplyFnContext,
|
|
|
|
uri: string,
|
|
|
|
requestBody: nock.Body,
|
|
|
|
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
inspectRequest(uri, requestBody);
|
|
|
|
} catch (err) {
|
|
|
|
cb(err, '');
|
|
|
|
}
|
|
|
|
cb(null, replyBody);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2021-05-19 22:12:49 +00:00
|
|
|
protected getInspectedReplyFileFunction(
|
|
|
|
inspectRequest: (uri: string, requestBody: nock.Body) => void,
|
|
|
|
replyBodyFile: string,
|
|
|
|
) {
|
|
|
|
return function (
|
|
|
|
this: nock.ReplyFnContext,
|
|
|
|
uri: string,
|
|
|
|
requestBody: nock.Body,
|
|
|
|
cb: (err: NodeJS.ErrnoException | null, result: nock.ReplyBody) => void,
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
inspectRequest(uri, requestBody);
|
|
|
|
} catch (err) {
|
|
|
|
cb(err, '');
|
|
|
|
}
|
|
|
|
|
|
|
|
const replyBody = fs.readFileSync(replyBodyFile);
|
|
|
|
cb(null, replyBody);
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2020-01-15 19:41:47 +00:00
|
|
|
public done() {
|
|
|
|
try {
|
|
|
|
// scope.done() will throw an error if there are expected api calls that have not happened.
|
|
|
|
// So ensure that all expected calls have been made.
|
|
|
|
this.scope.done();
|
|
|
|
} finally {
|
|
|
|
const count = NockMock.instanceCount - 1;
|
|
|
|
if (count < 0 && process.env.DEBUG) {
|
|
|
|
console.error(
|
|
|
|
`[debug] Warning: NockMock.instanceCount is negative (${count})`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
NockMock.instanceCount = Math.max(0, count);
|
|
|
|
if (NockMock.instanceCount === 0) {
|
|
|
|
// Remove 'no match' handler, for tests using nock without this module
|
|
|
|
nock.emitter.removeAllListeners('no match');
|
|
|
|
nock.cleanAll();
|
|
|
|
nock.restore();
|
|
|
|
} else if (process.env.DEBUG) {
|
|
|
|
console.error(
|
|
|
|
`[debug] NockMock.done() instance count is ${NockMock.instanceCount}`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected handleUnexpectedRequest(req: any) {
|
2021-07-20 13:57:00 +00:00
|
|
|
const { interceptorServerPort } =
|
|
|
|
require('./proxy-server') as typeof import('./proxy-server');
|
2020-01-15 19:41:47 +00:00
|
|
|
const o = req.options || {};
|
|
|
|
const u = o.uri || {};
|
2020-07-09 14:39:33 +00:00
|
|
|
const method = req.method;
|
|
|
|
const proto = req.protocol || req.proto || o.proto || u.protocol;
|
2020-06-15 22:53:04 +00:00
|
|
|
const host = req.host || req.headers?.host || o.host || u.host;
|
2020-07-09 14:39:33 +00:00
|
|
|
const path = req.path || o.path || u.path;
|
2020-06-15 22:53:04 +00:00
|
|
|
|
|
|
|
// Requests made by the local proxy/interceptor server are OK
|
|
|
|
if (host === `127.0.0.1:${interceptorServerPort}`) {
|
|
|
|
return;
|
|
|
|
}
|
2020-07-09 14:39:33 +00:00
|
|
|
console.error(
|
|
|
|
`NockMock: Unexpected HTTP request: ${method} ${proto}//${host}${path}`,
|
|
|
|
);
|
2020-01-15 19:41:47 +00:00
|
|
|
// Errors thrown here are not causing the tests to fail for some reason.
|
|
|
|
// Possibly due to CLI global error handlers? (error.js)
|
|
|
|
// (Also, nock should automatically throw an error, but also not happening)
|
|
|
|
// For now, the console.error is sufficient (will fail the test)
|
|
|
|
}
|
|
|
|
|
|
|
|
// For debugging tests
|
|
|
|
get unfulfilledCallCount(): number {
|
|
|
|
return this.scope.pendingMocks().length;
|
|
|
|
}
|
|
|
|
|
|
|
|
public debug() {
|
|
|
|
const scope = this.scope;
|
|
|
|
let mocks = scope.pendingMocks();
|
|
|
|
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
this.scope.on('request', function (_req, _interceptor, _body) {
|
2020-01-15 19:41:47 +00:00
|
|
|
console.log(`>> REQUEST:` + _req.path);
|
|
|
|
mocks = scope.pendingMocks();
|
|
|
|
console.error(`pending mocks ${mocks.length}: ${mocks}`);
|
|
|
|
});
|
|
|
|
|
2020-06-15 22:53:07 +00:00
|
|
|
this.scope.on('replied', function (_req) {
|
2020-01-15 19:41:47 +00:00
|
|
|
console.log(`<< REPLIED:` + _req.path);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|