mirror of https://github.com/grpc/grpc-node.git
grpc-js: define Server API contract
This commit defines the Server API contract, and implements the Server functionality, minus the actual handling of requests.
This commit is contained in:
parent
167625719d
commit
62e7f0c85a
|
@ -15,6 +15,7 @@
|
||||||
"types": "build/src/index.d.ts",
|
"types": "build/src/index.d.ts",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@grpc/proto-loader": "^0.4.0",
|
||||||
"@types/lodash": "^4.14.108",
|
"@types/lodash": "^4.14.108",
|
||||||
"@types/mocha": "^5.2.6",
|
"@types/mocha": "^5.2.6",
|
||||||
"@types/node": "^11.13.2",
|
"@types/node": "^11.13.2",
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 gRPC authors.
|
||||||
|
*
|
||||||
|
* 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 {EventEmitter} from 'events';
|
||||||
|
import {Duplex, Readable, Writable} from 'stream';
|
||||||
|
import {ServiceError} from './call';
|
||||||
|
import {Deserialize, Serialize} from './make-client';
|
||||||
|
import {Metadata} from './metadata';
|
||||||
|
|
||||||
|
|
||||||
|
export class ServerUnaryCall<RequestType> extends EventEmitter {
|
||||||
|
cancelled: boolean;
|
||||||
|
request: RequestType|null;
|
||||||
|
|
||||||
|
constructor(private call: ServerCall, public metadata: Metadata) {
|
||||||
|
super();
|
||||||
|
this.cancelled = false;
|
||||||
|
this.request = null; // TODO(cjihrig): Read the unary request here.
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeer(): string {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMetadata(responseMetadata: Metadata): void {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ServerReadableStream<RequestType> extends Readable {
|
||||||
|
cancelled: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private call: ServerCall, public metadata: Metadata,
|
||||||
|
private deserialize: Deserialize<RequestType>) {
|
||||||
|
super();
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeer(): string {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMetadata(responseMetadata: Metadata): void {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ServerWritableStream<RequestType, ResponseType> extends Writable {
|
||||||
|
cancelled: boolean;
|
||||||
|
request: RequestType|null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private call: ServerCall, public metadata: Metadata,
|
||||||
|
private serialize: Serialize<ResponseType>) {
|
||||||
|
super();
|
||||||
|
this.cancelled = false;
|
||||||
|
this.request = null; // TODO(cjihrig): Read the unary request here.
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeer(): string {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMetadata(responseMetadata: Metadata): void {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class ServerDuplexStream<RequestType, ResponseType> extends Duplex {
|
||||||
|
cancelled: boolean;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private call: ServerCall, public metadata: Metadata,
|
||||||
|
private serialize: Serialize<ResponseType>,
|
||||||
|
private deserialize: Deserialize<RequestType>) {
|
||||||
|
super();
|
||||||
|
this.cancelled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPeer(): string {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
|
||||||
|
sendMetadata(responseMetadata: Metadata): void {
|
||||||
|
throw new Error('not implemented yet');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Internal class that wraps the HTTP2 request.
|
||||||
|
export class ServerCall {}
|
||||||
|
|
||||||
|
|
||||||
|
// Unary response callback signature.
|
||||||
|
export type sendUnaryData<ResponseType> =
|
||||||
|
(error: ServiceError|null, value: ResponseType|null, trailer?: Metadata,
|
||||||
|
flags?: number) => void;
|
||||||
|
|
||||||
|
// User provided handler for unary calls.
|
||||||
|
export type handleUnaryCall<RequestType, ResponseType> =
|
||||||
|
(call: ServerUnaryCall<RequestType>,
|
||||||
|
callback: sendUnaryData<ResponseType>) => void;
|
||||||
|
|
||||||
|
// User provided handler for client streaming calls.
|
||||||
|
export type handleClientStreamingCall<RequestType, ResponseType> =
|
||||||
|
(call: ServerReadableStream<RequestType>,
|
||||||
|
callback: sendUnaryData<ResponseType>) => void;
|
||||||
|
|
||||||
|
// User provided handler for server streaming calls.
|
||||||
|
export type handleServerStreamingCall<RequestType, ResponseType> =
|
||||||
|
(call: ServerWritableStream<RequestType, ResponseType>) => void;
|
||||||
|
|
||||||
|
// User provided handler for bidirectional streaming calls.
|
||||||
|
export type handleBidiStreamingCall<RequestType, ResponseType> =
|
||||||
|
(call: ServerDuplexStream<RequestType, ResponseType>) => void;
|
||||||
|
|
||||||
|
export type HandleCall<RequestType, ResponseType> =
|
||||||
|
handleUnaryCall<RequestType, ResponseType>|
|
||||||
|
handleClientStreamingCall<RequestType, ResponseType>|
|
||||||
|
handleServerStreamingCall<RequestType, ResponseType>|
|
||||||
|
handleBidiStreamingCall<RequestType, ResponseType>;
|
||||||
|
|
||||||
|
export type Handler<RequestType, ResponseType> = {
|
||||||
|
func: HandleCall<RequestType, ResponseType>;
|
||||||
|
serialize: Serialize<ResponseType>;
|
||||||
|
deserialize: Deserialize<RequestType>;
|
||||||
|
type: HandlerType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HandlerType = 'bidi'|'clientStream'|'serverStream'|'unary';
|
|
@ -0,0 +1,230 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 gRPC authors.
|
||||||
|
*
|
||||||
|
* 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 http2 from 'http2';
|
||||||
|
import {AddressInfo, ListenOptions} from 'net';
|
||||||
|
import {URL} from 'url';
|
||||||
|
|
||||||
|
import {ServiceError} from './call';
|
||||||
|
import {Status} from './constants';
|
||||||
|
import {Deserialize, Serialize, ServiceDefinition} from './make-client';
|
||||||
|
import {HandleCall, Handler, HandlerType, sendUnaryData, ServerDuplexStream, ServerReadableStream, ServerUnaryCall, ServerWritableStream} from './server-call';
|
||||||
|
import {ServerCredentials} from './server-credentials';
|
||||||
|
|
||||||
|
function noop(): void {}
|
||||||
|
|
||||||
|
type PartialServiceError = Partial<ServiceError>;
|
||||||
|
const unimplementedStatusResponse: PartialServiceError = {
|
||||||
|
code: Status.UNIMPLEMENTED,
|
||||||
|
details: 'The server does not implement this method',
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable:no-any
|
||||||
|
type UntypedHandleCall = HandleCall<any, any>;
|
||||||
|
type UntypedHandler = Handler<any, any>;
|
||||||
|
type UntypedServiceImplementation = {
|
||||||
|
[name: string]: UntypedHandleCall
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultHandler = {
|
||||||
|
unary(call: ServerUnaryCall<any>, callback: sendUnaryData<any>): void {
|
||||||
|
callback(unimplementedStatusResponse as ServiceError, null);
|
||||||
|
},
|
||||||
|
clientStream(call: ServerReadableStream<any>, callback: sendUnaryData<any>):
|
||||||
|
void {
|
||||||
|
callback(unimplementedStatusResponse as ServiceError, null);
|
||||||
|
},
|
||||||
|
serverStream(call: ServerWritableStream<any, any>): void {
|
||||||
|
call.emit('error', unimplementedStatusResponse);
|
||||||
|
},
|
||||||
|
bidi(call: ServerDuplexStream<any, any>): void {
|
||||||
|
call.emit('error', unimplementedStatusResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// tslint:enable:no-any
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
private http2Server: http2.Http2Server|http2.Http2SecureServer|null = null;
|
||||||
|
private handlers: Map<string, UntypedHandler> =
|
||||||
|
new Map<string, UntypedHandler>();
|
||||||
|
private started = false;
|
||||||
|
|
||||||
|
constructor(options?: object) {}
|
||||||
|
|
||||||
|
addProtoService(): void {
|
||||||
|
throw new Error('Not implemented. Use addService() instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
addService(service: ServiceDefinition, implementation: object): void {
|
||||||
|
if (this.started === true) {
|
||||||
|
throw new Error('Can\'t add a service to a started server.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service === null || typeof service !== 'object' ||
|
||||||
|
implementation === null || typeof implementation !== 'object') {
|
||||||
|
throw new Error('addService() requires two objects as arguments');
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceKeys = Object.keys(service);
|
||||||
|
|
||||||
|
if (serviceKeys.length === 0) {
|
||||||
|
throw new Error('Cannot add an empty service to a server');
|
||||||
|
}
|
||||||
|
|
||||||
|
const implMap: UntypedServiceImplementation =
|
||||||
|
implementation as UntypedServiceImplementation;
|
||||||
|
|
||||||
|
serviceKeys.forEach((name) => {
|
||||||
|
const attrs = service[name];
|
||||||
|
let methodType: HandlerType;
|
||||||
|
|
||||||
|
if (attrs.requestStream) {
|
||||||
|
if (attrs.responseStream) {
|
||||||
|
methodType = 'bidi';
|
||||||
|
} else {
|
||||||
|
methodType = 'clientStream';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (attrs.responseStream) {
|
||||||
|
methodType = 'serverStream';
|
||||||
|
} else {
|
||||||
|
methodType = 'unary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let implFn = implMap[name];
|
||||||
|
let impl;
|
||||||
|
|
||||||
|
if (implFn === undefined && typeof attrs.originalName === 'string') {
|
||||||
|
implFn = implMap[attrs.originalName];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (implFn !== undefined) {
|
||||||
|
impl = implFn.bind(implementation);
|
||||||
|
} else {
|
||||||
|
impl = defaultHandler[methodType];
|
||||||
|
}
|
||||||
|
|
||||||
|
const success = this.register(
|
||||||
|
attrs.path, impl, attrs.responseSerialize, attrs.requestDeserialize,
|
||||||
|
methodType);
|
||||||
|
|
||||||
|
if (success === false) {
|
||||||
|
throw new Error(`Method handler for ${attrs.path} already provided.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bind(port: string, creds: ServerCredentials): void {
|
||||||
|
throw new Error('Not implemented. Use bindAsync() instead');
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAsync(
|
||||||
|
port: string, creds: ServerCredentials,
|
||||||
|
callback: (error: Error|null, port: number) => void): void {
|
||||||
|
if (this.started === true) {
|
||||||
|
throw new Error('server is already started');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof port !== 'string') {
|
||||||
|
throw new TypeError('port must be a string');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (creds === null || typeof creds !== 'object') {
|
||||||
|
throw new TypeError('creds must be an object');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof callback !== 'function') {
|
||||||
|
throw new TypeError('callback must be a function');
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(`http://${port}`);
|
||||||
|
const options: ListenOptions = {host: url.hostname, port: +url.port};
|
||||||
|
|
||||||
|
if (creds._isSecure()) {
|
||||||
|
this.http2Server = http2.createSecureServer(
|
||||||
|
creds._getSettings() as http2.SecureServerOptions);
|
||||||
|
} else {
|
||||||
|
this.http2Server = http2.createServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(cjihrig): Set up the handlers, to allow requests to be processed.
|
||||||
|
|
||||||
|
function onError(err: Error): void {
|
||||||
|
callback(err, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.http2Server.once('error', onError);
|
||||||
|
|
||||||
|
this.http2Server.listen(options, () => {
|
||||||
|
const server =
|
||||||
|
this.http2Server as http2.Http2Server | http2.Http2SecureServer;
|
||||||
|
const port = (server.address() as AddressInfo).port;
|
||||||
|
|
||||||
|
server.removeListener('error', onError);
|
||||||
|
callback(null, port);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
forceShutdown(): void {
|
||||||
|
throw new Error('Not yet implemented');
|
||||||
|
}
|
||||||
|
|
||||||
|
register<RequestType, ResponseType>(
|
||||||
|
name: string, handler: HandleCall<RequestType, ResponseType>,
|
||||||
|
serialize: Serialize<ResponseType>, deserialize: Deserialize<RequestType>,
|
||||||
|
type: string): boolean {
|
||||||
|
if (this.handlers.has(name)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.handlers.set(
|
||||||
|
name,
|
||||||
|
{func: handler, serialize, deserialize, type: type as HandlerType});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if (this.http2Server === null || this.http2Server.listening !== true) {
|
||||||
|
throw new Error('server must be bound in order to start');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.started === true) {
|
||||||
|
throw new Error('server is already started');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
tryShutdown(callback: (error?: Error) => void): void {
|
||||||
|
callback = typeof callback === 'function' ? callback : noop;
|
||||||
|
|
||||||
|
if (this.http2Server === null) {
|
||||||
|
callback(new Error('server is not running'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.http2Server.close((err?: Error) => {
|
||||||
|
this.started = false;
|
||||||
|
callback(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addHttp2Port(): void {
|
||||||
|
throw new Error('Not yet implemented');
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,8 +15,19 @@
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as loader from '@grpc/proto-loader';
|
||||||
import * as assert from 'assert';
|
import * as assert from 'assert';
|
||||||
|
|
||||||
|
import {GrpcObject, loadPackageDefinition} from '../src/make-client';
|
||||||
|
|
||||||
|
const protoLoaderOptions = {
|
||||||
|
keepCase: true,
|
||||||
|
longs: String,
|
||||||
|
enums: String,
|
||||||
|
defaults: true,
|
||||||
|
oneofs: true
|
||||||
|
};
|
||||||
|
|
||||||
export function mockFunction(): never {
|
export function mockFunction(): never {
|
||||||
throw new Error('Not implemented');
|
throw new Error('Not implemented');
|
||||||
}
|
}
|
||||||
|
@ -100,3 +111,8 @@ export namespace assert2 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadProtoFile(file: string): GrpcObject {
|
||||||
|
const packageDefinition = loader.loadSync(file, protoLoaderOptions);
|
||||||
|
return loadPackageDefinition(packageDefinition);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,50 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package math;
|
||||||
|
|
||||||
|
message DivArgs {
|
||||||
|
int64 dividend = 1;
|
||||||
|
int64 divisor = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message DivReply {
|
||||||
|
int64 quotient = 1;
|
||||||
|
int64 remainder = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FibArgs {
|
||||||
|
int64 limit = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Num {
|
||||||
|
int64 num = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message FibReply {
|
||||||
|
int64 count = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
service Math {
|
||||||
|
// Div divides DivArgs.dividend by DivArgs.divisor and returns the quotient
|
||||||
|
// and remainder.
|
||||||
|
rpc Div (DivArgs) returns (DivReply) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// DivMany accepts an arbitrary number of division args from the client stream
|
||||||
|
// and sends back the results in the reply stream. The stream continues until
|
||||||
|
// the client closes its end; the server does the same after sending all the
|
||||||
|
// replies. The stream ends immediately if either end aborts.
|
||||||
|
rpc DivMany (stream DivArgs) returns (stream DivReply) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fib generates numbers in the Fibonacci sequence. If FibArgs.limit > 0, Fib
|
||||||
|
// generates up to limit numbers; otherwise it continues until the call is
|
||||||
|
// canceled. Unlike Fib above, Fib has no final FibReply.
|
||||||
|
rpc Fib (FibArgs) returns (stream Num) {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum sums a stream of numbers, returning the final result once the stream
|
||||||
|
// is closed.
|
||||||
|
rpc Sum (stream Num) returns (Num) {
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,231 @@
|
||||||
|
/*
|
||||||
|
* Copyright 2019 gRPC authors.
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Allow `any` data type for testing runtime type checking.
|
||||||
|
// tslint:disable no-any
|
||||||
|
import * as assert from 'assert';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import {ServerCredentials} from '../src';
|
||||||
|
import {Server} from '../src/server';
|
||||||
|
|
||||||
|
import {loadProtoFile} from './common';
|
||||||
|
|
||||||
|
const ca = fs.readFileSync(path.join(__dirname, 'fixtures', 'ca.pem'));
|
||||||
|
const key = fs.readFileSync(path.join(__dirname, 'fixtures', 'server1.key'));
|
||||||
|
const cert = fs.readFileSync(path.join(__dirname, 'fixtures', 'server1.pem'));
|
||||||
|
function noop(): void {}
|
||||||
|
|
||||||
|
|
||||||
|
describe('Server', () => {
|
||||||
|
describe('constructor', () => {
|
||||||
|
it('should work with no arguments', () => {
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
new Server(); // tslint:disable-line:no-unused-expression
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with an empty object argument', () => {
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
new Server({}); // tslint:disable-line:no-unused-expression
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be an instance of Server', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert(server instanceof Server);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('bindAsync', () => {
|
||||||
|
it('binds with insecure credentials', (done) => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
server.bindAsync(
|
||||||
|
'localhost:0', ServerCredentials.createInsecure(), (err, port) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert(typeof port === 'number' && port > 0);
|
||||||
|
server.tryShutdown(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('binds with secure credentials', (done) => {
|
||||||
|
const server = new Server();
|
||||||
|
const creds = ServerCredentials.createSsl(
|
||||||
|
ca, [{private_key: key, cert_chain: cert}], true);
|
||||||
|
|
||||||
|
server.bindAsync('localhost:0', creds, (err, port) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
assert(typeof port === 'number' && port > 0);
|
||||||
|
server.tryShutdown(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if bind is called after the server is started', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
server.bindAsync(
|
||||||
|
'localhost:0', ServerCredentials.createInsecure(), (err, port) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
server.start();
|
||||||
|
assert.throws(() => {
|
||||||
|
server.bindAsync(
|
||||||
|
'localhost:0', ServerCredentials.createInsecure(), noop);
|
||||||
|
}, /server is already started/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws on invalid inputs', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.bindAsync(null as any, ServerCredentials.createInsecure(), noop);
|
||||||
|
}, /port must be a string/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.bindAsync('localhost:0', null as any, noop);
|
||||||
|
}, /creds must be an object/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.bindAsync(
|
||||||
|
'localhost:0', ServerCredentials.createInsecure(), null as any);
|
||||||
|
}, /callback must be a function/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tryShutdown', () => {
|
||||||
|
it('calls back with an error if the server is not running', (done) => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
server.tryShutdown((err) => {
|
||||||
|
assert(err !== undefined && err.message === 'server is not running');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('start', () => {
|
||||||
|
let server: Server;
|
||||||
|
|
||||||
|
beforeEach((done) => {
|
||||||
|
server = new Server();
|
||||||
|
server.bindAsync('localhost:0', ServerCredentials.createInsecure(), done);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach((done) => {
|
||||||
|
server.tryShutdown(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts without error', () => {
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
server.start();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if started twice', () => {
|
||||||
|
server.start();
|
||||||
|
assert.throws(() => {
|
||||||
|
server.start();
|
||||||
|
}, /server is already started/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws if the server is not bound', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.start();
|
||||||
|
}, /server must be bound in order to start/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addService', () => {
|
||||||
|
const mathProtoFile = path.join(__dirname, 'fixtures', 'math.proto');
|
||||||
|
const mathClient = (loadProtoFile(mathProtoFile).math as any).Math;
|
||||||
|
const mathServiceAttrs = mathClient.service;
|
||||||
|
const dummyImpls = {div() {}, divMany() {}, fib() {}, sum() {}};
|
||||||
|
const altDummyImpls = {Div() {}, DivMany() {}, Fib() {}, Sum() {}};
|
||||||
|
|
||||||
|
it('succeeds with a single service', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
server.addService(mathServiceAttrs, dummyImpls);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails to add an empty service', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.addService({}, dummyImpls);
|
||||||
|
}, /Cannot add an empty service to a server/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails with conflicting method names', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
server.addService(mathServiceAttrs, dummyImpls);
|
||||||
|
assert.throws(() => {
|
||||||
|
server.addService(mathServiceAttrs, dummyImpls);
|
||||||
|
}, /Method handler for .+ already provided/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('supports method names as originally written', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.doesNotThrow(() => {
|
||||||
|
server.addService(mathServiceAttrs, altDummyImpls);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fails if the server has been started', (done) => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
server.bindAsync(
|
||||||
|
'localhost:0', ServerCredentials.createInsecure(), (err, port) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
server.start();
|
||||||
|
assert.throws(() => {
|
||||||
|
server.addService(mathServiceAttrs, dummyImpls);
|
||||||
|
}, /Can't add a service to a started server\./);
|
||||||
|
server.tryShutdown(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('throws when unimplemented methods are called', () => {
|
||||||
|
const server = new Server();
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.addProtoService();
|
||||||
|
}, /Not implemented. Use addService\(\) instead/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.forceShutdown();
|
||||||
|
}, /Not yet implemented/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.addHttp2Port();
|
||||||
|
}, /Not yet implemented/);
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
server.bind('localhost:0', ServerCredentials.createInsecure());
|
||||||
|
}, /Not implemented. Use bindAsync\(\) instead/);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue