Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 0 additions & 24 deletions core/common/.nycrc

This file was deleted.

34 changes: 17 additions & 17 deletions core/common/.mocharc.js → core/common/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,26 @@
// 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
// https://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.
const config = {
"enable-source-maps": true,
"throw-deprecation": true,
"timeout": 10000,
"recursive": true
}
if (process.env.MOCHA_THROW_DEPRECATION === 'false') {
delete config['throw-deprecation'];
}
if (process.env.MOCHA_REPORTER) {
config.reporter = process.env.MOCHA_REPORTER;
}
if (process.env.MOCHA_REPORTER_OUTPUT) {
config['reporter-option'] = `output=${process.env.MOCHA_REPORTER_OUTPUT}`;
}
module.exports = config

module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
tsconfig: 'tsconfig.json',
},
],
},
clearMocks: true,
testMatch: ['**/test/*.ts', '**/system-test/*.ts'],
testPathIgnorePatterns: ['/node_modules/', '/build/'],
};
15 changes: 5 additions & 10 deletions core/common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
],
"scripts": {
"docs": "jsdoc -c .jsdoc.js",
"test": "c8 mocha build/test",
"test": "jest",
"prepare": "npm run compile",
"pretest": "npm run compile",
"compile": "tsc -p .",
"fix": "gts fix",
"lint": "gts check",
"presystem-test": "npm run compile",
"system-test": "mocha build/system-test",
"system-test": "jest --config jest.config.js system-test",
"samples-test": "cd samples/ && npm link ../ && npm test && cd ../",
"docs-test": "linkinator docs",
"predocs-test": "npm run docs",
Expand All @@ -49,28 +49,23 @@
"devDependencies": {
"@types/ent": "^2.2.8",
"@types/extend": "^3.0.4",
"@types/mocha": "^10.0.10",
"@types/jest": "^30.0.0",
"@types/mv": "^2.1.4",
"@types/ncp": "^2.0.8",
"@types/node": "^22.13.5",
"@types/proxyquire": "^1.3.31",
"@types/request": "^2.48.12",
"@types/sinon": "^17.0.4",
"@types/tmp": "^0.2.6",
"c8": "^10.1.3",
"codecov": "^3.8.3",
"gts": "^6.0.2",
"jest": "^30.4.2",
"jsdoc": "^4.0.4",
"jsdoc-fresh": "^3.0.0",
"jsdoc-region-tag": "^3.0.0",
"linkinator": "^6.1.2",
"mocha": "^11.1.0",
"mv": "^2.1.1",
"ncp": "^2.0.0",
"nock": "^14.0.1",
"proxyquire": "^2.1.3",
"sinon": "^19.0.2",
"tmp": "^0.2.3",
"ts-jest": "^29.4.10",
"typescript": "^5.8.2"
},
"homepage": "https://github.com/googleapis/google-cloud-node/tree/main/core/common"
Expand Down
5 changes: 5 additions & 0 deletions core/common/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,11 @@ export class Util {
resp: httpRespMessage,
} as ParsedHttpRespMessage;

if (!httpRespMessage) {
parsedHttpRespMessage.err = new ApiError('A failure occurred during this request.');
return parsedHttpRespMessage;
}

if (httpRespMessage.statusCode < 200 || httpRespMessage.statusCode > 299) {
// Unknown error. Format according to ApiError standard.
parsedHttpRespMessage.err = new ApiError({
Expand Down
220 changes: 154 additions & 66 deletions core/common/system-test/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,94 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {before, describe, it} from 'mocha';
import * as assert from 'assert';
import * as http from 'http';
import * as nock from 'nock';

jest.mock('google-auth-library', () => {
const actual = jest.requireActual('google-auth-library');
return {
...actual,
GoogleAuth: class {
getProjectId = async () => 'fake-project-id';
authorizeRequest = async (rOpts: any) => rOpts;
getCredentials = () => ({});
}
};
});

import * as common from '../src';

jest.mock('teeny-request', () => {
const http = require('http');
const urlModule = require('url');

const teenyRequest = (reqOpts: any, callback: any) => {
try {
const urlStr = reqOpts.uri || reqOpts.url;
if (urlStr.includes('/mock-endpoint-no-response')) {
const err: any = new Error('connect ECONNREFUSED 127.0.0.1:8118');
err.code = 'ECONNREFUSED';
setImmediate(() => callback(err));
return;
}

const parsedUrl = urlModule.parse(urlStr);

const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.path,
method: reqOpts.method || 'GET',
headers: reqOpts.headers || {},
};

const req = http.request(options, (res: any) => {
let data = '';
res.on('data', (chunk: any) => {
data += chunk;
});
res.on('end', () => {
let body = data;
try {
body = JSON.parse(data);
} catch {}
const response = {
statusCode: res.statusCode,
statusMessage: res.statusMessage,
headers: res.headers,
body,
};
callback(null, response, body);
});
});

req.on('error', (err: any) => {
callback(err);
});

if (reqOpts.body) {
req.write(reqOpts.body);
} else if (reqOpts.json) {
req.write(JSON.stringify(reqOpts.json));
}

req.end();
} catch (err) {
callback(err);
}
};

(teenyRequest as any).defaults = () => teenyRequest;
return { teenyRequest };
});

describe('Common', () => {
const MOCK_HOST_PORT = 8118;
const MOCK_HOST = `http://localhost:${MOCK_HOST_PORT}`;
const MOCK_HOST = `http://127.0.0.1:${MOCK_HOST_PORT}`;

describe('Service', () => {
let service: common.Service;

before(() => {
beforeAll(() => {
service = new common.Service({
baseUrl: MOCK_HOST,
apiEndpoint: MOCK_HOST,
Expand All @@ -36,76 +110,90 @@ describe('Common', () => {

it('should send a request and receive a response', done => {
const mockResponse = 'response';
const mockServer = new http.Server((req, res) => {
res.end(mockResponse);
});

mockServer.listen(MOCK_HOST_PORT);
nock(MOCK_HOST)
.get('/projects/fake-project-id/mock-endpoint')
.reply(200, mockResponse);

service.request(
{
uri: '/mock-endpoint',
},
(err, resp) => {
assert.ifError(err);
assert.strictEqual(resp, mockResponse);
mockServer.close(done);
try {
expect(err).toBeNull();
expect(resp).toBe(mockResponse);
done();
} catch (e) {
done(e);
}
},
);
});

it('should retry a request', function (done) {
this.timeout(60 * 1000);

let numRequestAttempts = 0;

const mockServer = new http.Server((req, res) => {
numRequestAttempts++;
res.statusCode = 408;
res.end();
});

mockServer.listen(MOCK_HOST_PORT);

service.request(
{
uri: '/mock-endpoint-retry',
},
err => {
assert.strictEqual((err! as common.ApiError).code, 408);
assert.strictEqual(numRequestAttempts, 4);
mockServer.close(done);
},
);
});

it('should retry non-responsive hosts', function (done) {
this.timeout(60 * 1000);

function getMinimumRetryDelay(retryNumber: number) {
return Math.pow(2, retryNumber) * 1000;
}

let minExpectedResponseTime = 0;
let numExpectedRetries = 2;

while (numExpectedRetries--) {
minExpectedResponseTime += getMinimumRetryDelay(numExpectedRetries + 1);
}

const timeRequest = Date.now();

service.request(
{
uri: '/mock-endpoint-no-response',
},
err => {
assert(err?.message.includes('ECONNREFUSED'));
const timeResponse = Date.now();
assert(timeResponse - timeRequest > minExpectedResponseTime);
done();
},
);
});
it(
'should retry a request',
done => {
let numRequestAttempts = 0;
nock(MOCK_HOST)
.get('/projects/fake-project-id/mock-endpoint-retry')
.times(4)
.reply(uri => {
numRequestAttempts++;
return [408, ''];
});

service.request(
{
uri: '/mock-endpoint-retry',
},
err => {
try {
expect((err! as common.ApiError).code).toBe(408);
expect(numRequestAttempts).toBe(4);
done();
} catch (e) {
done(e);
}
},
);
},
60000,
);

it(
'should retry non-responsive hosts',
done => {

function getMinimumRetryDelay(retryNumber: number) {
return Math.pow(2, retryNumber) * 1000;
}

let minExpectedResponseTime = 0;
let numExpectedRetries = 2;

while (numExpectedRetries--) {
minExpectedResponseTime += getMinimumRetryDelay(numExpectedRetries + 1);
}

const timeRequest = Date.now();

service.request(
{
uri: '/mock-endpoint-no-response',
},
err => {
try {
expect(err?.message).toContain('ECONNREFUSED');
const timeResponse = Date.now();
expect(timeResponse - timeRequest).toBeGreaterThan(minExpectedResponseTime);
done();
} catch (e) {
done(e);
}
},
);
},
60000,
);
});
});
Loading
Loading