From 2c0e13a3c8574a0b14b657b3806a20ee41f5f0ba Mon Sep 17 00:00:00 2001 From: Devesh Kumar Date: Thu, 30 Oct 2025 13:28:55 +0530 Subject: [PATCH 1/5] Add tests for V1 and V1 Alpha Server Reflection --- .eslintignore | 2 +- test/client.test.ts | 287 ++++++++++-------- test/fixtures/contact.proto | 7 - test/fixtures/phone.proto | 18 -- test/fixtures/v1/reflection.proto | 147 +++++++++ test/fixtures/v1/server_descriptor.protoset | Bin 0 -> 2274 bytes test/fixtures/v1/widgets.proto | 41 +++ test/fixtures/v1alpha/reflection.proto | 147 +++++++++ .../v1alpha/server_descriptor.protoset | Bin 0 -> 2274 bytes test/fixtures/v1alpha/widgets.proto | 41 +++ test/test-servers/v1-server.js | 279 +++++++++++++++++ test/test-servers/v1alpha-server.js | 279 +++++++++++++++++ 12 files changed, 1096 insertions(+), 152 deletions(-) delete mode 100644 test/fixtures/contact.proto delete mode 100644 test/fixtures/phone.proto create mode 100644 test/fixtures/v1/reflection.proto create mode 100644 test/fixtures/v1/server_descriptor.protoset create mode 100644 test/fixtures/v1/widgets.proto create mode 100644 test/fixtures/v1alpha/reflection.proto create mode 100644 test/fixtures/v1alpha/server_descriptor.protoset create mode 100644 test/fixtures/v1alpha/widgets.proto create mode 100644 test/test-servers/v1-server.js create mode 100644 test/test-servers/v1alpha-server.js diff --git a/.eslintignore b/.eslintignore index ab16bd6..f2f6ce2 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -build/src/reflection_* +build/* src/reflection_* diff --git a/test/client.test.ts b/test/client.test.ts index 84a0ba4..9345cd6 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -1,3 +1,4 @@ +import {describe, before, after, it} from 'mocha'; import {Client} from '../src/client'; import {credentials} from '@postman/grpc-js'; // eslint-disable-next-line node/no-unpublished-import @@ -10,135 +11,169 @@ import { FileDescriptorResponse, } from '../src/reflection_providers/v1/reflection_pb'; -// eslint-disable-next-line no-undef -describe('listServices', () => { - // eslint-disable-next-line no-undef - it('should return services', async () => { - const reflectionClient = new Client( - 'localhost:4770', - credentials.createInsecure() - ); - - const grpcCall = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - on: function (_event: string, listener: (...args: any[]) => void) { - const res = new ServerReflectionResponse(); - - const service1 = new ServiceResponse(); - service1.setName('grpc.reflection.v1alpha.ServerReflection'); - const service2 = new ServiceResponse(); - service2.setName('phone.Messenger'); - const serviceList = [service1, service2]; - - const listService = new ListServiceResponse(); - listService.setServiceList(serviceList); - res.setListServicesResponse(listService); - - listener(res); - }, - write: function () {}, - end: function () {}, - }; - - const mock = sinon.mock(reflectionClient.grpcClient); - mock.expects('serverReflectionInfo').once().returns(grpcCall); - - const expectedServices: string[] = [ - 'grpc.reflection.v1alpha.ServerReflection', - 'phone.Messenger', - ]; - assert.sameMembers(await reflectionClient.listServices(), expectedServices); +describe('server reflection tests', () => { + const reflectionMethodsToTest = [ + {name: 'v1', port: 4771}, + {name: 'v1alpha', port: 4772}, + ]; + + let mockV1Server: {port: number; shutdown: () => Promise}; + let mockV1AlphaServer: {port: number; shutdown: () => Promise}; + + before(async () => { + const v1ServerImport = await import('./test-servers/v1-server'), + v1AlphaServerImport = await import('./test-servers/v1alpha-server'); + + mockV1Server = await v1ServerImport.start(4771); + mockV1AlphaServer = await v1AlphaServerImport.start(4772); }); -}); -// eslint-disable-next-line no-undef -describe('fileContainingSymbol', () => { - // eslint-disable-next-line no-undef - it('should return Root', async () => { - const reflectionClient = new Client( - 'localhost:4770', - credentials.createInsecure() - ); - - const grpcCallPhone = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - on: function (_event: string, listener: (...args: any[]) => void) { - if (_event === 'error') { - return; - } - - const res = new ServerReflectionResponse(); - const fileDescriptorResponse = new FileDescriptorResponse(); - // eslint-disable-next-line prettier/prettier - const protoBytes = Buffer.from([10,11,112,104,111,110,101,46,112,114,111,116,111,18,5,112,104,111,110,101,26,13,99,111,110,116,97,99,116,46,112,114,111,116,111,34,97,10,11,84,101,120,116,82,101,113,117,101,115,116,18,14,10,2,105,100,24,1,32,1,40,9,82,2,105,100,18,24,10,7,109,101,115,115,97,103,101,24,2,32,1,40,9,82,7,109,101,115,115,97,103,101,18,40,10,7,99,111,110,116,97,99,116,24,3,32,1,40,11,50,14,46,112,104,111,110,101,46,67,111,110,116,97,99,116,82,7,99,111,110,116,97,99,116,34,40,10,12,84,101,120,116,82,101,115,112,111,110,115,101,18,24,10,7,115,117,99,99,101,115,115,24,1,32,1,40,8,82,7,115,117,99,99,101,115,115,50,63,10,9,77,101,115,115,101,110,103,101,114,18,50,10,7,77,101,115,115,97,103,101,18,18,46,112,104,111,110,101,46,84,101,120,116,82,101,113,117,101,115,116,26,19,46,112,104,111,110,101,46,84,101,120,116,82,101,115,112,111,110,115,101,98,6,112,114,111,116,111,51]); - fileDescriptorResponse.addFileDescriptorProto(protoBytes); - res.setFileDescriptorResponse(fileDescriptorResponse); - - listener(res); - }, - write: function () {}, - end: function () {}, - }; - - const grpcCallContact = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - on: function (_event: string, listener: (...args: any[]) => void) { - if (_event === 'error') { - return; - } - - const res = new ServerReflectionResponse(); - const fileDescriptorResponse = new FileDescriptorResponse(); - // eslint-disable-next-line prettier/prettier - const protoBytes = Buffer.from([10,13,99,111,110,116,97,99,116,46,112,114,111,116,111,18,5,112,104,111,110,101,34,53,10,7,67,111,110,116,97,99,116,18,18,10,4,110,97,109,101,24,1,32,1,40,9,82,4,110,97,109,101,18,22,10,6,110,117,109,98,101,114,24,2,32,1,40,9,82,6,110,117,109,98,101,114,98,6,112,114,111,116,111,51]); - fileDescriptorResponse.addFileDescriptorProto(protoBytes); - res.setFileDescriptorResponse(fileDescriptorResponse); - - listener(res); - }, - write: function () {}, - end: function () {}, - }; - - const mock = sinon.mock(reflectionClient.grpcClient); - mock.expects('serverReflectionInfo').once().returns(grpcCallPhone); - mock.expects('serverReflectionInfo').once().returns(grpcCallContact); - const root = await reflectionClient.fileContainingSymbol('phone.Messenger'); - assert.sameDeepMembers(root.files, ['contact.proto', 'phone.proto']); + after(async () => { + await mockV1Server.shutdown(); + await mockV1AlphaServer.shutdown(); + }); + + describe('listServices', async () => { + for (const version of reflectionMethodsToTest) { + it(`should return services: ${version.name}`, async () => { + const reflectionClient = new Client( + `0.0.0.0:${version.port}`, + credentials.createInsecure() + ); + + await reflectionClient.initialize(); + + const grpcCall = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + on: function (_event: string, listener: (...args: any[]) => void) { + const res = new ServerReflectionResponse(); + + const service1 = new ServiceResponse(); + service1.setName('grpc.reflection.v1alpha.ServerReflection'); + const service2 = new ServiceResponse(); + service2.setName('phone.Messenger'); + const serviceList = [service1, service2]; + + const listService = new ListServiceResponse(); + listService.setServiceList(serviceList); + res.setListServicesResponse(listService); + + listener(res); + }, + write: function () {}, + end: function () {}, + }; + + const mock = sinon.mock(reflectionClient.grpcClient); + mock.expects('serverReflectionInfo').once().returns(grpcCall); + + const expectedServices: string[] = [ + 'grpc.reflection.v1.ServerReflection', + 'widgets.v1.WidgetService', + ]; + assert.sameMembers( + await reflectionClient.listServices(), + expectedServices + ); + }); + } + }); + + describe('fileContainingSymbol', () => { + for (const version of reflectionMethodsToTest) { + it(`should return Root: ${version.name}`, async () => { + const reflectionClient = new Client( + `0.0.0.0:${version.port}`, + credentials.createInsecure() + ); + + await reflectionClient.initialize(); + + const grpcCallPhone = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + on: function (_event: string, listener: (...args: any[]) => void) { + if (_event === 'error') { + return; + } + + const res = new ServerReflectionResponse(); + const fileDescriptorResponse = new FileDescriptorResponse(); + // eslint-disable-next-line prettier/prettier + const protoBytes = Buffer.from([10,11,112,104,111,110,101,46,112,114,111,116,111,18,5,112,104,111,110,101,26,13,99,111,110,116,97,99,116,46,112,114,111,116,111,34,97,10,11,84,101,120,116,82,101,113,117,101,115,116,18,14,10,2,105,100,24,1,32,1,40,9,82,2,105,100,18,24,10,7,109,101,115,115,97,103,101,24,2,32,1,40,9,82,7,109,101,115,115,97,103,101,18,40,10,7,99,111,110,116,97,99,116,24,3,32,1,40,11,50,14,46,112,104,111,110,101,46,67,111,110,116,97,99,116,82,7,99,111,110,116,97,99,116,34,40,10,12,84,101,120,116,82,101,115,112,111,110,115,101,18,24,10,7,115,117,99,99,101,115,115,24,1,32,1,40,8,82,7,115,117,99,99,101,115,115,50,63,10,9,77,101,115,115,101,110,103,101,114,18,50,10,7,77,101,115,115,97,103,101,18,18,46,112,104,111,110,101,46,84,101,120,116,82,101,113,117,101,115,116,26,19,46,112,104,111,110,101,46,84,101,120,116,82,101,115,112,111,110,115,101,98,6,112,114,111,116,111,51]); + fileDescriptorResponse.addFileDescriptorProto(protoBytes); + res.setFileDescriptorResponse(fileDescriptorResponse); + + listener(res); + }, + write: function () {}, + end: function () {}, + }; + + const grpcCallContact = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + on: function (_event: string, listener: (...args: any[]) => void) { + if (_event === 'error') { + return; + } + + const res = new ServerReflectionResponse(); + const fileDescriptorResponse = new FileDescriptorResponse(); + // eslint-disable-next-line prettier/prettier + const protoBytes = Buffer.from([10,13,99,111,110,116,97,99,116,46,112,114,111,116,111,18,5,112,104,111,110,101,34,53,10,7,67,111,110,116,97,99,116,18,18,10,4,110,97,109,101,24,1,32,1,40,9,82,4,110,97,109,101,18,22,10,6,110,117,109,98,101,114,24,2,32,1,40,9,82,6,110,117,109,98,101,114,98,6,112,114,111,116,111,51]); + fileDescriptorResponse.addFileDescriptorProto(protoBytes); + res.setFileDescriptorResponse(fileDescriptorResponse); + + listener(res); + }, + write: function () {}, + end: function () {}, + }; + + const mock = sinon.mock(reflectionClient.grpcClient); + mock.expects('serverReflectionInfo').once().returns(grpcCallPhone); + mock.expects('serverReflectionInfo').once().returns(grpcCallContact); + const root = await reflectionClient.fileContainingSymbol( + 'phone.Messenger' + ); + assert.sameDeepMembers(root.files, ['contact.proto', 'phone.proto']); + }); + } }); -}); -// eslint-disable-next-line no-undef -describe('fileByFilename', () => { - // eslint-disable-next-line no-undef - it('should return Root', async () => { - const reflectionClient = new Client( - 'localhost:4770', - credentials.createInsecure() - ); - - const grpcCallContact = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - on: function (_event: string, listener: (...args: any[]) => void) { - if (_event === 'error') { - return; - } - const res = new ServerReflectionResponse(); - const fileDescriptorResponse = new FileDescriptorResponse(); - // eslint-disable-next-line prettier/prettier - const protoBytes = Buffer.from([10,13,99,111,110,116,97,99,116,46,112,114,111,116,111,18,5,112,104,111,110,101,34,53,10,7,67,111,110,116,97,99,116,18,18,10,4,110,97,109,101,24,1,32,1,40,9,82,4,110,97,109,101,18,22,10,6,110,117,109,98,101,114,24,2,32,1,40,9,82,6,110,117,109,98,101,114,98,6,112,114,111,116,111,51]); - fileDescriptorResponse.addFileDescriptorProto(protoBytes); - res.setFileDescriptorResponse(fileDescriptorResponse); - - listener(res); - }, - write: function () {}, - end: function () {}, - }; - - const mock = sinon.mock(reflectionClient.grpcClient); - mock.expects('serverReflectionInfo').once().returns(grpcCallContact); - const root = await reflectionClient.fileByFilename('contact.proto'); - assert.deepEqual(root.files, ['contact.proto']); + describe('fileByFilename', () => { + for (const version of reflectionMethodsToTest) { + it(`should return Root: ${version.name}`, async () => { + const reflectionClient = new Client( + `0.0.0.0:${version.port}`, + credentials.createInsecure() + ); + + await reflectionClient.initialize(); + + const grpcCallContact = { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + on: function (_event: string, listener: (...args: any[]) => void) { + if (_event === 'error') { + return; + } + const res = new ServerReflectionResponse(); + const fileDescriptorResponse = new FileDescriptorResponse(); + // eslint-disable-next-line prettier/prettier + const protoBytes = Buffer.from([10,13,99,111,110,116,97,99,116,46,112,114,111,116,111,18,5,112,104,111,110,101,34,53,10,7,67,111,110,116,97,99,116,18,18,10,4,110,97,109,101,24,1,32,1,40,9,82,4,110,97,109,101,18,22,10,6,110,117,109,98,101,114,24,2,32,1,40,9,82,6,110,117,109,98,101,114,98,6,112,114,111,116,111,51]); + fileDescriptorResponse.addFileDescriptorProto(protoBytes); + res.setFileDescriptorResponse(fileDescriptorResponse); + + listener(res); + }, + write: function () {}, + end: function () {}, + }; + + const mock = sinon.mock(reflectionClient.grpcClient); + mock.expects('serverReflectionInfo').once().returns(grpcCallContact); + const root = await reflectionClient.fileByFilename('contact.proto'); + assert.deepEqual(root.files, ['contact.proto']); + }); + } }); }); diff --git a/test/fixtures/contact.proto b/test/fixtures/contact.proto deleted file mode 100644 index 18a6761..0000000 --- a/test/fixtures/contact.proto +++ /dev/null @@ -1,7 +0,0 @@ -syntax = "proto3"; -package phone; - -message Contact { - string name = 1; - string number = 2; -} diff --git a/test/fixtures/phone.proto b/test/fixtures/phone.proto deleted file mode 100644 index 466f6ac..0000000 --- a/test/fixtures/phone.proto +++ /dev/null @@ -1,18 +0,0 @@ -syntax = "proto3"; -package phone; - -import "contact.proto"; - -service Messenger { - rpc Message(TextRequest) returns (TextResponse); -} - -message TextRequest { - string id = 1; - string message = 2; - Contact contact = 3; -} - -message TextResponse { - bool success = 1; -} diff --git a/test/fixtures/v1/reflection.proto b/test/fixtures/v1/reflection.proto new file mode 100644 index 0000000..1a2ceed --- /dev/null +++ b/test/fixtures/v1/reflection.proto @@ -0,0 +1,147 @@ +// Copyright 2016 The 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto + +syntax = "proto3"; + +package grpc.reflection.v1; + +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1"; +option java_outer_classname = "ServerReflectionProto"; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} + diff --git a/test/fixtures/v1/server_descriptor.protoset b/test/fixtures/v1/server_descriptor.protoset new file mode 100644 index 0000000000000000000000000000000000000000..b11cd2e44fbc93937168a1a67de427128a38f924 GIT binary patch literal 2274 zcmZ`)+iuf95GB1Ts1M7Yid0AX9zBun;ns{XBrI#aYE~paMm|5m_B8}b+&zK3?hI$kxsh2SP z!ah#SMdefDVvZ`8c5XuR94Cn%V2?^TUJXy$+tHAvta6R}8pA6mt-%^8+? z-fd=bS*Z1U4$Q05O-3Y6Fj@x~`%yUXazm~P$tyN>^buNwhCvwnk*-^$F`Nfw;9pH6 zNaQwk5Z&hxOflU7PWm()rG&EZvLwL^!`Ul~|B^A~Mdsov4>n@Kv8x(HVWWJ)SvEmvJ!J>MeqEN%xEJI4YRSk`+eXMP0 z7Ib_o=IRyyty8E^fz98RoKv3+uvnn;ZdFLSOn^2)&cagz5D%;B)`Z1dxh$NY6=)S- z6}o_SQT93>5>(3KqNRF*xAw2YVEPJ3PV815Ryspi4oQhuXStX|O#Xf4^W%V!Ai{P) zB0mmnLW2XovJWQbM|`svqOX)UKI|t4g#De>Ue?vWJjw53!j7%HDD$j zr~}3RWy9h7kew_(DAb1eK8XmGcgmF?)>A;7FEUP|m(x9{F(Rj!deTLb(vY$)k79Kt z(K1K1P4{6@Rj7!DZ1ruA4{d4fY7^@@EOjaNQ>+w|i(BN@qV#XJy?)w z1t^A7R)w-~{#2kY^YbCHTZK-*$bo`3@-5^Pnn!o8)vIbgKrLS-nsUf%bi}*)h|>l# Hq-Mx}`xX80 literal 0 HcmV?d00001 diff --git a/test/fixtures/v1/widgets.proto b/test/fixtures/v1/widgets.proto new file mode 100644 index 0000000..3db47f9 --- /dev/null +++ b/test/fixtures/v1/widgets.proto @@ -0,0 +1,41 @@ +// proto/widgets.proto +syntax = "proto3"; + +package widgets.v1; // Use a versioned package name + +// Represents a widget item +message Widget { + string id = 1; + string name = 2; + string color = 3; +} + +// Request to list widgets, optionally filtering by color +message ListWidgetsRequest { + string filter_color = 1; // Empty string means no filter +} + +// Response containing a list of widgets +message ListWidgetsResponse { + repeated Widget widgets = 1; +} + +// Request to create a new widget +message CreateWidgetRequest { + string name = 1; + string color = 2; +} + +// Response after creating a widget +message CreateWidgetResponse { + Widget widget = 1; +} + +// The Widget service definition +service WidgetService { + // Lists widgets, optionally filtering by color + rpc ListWidgets(ListWidgetsRequest) returns (ListWidgetsResponse); + + // Creates a new widget + rpc CreateWidget(CreateWidgetRequest) returns (CreateWidgetResponse); +} diff --git a/test/fixtures/v1alpha/reflection.proto b/test/fixtures/v1alpha/reflection.proto new file mode 100644 index 0000000..e168905 --- /dev/null +++ b/test/fixtures/v1alpha/reflection.proto @@ -0,0 +1,147 @@ +// Copyright 2016 The 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. + +// Service exported by server reflection. A more complete description of how +// server reflection works can be found at +// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md +// +// The canonical version of this proto can be found at +// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1alpha/reflection.proto + +syntax = "proto3"; + +package grpc.reflection.v1alpha; + +option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"; +option java_multiple_files = true; +option java_package = "io.grpc.reflection.v1alpha"; +option java_outer_classname = "ServerReflectionProto"; + +service ServerReflection { + // The reflection service is structured as a bidirectional stream, ensuring + // all related requests go to a single server. + rpc ServerReflectionInfo(stream ServerReflectionRequest) + returns (stream ServerReflectionResponse); +} + +// The message sent by the client when calling ServerReflectionInfo method. +message ServerReflectionRequest { + string host = 1; + // To use reflection service, the client should set one of the following + // fields in message_request. The server distinguishes requests by their + // defined field and then handles them using corresponding methods. + oneof message_request { + // Find a proto file by the file name. + string file_by_filename = 3; + + // Find the proto file that declares the given fully-qualified symbol name. + // This field should be a fully-qualified symbol name + // (e.g. .[.] or .). + string file_containing_symbol = 4; + + // Find the proto file which defines an extension extending the given + // message type with the given field number. + ExtensionRequest file_containing_extension = 5; + + // Finds the tag numbers used by all known extensions of the given message + // type, and appends them to ExtensionNumberResponse in an undefined order. + // Its corresponding method is best-effort: it's not guaranteed that the + // reflection service will implement this method, and it's not guaranteed + // that this method will provide all extensions. Returns + // StatusCode::UNIMPLEMENTED if it's not implemented. + // This field should be a fully-qualified type name. The format is + // . + string all_extension_numbers_of_type = 6; + + // List the full names of registered services. The content will not be + // checked. + string list_services = 7; + } +} + +// The type name and extension number sent by the client when requesting +// file_containing_extension. +message ExtensionRequest { + // Fully-qualified type name. The format should be . + string containing_type = 1; + int32 extension_number = 2; +} + +// The message sent by the server to answer ServerReflectionInfo method. +message ServerReflectionResponse { + string valid_host = 1; + ServerReflectionRequest original_request = 2; + // The server sets one of the following fields according to the message_request + // in the request. + oneof message_response { + // This message is used to answer file_by_filename, file_containing_symbol, + // file_containing_extension requests with transitive dependencies. + // As the repeated label is not allowed in oneof fields, we use a + // FileDescriptorResponse message to encapsulate the repeated fields. + // The reflection service is allowed to avoid sending FileDescriptorProtos + // that were previously sent in response to earlier requests in the stream. + FileDescriptorResponse file_descriptor_response = 4; + + // This message is used to answer all_extension_numbers_of_type requests. + ExtensionNumberResponse all_extension_numbers_response = 5; + + // This message is used to answer list_services requests. + ListServiceResponse list_services_response = 6; + + // This message is used when an error occurs. + ErrorResponse error_response = 7; + } +} + +// Serialized FileDescriptorProto messages sent by the server answering +// a file_by_filename, file_containing_symbol, or file_containing_extension +// request. +message FileDescriptorResponse { + // Serialized FileDescriptorProto messages. We avoid taking a dependency on + // descriptor.proto, which uses proto2 only features, by making them opaque + // bytes instead. + repeated bytes file_descriptor_proto = 1; +} + +// A list of extension numbers sent by the server answering +// all_extension_numbers_of_type request. +message ExtensionNumberResponse { + // Full name of the base type, including the package name. The format + // is . + string base_type_name = 1; + repeated int32 extension_number = 2; +} + +// A list of ServiceResponse sent by the server answering list_services request. +message ListServiceResponse { + // The information of each service may be expanded in the future, so we use + // ServiceResponse message to encapsulate it. + repeated ServiceResponse service = 1; +} + +// The information of a single service used by ListServiceResponse to answer +// list_services request. +message ServiceResponse { + // Full name of a registered service, including its package name. The format + // is . + string name = 1; +} + +// The error code and error message sent by the server when an error occurs. +message ErrorResponse { + // This field uses the error codes defined in grpc::StatusCode. + int32 error_code = 1; + string error_message = 2; +} + diff --git a/test/fixtures/v1alpha/server_descriptor.protoset b/test/fixtures/v1alpha/server_descriptor.protoset new file mode 100644 index 0000000000000000000000000000000000000000..b11cd2e44fbc93937168a1a67de427128a38f924 GIT binary patch literal 2274 zcmZ`)+iuf95GB1Ts1M7Yid0AX9zBun;ns{XBrI#aYE~paMm|5m_B8}b+&zK3?hI$kxsh2SP z!ah#SMdefDVvZ`8c5XuR94Cn%V2?^TUJXy$+tHAvta6R}8pA6mt-%^8+? z-fd=bS*Z1U4$Q05O-3Y6Fj@x~`%yUXazm~P$tyN>^buNwhCvwnk*-^$F`Nfw;9pH6 zNaQwk5Z&hxOflU7PWm()rG&EZvLwL^!`Ul~|B^A~Mdsov4>n@Kv8x(HVWWJ)SvEmvJ!J>MeqEN%xEJI4YRSk`+eXMP0 z7Ib_o=IRyyty8E^fz98RoKv3+uvnn;ZdFLSOn^2)&cagz5D%;B)`Z1dxh$NY6=)S- z6}o_SQT93>5>(3KqNRF*xAw2YVEPJ3PV815Ryspi4oQhuXStX|O#Xf4^W%V!Ai{P) zB0mmnLW2XovJWQbM|`svqOX)UKI|t4g#De>Ue?vWJjw53!j7%HDD$j zr~}3RWy9h7kew_(DAb1eK8XmGcgmF?)>A;7FEUP|m(x9{F(Rj!deTLb(vY$)k79Kt z(K1K1P4{6@Rj7!DZ1ruA4{d4fY7^@@EOjaNQ>+w|i(BN@qV#XJy?)w z1t^A7R)w-~{#2kY^YbCHTZK-*$bo`3@-5^Pnn!o8)vIbgKrLS-nsUf%bi}*)h|>l# Hq-Mx}`xX80 literal 0 HcmV?d00001 diff --git a/test/fixtures/v1alpha/widgets.proto b/test/fixtures/v1alpha/widgets.proto new file mode 100644 index 0000000..3db47f9 --- /dev/null +++ b/test/fixtures/v1alpha/widgets.proto @@ -0,0 +1,41 @@ +// proto/widgets.proto +syntax = "proto3"; + +package widgets.v1; // Use a versioned package name + +// Represents a widget item +message Widget { + string id = 1; + string name = 2; + string color = 3; +} + +// Request to list widgets, optionally filtering by color +message ListWidgetsRequest { + string filter_color = 1; // Empty string means no filter +} + +// Response containing a list of widgets +message ListWidgetsResponse { + repeated Widget widgets = 1; +} + +// Request to create a new widget +message CreateWidgetRequest { + string name = 1; + string color = 2; +} + +// Response after creating a widget +message CreateWidgetResponse { + Widget widget = 1; +} + +// The Widget service definition +service WidgetService { + // Lists widgets, optionally filtering by color + rpc ListWidgets(ListWidgetsRequest) returns (ListWidgetsResponse); + + // Creates a new widget + rpc CreateWidget(CreateWidgetRequest) returns (CreateWidgetResponse); +} diff --git a/test/test-servers/v1-server.js b/test/test-servers/v1-server.js new file mode 100644 index 0000000..f2d84bf --- /dev/null +++ b/test/test-servers/v1-server.js @@ -0,0 +1,279 @@ +const grpc = require('@postman/grpc-js'); + +// eslint-disable-next-line node/no-extraneous-require +const protoLoader = require('@postman/proto-loader'); +const path = require('path'); +const fs = require('fs'); +const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); + +const PROTO_DIR = path.join(__dirname, '../fixtures', 'v1'); +const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); +const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); +const DESCRIPTOR_SET_PATH = path.join(PROTO_DIR, 'server_descriptor.protoset'); + +let fileDescriptorProtos = []; + +const protosByFilename = new Map(); +const filenameBySymbol = new Map(); + +const serviceNames = []; + +let server, boundPort; + +module.exports.start = async function start(port = 0) { + if (server) return Promise.resolve({server, port: boundPort, shutdown}); + + const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); + const fileDescriptorSet = + descriptor.FileDescriptorSet.deserializeBinary(descriptorSetBytes); + fileDescriptorProtos = fileDescriptorSet.getFileList(); + + if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { + throw new Error('Descriptor set is empty or failed to parse.'); + } + + fileDescriptorProtos.forEach(proto => { + const filename = proto.getName(); + if (!filename) return; + + protosByFilename.set(filename, proto); + + const packageName = proto.getPackage(); + + function buildSymbolName(baseName) { + return packageName ? `${packageName}.${baseName}` : baseName; + } + + proto.getMessageTypeList().forEach(messageType => { + const fqMessageName = buildSymbolName(messageType.getName()); + filenameBySymbol.set(fqMessageName, filename); + }); + + proto.getEnumTypeList().forEach(enumType => { + const fqEnumName = buildSymbolName(enumType.getName()); + filenameBySymbol.set(fqEnumName, filename); + }); + + proto.getServiceList().forEach(service => { + const fqServiceName = buildSymbolName(service.getName()); + filenameBySymbol.set(fqServiceName, filename); + serviceNames.push(fqServiceName); + }); + }); + + const packageDefinitionOptions = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [PROTO_DIR], + }; + const widgetsPackageDefinition = protoLoader.loadSync( + WIDGETS_PROTO_PATH, + packageDefinitionOptions + ); + const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) + .widgets.v1; + + const reflectionPackageDefinition = protoLoader.loadSync( + REFLECTION_PROTO_PATH, + packageDefinitionOptions + ); + + const reflectionProto = grpc.loadPackageDefinition( + reflectionPackageDefinition + ).grpc.reflection.v1; + + const widgetServiceImpl = { + listWidgets: () => {}, + createWidget: () => {}, + }; + + function getDependenciesRecursive( + filename, + protosByFilenameMap, + visited = new Set() + ) { + if (visited.has(filename)) { + return []; + } + visited.add(filename); + + const proto = protosByFilenameMap.get(filename); + if (!proto) { + return []; + } + + const directDependencies = proto.getDependencyList(); + const allDependencies = [...directDependencies]; + + directDependencies.forEach(depFilename => { + const transitiveDeps = getDependenciesRecursive( + depFilename, + protosByFilenameMap, + visited + ); + transitiveDeps.forEach(transitiveDep => { + if (!allDependencies.includes(transitiveDep)) { + allDependencies.push(transitiveDep); + } + }); + }); + + const finalDeps = allDependencies.filter(dep => dep !== filename); + + return finalDeps; + } + + const reflectionServiceImpl = { + serverReflectionInfo: call => { + call.on('data', request => { + const responseObject = { + valid_host: request.host, + original_request: request, + + list_services_response: null, + file_descriptor_response: null, + error_response: null, + }; + + try { + const messageRequestCase = request.message_request; + + if (messageRequestCase === 'list_services') { + const listResponseObj = {service: []}; + serviceNames.forEach(name => { + listResponseObj.service.push({name: name}); + }); + responseObject.list_services_response = listResponseObj; + } else if (messageRequestCase === 'file_by_filename') { + const filename = request.file_by_filename; + const targetProto = protosByFilename.get(filename); + + if (!targetProto) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `File not found: ${filename}`, + }; + } else { + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) { + protosToSend.push(depProto); + } + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = + fileDescriptorResponseObj; + } + } else if (messageRequestCase === 'file_containing_symbol') { + const symbol = request.file_containing_symbol; + const filename = filenameBySymbol.get(symbol); + + if (!filename) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `Symbol not found: ${symbol}`, + }; + } else { + const targetProto = protosByFilename.get(filename); + if (!targetProto) { + throw new Error( + `Internal error: Symbol '${symbol}' maps to filename '${filename}' but proto not found.` + ); + } + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) protosToSend.push(depProto); + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = + fileDescriptorResponseObj; + } + } else { + responseObject.error_response = { + error_code: grpc.status.UNIMPLEMENTED, + error_message: `Request type '${messageRequestCase}' not implemented`, + }; + } + + call.write(responseObject); + } catch (error) { + const errorRespWrapper = { + valid_host: request.host, + original_request: request, + error_response: { + error_code: grpc.status.INTERNAL, + error_message: `Error processing reflection request: ${error.message}`, + }, + }; + call.write(errorRespWrapper); + } + }); + + call.on('end', () => { + call.end(); + }); + }, + }; + + const grpcServer = new grpc.Server(); + + grpcServer.addService(widgetsProto.WidgetService.service, widgetServiceImpl); + grpcServer.addService( + reflectionProto.ServerReflection.service, + reflectionServiceImpl + ); + + return new Promise((resolve, reject) => { + grpcServer.bindAsync( + `0.0.0.0:${port}`, + grpc.ServerCredentials.createInsecure(), + (err, receivedPort) => { + if (err) return reject(err); + grpcServer.start(); + server = grpcServer; + boundPort = receivedPort; + resolve({server: grpcServer, port: boundPort, shutdown}); + } + ); + }); +}; + +async function shutdown() { + if (!server) return Promise.resolve(); + + return new Promise(resolve => { + if (!server) return resolve(); + server.tryShutdown(err => { + if (err) server.forceShutdown(); + resolve(); + }); + }); +} + +module.exports.shutdown = shutdown; diff --git a/test/test-servers/v1alpha-server.js b/test/test-servers/v1alpha-server.js new file mode 100644 index 0000000..a463e67 --- /dev/null +++ b/test/test-servers/v1alpha-server.js @@ -0,0 +1,279 @@ +const grpc = require('@postman/grpc-js'); + +// eslint-disable-next-line node/no-extraneous-require +const protoLoader = require('@postman/proto-loader'); +const path = require('path'); +const fs = require('fs'); +const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); + +const PROTO_DIR = path.join(__dirname, '../fixtures', 'v1alpha'); +const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); +const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); +const DESCRIPTOR_SET_PATH = path.join(PROTO_DIR, 'server_descriptor.protoset'); + +let fileDescriptorProtos = []; + +const protosByFilename = new Map(); +const filenameBySymbol = new Map(); + +const serviceNames = []; + +let server, boundPort; + +module.exports.start = async function start(port = 0) { + if (server) return Promise.resolve({server, port: boundPort, shutdown}); + + const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); + const fileDescriptorSet = + descriptor.FileDescriptorSet.deserializeBinary(descriptorSetBytes); + fileDescriptorProtos = fileDescriptorSet.getFileList(); + + if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { + throw new Error('Descriptor set is empty or failed to parse.'); + } + + fileDescriptorProtos.forEach(proto => { + const filename = proto.getName(); + if (!filename) return; + + protosByFilename.set(filename, proto); + + const packageName = proto.getPackage(); + + function buildSymbolName(baseName) { + return packageName ? `${packageName}.${baseName}` : baseName; + } + + proto.getMessageTypeList().forEach(messageType => { + const fqMessageName = buildSymbolName(messageType.getName()); + filenameBySymbol.set(fqMessageName, filename); + }); + + proto.getEnumTypeList().forEach(enumType => { + const fqEnumName = buildSymbolName(enumType.getName()); + filenameBySymbol.set(fqEnumName, filename); + }); + + proto.getServiceList().forEach(service => { + const fqServiceName = buildSymbolName(service.getName()); + filenameBySymbol.set(fqServiceName, filename); + serviceNames.push(fqServiceName); + }); + }); + + const packageDefinitionOptions = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [PROTO_DIR], + }; + const widgetsPackageDefinition = protoLoader.loadSync( + WIDGETS_PROTO_PATH, + packageDefinitionOptions + ); + const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) + .widgets.v1; + + const reflectionPackageDefinition = protoLoader.loadSync( + REFLECTION_PROTO_PATH, + packageDefinitionOptions + ); + + const reflectionProto = grpc.loadPackageDefinition( + reflectionPackageDefinition + ).grpc.reflection.v1alpha; + + const widgetServiceImpl = { + listWidgets: () => {}, + createWidget: () => {}, + }; + + function getDependenciesRecursive( + filename, + protosByFilenameMap, + visited = new Set() + ) { + if (visited.has(filename)) { + return []; + } + visited.add(filename); + + const proto = protosByFilenameMap.get(filename); + if (!proto) { + return []; + } + + const directDependencies = proto.getDependencyList(); + const allDependencies = [...directDependencies]; + + directDependencies.forEach(depFilename => { + const transitiveDeps = getDependenciesRecursive( + depFilename, + protosByFilenameMap, + visited + ); + transitiveDeps.forEach(transitiveDep => { + if (!allDependencies.includes(transitiveDep)) { + allDependencies.push(transitiveDep); + } + }); + }); + + const finalDeps = allDependencies.filter(dep => dep !== filename); + + return finalDeps; + } + + const reflectionServiceImpl = { + serverReflectionInfo: call => { + call.on('data', request => { + const responseObject = { + valid_host: request.host, + original_request: request, + + list_services_response: null, + file_descriptor_response: null, + error_response: null, + }; + + try { + const messageRequestCase = request.message_request; + + if (messageRequestCase === 'list_services') { + const listResponseObj = {service: []}; + serviceNames.forEach(name => { + listResponseObj.service.push({name: name}); + }); + responseObject.list_services_response = listResponseObj; + } else if (messageRequestCase === 'file_by_filename') { + const filename = request.file_by_filename; + const targetProto = protosByFilename.get(filename); + + if (!targetProto) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `File not found: ${filename}`, + }; + } else { + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) { + protosToSend.push(depProto); + } + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = + fileDescriptorResponseObj; + } + } else if (messageRequestCase === 'file_containing_symbol') { + const symbol = request.file_containing_symbol; + const filename = filenameBySymbol.get(symbol); + + if (!filename) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `Symbol not found: ${symbol}`, + }; + } else { + const targetProto = protosByFilename.get(filename); + if (!targetProto) { + throw new Error( + `Internal error: Symbol '${symbol}' maps to filename '${filename}' but proto not found.` + ); + } + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) protosToSend.push(depProto); + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = + fileDescriptorResponseObj; + } + } else { + responseObject.error_response = { + error_code: grpc.status.UNIMPLEMENTED, + error_message: `Request type '${messageRequestCase}' not implemented`, + }; + } + + call.write(responseObject); + } catch (error) { + const errorRespWrapper = { + valid_host: request.host, + original_request: request, + error_response: { + error_code: grpc.status.INTERNAL, + error_message: `Error processing reflection request: ${error.message}`, + }, + }; + call.write(errorRespWrapper); + } + }); + + call.on('end', () => { + call.end(); + }); + }, + }; + + const grpcServer = new grpc.Server(); + + grpcServer.addService(widgetsProto.WidgetService.service, widgetServiceImpl); + grpcServer.addService( + reflectionProto.ServerReflection.service, + reflectionServiceImpl + ); + + return new Promise((resolve, reject) => { + grpcServer.bindAsync( + `0.0.0.0:${port}`, + grpc.ServerCredentials.createInsecure(), + (err, receivedPort) => { + if (err) return reject(err); + grpcServer.start(); + server = grpcServer; + boundPort = receivedPort; + resolve({server: grpcServer, port: boundPort, shutdown}); + } + ); + }); +}; + +async function shutdown() { + if (!server) return Promise.resolve(); + + return new Promise(resolve => { + if (!server) return resolve(); + server.tryShutdown(err => { + if (err) server.forceShutdown(); + resolve(); + }); + }); +} + +module.exports.shutdown = shutdown; From 28fb948b88b8ef6506a564fd60294a8b918b77c1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Date: Thu, 30 Oct 2025 14:06:25 +0530 Subject: [PATCH 2/5] De-duplicate grpc servers with a base --- test/test-servers/base.js | 288 ++++++++++++++++++++++++++++ test/test-servers/v1-server.js | 280 +-------------------------- test/test-servers/v1alpha-server.js | 280 +-------------------------- 3 files changed, 292 insertions(+), 556 deletions(-) create mode 100644 test/test-servers/base.js diff --git a/test/test-servers/base.js b/test/test-servers/base.js new file mode 100644 index 0000000..fb4e9aa --- /dev/null +++ b/test/test-servers/base.js @@ -0,0 +1,288 @@ +function createServer({apiVersion = 'v1'} = {}) { + const grpc = require('@postman/grpc-js'); + + // eslint-disable-next-line node/no-extraneous-require + const protoLoader = require('@postman/proto-loader'); + const path = require('path'); + const fs = require('fs'); + const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); + + const PROTO_DIR = path.join(__dirname, '../fixtures', apiVersion); + const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); + const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); + const DESCRIPTOR_SET_PATH = path.join( + PROTO_DIR, + 'server_descriptor.protoset' + ); + + let fileDescriptorProtos = []; + + const protosByFilename = new Map(); + const filenameBySymbol = new Map(); + + const serviceNames = []; + + let server, boundPort; + + async function start(port = 0) { + if (server) return Promise.resolve({server, port: boundPort, shutdown}); + + const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); + const fileDescriptorSet = descriptor.FileDescriptorSet.deserializeBinary( + descriptorSetBytes + ); + fileDescriptorProtos = fileDescriptorSet.getFileList(); + + if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { + throw new Error('Descriptor set is empty or failed to parse.'); + } + + fileDescriptorProtos.forEach(proto => { + const filename = proto.getName(); + if (!filename) return; + + protosByFilename.set(filename, proto); + + const packageName = proto.getPackage(); + + function buildSymbolName(baseName) { + return packageName ? `${packageName}.${baseName}` : baseName; + } + + proto.getMessageTypeList().forEach(messageType => { + const fqMessageName = buildSymbolName(messageType.getName()); + filenameBySymbol.set(fqMessageName, filename); + }); + + proto.getEnumTypeList().forEach(enumType => { + const fqEnumName = buildSymbolName(enumType.getName()); + filenameBySymbol.set(fqEnumName, filename); + }); + + proto.getServiceList().forEach(service => { + const fqServiceName = buildSymbolName(service.getName()); + filenameBySymbol.set(fqServiceName, filename); + serviceNames.push(fqServiceName); + }); + }); + + const packageDefinitionOptions = { + keepCase: true, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs: [PROTO_DIR], + }; + const widgetsPackageDefinition = protoLoader.loadSync( + WIDGETS_PROTO_PATH, + packageDefinitionOptions + ); + const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) + .widgets.v1; + + const reflectionPackageDefinition = protoLoader.loadSync( + REFLECTION_PROTO_PATH, + packageDefinitionOptions + ); + + const reflectionProto = grpc.loadPackageDefinition( + reflectionPackageDefinition + ).grpc.reflection[apiVersion]; + + const widgetServiceImpl = { + listWidgets: () => {}, + createWidget: () => {}, + }; + + function getDependenciesRecursive( + filename, + protosByFilenameMap, + visited = new Set() + ) { + if (visited.has(filename)) { + return []; + } + visited.add(filename); + + const proto = protosByFilenameMap.get(filename); + if (!proto) { + return []; + } + + const directDependencies = proto.getDependencyList(); + const allDependencies = [...directDependencies]; + + directDependencies.forEach(depFilename => { + const transitiveDeps = getDependenciesRecursive( + depFilename, + protosByFilenameMap, + visited + ); + transitiveDeps.forEach(transitiveDep => { + if (!allDependencies.includes(transitiveDep)) { + allDependencies.push(transitiveDep); + } + }); + }); + + const finalDeps = allDependencies.filter(dep => dep !== filename); + + return finalDeps; + } + + const reflectionServiceImpl = { + serverReflectionInfo: call => { + call.on('data', request => { + const responseObject = { + valid_host: request.host, + original_request: request, + + list_services_response: null, + file_descriptor_response: null, + error_response: null, + }; + + try { + const messageRequestCase = request.message_request; + + if (messageRequestCase === 'list_services') { + const listResponseObj = {service: []}; + serviceNames.forEach(name => { + listResponseObj.service.push({name: name}); + }); + responseObject.list_services_response = listResponseObj; + } else if (messageRequestCase === 'file_by_filename') { + const filename = request.file_by_filename; + const targetProto = protosByFilename.get(filename); + + if (!targetProto) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `File not found: ${filename}`, + }; + } else { + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) { + protosToSend.push(depProto); + } + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = fileDescriptorResponseObj; + } + } else if (messageRequestCase === 'file_containing_symbol') { + const symbol = request.file_containing_symbol; + const filename = filenameBySymbol.get(symbol); + + if (!filename) { + responseObject.error_response = { + error_code: grpc.status.NOT_FOUND, + error_message: `Symbol not found: ${symbol}`, + }; + } else { + const targetProto = protosByFilename.get(filename); + if (!targetProto) { + throw new Error( + `Internal error: Symbol '${symbol}' maps to filename '${filename}' but proto not found.` + ); + } + const dependencies = getDependenciesRecursive( + filename, + protosByFilename + ); + const protosToSend = [targetProto]; + dependencies.forEach(depFilename => { + const depProto = protosByFilename.get(depFilename); + if (depProto) protosToSend.push(depProto); + }); + + const fileDescriptorResponseObj = {file_descriptor_proto: []}; + protosToSend.forEach(proto => { + fileDescriptorResponseObj.file_descriptor_proto.push( + proto.serializeBinary() + ); + }); + responseObject.file_descriptor_response = fileDescriptorResponseObj; + } + } else { + responseObject.error_response = { + error_code: grpc.status.UNIMPLEMENTED, + error_message: `Request type '${messageRequestCase}' not implemented`, + }; + } + + call.write(responseObject); + } catch (error) { + const errorRespWrapper = { + valid_host: request.host, + original_request: request, + error_response: { + error_code: grpc.status.INTERNAL, + error_message: `Error processing reflection request: ${error.message}`, + }, + }; + call.write(errorRespWrapper); + } + }); + + call.on('end', () => { + call.end(); + }); + }, + }; + + const grpcServer = new grpc.Server(); + + grpcServer.addService( + widgetsProto.WidgetService.service, + widgetServiceImpl + ); + grpcServer.addService( + reflectionProto.ServerReflection.service, + reflectionServiceImpl + ); + + return new Promise((resolve, reject) => { + grpcServer.bindAsync( + `0.0.0.0:${port}`, + grpc.ServerCredentials.createInsecure(), + (err, receivedPort) => { + if (err) return reject(err); + grpcServer.start(); + server = grpcServer; + boundPort = receivedPort; + resolve({server: grpcServer, port: boundPort, shutdown}); + } + ); + }); + } + + async function shutdown() { + if (!server) return Promise.resolve(); + + return new Promise(resolve => { + if (!server) return resolve(); + server.tryShutdown(err => { + if (err) server.forceShutdown(); + resolve(); + }); + }); + } + + return {start, shutdown}; +} + +module.exports = createServer; diff --git a/test/test-servers/v1-server.js b/test/test-servers/v1-server.js index f2d84bf..c4989d0 100644 --- a/test/test-servers/v1-server.js +++ b/test/test-servers/v1-server.js @@ -1,279 +1,3 @@ -const grpc = require('@postman/grpc-js'); +const createServer = require('./base'); -// eslint-disable-next-line node/no-extraneous-require -const protoLoader = require('@postman/proto-loader'); -const path = require('path'); -const fs = require('fs'); -const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); - -const PROTO_DIR = path.join(__dirname, '../fixtures', 'v1'); -const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); -const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); -const DESCRIPTOR_SET_PATH = path.join(PROTO_DIR, 'server_descriptor.protoset'); - -let fileDescriptorProtos = []; - -const protosByFilename = new Map(); -const filenameBySymbol = new Map(); - -const serviceNames = []; - -let server, boundPort; - -module.exports.start = async function start(port = 0) { - if (server) return Promise.resolve({server, port: boundPort, shutdown}); - - const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); - const fileDescriptorSet = - descriptor.FileDescriptorSet.deserializeBinary(descriptorSetBytes); - fileDescriptorProtos = fileDescriptorSet.getFileList(); - - if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { - throw new Error('Descriptor set is empty or failed to parse.'); - } - - fileDescriptorProtos.forEach(proto => { - const filename = proto.getName(); - if (!filename) return; - - protosByFilename.set(filename, proto); - - const packageName = proto.getPackage(); - - function buildSymbolName(baseName) { - return packageName ? `${packageName}.${baseName}` : baseName; - } - - proto.getMessageTypeList().forEach(messageType => { - const fqMessageName = buildSymbolName(messageType.getName()); - filenameBySymbol.set(fqMessageName, filename); - }); - - proto.getEnumTypeList().forEach(enumType => { - const fqEnumName = buildSymbolName(enumType.getName()); - filenameBySymbol.set(fqEnumName, filename); - }); - - proto.getServiceList().forEach(service => { - const fqServiceName = buildSymbolName(service.getName()); - filenameBySymbol.set(fqServiceName, filename); - serviceNames.push(fqServiceName); - }); - }); - - const packageDefinitionOptions = { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs: [PROTO_DIR], - }; - const widgetsPackageDefinition = protoLoader.loadSync( - WIDGETS_PROTO_PATH, - packageDefinitionOptions - ); - const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) - .widgets.v1; - - const reflectionPackageDefinition = protoLoader.loadSync( - REFLECTION_PROTO_PATH, - packageDefinitionOptions - ); - - const reflectionProto = grpc.loadPackageDefinition( - reflectionPackageDefinition - ).grpc.reflection.v1; - - const widgetServiceImpl = { - listWidgets: () => {}, - createWidget: () => {}, - }; - - function getDependenciesRecursive( - filename, - protosByFilenameMap, - visited = new Set() - ) { - if (visited.has(filename)) { - return []; - } - visited.add(filename); - - const proto = protosByFilenameMap.get(filename); - if (!proto) { - return []; - } - - const directDependencies = proto.getDependencyList(); - const allDependencies = [...directDependencies]; - - directDependencies.forEach(depFilename => { - const transitiveDeps = getDependenciesRecursive( - depFilename, - protosByFilenameMap, - visited - ); - transitiveDeps.forEach(transitiveDep => { - if (!allDependencies.includes(transitiveDep)) { - allDependencies.push(transitiveDep); - } - }); - }); - - const finalDeps = allDependencies.filter(dep => dep !== filename); - - return finalDeps; - } - - const reflectionServiceImpl = { - serverReflectionInfo: call => { - call.on('data', request => { - const responseObject = { - valid_host: request.host, - original_request: request, - - list_services_response: null, - file_descriptor_response: null, - error_response: null, - }; - - try { - const messageRequestCase = request.message_request; - - if (messageRequestCase === 'list_services') { - const listResponseObj = {service: []}; - serviceNames.forEach(name => { - listResponseObj.service.push({name: name}); - }); - responseObject.list_services_response = listResponseObj; - } else if (messageRequestCase === 'file_by_filename') { - const filename = request.file_by_filename; - const targetProto = protosByFilename.get(filename); - - if (!targetProto) { - responseObject.error_response = { - error_code: grpc.status.NOT_FOUND, - error_message: `File not found: ${filename}`, - }; - } else { - const dependencies = getDependenciesRecursive( - filename, - protosByFilename - ); - const protosToSend = [targetProto]; - - dependencies.forEach(depFilename => { - const depProto = protosByFilename.get(depFilename); - if (depProto) { - protosToSend.push(depProto); - } - }); - - const fileDescriptorResponseObj = {file_descriptor_proto: []}; - protosToSend.forEach(proto => { - fileDescriptorResponseObj.file_descriptor_proto.push( - proto.serializeBinary() - ); - }); - responseObject.file_descriptor_response = - fileDescriptorResponseObj; - } - } else if (messageRequestCase === 'file_containing_symbol') { - const symbol = request.file_containing_symbol; - const filename = filenameBySymbol.get(symbol); - - if (!filename) { - responseObject.error_response = { - error_code: grpc.status.NOT_FOUND, - error_message: `Symbol not found: ${symbol}`, - }; - } else { - const targetProto = protosByFilename.get(filename); - if (!targetProto) { - throw new Error( - `Internal error: Symbol '${symbol}' maps to filename '${filename}' but proto not found.` - ); - } - const dependencies = getDependenciesRecursive( - filename, - protosByFilename - ); - const protosToSend = [targetProto]; - dependencies.forEach(depFilename => { - const depProto = protosByFilename.get(depFilename); - if (depProto) protosToSend.push(depProto); - }); - - const fileDescriptorResponseObj = {file_descriptor_proto: []}; - protosToSend.forEach(proto => { - fileDescriptorResponseObj.file_descriptor_proto.push( - proto.serializeBinary() - ); - }); - responseObject.file_descriptor_response = - fileDescriptorResponseObj; - } - } else { - responseObject.error_response = { - error_code: grpc.status.UNIMPLEMENTED, - error_message: `Request type '${messageRequestCase}' not implemented`, - }; - } - - call.write(responseObject); - } catch (error) { - const errorRespWrapper = { - valid_host: request.host, - original_request: request, - error_response: { - error_code: grpc.status.INTERNAL, - error_message: `Error processing reflection request: ${error.message}`, - }, - }; - call.write(errorRespWrapper); - } - }); - - call.on('end', () => { - call.end(); - }); - }, - }; - - const grpcServer = new grpc.Server(); - - grpcServer.addService(widgetsProto.WidgetService.service, widgetServiceImpl); - grpcServer.addService( - reflectionProto.ServerReflection.service, - reflectionServiceImpl - ); - - return new Promise((resolve, reject) => { - grpcServer.bindAsync( - `0.0.0.0:${port}`, - grpc.ServerCredentials.createInsecure(), - (err, receivedPort) => { - if (err) return reject(err); - grpcServer.start(); - server = grpcServer; - boundPort = receivedPort; - resolve({server: grpcServer, port: boundPort, shutdown}); - } - ); - }); -}; - -async function shutdown() { - if (!server) return Promise.resolve(); - - return new Promise(resolve => { - if (!server) return resolve(); - server.tryShutdown(err => { - if (err) server.forceShutdown(); - resolve(); - }); - }); -} - -module.exports.shutdown = shutdown; +module.exports = createServer({apiVersion: 'v1'}); diff --git a/test/test-servers/v1alpha-server.js b/test/test-servers/v1alpha-server.js index a463e67..aa338f6 100644 --- a/test/test-servers/v1alpha-server.js +++ b/test/test-servers/v1alpha-server.js @@ -1,279 +1,3 @@ -const grpc = require('@postman/grpc-js'); +const createServer = require('./base'); -// eslint-disable-next-line node/no-extraneous-require -const protoLoader = require('@postman/proto-loader'); -const path = require('path'); -const fs = require('fs'); -const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); - -const PROTO_DIR = path.join(__dirname, '../fixtures', 'v1alpha'); -const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); -const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); -const DESCRIPTOR_SET_PATH = path.join(PROTO_DIR, 'server_descriptor.protoset'); - -let fileDescriptorProtos = []; - -const protosByFilename = new Map(); -const filenameBySymbol = new Map(); - -const serviceNames = []; - -let server, boundPort; - -module.exports.start = async function start(port = 0) { - if (server) return Promise.resolve({server, port: boundPort, shutdown}); - - const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); - const fileDescriptorSet = - descriptor.FileDescriptorSet.deserializeBinary(descriptorSetBytes); - fileDescriptorProtos = fileDescriptorSet.getFileList(); - - if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { - throw new Error('Descriptor set is empty or failed to parse.'); - } - - fileDescriptorProtos.forEach(proto => { - const filename = proto.getName(); - if (!filename) return; - - protosByFilename.set(filename, proto); - - const packageName = proto.getPackage(); - - function buildSymbolName(baseName) { - return packageName ? `${packageName}.${baseName}` : baseName; - } - - proto.getMessageTypeList().forEach(messageType => { - const fqMessageName = buildSymbolName(messageType.getName()); - filenameBySymbol.set(fqMessageName, filename); - }); - - proto.getEnumTypeList().forEach(enumType => { - const fqEnumName = buildSymbolName(enumType.getName()); - filenameBySymbol.set(fqEnumName, filename); - }); - - proto.getServiceList().forEach(service => { - const fqServiceName = buildSymbolName(service.getName()); - filenameBySymbol.set(fqServiceName, filename); - serviceNames.push(fqServiceName); - }); - }); - - const packageDefinitionOptions = { - keepCase: true, - longs: String, - enums: String, - defaults: true, - oneofs: true, - includeDirs: [PROTO_DIR], - }; - const widgetsPackageDefinition = protoLoader.loadSync( - WIDGETS_PROTO_PATH, - packageDefinitionOptions - ); - const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) - .widgets.v1; - - const reflectionPackageDefinition = protoLoader.loadSync( - REFLECTION_PROTO_PATH, - packageDefinitionOptions - ); - - const reflectionProto = grpc.loadPackageDefinition( - reflectionPackageDefinition - ).grpc.reflection.v1alpha; - - const widgetServiceImpl = { - listWidgets: () => {}, - createWidget: () => {}, - }; - - function getDependenciesRecursive( - filename, - protosByFilenameMap, - visited = new Set() - ) { - if (visited.has(filename)) { - return []; - } - visited.add(filename); - - const proto = protosByFilenameMap.get(filename); - if (!proto) { - return []; - } - - const directDependencies = proto.getDependencyList(); - const allDependencies = [...directDependencies]; - - directDependencies.forEach(depFilename => { - const transitiveDeps = getDependenciesRecursive( - depFilename, - protosByFilenameMap, - visited - ); - transitiveDeps.forEach(transitiveDep => { - if (!allDependencies.includes(transitiveDep)) { - allDependencies.push(transitiveDep); - } - }); - }); - - const finalDeps = allDependencies.filter(dep => dep !== filename); - - return finalDeps; - } - - const reflectionServiceImpl = { - serverReflectionInfo: call => { - call.on('data', request => { - const responseObject = { - valid_host: request.host, - original_request: request, - - list_services_response: null, - file_descriptor_response: null, - error_response: null, - }; - - try { - const messageRequestCase = request.message_request; - - if (messageRequestCase === 'list_services') { - const listResponseObj = {service: []}; - serviceNames.forEach(name => { - listResponseObj.service.push({name: name}); - }); - responseObject.list_services_response = listResponseObj; - } else if (messageRequestCase === 'file_by_filename') { - const filename = request.file_by_filename; - const targetProto = protosByFilename.get(filename); - - if (!targetProto) { - responseObject.error_response = { - error_code: grpc.status.NOT_FOUND, - error_message: `File not found: ${filename}`, - }; - } else { - const dependencies = getDependenciesRecursive( - filename, - protosByFilename - ); - const protosToSend = [targetProto]; - - dependencies.forEach(depFilename => { - const depProto = protosByFilename.get(depFilename); - if (depProto) { - protosToSend.push(depProto); - } - }); - - const fileDescriptorResponseObj = {file_descriptor_proto: []}; - protosToSend.forEach(proto => { - fileDescriptorResponseObj.file_descriptor_proto.push( - proto.serializeBinary() - ); - }); - responseObject.file_descriptor_response = - fileDescriptorResponseObj; - } - } else if (messageRequestCase === 'file_containing_symbol') { - const symbol = request.file_containing_symbol; - const filename = filenameBySymbol.get(symbol); - - if (!filename) { - responseObject.error_response = { - error_code: grpc.status.NOT_FOUND, - error_message: `Symbol not found: ${symbol}`, - }; - } else { - const targetProto = protosByFilename.get(filename); - if (!targetProto) { - throw new Error( - `Internal error: Symbol '${symbol}' maps to filename '${filename}' but proto not found.` - ); - } - const dependencies = getDependenciesRecursive( - filename, - protosByFilename - ); - const protosToSend = [targetProto]; - dependencies.forEach(depFilename => { - const depProto = protosByFilename.get(depFilename); - if (depProto) protosToSend.push(depProto); - }); - - const fileDescriptorResponseObj = {file_descriptor_proto: []}; - protosToSend.forEach(proto => { - fileDescriptorResponseObj.file_descriptor_proto.push( - proto.serializeBinary() - ); - }); - responseObject.file_descriptor_response = - fileDescriptorResponseObj; - } - } else { - responseObject.error_response = { - error_code: grpc.status.UNIMPLEMENTED, - error_message: `Request type '${messageRequestCase}' not implemented`, - }; - } - - call.write(responseObject); - } catch (error) { - const errorRespWrapper = { - valid_host: request.host, - original_request: request, - error_response: { - error_code: grpc.status.INTERNAL, - error_message: `Error processing reflection request: ${error.message}`, - }, - }; - call.write(errorRespWrapper); - } - }); - - call.on('end', () => { - call.end(); - }); - }, - }; - - const grpcServer = new grpc.Server(); - - grpcServer.addService(widgetsProto.WidgetService.service, widgetServiceImpl); - grpcServer.addService( - reflectionProto.ServerReflection.service, - reflectionServiceImpl - ); - - return new Promise((resolve, reject) => { - grpcServer.bindAsync( - `0.0.0.0:${port}`, - grpc.ServerCredentials.createInsecure(), - (err, receivedPort) => { - if (err) return reject(err); - grpcServer.start(); - server = grpcServer; - boundPort = receivedPort; - resolve({server: grpcServer, port: boundPort, shutdown}); - } - ); - }); -}; - -async function shutdown() { - if (!server) return Promise.resolve(); - - return new Promise(resolve => { - if (!server) return resolve(); - server.tryShutdown(err => { - if (err) server.forceShutdown(); - resolve(); - }); - }); -} - -module.exports.shutdown = shutdown; +module.exports = createServer({apiVersion: 'v1alpha'}); From 925cd47421ee9588f06f3f823b16258ee7183788 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Date: Thu, 30 Oct 2025 17:00:00 +0530 Subject: [PATCH 3/5] Remove unnecessary reflection files --- test/fixtures/v1/reflection.proto | 147 ------------------------- test/fixtures/v1alpha/reflection.proto | 147 ------------------------- test/test-servers/base.js | 18 ++- 3 files changed, 12 insertions(+), 300 deletions(-) delete mode 100644 test/fixtures/v1/reflection.proto delete mode 100644 test/fixtures/v1alpha/reflection.proto diff --git a/test/fixtures/v1/reflection.proto b/test/fixtures/v1/reflection.proto deleted file mode 100644 index 1a2ceed..0000000 --- a/test/fixtures/v1/reflection.proto +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2016 The 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. - -// Service exported by server reflection. A more complete description of how -// server reflection works can be found at -// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md -// -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto - -syntax = "proto3"; - -package grpc.reflection.v1; - -option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1"; -option java_multiple_files = true; -option java_package = "io.grpc.reflection.v1"; -option java_outer_classname = "ServerReflectionProto"; - -service ServerReflection { - // The reflection service is structured as a bidirectional stream, ensuring - // all related requests go to a single server. - rpc ServerReflectionInfo(stream ServerReflectionRequest) - returns (stream ServerReflectionResponse); -} - -// The message sent by the client when calling ServerReflectionInfo method. -message ServerReflectionRequest { - string host = 1; - // To use reflection service, the client should set one of the following - // fields in message_request. The server distinguishes requests by their - // defined field and then handles them using corresponding methods. - oneof message_request { - // Find a proto file by the file name. - string file_by_filename = 3; - - // Find the proto file that declares the given fully-qualified symbol name. - // This field should be a fully-qualified symbol name - // (e.g. .[.] or .). - string file_containing_symbol = 4; - - // Find the proto file which defines an extension extending the given - // message type with the given field number. - ExtensionRequest file_containing_extension = 5; - - // Finds the tag numbers used by all known extensions of the given message - // type, and appends them to ExtensionNumberResponse in an undefined order. - // Its corresponding method is best-effort: it's not guaranteed that the - // reflection service will implement this method, and it's not guaranteed - // that this method will provide all extensions. Returns - // StatusCode::UNIMPLEMENTED if it's not implemented. - // This field should be a fully-qualified type name. The format is - // . - string all_extension_numbers_of_type = 6; - - // List the full names of registered services. The content will not be - // checked. - string list_services = 7; - } -} - -// The type name and extension number sent by the client when requesting -// file_containing_extension. -message ExtensionRequest { - // Fully-qualified type name. The format should be . - string containing_type = 1; - int32 extension_number = 2; -} - -// The message sent by the server to answer ServerReflectionInfo method. -message ServerReflectionResponse { - string valid_host = 1; - ServerReflectionRequest original_request = 2; - // The server sets one of the following fields according to the message_request - // in the request. - oneof message_response { - // This message is used to answer file_by_filename, file_containing_symbol, - // file_containing_extension requests with transitive dependencies. - // As the repeated label is not allowed in oneof fields, we use a - // FileDescriptorResponse message to encapsulate the repeated fields. - // The reflection service is allowed to avoid sending FileDescriptorProtos - // that were previously sent in response to earlier requests in the stream. - FileDescriptorResponse file_descriptor_response = 4; - - // This message is used to answer all_extension_numbers_of_type requests. - ExtensionNumberResponse all_extension_numbers_response = 5; - - // This message is used to answer list_services requests. - ListServiceResponse list_services_response = 6; - - // This message is used when an error occurs. - ErrorResponse error_response = 7; - } -} - -// Serialized FileDescriptorProto messages sent by the server answering -// a file_by_filename, file_containing_symbol, or file_containing_extension -// request. -message FileDescriptorResponse { - // Serialized FileDescriptorProto messages. We avoid taking a dependency on - // descriptor.proto, which uses proto2 only features, by making them opaque - // bytes instead. - repeated bytes file_descriptor_proto = 1; -} - -// A list of extension numbers sent by the server answering -// all_extension_numbers_of_type request. -message ExtensionNumberResponse { - // Full name of the base type, including the package name. The format - // is . - string base_type_name = 1; - repeated int32 extension_number = 2; -} - -// A list of ServiceResponse sent by the server answering list_services request. -message ListServiceResponse { - // The information of each service may be expanded in the future, so we use - // ServiceResponse message to encapsulate it. - repeated ServiceResponse service = 1; -} - -// The information of a single service used by ListServiceResponse to answer -// list_services request. -message ServiceResponse { - // Full name of a registered service, including its package name. The format - // is . - string name = 1; -} - -// The error code and error message sent by the server when an error occurs. -message ErrorResponse { - // This field uses the error codes defined in grpc::StatusCode. - int32 error_code = 1; - string error_message = 2; -} - diff --git a/test/fixtures/v1alpha/reflection.proto b/test/fixtures/v1alpha/reflection.proto deleted file mode 100644 index e168905..0000000 --- a/test/fixtures/v1alpha/reflection.proto +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2016 The 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. - -// Service exported by server reflection. A more complete description of how -// server reflection works can be found at -// https://github.com/grpc/grpc/blob/master/doc/server-reflection.md -// -// The canonical version of this proto can be found at -// https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1alpha/reflection.proto - -syntax = "proto3"; - -package grpc.reflection.v1alpha; - -option go_package = "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"; -option java_multiple_files = true; -option java_package = "io.grpc.reflection.v1alpha"; -option java_outer_classname = "ServerReflectionProto"; - -service ServerReflection { - // The reflection service is structured as a bidirectional stream, ensuring - // all related requests go to a single server. - rpc ServerReflectionInfo(stream ServerReflectionRequest) - returns (stream ServerReflectionResponse); -} - -// The message sent by the client when calling ServerReflectionInfo method. -message ServerReflectionRequest { - string host = 1; - // To use reflection service, the client should set one of the following - // fields in message_request. The server distinguishes requests by their - // defined field and then handles them using corresponding methods. - oneof message_request { - // Find a proto file by the file name. - string file_by_filename = 3; - - // Find the proto file that declares the given fully-qualified symbol name. - // This field should be a fully-qualified symbol name - // (e.g. .[.] or .). - string file_containing_symbol = 4; - - // Find the proto file which defines an extension extending the given - // message type with the given field number. - ExtensionRequest file_containing_extension = 5; - - // Finds the tag numbers used by all known extensions of the given message - // type, and appends them to ExtensionNumberResponse in an undefined order. - // Its corresponding method is best-effort: it's not guaranteed that the - // reflection service will implement this method, and it's not guaranteed - // that this method will provide all extensions. Returns - // StatusCode::UNIMPLEMENTED if it's not implemented. - // This field should be a fully-qualified type name. The format is - // . - string all_extension_numbers_of_type = 6; - - // List the full names of registered services. The content will not be - // checked. - string list_services = 7; - } -} - -// The type name and extension number sent by the client when requesting -// file_containing_extension. -message ExtensionRequest { - // Fully-qualified type name. The format should be . - string containing_type = 1; - int32 extension_number = 2; -} - -// The message sent by the server to answer ServerReflectionInfo method. -message ServerReflectionResponse { - string valid_host = 1; - ServerReflectionRequest original_request = 2; - // The server sets one of the following fields according to the message_request - // in the request. - oneof message_response { - // This message is used to answer file_by_filename, file_containing_symbol, - // file_containing_extension requests with transitive dependencies. - // As the repeated label is not allowed in oneof fields, we use a - // FileDescriptorResponse message to encapsulate the repeated fields. - // The reflection service is allowed to avoid sending FileDescriptorProtos - // that were previously sent in response to earlier requests in the stream. - FileDescriptorResponse file_descriptor_response = 4; - - // This message is used to answer all_extension_numbers_of_type requests. - ExtensionNumberResponse all_extension_numbers_response = 5; - - // This message is used to answer list_services requests. - ListServiceResponse list_services_response = 6; - - // This message is used when an error occurs. - ErrorResponse error_response = 7; - } -} - -// Serialized FileDescriptorProto messages sent by the server answering -// a file_by_filename, file_containing_symbol, or file_containing_extension -// request. -message FileDescriptorResponse { - // Serialized FileDescriptorProto messages. We avoid taking a dependency on - // descriptor.proto, which uses proto2 only features, by making them opaque - // bytes instead. - repeated bytes file_descriptor_proto = 1; -} - -// A list of extension numbers sent by the server answering -// all_extension_numbers_of_type request. -message ExtensionNumberResponse { - // Full name of the base type, including the package name. The format - // is . - string base_type_name = 1; - repeated int32 extension_number = 2; -} - -// A list of ServiceResponse sent by the server answering list_services request. -message ListServiceResponse { - // The information of each service may be expanded in the future, so we use - // ServiceResponse message to encapsulate it. - repeated ServiceResponse service = 1; -} - -// The information of a single service used by ListServiceResponse to answer -// list_services request. -message ServiceResponse { - // Full name of a registered service, including its package name. The format - // is . - string name = 1; -} - -// The error code and error message sent by the server when an error occurs. -message ErrorResponse { - // This field uses the error codes defined in grpc::StatusCode. - int32 error_code = 1; - string error_message = 2; -} - diff --git a/test/test-servers/base.js b/test/test-servers/base.js index fb4e9aa..a015e42 100644 --- a/test/test-servers/base.js +++ b/test/test-servers/base.js @@ -9,7 +9,12 @@ function createServer({apiVersion = 'v1'} = {}) { const PROTO_DIR = path.join(__dirname, '../fixtures', apiVersion); const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); - const REFLECTION_PROTO_PATH = path.join(PROTO_DIR, 'reflection.proto'); + const REFLECTION_PROTO_PATH = path.join( + __dirname, + '../../static/grpc/reflection', + apiVersion, + 'reflection.proto' + ); const DESCRIPTOR_SET_PATH = path.join( PROTO_DIR, 'server_descriptor.protoset' @@ -28,9 +33,8 @@ function createServer({apiVersion = 'v1'} = {}) { if (server) return Promise.resolve({server, port: boundPort, shutdown}); const descriptorSetBytes = fs.readFileSync(DESCRIPTOR_SET_PATH); - const fileDescriptorSet = descriptor.FileDescriptorSet.deserializeBinary( - descriptorSetBytes - ); + const fileDescriptorSet = + descriptor.FileDescriptorSet.deserializeBinary(descriptorSetBytes); fileDescriptorProtos = fileDescriptorSet.getFileList(); if (!fileDescriptorProtos || fileDescriptorProtos.length === 0) { @@ -181,7 +185,8 @@ function createServer({apiVersion = 'v1'} = {}) { proto.serializeBinary() ); }); - responseObject.file_descriptor_response = fileDescriptorResponseObj; + responseObject.file_descriptor_response = + fileDescriptorResponseObj; } } else if (messageRequestCase === 'file_containing_symbol') { const symbol = request.file_containing_symbol; @@ -215,7 +220,8 @@ function createServer({apiVersion = 'v1'} = {}) { proto.serializeBinary() ); }); - responseObject.file_descriptor_response = fileDescriptorResponseObj; + responseObject.file_descriptor_response = + fileDescriptorResponseObj; } } else { responseObject.error_response = { From 6d469889292ffeb49223d9e1ef86f1dbdbe5fb98 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Date: Thu, 30 Oct 2025 17:10:29 +0530 Subject: [PATCH 4/5] tests: Revert complicated widgets test to original proto files --- test/fixtures/contact.proto | 7 +++ test/fixtures/phone.proto | 18 ++++++++ .../{v1 => }/server_descriptor.protoset | Bin test/fixtures/v1/widgets.proto | 41 ------------------ .../v1alpha/server_descriptor.protoset | Bin 2274 -> 0 bytes test/fixtures/v1alpha/widgets.proto | 41 ------------------ test/test-servers/base.js | 20 ++++----- 7 files changed, 33 insertions(+), 94 deletions(-) create mode 100644 test/fixtures/contact.proto create mode 100644 test/fixtures/phone.proto rename test/fixtures/{v1 => }/server_descriptor.protoset (100%) delete mode 100644 test/fixtures/v1/widgets.proto delete mode 100644 test/fixtures/v1alpha/server_descriptor.protoset delete mode 100644 test/fixtures/v1alpha/widgets.proto diff --git a/test/fixtures/contact.proto b/test/fixtures/contact.proto new file mode 100644 index 0000000..ef26844 --- /dev/null +++ b/test/fixtures/contact.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; +package phone; + +message Contact { + string name = 1; + string number = 2; +} \ No newline at end of file diff --git a/test/fixtures/phone.proto b/test/fixtures/phone.proto new file mode 100644 index 0000000..cceb957 --- /dev/null +++ b/test/fixtures/phone.proto @@ -0,0 +1,18 @@ +syntax = "proto3"; +package phone; + +import "contact.proto"; + +service Messenger { + rpc Message(TextRequest) returns (TextResponse); +} + +message TextRequest { + string id = 1; + string message = 2; + Contact contact = 3; +} + +message TextResponse { + bool success = 1; +} \ No newline at end of file diff --git a/test/fixtures/v1/server_descriptor.protoset b/test/fixtures/server_descriptor.protoset similarity index 100% rename from test/fixtures/v1/server_descriptor.protoset rename to test/fixtures/server_descriptor.protoset diff --git a/test/fixtures/v1/widgets.proto b/test/fixtures/v1/widgets.proto deleted file mode 100644 index 3db47f9..0000000 --- a/test/fixtures/v1/widgets.proto +++ /dev/null @@ -1,41 +0,0 @@ -// proto/widgets.proto -syntax = "proto3"; - -package widgets.v1; // Use a versioned package name - -// Represents a widget item -message Widget { - string id = 1; - string name = 2; - string color = 3; -} - -// Request to list widgets, optionally filtering by color -message ListWidgetsRequest { - string filter_color = 1; // Empty string means no filter -} - -// Response containing a list of widgets -message ListWidgetsResponse { - repeated Widget widgets = 1; -} - -// Request to create a new widget -message CreateWidgetRequest { - string name = 1; - string color = 2; -} - -// Response after creating a widget -message CreateWidgetResponse { - Widget widget = 1; -} - -// The Widget service definition -service WidgetService { - // Lists widgets, optionally filtering by color - rpc ListWidgets(ListWidgetsRequest) returns (ListWidgetsResponse); - - // Creates a new widget - rpc CreateWidget(CreateWidgetRequest) returns (CreateWidgetResponse); -} diff --git a/test/fixtures/v1alpha/server_descriptor.protoset b/test/fixtures/v1alpha/server_descriptor.protoset deleted file mode 100644 index b11cd2e44fbc93937168a1a67de427128a38f924..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2274 zcmZ`)+iuf95GB1Ts1M7Yid0AX9zBun;ns{XBrI#aYE~paMm|5m_B8}b+&zK3?hI$kxsh2SP z!ah#SMdefDVvZ`8c5XuR94Cn%V2?^TUJXy$+tHAvta6R}8pA6mt-%^8+? z-fd=bS*Z1U4$Q05O-3Y6Fj@x~`%yUXazm~P$tyN>^buNwhCvwnk*-^$F`Nfw;9pH6 zNaQwk5Z&hxOflU7PWm()rG&EZvLwL^!`Ul~|B^A~Mdsov4>n@Kv8x(HVWWJ)SvEmvJ!J>MeqEN%xEJI4YRSk`+eXMP0 z7Ib_o=IRyyty8E^fz98RoKv3+uvnn;ZdFLSOn^2)&cagz5D%;B)`Z1dxh$NY6=)S- z6}o_SQT93>5>(3KqNRF*xAw2YVEPJ3PV815Ryspi4oQhuXStX|O#Xf4^W%V!Ai{P) zB0mmnLW2XovJWQbM|`svqOX)UKI|t4g#De>Ue?vWJjw53!j7%HDD$j zr~}3RWy9h7kew_(DAb1eK8XmGcgmF?)>A;7FEUP|m(x9{F(Rj!deTLb(vY$)k79Kt z(K1K1P4{6@Rj7!DZ1ruA4{d4fY7^@@EOjaNQ>+w|i(BN@qV#XJy?)w z1t^A7R)w-~{#2kY^YbCHTZK-*$bo`3@-5^Pnn!o8)vIbgKrLS-nsUf%bi}*)h|>l# Hq-Mx}`xX80 diff --git a/test/fixtures/v1alpha/widgets.proto b/test/fixtures/v1alpha/widgets.proto deleted file mode 100644 index 3db47f9..0000000 --- a/test/fixtures/v1alpha/widgets.proto +++ /dev/null @@ -1,41 +0,0 @@ -// proto/widgets.proto -syntax = "proto3"; - -package widgets.v1; // Use a versioned package name - -// Represents a widget item -message Widget { - string id = 1; - string name = 2; - string color = 3; -} - -// Request to list widgets, optionally filtering by color -message ListWidgetsRequest { - string filter_color = 1; // Empty string means no filter -} - -// Response containing a list of widgets -message ListWidgetsResponse { - repeated Widget widgets = 1; -} - -// Request to create a new widget -message CreateWidgetRequest { - string name = 1; - string color = 2; -} - -// Response after creating a widget -message CreateWidgetResponse { - Widget widget = 1; -} - -// The Widget service definition -service WidgetService { - // Lists widgets, optionally filtering by color - rpc ListWidgets(ListWidgetsRequest) returns (ListWidgetsResponse); - - // Creates a new widget - rpc CreateWidget(CreateWidgetRequest) returns (CreateWidgetResponse); -} diff --git a/test/test-servers/base.js b/test/test-servers/base.js index a015e42..16db72a 100644 --- a/test/test-servers/base.js +++ b/test/test-servers/base.js @@ -7,8 +7,8 @@ function createServer({apiVersion = 'v1'} = {}) { const fs = require('fs'); const descriptor = require('google-protobuf/google/protobuf/descriptor_pb'); - const PROTO_DIR = path.join(__dirname, '../fixtures', apiVersion); - const WIDGETS_PROTO_PATH = path.join(PROTO_DIR, 'widgets.proto'); + const PROTO_DIR = path.join(__dirname, '../fixtures'); + const PHONE_PROTO_PATH = path.join(PROTO_DIR, 'phone.proto'); const REFLECTION_PROTO_PATH = path.join( __dirname, '../../static/grpc/reflection', @@ -78,12 +78,11 @@ function createServer({apiVersion = 'v1'} = {}) { oneofs: true, includeDirs: [PROTO_DIR], }; - const widgetsPackageDefinition = protoLoader.loadSync( - WIDGETS_PROTO_PATH, + const phonePackageDefinition = protoLoader.loadSync( + PHONE_PROTO_PATH, packageDefinitionOptions ); - const widgetsProto = grpc.loadPackageDefinition(widgetsPackageDefinition) - .widgets.v1; + const phoneProto = grpc.loadPackageDefinition(phonePackageDefinition).phone; const reflectionPackageDefinition = protoLoader.loadSync( REFLECTION_PROTO_PATH, @@ -94,10 +93,7 @@ function createServer({apiVersion = 'v1'} = {}) { reflectionPackageDefinition ).grpc.reflection[apiVersion]; - const widgetServiceImpl = { - listWidgets: () => {}, - createWidget: () => {}, - }; + const phoneServiceImplementation = {message: () => {}}; function getDependenciesRecursive( filename, @@ -253,8 +249,8 @@ function createServer({apiVersion = 'v1'} = {}) { const grpcServer = new grpc.Server(); grpcServer.addService( - widgetsProto.WidgetService.service, - widgetServiceImpl + phoneProto.Messenger.service, + phoneServiceImplementation ); grpcServer.addService( reflectionProto.ServerReflection.service, From 37e175ae528a4cc73a9bcbb6f9a7b32037b384b4 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Date: Thu, 30 Oct 2025 17:21:13 +0530 Subject: [PATCH 5/5] tests: Update protoset definitions --- test/client.test.ts | 5 +---- test/fixtures/contact.proto | 2 +- test/fixtures/phone.proto | 2 +- test/fixtures/server_descriptor.protoset | Bin 2274 -> 306 bytes 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/test/client.test.ts b/test/client.test.ts index 9345cd6..9bb9198 100644 --- a/test/client.test.ts +++ b/test/client.test.ts @@ -67,10 +67,7 @@ describe('server reflection tests', () => { const mock = sinon.mock(reflectionClient.grpcClient); mock.expects('serverReflectionInfo').once().returns(grpcCall); - const expectedServices: string[] = [ - 'grpc.reflection.v1.ServerReflection', - 'widgets.v1.WidgetService', - ]; + const expectedServices: string[] = ['phone.Messenger']; assert.sameMembers( await reflectionClient.listServices(), expectedServices diff --git a/test/fixtures/contact.proto b/test/fixtures/contact.proto index ef26844..18a6761 100644 --- a/test/fixtures/contact.proto +++ b/test/fixtures/contact.proto @@ -4,4 +4,4 @@ package phone; message Contact { string name = 1; string number = 2; -} \ No newline at end of file +} diff --git a/test/fixtures/phone.proto b/test/fixtures/phone.proto index cceb957..466f6ac 100644 --- a/test/fixtures/phone.proto +++ b/test/fixtures/phone.proto @@ -15,4 +15,4 @@ message TextRequest { message TextResponse { bool success = 1; -} \ No newline at end of file +} diff --git a/test/fixtures/server_descriptor.protoset b/test/fixtures/server_descriptor.protoset index b11cd2e44fbc93937168a1a67de427128a38f924..b3013d248fcc5455931a513f666dc0c693187511 100644 GIT binary patch literal 306 zcmY+Ay$*sv5QHQCpo_6s6N6TUiV6~pwS~0}@dXf9q~H#@yM(v$6+Q2cM7y2Y>^GBz zw+CBELZ*+pm#QO?wolF|X*(`CPK}6;;Ga2@MJW+s!it8%(S8&!ax>!wW=u6V>O#-B zPDKR-SL$U>FQpT31zvr3hkC*g{Dy=mDsp#Eb2=7nQT46;>XTe%kbu#RF_+6d8d tCg{r)sw_3u1kLk7W+zx&v`ws{4m0q-LPsMy^iR{>`p>iI+swS=@Bu|YT9yC+ literal 2274 zcmZ`)+iuf95GB1Ts1M7Yid0AX9zBun;ns{XBrI#aYE~paMm|5m_B8}b+&zK3?hI$kxsh2SP z!ah#SMdefDVvZ`8c5XuR94Cn%V2?^TUJXy$+tHAvta6R}8pA6mt-%^8+? z-fd=bS*Z1U4$Q05O-3Y6Fj@x~`%yUXazm~P$tyN>^buNwhCvwnk*-^$F`Nfw;9pH6 zNaQwk5Z&hxOflU7PWm()rG&EZvLwL^!`Ul~|B^A~Mdsov4>n@Kv8x(HVWWJ)SvEmvJ!J>MeqEN%xEJI4YRSk`+eXMP0 z7Ib_o=IRyyty8E^fz98RoKv3+uvnn;ZdFLSOn^2)&cagz5D%;B)`Z1dxh$NY6=)S- z6}o_SQT93>5>(3KqNRF*xAw2YVEPJ3PV815Ryspi4oQhuXStX|O#Xf4^W%V!Ai{P) zB0mmnLW2XovJWQbM|`svqOX)UKI|t4g#De>Ue?vWJjw53!j7%HDD$j zr~}3RWy9h7kew_(DAb1eK8XmGcgmF?)>A;7FEUP|m(x9{F(Rj!deTLb(vY$)k79Kt z(K1K1P4{6@Rj7!DZ1ruA4{d4fY7^@@EOjaNQ>+w|i(BN@qV#XJy?)w z1t^A7R)w-~{#2kY^YbCHTZK-*$bo`3@-5^Pnn!o8)vIbgKrLS-nsUf%bi}*)h|>l# Hq-Mx}`xX80