Skip to content

Commit 899811a

Browse files
committed
feat: demo using quilt
1 parent 35b133c commit 899811a

File tree

5 files changed

+337
-6
lines changed

5 files changed

+337
-6
lines changed

package-lock.json

Lines changed: 103 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
"version": "0.2.0",
44
"private": true,
55
"dependencies": {
6-
"axios": "^0.27.2"
6+
"axios": "^0.27.2",
7+
"get-port": "^7.1.0",
8+
"wait-port": "^1.1.0"
79
},
810
"scripts": {
911
"test": "cross-env CI=true npx jest --colors --testTimeout 30000 --testMatch \"**/*.pact.spec.ts\"",
10-
"test:pact": "cross-env CI=true npx jest --colors --testTimeout 30000 --testMatch \"**/*.pact.spec.ts\""
12+
"test:pact": "cross-env CI=true npx jest --colors --testTimeout 30000 --testMatch \"**/*.pact.spec.ts\"",
13+
"test:quilt": "cross-env CI=true npx jest --colors --testTimeout 30000 --testMatch \"**/*.quilt.spec.ts\" --transformIgnorePatterns 'node_modules/(?!get-port)/'"
1114
},
1215
"devDependencies": {
1316
"@babel/core": "^7.25.2",

src/api.pact.spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { SpecificationVersion, PactV4, MatchersV3 } from "@pact-foundation/pact";
2+
import { API } from './api';
3+
4+
const { like } = MatchersV3;
5+
6+
describe("Product API", () => {
7+
const pact = new PactV4({
8+
consumer: "ProductConsumer",
9+
provider: "ProductProvider",
10+
spec: SpecificationVersion.SPECIFICATION_VERSION_V4,
11+
logLevel: "error",
12+
});
13+
14+
describe("GET /product/:id", () => {
15+
test("given a valid product, returns 200", async () => {
16+
await pact
17+
.addInteraction()
18+
.given("a product with id 1234 exists")
19+
.uponReceiving("a request for a valid product")
20+
.withRequest("GET", "/product/1234", (builder) => {
21+
builder.headers({ Accept: "application/json; charset=utf-8" });
22+
})
23+
.willRespondWith(200, (builder) => {
24+
builder.jsonBody(
25+
like({
26+
id: "1234",
27+
name: "Product Name",
28+
type: "food",
29+
})
30+
);
31+
})
32+
.executeTest(async (mockserver) => {
33+
const api = new API(mockserver.url);
34+
const product = await api.getProduct("1234");
35+
expect(product).toEqual({
36+
id: "1234",
37+
name: "Product Name",
38+
type: "food",
39+
});
40+
});
41+
});
42+
43+
test("given a non-existent product, returns 404", async () => {
44+
await pact
45+
.addInteraction()
46+
.given("no product with id 9999 exists")
47+
.uponReceiving("a request for a non-existent product")
48+
.withRequest("GET", "/product/9999", (builder) => {
49+
builder.headers({ Accept: "application/json; charset=utf-8" });
50+
})
51+
.willRespondWith(404)
52+
.executeTest(async (mockserver) => {
53+
const api = new API(mockserver.url);
54+
await expect(api.getProduct("9999")).rejects.toThrow();
55+
});
56+
});
57+
58+
test("given an invalid product id, returns 400", async () => {
59+
await pact
60+
.addInteraction()
61+
.uponReceiving("a request with an invalid product id")
62+
.withRequest("GET", "/product/invalid-id", (builder) => {
63+
builder.headers({ Accept: "application/json; charset=utf-8" });
64+
})
65+
.willRespondWith(400)
66+
.executeTest(async (mockserver) => {
67+
const api = new API(mockserver.url);
68+
await expect(api.getProduct("invalid-id")).rejects.toThrow();
69+
});
70+
});
71+
72+
test("given no authorization, returns 401", async () => {
73+
await pact
74+
.addInteraction()
75+
.uponReceiving("a request without authorization")
76+
.withRequest("GET", "/product/1234", (builder) => {
77+
builder.headers({ Accept: "application/json; charset=utf-8" });
78+
})
79+
.willRespondWith(401)
80+
.executeTest(async (mockserver) => {
81+
const api = new API(mockserver.url);
82+
await expect(api.getProduct("1234")).rejects.toThrow();
83+
});
84+
});
85+
});
86+
87+
describe("GET /products", () => {
88+
test("returns all products with 200", async () => {
89+
await pact
90+
.addInteraction()
91+
.given("products exist")
92+
.uponReceiving("a request for all products")
93+
.withRequest("GET", "/products", (builder) => {
94+
builder.headers({ Accept: "application/json; charset=utf-8" });
95+
})
96+
.willRespondWith(200, (builder) => {
97+
builder.jsonBody(
98+
like([
99+
{
100+
id: "1234",
101+
name: "Product Name",
102+
type: "food",
103+
},
104+
])
105+
);
106+
})
107+
.executeTest(async (mockserver) => {
108+
const api = new API(mockserver.url);
109+
const products = await api.getAllProducts();
110+
expect(products).toEqual([
111+
{
112+
id: "1234",
113+
name: "Product Name",
114+
type: "food",
115+
},
116+
]);
117+
});
118+
});
119+
});
120+
});

src/api.quilt.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { API } from "./api";
2+
import { startMockServer, stopMockServer } from "./mockserver";
3+
4+
let mockserver: string;
5+
6+
describe("Product API", () => {
7+
beforeAll(async () => mockserver = await startMockServer());
8+
afterAll(() => stopMockServer())
9+
10+
describe("GET /product/:id", () => {
11+
test("given a valid product, returns 200", async () => {
12+
const api = new API(mockserver);
13+
const product = await api.getProduct("10");
14+
expect(product).toEqual({
15+
id: 10,
16+
name: "cola",
17+
type: "beverage",
18+
});
19+
});
20+
21+
test("given a non-existent product, returns 404", async () => {
22+
const api = new API(mockserver);
23+
await expect(api.getProduct("9999")).rejects.toThrow();
24+
});
25+
26+
test("given an invalid product id, returns 400", async () => {
27+
const api = new API(mockserver);
28+
await expect(api.getProduct("invalid-id")).rejects.toThrow();
29+
});
30+
31+
test("given no authorization, returns 401", async () => {
32+
const api = new API(mockserver);
33+
api.generateAuthToken = jest.fn(() => undefined as any as string);
34+
await expect(api.getProduct("10")).rejects.toThrow();
35+
});
36+
});
37+
38+
describe.skip("GET /products", () => {
39+
test("returns all products with 200", async () => {
40+
const api = new API(mockserver);
41+
try {
42+
const products = await api.getAllProducts();
43+
console.log(products);
44+
expect(products).toContain([
45+
{
46+
id: 10,
47+
name: "cola",
48+
type: "beverage",
49+
},
50+
]);
51+
} catch (error) {
52+
console.error("Error fetching products:", error);
53+
throw error;
54+
}
55+
});
56+
});
57+
});

src/mockserver.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { spawn, ChildProcess } from "child_process";
2+
import getPort from "get-port";
3+
import waitPort from "wait-port";
4+
5+
let mockserverProcess: ChildProcess;
6+
export const startMockServer = async (): Promise<string> => {
7+
console.log("starting mock server...");
8+
const port = await getPort();
9+
10+
return new Promise((resolve, reject) => {
11+
mockserverProcess = spawn(
12+
"bin/quilt",
13+
[
14+
"mock",
15+
"--test-file",
16+
"/Users/matthew.fellows/development/public/api-testing-tool/quilt-cli/example/product.testcases.yaml",
17+
"--port",
18+
port.toString(),
19+
"--log-level",
20+
"error",
21+
],
22+
{
23+
stdio: "inherit",
24+
shell: true,
25+
detached: true,
26+
}
27+
);
28+
mockserverProcess.on("error", (error: any) => {
29+
console.error(`error starting mock server: ${error.message}`);
30+
reject(error);
31+
});
32+
33+
waitPort({ port: port, host: "localhost", output: "silent" })
34+
.then(() => {
35+
console.log(`mock server is ready on port ${port}`);
36+
resolve(`http://localhost:${port}`);
37+
})
38+
.catch((error) => {
39+
console.error(`Error waiting for port ${port}: ${error.message}`);
40+
reject(error);
41+
});
42+
});
43+
};
44+
45+
export const stopMockServer = (): void => {
46+
if (mockserverProcess) {
47+
console.log("stopping mock server...");
48+
mockserverProcess.kill();
49+
} else {
50+
console.warn("No mock server process to stop.");
51+
}
52+
};

0 commit comments

Comments
 (0)