Skip to content

Commit

Permalink
Merge pull request #152 from pmcelhaney/135-responserandom
Browse files Browse the repository at this point in the history
response[statusCode].random()
  • Loading branch information
pmcelhaney committed Aug 8, 2022
2 parents bb9b693 + 860f67f commit ff307eb
Show file tree
Hide file tree
Showing 9 changed files with 326 additions and 21 deletions.
5 changes: 5 additions & 0 deletions .changeset/plenty-peaches-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"counterfact": minor
---

Counterfact is now able to read an OpenAPI document and use it to generate a random response.
57 changes: 57 additions & 0 deletions demo/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
openapi: 3.0.3
info:
version: 1.0.0
title: Sample API
description: A sample API to illustrate OpenAPI concepts
paths:
/count:
get:
description: outputs the number of time each URL was visited
responses:
default:
description: Successful response
content:
application/json:
schema:
type:
string
examples:
no visits:
value: You have not visited anyone yet
/hello/kitty:
get:
description: HTML with a hello kitty image
responses:
default:
description: Successful response
content:
application/json:
schema:
type:
string
examples:
hello kitty:
value: >-
<img
src="https://upload.wikimedia.org/wikipedia/en/0/05/Hello_kitty_character_portrait.png">
/hello/{name}:
get:
parameters:
- in: path
name: name
required: true
schema:
type: string
description: says hello to the name
description: says hello to someone
responses:
default:
description: Successful response
content:
application/json:
schema:
type:
string
examples:
hello-world:
value: Hello, world
24 changes: 22 additions & 2 deletions src/counterfact.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,31 @@
import fs from "node:fs/promises";
import nodePath from "node:path";

import yaml from "js-yaml";

import { Registry } from "./registry.js";
import { Dispatcher } from "./dispatcher.js";
import { koaMiddleware } from "./koa-middleware.js";
import { ModuleLoader } from "./module-loader.js";

export async function counterfact(basePath, context = {}) {
async function loadOpenApiDocument(source) {
try {
return yaml.load(await fs.readFile(source, "utf8"));
} catch {
return undefined;
}
}

export async function counterfact(
basePath,
context = {},
openApiPath = nodePath.join(basePath, "../openapi.yaml")
) {
const openApiDocument = await loadOpenApiDocument(openApiPath);

const registry = new Registry(context);
const dispatcher = new Dispatcher(registry);

const dispatcher = new Dispatcher(registry, openApiDocument);
const moduleLoader = new ModuleLoader(basePath, registry);

await moduleLoader.load();
Expand Down
42 changes: 27 additions & 15 deletions src/dispatcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,37 @@ import { Tools } from "./tools.js";
export class Dispatcher {
registry;

constructor(registry) {
constructor(registry, openApiDocument) {
this.registry = registry;
this.openApiDocument = openApiDocument;
}

request({ method, path, headers, body, query }) {
operationForPathAndMethod(path, method) {
return this.openApiDocument?.paths?.[path]?.[method.toLowerCase()];
}

async request({ method, path, headers, body, query }) {
const response = await this.registry.endpoint(
method,
path
)({
tools: new Tools({ headers }),

context: this.registry.context,
body,
query,
headers,

response: createResponseBuilder(
this.operationForPathAndMethod(
this.registry.handler(path).matchedPath,
method
)
),
});

const normalizedResponse = this.normalizeResponse(
this.registry.endpoint(
method,
path
)({
tools: new Tools({ headers }),

context: this.registry.context,
body,
query,
headers,
response: createResponseBuilder(),
}),
response,
headers?.accept ?? "*/*"
);

Expand Down Expand Up @@ -61,7 +74,6 @@ export class Dispatcher {
...response,
contentType: content.type,
body: content.body,
content: undefined,
};

delete normalizedResponse.content;
Expand Down
7 changes: 6 additions & 1 deletion src/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export class Registry {
});
}

return ({ ...context }) => lambda({ ...context, path: handler.path });
return ({ ...context }) =>
lambda({
...context,
path: handler.path,
matchedPath: handler.matchedPath ?? "none",
});
}
}
36 changes: 35 additions & 1 deletion src/response-builder.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export function createResponseBuilder() {
import jsf from "json-schema-faker";

jsf.option("useExamplesValue", true);

export function createResponseBuilder(operation) {
return new Proxy(
{},
{
Expand Down Expand Up @@ -41,6 +45,36 @@ export function createResponseBuilder() {
json(body) {
return this.match("application/json", body);
},

random() {
const response =
operation.responses[this.status] ?? operation.responses.default;

if (response === undefined) {
return {
status: 500,

content: [
{
type: "text/plain",
body: `The Open API document does not specify a response for status code ${this.status}`,
},
],
};
}

const { content } = response;

return {
...this,

content: Object.keys(content).map((type) => ({
type,

body: jsf.generate(content[type].schema),
})),
};
},
}),
}
);
Expand Down
46 changes: 46 additions & 0 deletions test/counterfact.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,50 @@ describe("integration test", () => {
await moduleLoader.stopWatching();
});
});

it("uses an OpenAPI document to generate a random response", async () => {
const app = new Koa();
const request = supertest(app.callback());
const files = {
"openapi.yaml": `
openapi: 3.0.0
info:
title: Counterfact
version: 1.0.0
paths:
/hello:
get:
responses:
200:
content:
text/plain:
schema:
type: string
examples:
- "hello"
`,

"paths/hello.mjs": `
export async function GET({response}) {
return response[200].random();
}
`,
};

await withTemporaryFiles(files, async (basePath) => {
const { koaMiddleware, moduleLoader } = await counterfact(
`${basePath}/paths`,
{ name: "World" },
`${basePath}/openapi.yaml`
);

app.use(koaMiddleware);

const getResponse = await request.get("/hello");

expect(getResponse.text).toBe("hello");

await moduleLoader.stopWatching();
});
});
});
49 changes: 47 additions & 2 deletions test/dispatcher.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,49 @@ describe("a dispatcher", () => {
expect(htmlResponse.body).toBe("<h1>hello</h1>");
});

it("gives the response builder the OpenAPI object it needs to generate a random response", async () => {
const registry = new Registry();

registry.add("/a", {
GET({ response }) {
return response[200].random();
},
});

const openApiDocument = {
paths: {
"/a": {
get: {
responses: {
200: {
content: {
"text/plain": {
schema: {
type: "string",
examples: ["hello"],
},
},
},
},
},
},
},
},
};

const dispatcher = new Dispatcher(registry, openApiDocument);
const htmlResponse = await dispatcher.request({
method: "GET",
path: "/a",

headers: {
accept: "text/plain",
},
});

expect(htmlResponse.body).toBe("hello");
});

it("passes status code in the response", async () => {
const registry = new Registry();

Expand Down Expand Up @@ -343,7 +386,7 @@ describe("a dispatcher", () => {
});

describe("given an invalid path", () => {
it("returns a 404 when the route is not found", () => {
it("returns a 404 when the route is not found", async () => {
const registry = new Registry();

registry.add("/your/{side}/{bodyPart}/in/and/your/left/foot/out", {
Expand All @@ -355,7 +398,9 @@ describe("given an invalid path", () => {
},
});

const response = new Dispatcher(registry).request({
const dispatcher = new Dispatcher(registry);

const response = await dispatcher.request({
method: "PUT",
path: "/your/left/foot/in/and/your/right/foot/out",
});
Expand Down
Loading

0 comments on commit ff307eb

Please sign in to comment.