Skip to content

Instantly share code, notes, and snippets.

@thebrubaker
Last active February 14, 2019 19:33
Show Gist options
  • Save thebrubaker/ca27db43243e068b39f6602ed055effe to your computer and use it in GitHub Desktop.
Save thebrubaker/ca27db43243e068b39f6602ed055effe to your computer and use it in GitHub Desktop.
An example Node gRPC service.
ADDRESS_PORT='localhost:50051'
PROTO_PATH='./path/to/file.proto'
const grpc = require('grpc');
const someService = require('./some-external-service');
module.exports = {
yourAction: (call, callback) => {
const { request } = call;
if (!request.someAttribute) {
return callback({
code: grpc.status.INVALID_ARGUMENT,
message: 'Missing `someAttribute` argument.',
});
}
const response = {
someAttribute: 'foo',
}
someService.fetchDataOrSomething()
.then(() => {
callback(null, response);
})
.catch(error => {
console.error(error);
callback({
code: grpc.status.INTERNAL,
message: error.message,
});
});
}
}
const actions = require('./actions');
const someService = require('./some-external-service');
describe('yourAction', () => {
beforeAll(() => {
someService.fetchDataOrSomething = jest.fn(() => Promise.resolve()); // mock out unrelated functions
console.error = jest.fn();
});
afterAll(() => {
someService.fetchDataOrSomething.mockRestore(); // restore the function after tests are run
console.error.mockRestore();
});
it('describe your action here', done => {
const call = {
request: {
someAttribute: 'fooBar',
},
};
actions.yourAction(call, (error, response) => {
expect(error).toBeNull();
expect(someService.run).toBeCalled(); // assert external functions were called
expect(response).toEqual({
someAttribute: 'foo',
});
done();
});
});
it('validates type argument as required', done => {
const call = {
request: {
someAttribute: null,
},
};
const callback = error => {
expect(error).toEqual(argumentErrorShape);
done();
};
actions.yourAction(call, callback);
});
it('handles when an external function throws errors', done => {
someService.run = jest.fn(() =>
Promise.reject(Error('Testing error')),
);
console.error = jest.fn();
const call = {
request: {
someAttribute: 'fooBar',
},
};
const callback = (error, _) => {
expect(error).toEqual(someServiceErrorShape);
expect(console.error).toBeCalled();
done();
};
create(call, callback);
});
});
const argumentErrorShape = expect.objectContaining({
code: expect.any(Number),
message: expect.any(String),
});
const someServiceErrorShape = expect.objectContaining({
code: 13,
message: expect.any(String),
});
FROM node:latest
WORKDIR /app
RUN mkdir /app/storage
COPY ./ /app
RUN npm install
CMD ["npm", "start"]
require('dotenv').config();
const path = require('path');
const Server = require('./server');
const {
ADDRESS_PORT = '0.0.0.0:50051',
PROTO_PATH = '../path/to/file.proto',
} = process.env;
const server = new Server({
addressPort: ADDRESS_PORT,
protoPath: path.join(__dirname, PROTO_PATH),
});
server.start();
console.log(`Your gRPC service running on ${ADDRESS_PORT}`);
module.exports = {
testEnvironment: 'node',
coverageDirectory: './storage/coverage',
coverageReporters: ['json-summary', 'text'],
collectCoverageFrom: [
'**/*.{js,jsx}',
'!**/node_modules/**',
'!jest.config.js',
'!index.js',
],
};
{
"name": "some-service",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest --coverage",
"test:unit": "jest unit --coverage",
"test:integration": "jest int --coverage",
"start": "node index.js",
"watch": "nodemon --inspect=0.0.0.0:9222 --nolazy"
},
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^23.6.0",
"nodemon": "^1.18.9"
},
"dependencies": {
"dotenv": "^6.2.0"
}
}
const path = require('path');
const Server = require('./server');
const server = new Server({
addressPort: '127.0.0.1:50060',
protoPath: path.join(__dirname, '../path/to/file.proto'),
});
const client = server.client();
describe('ProtoService', () => {
beforeAll(() => {
server.start();
});
afterAll(() => {
server.stop();
});
it('name your grpc action here', done => {
const payload = {
someAttribute: 'foo',
};
client.yourAction(payload, (error, response) => {
expect(error).toBeNull();
expect(response).toEqual(responseShape);
done();
});
});
});
const responseShape = expect.objectContaining({
someAttribute: expect.any(String),
});
const grpc = require('grpc');
const protoLoader = require('@grpc/proto-loader');
const actions = require('./actions');
module.exports = class Server {
constructor({ addressPort, protoPath, credentials }) {
this.addressPort = addressPort;
this.definition = this.loadPackageDefinition(protoPath);
this.credentials = credentials;
this.server = new grpc.Server();
}
start() {
const { ProtoService } = this.definition; // replace with your service name
this.server.addService(ProtoService.service, actions);
this.server.bind(this.addressPort, this.serverCredentials());
return this.server.start();
}
stop() {
return this.server.forceShutdown();
}
client() {
const { ProtoService } = this.definition; // replace with your service name
return new ProtoService(this.addressPort, this.clientCredentials());
}
loadPackageDefinition(protoPath) {
return grpc.loadPackageDefinition(
protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
}),
);
}
serverCredentials() {
if (!this.credentials) {
return grpc.ServerCredentials.createInsecure();
}
return grpc.ServerCredentials.createSsl(
this.credentials.rootCertificate,
this.credentials.keyCertificatePairs,
this.credentials.checkClientCertificate,
);
}
clientCredentials() {
if (!this.credentials) {
return grpc.credentials.createInsecure();
}
return grpc.credentials.createSsl(
this.credentials.rootCertificate,
this.credentials.keyCertificatePairs,
this.credentials.checkClientCertificate,
);
}
};
syntax = "proto3";
service ProtoService {
rpc yourAction (Request) returns (Response) {}
}
message Request {
string someAttribute = 1;
}
message Response {
string someAttribute = 1;
}
module.exports = {
async fetchDataOrSomething() {
return await true;
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment