Skip to content

Commit ec0612b

Browse files
authored
Merge pull request #97 from jaredwray/feat-adding-in-image-routes
feat: adding in image routes
2 parents 757ab11 + 695863e commit ec0612b

8 files changed

Lines changed: 754 additions & 2 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,4 +13,5 @@ package-lock.json
1313
DOCKER.md
1414

1515
# AI
16-
.claude
16+
.claude
17+
.DS_Store

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,16 @@ new MockHttp(options?)
265265
- `helmet?`: boolean - Use Helmet for security headers (default: true)
266266
- `apiDocs?`: boolean - Enable Swagger API documentation (default: true)
267267
- `httpBin?`: HttpBinOptions - Configure which httpbin routes to enable
268+
- `httpMethods?`: boolean - Enable HTTP method routes (default: true)
269+
- `redirects?`: boolean - Enable redirect routes (default: true)
270+
- `requestInspection?`: boolean - Enable request inspection routes (default: true)
271+
- `responseInspection?`: boolean - Enable response inspection routes (default: true)
272+
- `statusCodes?`: boolean - Enable status code routes (default: true)
273+
- `responseFormats?`: boolean - Enable response format routes (default: true)
274+
- `cookies?`: boolean - Enable cookie routes (default: true)
275+
- `anything?`: boolean - Enable anything routes (default: true)
276+
- `auth?`: boolean - Enable authentication routes (default: true)
277+
- `images?`: boolean - Enable image routes (default: true)
268278
- `hookOptions?`: HookifiedOptions - Hookified options
269279
270280
### Properties
@@ -334,6 +344,10 @@ Register "anything" catch-all routes.
334344
335345
Register authentication routes (basic, bearer, digest, hidden-basic).
336346
347+
#### `async registerImageRoutes(fastifyInstance?)`
348+
349+
Register image routes (jpeg, png, svg, webp) with content negotiation support.
350+
337351
## Taps (Response Injection)
338352
339353
Access the TapManager via `mockHttp.taps` to inject custom responses.

public/logo.jpg

14.5 KB
Loading

public/logo.png

2.15 KB
Loading

public/logo.webp

3.94 KB
Loading

