|
| 1 | +import type { INestApplication } from "@nestjs/common"; |
| 2 | +import request from "supertest"; |
| 3 | +import { afterAll, beforeAll, describe, expect, it } from "vitest"; |
| 4 | + |
| 5 | +import { isPathProtected } from "../../src/core/auth/jwt-middleware.js"; |
| 6 | +import { isTenantExempt } from "../../src/core/multi-tenancy/tenant-guard.js"; |
| 7 | +import { bootstrap } from "../../src/core/app/bootstrap.js"; |
| 8 | + |
| 9 | +const SILENT_LOGGER = { log() {}, warn() {}, error() {}, debug() {}, verbose() {} }; |
| 10 | + |
| 11 | +/** |
| 12 | + * Story · Legacy `/api-docs-json` alias. |
| 13 | + * |
| 14 | + * Older `nuxt-base-starter` workspaces hard-code |
| 15 | + * `http://127.0.0.1:3000/api-docs-json` in their `openapi-ts.config.ts` |
| 16 | + * fallback. Since this server canonicalised the OpenAPI doc at |
| 17 | + * `/api/openapi.json`, those workspaces 401 on `pnpm run generate-types` |
| 18 | + * until they upgrade. We mount `/api-docs-json` as a deprecated alias |
| 19 | + * that returns the exact same document plus `Deprecation` / |
| 20 | + * `Link: rel="successor-version"` headers, buying time until the |
| 21 | + * upstream fix (lenneTech/nuxt-base-starter#13) propagates. |
| 22 | + * |
| 23 | + * Tracked at https://github.com/lenneTech/nuxt-base-starter/issues/13. |
| 24 | + */ |
| 25 | +describe("Story · Legacy /api-docs-json alias", () => { |
| 26 | + describe("auth + tenant exemptions", () => { |
| 27 | + it("treats /api-docs-json as public (no JWT required)", () => { |
| 28 | + expect(isPathProtected("/api-docs-json")).toBe(false); |
| 29 | + }); |
| 30 | + |
| 31 | + it("treats /api-docs-json as tenant-exempt", () => { |
| 32 | + expect(isTenantExempt("/api-docs-json")).toBe(true); |
| 33 | + }); |
| 34 | + }); |
| 35 | + |
| 36 | + describe("HTTP behaviour", () => { |
| 37 | + let app: INestApplication; |
| 38 | + |
| 39 | + beforeAll(async () => { |
| 40 | + app = await bootstrap({ listen: false, logger: SILENT_LOGGER }); |
| 41 | + }); |
| 42 | + |
| 43 | + afterAll(async () => { |
| 44 | + await app?.close(); |
| 45 | + }); |
| 46 | + |
| 47 | + it("returns the same OpenAPI document as /api/openapi.json", async () => { |
| 48 | + const canonical = await request(app.getHttpServer()).get("/api/openapi.json"); |
| 49 | + const legacy = await request(app.getHttpServer()).get("/api-docs-json"); |
| 50 | + expect(canonical.status).toBe(200); |
| 51 | + expect(legacy.status).toBe(200); |
| 52 | + // Compare the structural body — both endpoints serve the same |
| 53 | + // SwaggerModule document instance, so deep equality must hold. |
| 54 | + expect(legacy.body).toEqual(canonical.body); |
| 55 | + expect(legacy.body?.openapi).toBeDefined(); |
| 56 | + expect(legacy.body?.paths).toBeDefined(); |
| 57 | + }); |
| 58 | + |
| 59 | + it("sends a Deprecation header pointing clients at the canonical URL", async () => { |
| 60 | + const res = await request(app.getHttpServer()).get("/api-docs-json"); |
| 61 | + expect(res.status).toBe(200); |
| 62 | + // RFC 8594 — Deprecation header signals to clients (and human |
| 63 | + // log-readers) that the endpoint is on its way out. |
| 64 | + expect(res.headers.deprecation).toBeDefined(); |
| 65 | + // RFC 8288 Link header with rel="successor-version" points at |
| 66 | + // /api/openapi.json. SDK generators that respect the hint can |
| 67 | + // self-heal without a config change. |
| 68 | + expect(res.headers.link).toContain("</api/openapi.json>"); |
| 69 | + expect(res.headers.link).toContain('rel="successor-version"'); |
| 70 | + }); |
| 71 | + }); |
| 72 | +}); |
0 commit comments