@scalar/fastify-api-reference 1.49.0 → 1.49.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,441 @@
1
+ import FastifyBasicAuth, {} from '@fastify/basic-auth';
2
+ import fastifySwagger from '@fastify/swagger';
3
+ import Fastify, {} from 'fastify';
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import YAML from 'yaml';
6
+ import fastifyApiReference from './index.js';
7
+ const authOptions = {
8
+ validate(username, password, _req, _reply, done) {
9
+ if (username === 'admin' && password === 'admin') {
10
+ done();
11
+ }
12
+ else {
13
+ done(new Error('Invalid credentials'));
14
+ }
15
+ },
16
+ authenticate: true,
17
+ };
18
+ function basicAuthEncode(username, password) {
19
+ return 'Basic ' + Buffer.from(username + ':' + password).toString('base64');
20
+ }
21
+ function exampleDocument() {
22
+ return {
23
+ openapi: '3.1.0',
24
+ info: {
25
+ title: 'Example API',
26
+ version: '1.0.0',
27
+ },
28
+ paths: {},
29
+ /**
30
+ * Required, to match result of the exposed spec endpoint, provided by `@scalar/openapi-parser`
31
+ */
32
+ components: {
33
+ schemas: {},
34
+ },
35
+ };
36
+ }
37
+ describe('fastifyApiReference', () => {
38
+ it('returns 200 OK for the HTML and redirects to the route with a trailing slash', async () => {
39
+ const fastify = Fastify({
40
+ logger: false,
41
+ });
42
+ await fastify.register(fastifyApiReference, {
43
+ routePrefix: '/reference',
44
+ configuration: {
45
+ url: '/openapi.json',
46
+ },
47
+ });
48
+ const address = await fastify.listen({ port: 0 });
49
+ const response = await fetch(`${address}/reference`, { redirect: 'manual' });
50
+ expect(response.status).toBe(301);
51
+ expect(response.headers.get('location')).toBe('/reference/');
52
+ const finalResponse = await fetch(`${address}/reference/`);
53
+ expect(finalResponse.status).toBe(200);
54
+ });
55
+ it('returns 200 OK for the HTML and redirects to the route with a trailing slash (ignoreTrailingSlash: true)', async () => {
56
+ const fastify = Fastify({
57
+ logger: false,
58
+ ignoreTrailingSlash: true,
59
+ });
60
+ await fastify.register(fastifyApiReference, {
61
+ routePrefix: '/reference',
62
+ configuration: {
63
+ url: '/openapi.json',
64
+ },
65
+ });
66
+ const address = await fastify.listen({ port: 0 });
67
+ const response = await fetch(`${address}/reference`, { redirect: 'manual' });
68
+ expect(response.status).toBe(301);
69
+ expect(response.headers.get('location')).toBe('/reference/');
70
+ const finalResponse = await fetch(`${address}/reference/`);
71
+ expect(finalResponse.status).toBe(200);
72
+ });
73
+ it('accounts for plugin `prefix` during redirects to add trailing slash', async () => {
74
+ const innerPlugin = async (fastify) => {
75
+ await fastify.register(fastifyApiReference, {
76
+ routePrefix: '/reference',
77
+ configuration: {
78
+ url: '/openapi.json',
79
+ },
80
+ });
81
+ };
82
+ const fastify = Fastify({
83
+ logger: false,
84
+ });
85
+ const pluginPrefix = '/api';
86
+ await fastify.register(innerPlugin, { prefix: '/api' });
87
+ const address = await fastify.listen({ port: 0 });
88
+ const response = await fetch(`${address}${pluginPrefix}/reference`, { redirect: 'manual' });
89
+ expect(response.status).toBe(301);
90
+ expect(response.headers.get('location')).toBe(`${pluginPrefix}/reference/`);
91
+ const finalResponse = await fetch(`${address}${pluginPrefix}/reference/`);
92
+ expect(finalResponse.status).toBe(200);
93
+ });
94
+ it('works with ignoreTrailingSlash', async () => {
95
+ const fastify = Fastify({
96
+ logger: false,
97
+ ignoreTrailingSlash: true,
98
+ });
99
+ await fastify.register(fastifyApiReference, {
100
+ routePrefix: '/reference',
101
+ configuration: {
102
+ url: '/openapi.json',
103
+ },
104
+ });
105
+ const address = await fastify.listen({ port: 0 });
106
+ const response = await fetch(`${address}/reference`);
107
+ expect(response.status).toBe(200);
108
+ });
109
+ it('hasPlugin(fastifyApiReference) returns true', async () => {
110
+ const fastify = Fastify({
111
+ logger: false,
112
+ });
113
+ await fastify.register(fastifyApiReference, {
114
+ routePrefix: '/reference',
115
+ configuration: {
116
+ url: '/openapi.json',
117
+ },
118
+ });
119
+ expect(fastify.hasPlugin('@scalar/fastify-api-reference')).toBeTruthy();
120
+ });
121
+ it('no fastify-html exposed', async () => {
122
+ const fastify = Fastify({
123
+ logger: false,
124
+ });
125
+ await fastify.register(fastifyApiReference, {
126
+ routePrefix: '/reference',
127
+ configuration: {
128
+ url: '/openapi.json',
129
+ },
130
+ });
131
+ // @ts-expect-error
132
+ expect(fastify.html).toEqual(undefined);
133
+ });
134
+ it('the routePrefix is optional', async () => {
135
+ const fastify = Fastify({
136
+ logger: false,
137
+ });
138
+ await fastify.register(fastifyApiReference, {
139
+ configuration: {
140
+ url: '/openapi.json',
141
+ },
142
+ });
143
+ const address = await fastify.listen({ port: 0 });
144
+ const response = await fetch(`${address}/reference`);
145
+ expect(response.status).toBe(200);
146
+ });
147
+ describe('OpenAPI document endpoints', () => {
148
+ describe.each([
149
+ { specProvidedVia: '@fastify/swagger' },
150
+ { specProvidedVia: 'content: spec' },
151
+ { specProvidedVia: 'content: () => spec' },
152
+ ])('provided via $specProvidedVia', ({ specProvidedVia }) => {
153
+ describe.each([
154
+ {
155
+ endpointConfig: 'default',
156
+ },
157
+ {
158
+ endpointConfig: 'custom',
159
+ json: '/foo-json',
160
+ yaml: '/bar-yaml',
161
+ },
162
+ ])('on the $endpointConfig endpoint', ({ json, yaml }) => {
163
+ beforeEach(async (context) => {
164
+ const spec = exampleDocument();
165
+ const fastify = Fastify({
166
+ logger: false,
167
+ });
168
+ const openApiDocumentEndpoints = {
169
+ ...(json && { json }),
170
+ ...(yaml && { yaml }),
171
+ };
172
+ switch (specProvidedVia) {
173
+ case '@fastify/swagger': {
174
+ await fastify.register(fastifySwagger, { openapi: exampleDocument() });
175
+ await fastify.register(fastifyApiReference, {
176
+ openApiDocumentEndpoints,
177
+ configuration: {},
178
+ });
179
+ break;
180
+ }
181
+ case 'content: spec': {
182
+ await fastify.register(fastifyApiReference, {
183
+ openApiDocumentEndpoints,
184
+ configuration: {
185
+ content: spec,
186
+ },
187
+ });
188
+ break;
189
+ }
190
+ case 'content: () => spec': {
191
+ await fastify.register(fastifyApiReference, {
192
+ openApiDocumentEndpoints,
193
+ configuration: {
194
+ content: () => spec,
195
+ },
196
+ });
197
+ break;
198
+ }
199
+ }
200
+ const address = await fastify.listen({ port: 0 });
201
+ context.spec = spec;
202
+ context.address = address;
203
+ });
204
+ const endpoints = {
205
+ json: json ?? '/openapi.json',
206
+ yaml: yaml ?? '/openapi.yaml',
207
+ };
208
+ describe(`of "${endpoints.json}"`, () => {
209
+ it('should be equivalent to the original spec', async (ctx) => {
210
+ const { spec, address } = ctx;
211
+ const response = await fetch(`${address}/reference${endpoints.json}`);
212
+ expect(response.status).toBe(200);
213
+ expect(await response.json()).toEqual(spec);
214
+ });
215
+ it('should have proper CORS headers', async (ctx) => {
216
+ const { address } = ctx;
217
+ const response = await fetch(`${address}/reference${endpoints.json}`);
218
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
219
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
220
+ });
221
+ });
222
+ describe(`of "${endpoints.yaml}"`, () => {
223
+ it('should be equivalent to the original spec', async (ctx) => {
224
+ const { spec, address } = ctx;
225
+ const response = await fetch(`${address}/reference${endpoints.yaml}`);
226
+ expect(response.status).toBe(200);
227
+ expect(YAML.parse(await response.text())).toEqual(spec);
228
+ });
229
+ it('should have proper CORS headers', async (ctx) => {
230
+ const { address } = ctx;
231
+ const response = await fetch(`${address}/reference${endpoints.yaml}`);
232
+ expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*');
233
+ expect(response.headers.get('Access-Control-Allow-Methods')).toBe('*');
234
+ });
235
+ });
236
+ });
237
+ });
238
+ });
239
+ it('has the JS url', async () => {
240
+ const fastify = Fastify({
241
+ logger: false,
242
+ });
243
+ await fastify.register(fastifyApiReference, {
244
+ routePrefix: '/reference',
245
+ configuration: {
246
+ url: '/openapi.json',
247
+ },
248
+ });
249
+ const address = await fastify.listen({ port: 0 });
250
+ const response = await fetch(`${address}/reference`);
251
+ expect(await response.text()).toContain('js/scalar.js');
252
+ });
253
+ describe('has the spec URL', () => {
254
+ const urlOwn = '/openapi.json';
255
+ const urlExt = 'https://petstore.swagger.io/v2/swagger.json';
256
+ it.each([
257
+ { expectedUrl: urlOwn, specProvidedVia: '@fastify/swagger' },
258
+ { expectedUrl: urlOwn, specProvidedVia: 'content: spec' },
259
+ { expectedUrl: urlOwn, specProvidedVia: 'content: () => spec' },
260
+ { expectedUrl: urlOwn, specProvidedVia: 'url: urlOwn' },
261
+ { expectedUrl: urlExt, specProvidedVia: 'url: urlExt' },
262
+ ])('when spec is provided via $specProvidedVia', async ({ expectedUrl, specProvidedVia }) => {
263
+ const spec = exampleDocument();
264
+ const fastify = Fastify({
265
+ logger: false,
266
+ });
267
+ switch (specProvidedVia) {
268
+ case '@fastify/swagger': {
269
+ await fastify.register(fastifySwagger, { openapi: exampleDocument() });
270
+ await fastify.register(fastifyApiReference);
271
+ break;
272
+ }
273
+ case 'content: spec': {
274
+ await fastify.register(fastifyApiReference, {
275
+ configuration: { content: spec },
276
+ });
277
+ break;
278
+ }
279
+ case 'content: () => spec': {
280
+ await fastify.register(fastifyApiReference, {
281
+ configuration: { content: () => spec },
282
+ });
283
+ break;
284
+ }
285
+ case 'url: urlOwn': {
286
+ await fastify.register(fastifyApiReference, {
287
+ configuration: { url: urlOwn },
288
+ });
289
+ break;
290
+ }
291
+ case 'url: urlExt': {
292
+ await fastify.register(fastifyApiReference, {
293
+ configuration: { url: urlExt },
294
+ });
295
+ break;
296
+ }
297
+ }
298
+ const address = await fastify.listen({ port: 0 });
299
+ const response = await fetch(`${address}/reference`);
300
+ expect(await response.text()).toContain(expectedUrl);
301
+ });
302
+ });
303
+ it('has the default title', async () => {
304
+ const fastify = Fastify({
305
+ logger: false,
306
+ });
307
+ await fastify.register(fastifyApiReference, {
308
+ routePrefix: '/reference',
309
+ configuration: {
310
+ url: '/openapi.json',
311
+ },
312
+ });
313
+ const address = await fastify.listen({ port: 0 });
314
+ const response = await fetch(`${address}/reference`);
315
+ expect(await response.text()).toContain('<title>Scalar API Reference</title>');
316
+ });
317
+ it('has the correct content type', async () => {
318
+ const fastify = Fastify({
319
+ logger: false,
320
+ });
321
+ await fastify.register(fastifyApiReference, {
322
+ routePrefix: '/reference',
323
+ configuration: {
324
+ url: '/openapi.json',
325
+ },
326
+ });
327
+ const address = await fastify.listen({ port: 0 });
328
+ const response = await fetch(`${address}/reference`);
329
+ expect(response.headers.has('content-type')).toBe(true);
330
+ expect(response.headers.get('content-type')).toContain('text/html');
331
+ });
332
+ it('returns 401 Unauthorized for requests without authentication', async () => {
333
+ const fastify = Fastify({
334
+ logger: false,
335
+ });
336
+ await fastify.register(FastifyBasicAuth, authOptions);
337
+ await fastify.register(fastifyApiReference, {
338
+ configuration: {
339
+ url: '/openapi.json',
340
+ },
341
+ hooks: {
342
+ onRequest: fastify.basicAuth,
343
+ },
344
+ });
345
+ const address = await fastify.listen({ port: 0 });
346
+ let response = await fetch(`${address}/reference`);
347
+ expect(response.status).toBe(401);
348
+ response = await fetch(`${address}/reference/js/scalar.js`);
349
+ expect(response.status).toBe(401);
350
+ });
351
+ it('returns 200 OK for requests with authentication', async () => {
352
+ const fastify = Fastify({
353
+ logger: false,
354
+ });
355
+ await fastify.register(FastifyBasicAuth, authOptions);
356
+ await fastify.register(fastifyApiReference, {
357
+ configuration: {
358
+ url: '/openapi.json',
359
+ },
360
+ hooks: {
361
+ onRequest: fastify.basicAuth,
362
+ },
363
+ });
364
+ const address = await fastify.listen({ port: 0 });
365
+ let response = await fetch(`${address}/reference`, {
366
+ headers: {
367
+ authorization: basicAuthEncode('admin', 'admin'),
368
+ },
369
+ });
370
+ expect(response.status).toBe(200);
371
+ response = await fetch(`${address}/reference/js/scalar.js`, {
372
+ headers: {
373
+ authorization: basicAuthEncode('admin', 'admin'),
374
+ },
375
+ });
376
+ expect(response.status).toBe(200);
377
+ });
378
+ it('respects logLevel configuration for routes', async () => {
379
+ const loggedRequests = [];
380
+ const fastify = Fastify({
381
+ logger: {
382
+ level: 'info',
383
+ serializers: {
384
+ req(request) {
385
+ loggedRequests.push(`${request.method} ${request.url}`);
386
+ return {
387
+ method: request.method,
388
+ url: request.url,
389
+ };
390
+ },
391
+ },
392
+ },
393
+ });
394
+ await fastify.register(fastifyApiReference, {
395
+ configuration: {
396
+ url: '/openapi.json',
397
+ },
398
+ logLevel: 'silent',
399
+ });
400
+ const address = await fastify.listen({ port: 0 });
401
+ // Make requests to different routes
402
+ await fetch(`${address}/reference/`);
403
+ await fetch(`${address}/reference/openapi.json`);
404
+ await fetch(`${address}/reference/openapi.yaml`);
405
+ await fetch(`${address}/reference/js/scalar.js`);
406
+ expect(loggedRequests).toStrictEqual([]);
407
+ });
408
+ it('does not fail when registered without specSource configuration', async () => {
409
+ const fastify = Fastify({
410
+ logger: false,
411
+ });
412
+ const warnSpy = vi.spyOn(fastify.log, 'warn');
413
+ await fastify.register(fastifyApiReference, {
414
+ routePrefix: '/reference',
415
+ configuration: {},
416
+ });
417
+ expect(warnSpy).toHaveBeenCalledExactlyOnceWith(expect.stringContaining("[@scalar/fastify-api-reference] You didn't provide a `content`, `url`, `sources` or @fastify/swagger could not be found."));
418
+ expect(fastify.hasPlugin('@scalar/fastify-api-reference')).toBeTruthy();
419
+ });
420
+ it('serves Scalar UI when only sources option is provided', async () => {
421
+ const fastify = Fastify({
422
+ logger: false,
423
+ });
424
+ await fastify.register(fastifyApiReference, {
425
+ routePrefix: '/reference',
426
+ configuration: {
427
+ sources: [{ url: '/openapi.json' }],
428
+ },
429
+ });
430
+ const address = await fastify.listen({ port: 0 });
431
+ const response1 = await fetch(`${address}/reference/`);
432
+ const response2 = await fetch(`${address}/reference/js/scalar.js`);
433
+ const response3 = await fetch(`${address}/reference/openapi.json`);
434
+ const response4 = await fetch(`${address}/reference/openapi.yaml`);
435
+ expect(response1.status).toBe(200);
436
+ expect(response2.status).toBe(200);
437
+ expect(response3.status).toBe(404);
438
+ expect(response4.status).toBe(404);
439
+ expect(await response1.text()).toContain('/openapi.json');
440
+ });
441
+ });
package/dist/index.js CHANGED
@@ -1,6 +1,2 @@
1
- import { default as default2 } from "./fastifyApiReference.js";
2
- export * from "./types.js";
3
- export {
4
- default2 as default
5
- };
6
- //# sourceMappingURL=index.js.map
1
+ export { default } from './fastifyApiReference.js';
2
+ export * from './types.js';