src/mock-http.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import {
2626
postRoute,
2727
putRoute,
2828
} from "./routes/http-methods/index.js";
29+
import { imageRoutes } from "./routes/images/index.js";
2930
import { indexRoute } from "./routes/index.js";
3031
import {
3132
absoluteRedirectRoute,
@@ -58,6 +59,7 @@ export type HttpBinOptions = {
5859
cookies?: boolean;
5960
anything?: boolean;
6061
auth?: boolean;
62+
images?: boolean;
6163
};
6264

6365
export type MockHttpOptions = {
@@ -108,6 +110,7 @@ export class MockHttp extends Hookified {
108110
cookies: true,
109111
anything: true,
110112
auth: true,
113+
images: true,
111114
};
112115

113116
private _server: FastifyInstance = Fastify();
@@ -317,7 +320,21 @@ export class MockHttp extends Hookified {
317320

318321
// Register the Helmet plugin for security headers
319322
if (this._helmet) {
320-
await this._server.register(fastifyHelmet);
323+
await this._server.register(fastifyHelmet, {
324+
contentSecurityPolicy: {
325+
directives: {
326+
defaultSrc: ["'self'"],
327+
scriptSrc: ["'self'", "'unsafe-inline'", "'wasm-unsafe-eval'"],
328+
styleSrc: ["'self'", "'unsafe-inline'"],
329+
imgSrc: ["'self'", "data:", "https:"],
330+
fontSrc: ["'self'", "data:"],
331+
connectSrc: ["'self'"],
332+
frameSrc: ["'self'"],
333+
objectSrc: ["'none'"],
334+
upgradeInsecureRequests: [],
335+
},
336+
},
337+
});
321338
}
322339

323340
if (this._apiDocs) {
@@ -334,6 +351,7 @@ export class MockHttp extends Hookified {
334351
cookies,
335352
anything,
336353
auth,
354+
images,
337355
} = this._httpBin;
338356

339357
if (httpMethods) {
@@ -372,6 +390,10 @@ export class MockHttp extends Hookified {
372390
await this.registerAuthRoutes();
373391
}
374392

393+
if (images) {
394+
await this.registerImageRoutes();
395+
}
396+
375397
if (this._autoDetectPort) {
376398
const originalPort = this._port;
377399
this._port = await this.detectPort();
@@ -535,6 +557,17 @@ export class MockHttp extends Hookified {
535557
await fastify.register(bearerAuthRoute);
536558
await fastify.register(digestAuthRoute);
537559
}
560+
561+
/**
562+
* Register the image routes.
563+
* @param fastifyInstance - the server instance to register the routes on.
564+
*/
565+
public async registerImageRoutes(
566+
fastifyInstance?: FastifyInstance,
567+
): Promise<void> {
568+
const fastify = fastifyInstance ?? this._server;
569+
await fastify.register(imageRoutes);
570+
}
538571
}
539572

540573
export const mockhttp = MockHttp;

src/routes/images/index.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { readFileSync } from "node:fs";
2+
import { join } from "node:path";
3+
import type {
4+
FastifyInstance,
5+
FastifyReply,
6+
FastifyRequest,
7+
FastifySchema,
8+
} from "fastify";
9+
10+
const imageSchema: FastifySchema = {
11+
description:
12+
"Returns an image based on Accept header (supports jpeg, png, svg, webp)",
13+
tags: ["Images"],
14+
headers: {
15+
type: "object",
16+
properties: {
17+
accept: {
18+
type: "string",
19+
description: "Accept header for content negotiation",
20+
},
21+
},
22+
},
23+
response: {
24+
200: {
25+
description: "Image file",
26+
type: "string",
27+
format: "binary",
28+
},
29+
},
30+
};
31+
32+
const jpegSchema: FastifySchema = {
33+
description: "Returns a JPEG image",
34+
tags: ["Images"],
35+
response: {
36+
200: {
37+
description: "JPEG image",
38+
type: "string",
39+
format: "binary",
40+
},
41+
},
42+
};
43+
44+
const pngSchema: FastifySchema = {
45+
description: "Returns a PNG image",
46+
tags: ["Images"],
47+
response: {
48+
200: {
49+
description: "PNG image",
50+
type: "string",
51+
format: "binary",
52+
},
53+
},
54+
};
55+
56+
const svgSchema: FastifySchema = {
57+
description: "Returns an SVG image",
58+
tags: ["Images"],
59+
response: {
60+
200: {
61+
description: "SVG image",
62+
type: "string",
63+
format: "binary",
64+
},
65+
},
66+
};
67+
68+
const webpSchema: FastifySchema = {
69+
description: "Returns a WEBP image",
70+
tags: ["Images"],
71+
response: {
72+
200: {
73+
description: "WEBP image",
74+
type: "string",
75+
format: "binary",
76+
},
77+
},
78+
};
79+
80+
export const imageRoutes = (fastify: FastifyInstance) => {
81+
const publicPath = join(process.cwd(), "public");
82+
83+
// Content negotiation route
84+
fastify.get(
85+
"/image",
86+
{ schema: imageSchema },
87+
async (request: FastifyRequest, reply: FastifyReply) => {
88+
const accept = request.headers.accept || "";
89+
90+
let file = "logo.png";
91+
let contentType = "image/png";
92+
93+
if (accept.includes("image/jpeg") || accept.includes("image/jpg")) {
94+
file = "logo.jpg";
95+
contentType = "image/jpeg";
96+
} else if (
97+
accept.includes("image/svg+xml") ||
98+
accept.includes("image/svg")
99+
) {
100+
file = "logo.svg";
101+
contentType = "image/svg+xml";
102+
} else if (accept.includes("image/webp")) {
103+
file = "logo.webp";
104+
contentType = "image/webp";
105+
} else if (accept.includes("image/png")) {
106+
file = "logo.png";
107+
contentType = "image/png";
108+
}
109+
110+
try {
111+
const imagePath = join(publicPath, file);
112+
const imageBuffer = readFileSync(imagePath);
113+
114+
reply.type(contentType);
115+
return imageBuffer;
116+
} catch (error) {
117+
/* v8 ignore next -- @preserve */
118+
reply.code(500);
119+
/* v8 ignore next -- @preserve */
120+
return { error: `${error}` };
121+
}
122+
},
123+
);
124+
125+
// JPEG specific route
126+
fastify.get(
127+
"/image/jpeg",
128+
{ schema: jpegSchema },
129+
async (_request: FastifyRequest, reply: FastifyReply) => {
130+
try {
131+
const imagePath = join(publicPath, "logo.jpg");
132+
const imageBuffer = readFileSync(imagePath);
133+
reply.type("image/jpeg");
134+
return imageBuffer;
135+
} catch (error) {
136+
/* v8 ignore next -- @preserve */
137+
reply.code(500);
138+
/* v8 ignore next -- @preserve */
139+
return { error: `${error}` };
140+
}
141+
},
142+
);
143+
144+
// PNG specific route
145+
fastify.get(
146+
"/image/png",
147+
{ schema: pngSchema },
148+
async (_request: FastifyRequest, reply: FastifyReply) => {
149+
try {
150+
const imagePath = join(publicPath, "logo.png");
151+
const imageBuffer = readFileSync(imagePath);
152+
reply.type("image/png");
153+
return imageBuffer;
154+
} catch (error) {
155+
/* v8 ignore next -- @preserve */
156+
reply.code(500);
157+
/* v8 ignore next -- @preserve */
158+
return { error: `${error}` };
159+
}
160+
},
161+
);
162+
163+
// SVG specific route
164+
fastify.get(
165+
"/image/svg",
166+
{ schema: svgSchema },
167+
async (_request: FastifyRequest, reply: FastifyReply) => {
168+
try {
169+
const imagePath = join(publicPath, "logo.svg");
170+
const imageBuffer = readFileSync(imagePath);
171+
reply.type("image/svg+xml");
172+
return imageBuffer;
173+
} catch (error) {
174+
/* v8 ignore next -- @preserve */
175+
reply.code(500);
176+
/* v8 ignore next -- @preserve */
177+
return { error: `${error}` };
178+
}
179+
},
180+
);
181+
182+
// WEBP specific route
183+
fastify.get(
184+
"/image/webp",
185+
{ schema: webpSchema },
186+
async (_request: FastifyRequest, reply: FastifyReply) => {
187+
try {
188+
const imagePath = join(publicPath, "logo.webp");
189+
const imageBuffer = readFileSync(imagePath);
190+
reply.type("image/webp");
191+
return imageBuffer;
192+
} catch (error) {
193+
/* v8 ignore next -- @preserve */
194+
reply.code(500);
195+
/* v8 ignore next -- @preserve */
196+
return { error: `${error}` };
197+
}
198+
},
199+
);
200+
};

0 commit comments

Comments
 (0)