GRPC

GRPC module of Camouflage lets you mock your backends based on grpc protocols. You can create a Camouflage grpc object from CamouflageGrpc class and configure it to serve mocks for your incoming requests.

Start by installing required dependencies

npx jsr add @camouflage/grpc
  • You can create the Camouflage object without any parameters, and load the required options as needed
import CamouflageGrpc from "@camouflage/grpc";
import * as protoloader from "@grpc/proto-loader";
import * as grpc from "@grpc/grpc-js";

const camouflageGrpc = new CamouflageGrpc();
camouflageGrpc.loadConfigFromJson("./config_grpc.json");

//...add your services

camouflageGrpc.start();
  • Or you can create the Camouflage object with the options
import CamouflageGrpc, { CamouflageGrpcConfig } from "@camouflage/grpc";

const config = {};

const camouflageGrpc = new CamouflageGrpc(config);
camouflageGrpc.start();

Available methods

loadConfigFromJson = (configFilePath: string): void

While you can include your config as part of your code and ensure the types yourself, you may at times want to maintain the configuration for your Camouflage server separate from the application code. This is usually a good practice from maintainability point of view, or even practical if you want to maintain multiple config files for different usecases.

loadConfigFromJson lets you load a config via a .json file. You don't need to worry about validating your config file, Camouflage takes care of validating your config and prints relevant errors which help you fix your config files, if you miss something.

getHandlers = (): CamouflageGrpcHandler | undefined

Camouflage provides some ready to use handlers which you can use to load your services/methods into the Camouflage grpc servers. Depending on the type of method your mocks require, you can use one of the following:

  • unaryHandler
  • serverSideStreamingHandler
  • clientSideStreamingHandler
  • bidiStreamingHandler
getHelpers = (): Helpers

When you create a CamouflageGrpc object, it automatically creates an instance of helpers. You can use getHelpers() to get a reference to this helpers object. This is useful when you want add custom helpers that are specific to your requirements.

import Helpers from "@camouflage/helpers";

const helpers = camouflageGrpc.getHelpers();

helpers.addHelper("ping", (context) => {
  return "pong";
});

camouflageGrpc.start();

You can take a look at how inbuilt helpers have been created, in case you want to understand how custom helpers can be created. Refer to the helper source code

addService = (service: grpc.ServiceDefinition, implementation: grpc.UntypedServiceImplementation): void

camouflageGrpc.addService is wrapper on @grpc/grpc-js addService method. It allows you to load your proto package definitions/services/methods into Camouflage's grpc server. In the following example we load the blog.proto definition into a grpcObject and use it to provide implementation of the createBlog and listBlogs methods required by the proto definition

import CamouflageGrpc, { CamouflageGrpcHandler } from "@camouflage/grpc";
import * as protoloader from "@grpc/proto-loader";
import * as grpc from "@grpc/grpc-js";
const camouflageGrpc = new CamouflageGrpc();
camouflageGrpc.loadConfigFromJson("./config_grpc.json");
const handlers = camouflageGrpc.getHandlers();

const blogPackageDef = protoloader.loadSync("./blog.proto", {});
const blogGrpcObject = grpc.loadPackageDefinition(blogPackageDef);
const blogPackage = blogGrpcObject.blogPackage;

if (handlers) {
  camouflageGrpc.addService(blogPackage.BlogService.service, {
    createBlog: handlers.unaryHandler,
    listBlogs: handlers.unaryHandler,
  });
}

camouflageGrpc.start();

Here we are using Camouflage's unaryHandler as the implementation of the required methods, but you might as well write your own implementation, making it easier to mock only the required methods instead of everything.

start = async (): Promise

Self explanatory. Starts the Camouflage grpc server.

stop = async (): Promise

Self explanatory. Stops the Camouflage grpc server.

Hooks

Note

COMING SOON

Camouflage GRPC Configuration

You can provide following configuration options in your config.json file and load it to Camouflage before you start the server

{
  "log": {
    "enable": true, // enables or disables the logs
    "level": "trace" // // if enable=true, sets the log level. Available options are "fatal", "error", "warn", "info", "debug", "trace"
  },
  "host": "0.0.0.0", // host part of the address on which you'd want grpc server to listen on
  "port": 8082, // port part of the address on which you'd want grpc server to listen on
  "ssl": {
    "enable": false, // enables or disabled ssl, if disabled credentials will be created using grpc.ServerCredentials.createInsecure()
    "cert": "location/to/server.cert", // if enable=true, required config for .cert file
    "key": "location/to/server/key", // if enable=true, required config for .key file
    "rootCert": "location/to/rootCert" // Optionally, provide location to root cert
  },
  "mocksDir": "./grpcMocks", // location of the mocks folder
  "monitoring": {
    "enable": true, // enables or disables monitoring
    "port": 40000 // required port for monitoring server
  }
}

Folder Structure

