From ecae5f3bc00210c44513a40895a3c128938383bf Mon Sep 17 00:00:00 2001 From: danieljbruce Date: Mon, 4 Jul 2022 15:21:55 -0400 Subject: [PATCH] feat: Errors from gax layer (#1090) * Getting grpc set up for tests * test for error sent through gax * Trying other things * Group everything into describe blocks. * Add Google header * Added more tests * Create mock service files * refactor a check * mock server tests * undo tests * Pass metadata through * build fix * work in progress * Add service error check and change test * Remove TODO and add done hook. * Logs for debugging * use the tcp-port-used library instead * use await Co-authored-by: Benjamin E. Coe --- package.json | 1 + src/util/mock-servers/mock-server.ts | 63 ++++++++ src/util/mock-servers/mock-service.ts | 34 ++++ .../bigtable-client-mock-service.ts | 38 +++++ test/errors.ts | 151 ++++++++++++++++++ test/util/mock-server.ts | 51 ++++++ 6 files changed, 338 insertions(+) create mode 100644 src/util/mock-servers/mock-server.ts create mode 100644 src/util/mock-servers/mock-service.ts create mode 100644 src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts create mode 100644 test/errors.ts create mode 100644 test/util/mock-server.ts diff --git a/package.json b/package.json index 40e122b4a..e559e4683 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "pack-n-play": "^1.0.0-2", "proxyquire": "^2.0.0", "sinon": "^14.0.0", + "tcp-port-used": "^1.0.2", "snap-shot-it": "^7.9.1", "ts-loader": "^9.0.0", "typescript": "^4.6.4", diff --git a/src/util/mock-servers/mock-server.ts b/src/util/mock-servers/mock-server.ts new file mode 100644 index 000000000..c4ebb9779 --- /dev/null +++ b/src/util/mock-servers/mock-server.ts @@ -0,0 +1,63 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {grpc} from 'google-gax'; + +const DEFAULT_PORT = 1234; + +export class MockServer { + port: string; + services: Set = new Set(); + server: grpc.Server; + + constructor( + callback?: (port: string) => void, + port?: string | number | undefined + ) { + const portString = Number(port ? port : DEFAULT_PORT).toString(); + this.port = portString; + const server = new grpc.Server(); + this.server = server; + server.bindAsync( + `localhost:${this.port}`, + grpc.ServerCredentials.createInsecure(), + () => { + server.start(); + callback ? callback(portString) : undefined; + } + ); + } + + setService( + service: grpc.ServiceDefinition, + implementation: grpc.UntypedServiceImplementation + ) { + if (this.services.has(service)) { + this.server.removeService(service); + } else { + this.services.add(service); + } + this.server.addService(service, implementation); + } + + shutdown(callback: (err?: Error) => void) { + this.server.tryShutdown((err?: Error) => { + callback(err); + }); + } +} diff --git a/src/util/mock-servers/mock-service.ts b/src/util/mock-servers/mock-service.ts new file mode 100644 index 000000000..37c25d433 --- /dev/null +++ b/src/util/mock-servers/mock-service.ts @@ -0,0 +1,34 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {grpc} from 'google-gax'; + +import {MockServer} from './mock-server'; + +export abstract class MockService { + abstract service: grpc.ServiceDefinition; + server: MockServer; + + constructor(server: MockServer) { + this.server = server; + } + + setService(implementation: grpc.UntypedServiceImplementation) { + this.server.setService(this.service, implementation); + } +} diff --git a/src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts b/src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts new file mode 100644 index 000000000..614593252 --- /dev/null +++ b/src/util/mock-servers/service-implementations/bigtable-client-mock-service.ts @@ -0,0 +1,38 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import grpc = require('@grpc/grpc-js'); +import jsonProtos = require('../../../../protos/protos.json'); +import protoLoader = require('@grpc/proto-loader'); +import {MockService} from '../mock-service'; + +const packageDefinition = protoLoader.fromJSON(jsonProtos, { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, +}); +const proto = grpc.loadPackageDefinition(packageDefinition); + +export class BigtableClientMockService extends MockService { + service = + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + proto.google.bigtable.v2.Bigtable.service; +} diff --git a/test/errors.ts b/test/errors.ts new file mode 100644 index 000000000..33721b743 --- /dev/null +++ b/test/errors.ts @@ -0,0 +1,151 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {before, describe, it} from 'mocha'; +import {Bigtable} from '../src'; +import * as assert from 'assert'; + +import {GoogleError, grpc, ServiceError} from 'google-gax'; +import {MockServer} from '../src/util/mock-servers/mock-server'; +import {BigtableClientMockService} from '../src/util/mock-servers/service-implementations/bigtable-client-mock-service'; +import {MockService} from '../src/util/mock-servers/mock-service'; + +function isServiceError(error: any): error is ServiceError { + return ( + error.code !== undefined && + error.details !== undefined && + error.metadata !== undefined + ); +} + +describe('Bigtable/Errors', () => { + let server: MockServer; + let bigtable: Bigtable; + let table: any; + + before(done => { + server = new MockServer(() => { + bigtable = new Bigtable({ + apiEndpoint: `localhost:${server.port}`, + }); + table = bigtable.instance('fake-instance').table('fake-table'); + done(); + }); + }); + + describe('with the bigtable data client', () => { + let service: MockService; + before(async () => { + service = new BigtableClientMockService(server); + }); + + describe('sends errors through a streaming request', () => { + const errorDetails = + 'Table not found: projects/my-project/instances/my-instance/tables/my-table'; + const emitTableNotExistsError = (stream: any) => { + // TODO: Replace stream with type + const metadata = new grpc.Metadata(); + metadata.set( + 'grpc-server-stats-bin', + Buffer.from([0, 0, 116, 73, 159, 3, 0, 0, 0, 0]) + ); + stream.emit('error', { + code: 5, + details: errorDetails, + metadata, + }); + }; + function checkTableNotExistError(err: any) { + if (isServiceError(err)) { + const {code, message, details} = err; + assert.strictEqual(details, errorDetails); + assert.strictEqual(code, 5); + assert.strictEqual(message, `5 NOT_FOUND: ${errorDetails}`); + } else { + assert.fail( + 'Errors checked using this function should all be GoogleErrors' + ); + } + } + describe('with ReadRows service', () => { + before(async () => { + service.setService({ + ReadRows: emitTableNotExistsError, + }); + }); + it('should produce human readable error when passing through gax', done => { + const readStream = table.createReadStream({}); + readStream.on('error', (err: GoogleError) => { + checkTableNotExistError(err); + done(); + }); + }); + }); + describe('with mutateRows service through insert', () => { + before(async () => { + service.setService({ + mutateRows: emitTableNotExistsError, + }); + }); + it('should produce human readable error when passing through gax', async () => { + const timestamp = new Date(); + const rowsToInsert = [ + { + key: 'r2', + data: { + cf1: { + c1: { + value: 'test-value2', + labels: [], + timestamp, + }, + }, + }, + }, + ]; + try { + await table.insert(rowsToInsert); + } catch (err) { + checkTableNotExistError(err); + return; + } + assert.fail('An error should have been thrown by the stream'); + }); + }); + describe('with sampleRowKeys', () => { + before(async () => { + service.setService({ + sampleRowKeys: emitTableNotExistsError, + }); + }); + it('should produce human readable error when passing through gax', async () => { + try { + await table.sampleRowKeys({}); + } catch (err) { + checkTableNotExistError(err); + return; + } + assert.fail('An error should have been thrown by the stream'); + }); + }); + }); + }); + after(async () => { + server.shutdown(() => {}); + }); +}); diff --git a/test/util/mock-server.ts b/test/util/mock-server.ts new file mode 100644 index 000000000..920673579 --- /dev/null +++ b/test/util/mock-server.ts @@ -0,0 +1,51 @@ +// Copyright 2022 Google LLC +// +// 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 +// +// 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. +// +// ** This file is automatically generated by gapic-generator-typescript. ** +// ** https://github.com/googleapis/gapic-generator-typescript ** +// ** All changes to this file may be overwritten. ** + +import {describe, it} from 'mocha'; +import {MockServer} from '../../src/util/mock-servers/mock-server'; +import * as assert from 'assert'; + +const tcpPortUsed = require('tcp-port-used'); + +describe('Bigtable/Mock-Server', () => { + const inputPort = '1234'; + let server: MockServer; + async function checkPort(port: string, inUse: boolean, callback: () => void) { + const isInUse: boolean = await tcpPortUsed.check( + parseInt(port), + 'localhost' + ); + assert.strictEqual(isInUse, inUse); + callback(); + } + describe('Ensure server shuts down properly when destroyed', () => { + it('should start a mock server', done => { + server = new MockServer(port => { + checkPort(port, true, done); + }, inputPort); + }); + }); + after(done => { + checkPort(server.port, true, () => { + server.shutdown((err?: Error) => { + assert.deepStrictEqual(err, undefined); + checkPort(server.port, false, done); + }); + }); + }); +});