Skip to content

Commit

Permalink
feat: grpc enhancements, validation bug fixes (#211)
Browse files Browse the repository at this point in the history
* fix: lazy loading the validation modules #203 (#206)
* feat: state helper (#207)
* feat: cookie support for the capture helper (#209)
* implement basic error response and metadata. #188
* docs: grpc documentation
* fix: race condition with lazy loading of the validation adapters (#210)
  • Loading branch information
shubhendumadhukar authored Nov 22, 2022
1 parent c57810f commit 30ec77f
Show file tree
Hide file tree
Showing 13 changed files with 155 additions and 20 deletions.
8 changes: 6 additions & 2 deletions docs/capture-helper.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ Example `{{capture using='jsonpath' selector='$.lastName'}}`

Similar to websockets, while using `capture` helper with gRPC, available/required arguments are `using` and `selector`.

Example `{{capture using='jsonpath' selector='$.lastName'}}`

1. For unary calls and server side streams, helpers have access to request payload as is.
2. For client side streaming calls, payloads from each stream are stored in an array, which is then made available to helpers.
3. For bidi side streams, helpers can access the request payload as is during each streaming/ping-pong interaction i.e. while sending the "data" stream. Additionally, each payload is also stored in an array which is then made available to helpers while sending the "end" stream
2. For unary calls and server side streams, helpers have access to metadata as well, which can be captured by specifying `key` and `from`.
Example: `{{capture from='metadata' key='metadata_key'}}`
3. For client side streaming calls, payloads from each stream are stored in an array, which is then made available to helpers.
4. For bidi side streams, helpers can access the request payload as is during each streaming/ping-pong interaction i.e. while sending the "data" stream. Additionally, each payload is also stored in an array which is then made available to helpers while sending the "end" stream.
5 changes: 3 additions & 2 deletions docs/handlebars.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,9 @@ Usage:
}
```

2. **{{capture from='path' regex='\/users\/get\/(.*)?'}}** - For path, you'd need to specify a regex to capture a value.
3. **{{capture from='body' using='jsonpath' selector='$.lastName'}}** - To capture values from the request body, your options are either using='regex' or using='jsonpath'. Selector will change accordingly.
2. **{{capture from='cookies' key='mycookie'}}** - For cookies, you'd need to specify a key to capture a value.
3. **{{capture from='path' regex='\/users\/get\/(.*)?'}}** - For path, you'd need to specify a regex to capture a value.
4. **{{capture from='body' using='jsonpath' selector='$.lastName'}}** - To capture values from the request body, your options are either using='regex' or using='jsonpath'. Selector will change accordingly.

!!!note

Expand Down
26 changes: 26 additions & 0 deletions docs/mocking-gRPC.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,32 @@ You can also add delays in your grpc mock services, by adding a delay key with t

You don't need to modify your proto file to accommodate the additional key, since Camouflage will delete the "delay" key from the response before sending it to the client.

## Sending GRPC Error responses

Camouflage provides an experimental support to send error responses starting v0.11.0 onwards, for unary and client side streaming calls. To send an error response, append a json error object with `code` and optional `message` to your mock content.

```json
{
"error": {
"code": 16,
"message": "User is unauthenticted."
}
}
```

## Sending GRPC response metadata

Camouflage provides an experimental support to send metadata/trailers with responses starting v0.11.0 onwards, for unary and client side streaming calls. To send metadata, append a json metadata object with relevant keys and values to your mock content.

```json
{
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
```

!!!caution

Since the Camouflage gRPC server needs to register the new services, everytime you add a new protofile, you'd need to restart the Camouflage server. Good news is, you can do so easily by making a get request to /restart endpoint. Though the downtime is minimal (less than a second, we do not recommend restarting the server during a performance test. Note that restart is required only if you add a new protofile. If you have added a new mock file or updated an existing one, a restart is not required.
Expand Down
10 changes: 9 additions & 1 deletion grpc/mocks/blogPackage/BlogService/createBlog.mock
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
{
"id": {{num_between lower=500 upper=600}},
"title": "something"
"title": "something",
"error": {
"code": 16,
"message": "User is unauthenticated."
},
"metadata": {
"key1": "value1",
"key2": "value2"
}
}
12 changes: 12 additions & 0 deletions mocks/capture/cookies/GET.mock
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
HTTP/1.1 200 OK
Content-Type: application/json

{{#if (capture from='cookies' key='scenario') }}
{
"scenario": "{{capture from='cookies' key='scenario'}}"
}
{{else}}
{
"scenario": "no cookie found for scenario"
}
{{/if}}
56 changes: 56 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"apicache": "^1.6.3",
"compression": "^1.7.4",
"convert-csv-to-json": "^1.3.3",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
"express": "^4.18.1",
"express-query-parser": "^1.3.3",
Expand Down Expand Up @@ -76,6 +77,7 @@
"yargs": "^16.2.0"
},
"devDependencies": {
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.11",
"@types/http-errors": "^1.8.2",
"@types/js-yaml": "^4.0.5",
Expand Down
4 changes: 3 additions & 1 deletion src/handlebar/RequestHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export class RequestHelper {
* Registers capture helper
* - Get the request object passed in from the context by calling template({request: req})
* - Get the from value passed in while calling {{capture from=}}, accepted values query, headers, path, body
* - For query and headers, key is required, else if not found a null/undefined value will be automatically returned.
* - For query, cookies and headers, key is required, else if not found a null/undefined value will be automatically returned.
* - For path additional input regex is mandatory, if not passed return error
* - For body additional inputs using and selector are mandatory, if not passed return error
* @returns {void}
Expand All @@ -28,6 +28,8 @@ export class RequestHelper {
return request.query[context.hash.key];
case "headers":
return request.get(context.hash.key);
case "cookies":
return request.cookies[context.hash.key] || "";
case "path":
if (typeof context.hash.regex === "undefined") {
logger.debug("ERROR: No regex specified");
Expand Down
7 changes: 3 additions & 4 deletions src/handlebar/StateHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,9 @@ export class StateHelper {
*/
register = () => {
this.Handlebars.registerHelper("state", (context: any) => {
const cookie = context.data.root.request.headers.cookie;
const key = context.hash.key;
const value = new RegExp(`mocked-state-${key}=([^;]+)`).exec(cookie);
return value ? value[1] : context.fn(this);
const cookies = context.data.root.request.cookies || {};
const key = `mocked-state-${context.hash.key}`;
return cookies[key] || context.fn(this);
});
};
}
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ import swStats from "swagger-stats";
import cors from "cors";
import compression from "compression";
import { queryParser } from "express-query-parser";
import cookieParser from "cookie-parser";
import ConfigLoader, {
getLoaderInstance,
setLoaderInstance,
} from "./ConfigLoader";
import { CamouflageConfig } from "./ConfigLoader/LoaderInterface";
import { Validation } from "./validation";

const app = express();
// Configure logging for express requests
Expand Down Expand Up @@ -64,6 +64,8 @@ app.use(
parseNumber: true,
})
);
// parse cookies
app.use(cookieParser());
/**
* Initializes required variables and starts a 1 master X workers configuration - FUTURE IMPROVEMENT - Pass a single config object
* @param {string[]} protoIgnore array of files to be ignored during loading proto files
Expand Down
33 changes: 27 additions & 6 deletions src/parser/GrpcParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getHandlebars } from '../handlebar'
import { getLoaderInstance } from "../ConfigLoader";
import { CamouflageConfig } from "../ConfigLoader/LoaderInterface";
const Handlebars = getHandlebars()
import * as grpc from "@grpc/grpc-js";
/**
* Parser class for GRPC Protocol mocks to define handlers for:
* - Unary calls
Expand Down Expand Up @@ -42,16 +43,26 @@ export default class GrpcParser {
const response = JSON.parse(fileContent);
const delay: number = response.delay || 0;
delete response.delay;
const error = response.error || null
delete response.error;
const metadata = response.metadata || null;
delete response.metadata;
const trailers = new grpc.Metadata();
if (metadata) {
for (const key in metadata) {
trailers.add(key, metadata[key])
}
}
setTimeout(() => {
callback(null, response);
callback(error, response, trailers);
}, delay);
} else {
logger.error(`No suitable mock file was found for ${mockFilePath}`);
callback(null, { error: `No suitable mock file was found for ${mockFilePath}` });
callback({ code: 5, message: `No suitable mock file was found for ${mockFilePath}` }, {});
}
} catch (error) {
logger.error(error);
callback(null, { error: error });
callback({ code: 10, message: error }, {});
}
};
/**
Expand Down Expand Up @@ -137,16 +148,26 @@ export default class GrpcParser {
const response = JSON.parse(fileContent);
const delay: number = response.delay || 0;
delete response.delay;
const error = response.error || null;
delete response.error
const metadata = response.metadata || null;
delete response.metadata;
const trailers = new grpc.Metadata();
if (metadata) {
for (const key in metadata) {
trailers.add(key, metadata[key])
}
}
setTimeout(() => {
callback(null, response);
callback(error, response, trailers);
}, delay);
} else {
logger.error(`No suitable mock file was found for ${mockFilePath}`);
callback(null, { error: `No suitable mock file was found for ${mockFilePath}` });
callback({ code: 5, error: `No suitable mock file was found for ${mockFilePath}` }, {});
}
} catch (error) {
logger.error(error);
callback(null, { error: error });
callback({ code: 10, message: error }, {});
}
});
};
Expand Down
1 change: 0 additions & 1 deletion src/validation/ValidationAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ export abstract class ValidationAdapter {

constructor(config: ValidationSchema) {
this.config = config;
this.load();
}

abstract load(): Promise<void>;
Expand Down
7 changes: 5 additions & 2 deletions src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ export class Validation {
}
}

private loadSchemas(schemas: ValidationSchema[]) {
private async loadSchemas(schemas: ValidationSchema[]) {
for (let x = 0; x < schemas.length; x++) {
const schema = schemas[x];
// for now only OpenApi json schemas are supported
// in the future more types can be added like xml-rpc
switch (schema.type) {
case ValidationSchemaType.OpenApi:
this.adapters.push(new OpenApiAdapter(schema));
// eslint-disable-next-line no-case-declarations
const adapter = new OpenApiAdapter(schema);
await adapter.load();
this.adapters.push(adapter);
break;
default:
logger.warn(
Expand Down

0 comments on commit 30ec77f

Please sign in to comment.