The way you organize your directories inside the mocksDir, determine how your endpoints will be available. Assuming that you have configured your mocksDir as ./grpcMocks, the folder structure would follow the pattern as shown below.

Let's take a look at this proto file

syntax = "proto3";

package foo.todoPackage;

import "./todoEnum.proto";

service TodoService{
    rpc readTodo(Empty) returns (Todos);
    rpc readTodoStream(Empty) returns (stream Todo);
    rpc createTodoStream(stream Todo) returns (Todos);
    rpc createTodoBidiStream(stream Todo) returns (stream Todo);
}

The expected mock file path for each required methods would be:

  • ./grpcMocks/foo/todoPackage/readTodo.mock
  • ./grpcMocks/foo/todoPackage/readTodoStream.mock
  • ./grpcMocks/foo/todoPackage/createTodoStream.mock
  • ./grpcMocks/foo/todoPackage/createTodoBidiStream.mock

Camouflage GRPC Helpers

capture Helper

Usage:

  • {{capture from='metadata' key='firstName'}} - Pretty self-explanatory, but if you want to capture some data from the request metadata, you can do so by providing the required key.
  • {{capture from='request' using="jsonpath" selector='$.title'}} - To capture values from the actual request, your options are either using='regex' or using='jsonpath'. Selector will change accordingly.

What data to put in .mock files

Unary Response

A typical unary response mock file would look like following snippet

{
  "id": {{num_between lower=100 upper=500}},
  "title": "something",
  "metadata": {
    "headers": {
      "random": "{{random}}"
    },
    "trailers": {
      "now": "{{now format='epoch'}}",
      "random": "{{random}}"
    }
  },
  "delay": 2000
}

You only need to provide the a json object matching your expected response. In our example, let's say we are creating a mock for createBlog unary method which return a Blog as shown below

syntax = "proto3";

package blogPackage;

message Blog {
    int32 id = 1;
    string title = 2;
}

service BlogService{
    rpc createBlog(Blog) returns (Blog);
}

Corresponding to our Blog schema as per the proto file, we are responding with an random integer between 100-500 using num_between helper. And a random string as title using random helper. Optionally, you can also send metadata, i.e. headers or trailers or both, as shown in the example above. And finally, and optionally, you can simulate a delay of 2 seconds, by including delay: 2000 field. And you might already know by now, you can make the delay random by using the num_between helper.

Server Side Streaming Response

Mock file for server side streaming response would be mostly similar to unary, except for one distinction. Since we are streaming a response back to client, you'll be providing multiple responses. You can do so by separating each response by a delimiter, i.e. "====" (four equals). Other than that, as you would in unary response, metadata.headers, metadata.trailers and delay are optional properties

{
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}",
    "delay": {{num_between lower=500 upper=600}}
}
====
{
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}",
    "delay": {{num_between lower=500 upper=600}}
}
====
{
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}",
    "delay": {{num_between lower=500 upper=600}}
}
====
{
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}",
    "delay": {{num_between lower=500 upper=600}}
}

Client Side Streaming Response

Client streaming and unary responses are identical.

{
    "todos": [
        {
            "id": "{{random type='UUID'}}",
            "text": "{{random type='ALPHABETIC' length='100'}}"
        },
        {
            "id": "{{random type='UUID'}}",
            "text": "{{random type='ALPHABETIC' length='100'}}"
        },
        {
            "id": "{{randomValue type='UUID'}}",
            "text": "{{random type='ALPHABETIC' length='100'}}"
        }
    ],
    "metadata": {
        "trailers":{
            "key": "value"
        }
    },
    "delay": {{num_between lower=500 upper=600}}
}

Bidi Streaming Response

Bidi streaming supported currently by Camouflage is like ping-pong in nature. For each request you stream to the server, you get one response back.

Bidi streaming responses differ from the other responses. Your mockfile would include a required object data. This is what Camouflage will respond back for each of your requests. You can include an optional end object, which would be sent when you end the client side stream. This is an optional object, in absence of which, Camouflage will simply end the server side stream without any response.

{
  "data": {
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}"
  },
  "end": {
    "id": "{{random type='UUID'}}",
    "text": "{{random type='ALPHABETIC' length='100'}}"
  }
}

Request matching

Request matching in Camouflage grpc module can be done with a combination of helpers like if, unless and is along with capture helper.

GRPC capture helper has access to request and metadata objects from your requests. Usage can be found in the helper section above

Note

Structure of request object might vary depending on the calls you are making. For example:

  • Unary: In this case, request object is what you would expect it to be. The object you send from the client, as is.
  • Server Side Streaming: Since you make one call from client, and recieve n streams in response as defined in your mock file, same request object is available for each of your responses.
  • Client Side Streaming: All of your request objects are stored in an array, and this array of objects (instead of an object), is made available to your response
  • Bidi Streaming: In this case, request object that you send from the client, is available for each of streams. However the end object that you create, would have access to the array of all request objects sent.