From 43b53bb6103198b771f8628ff3425b23f74eb6f4 Mon Sep 17 00:00:00 2001 From: Arthur Bassi Date: Wed, 17 Jun 2026 09:35:56 -0300 Subject: [PATCH] test(api): cover API URL contract and prefix resolution --- config/api-url-contract.test.ts | 41 +++++++++++++++++++ src/test/lib/resolve-api-prefix-url.test.ts | 44 +++++++++++++++++++++ vitest.config.ts | 6 ++- 3 files changed, 90 insertions(+), 1 deletion(-) create mode 100644 config/api-url-contract.test.ts create mode 100644 src/test/lib/resolve-api-prefix-url.test.ts diff --git a/config/api-url-contract.test.ts b/config/api-url-contract.test.ts new file mode 100644 index 00000000..a087ff50 --- /dev/null +++ b/config/api-url-contract.test.ts @@ -0,0 +1,41 @@ +import { readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { assertAbsoluteApiUrlHasApiSegment } from "./api-url-contract"; + +describe("assertAbsoluteApiUrlHasApiSegment", () => { + it("allows relative /api", () => { + expect(() => assertAbsoluteApiUrlHasApiSegment("/api")).not.toThrow(); + }); + + it("allows empty or undefined", () => { + expect(() => assertAbsoluteApiUrlHasApiSegment(undefined)).not.toThrow(); + expect(() => assertAbsoluteApiUrlHasApiSegment("")).not.toThrow(); + }); + + it("allows absolute URL ending with /api", () => { + expect(() => + assertAbsoluteApiUrlHasApiSegment("https://api.seahavenindustries.com/api"), + ).not.toThrow(); + }); + + it("rejects absolute URL without /api", () => { + expect(() => assertAbsoluteApiUrlHasApiSegment("https://api.seahavenindustries.com")).toThrow( + /must end with \/api/, + ); + }); +}); + +describe(".env.example contract", () => { + it("documents an absolute VITE_API_URL that includes /api", () => { + const envExample = readFileSync(path.resolve(process.cwd(), ".env.example"), "utf8"); + const match = envExample.match(/^VITE_API_URL=(.+)$/m); + + expect(match).not.toBeNull(); + const value = match![1]!.trim(); + + if (value.startsWith("http")) { + expect(() => assertAbsoluteApiUrlHasApiSegment(value)).not.toThrow(); + } + }); +}); diff --git a/src/test/lib/resolve-api-prefix-url.test.ts b/src/test/lib/resolve-api-prefix-url.test.ts new file mode 100644 index 00000000..0e66563b --- /dev/null +++ b/src/test/lib/resolve-api-prefix-url.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, it } from "vitest"; +import { resolveApiBaseUrl, resolveApiPrefixUrl } from "@/lib/resolve-api-prefix-url"; + +const ORIGIN = "http://localhost:3000"; + +describe("resolveApiPrefixUrl", () => { + it("joins relative /api with origin", () => { + expect(resolveApiPrefixUrl("/api", ORIGIN)).toBe(`${ORIGIN}/api/`); + }); + + it("normalizes relative api without leading slash", () => { + expect(resolveApiPrefixUrl("api", ORIGIN)).toBe(`${ORIGIN}/api/`); + }); + + it("preserves absolute URL with /api and adds trailing slash", () => { + expect(resolveApiPrefixUrl("https://api.seahavenindustries.com/api", ORIGIN)).toBe( + "https://api.seahavenindustries.com/api/", + ); + }); + + it("preserves absolute URL with /api/ trailing slash", () => { + expect(resolveApiPrefixUrl("https://api.seahavenindustries.com/api/", ORIGIN)).toBe( + "https://api.seahavenindustries.com/api/", + ); + }); + + it("uses absolute host without /api as-is (invalid for production)", () => { + expect(resolveApiPrefixUrl("https://api.seahavenindustries.com", ORIGIN)).toBe( + "https://api.seahavenindustries.com/", + ); + }); + + it("falls back to /api when empty", () => { + expect(resolveApiPrefixUrl("", ORIGIN)).toBe(`${ORIGIN}/api/`); + expect(resolveApiPrefixUrl(" ", ORIGIN)).toBe(`${ORIGIN}/api/`); + }); +}); + +describe("resolveApiBaseUrl", () => { + it("strips trailing slash from prefix", () => { + expect(resolveApiBaseUrl("/api", ORIGIN)).toBe(`${ORIGIN}/api`); + expect(resolveApiBaseUrl("https://host/api/", ORIGIN)).toBe("https://host/api"); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index 87494839..30927fa4 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -13,7 +13,11 @@ export default defineConfig({ environment: "jsdom", setupFiles: ["./src/test/setup.ts"], globals: false, - include: ["src/test/**/*.test.{ts,tsx}", "src/test/**/*.integration.test.{ts,tsx}"], + include: [ + "src/test/**/*.test.{ts,tsx}", + "src/test/**/*.integration.test.{ts,tsx}", + "config/**/*.test.ts", + ], exclude: ["e2e/**", "node_modules/**"], }, });