httpapi-public-openapi.test.ts 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import { describe, expect, test } from "bun:test"
  2. import { OpenApi } from "effect/unstable/httpapi"
  3. import { PublicApi } from "../../src/server/routes/instance/httpapi/public"
  4. type Method = "get" | "post" | "put" | "delete" | "patch"
  5. type OpenApiSchema = { readonly $ref?: string }
  6. type OpenApiResponse = {
  7. readonly description?: string
  8. readonly content?: Record<string, { readonly schema?: OpenApiSchema }>
  9. }
  10. type OpenApiOperation = {
  11. readonly responses?: Record<string, OpenApiResponse>
  12. readonly security?: unknown
  13. }
  14. type OpenApiPathItem = Partial<Record<Method, OpenApiOperation>>
  15. type OpenApiSpec = { readonly paths: Record<string, OpenApiPathItem> }
  16. const methods = ["get", "post", "put", "delete", "patch"] as const
  17. const allowedV2BuiltInEndpointErrors: string[] = []
  18. function v2Operations(spec: OpenApiSpec) {
  19. return Object.entries(spec.paths).flatMap(([path, item]) =>
  20. path.startsWith("/api/")
  21. ? methods.flatMap((method) => {
  22. const operation = item[method]
  23. return operation ? [{ method, path, operation }] : []
  24. })
  25. : [],
  26. )
  27. }
  28. function responseRef(response: OpenApiResponse | undefined) {
  29. return response?.content?.["application/json"]?.schema?.$ref
  30. }
  31. function componentName(ref: string) {
  32. return ref.replace("#/components/schemas/", "")
  33. }
  34. function isBuiltInEndpointError(name: string) {
  35. return name.startsWith("EffectHttpApiError") || name.startsWith("effect_HttpApiError_")
  36. }
  37. describe("PublicApi OpenAPI v2 errors", () => {
  38. test("preserves /api auth responses", () => {
  39. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  40. for (const route of v2Operations(spec)) {
  41. expect(route.operation.responses?.["401"], `${route.method.toUpperCase()} ${route.path}`).toBeDefined()
  42. expect(route.operation.security, `${route.method.toUpperCase()} ${route.path}`).toEqual([])
  43. }
  44. })
  45. test("does not rewrite /api endpoint errors to legacy error components", () => {
  46. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  47. const refs = v2Operations(spec)
  48. .flatMap((route) =>
  49. Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
  50. const ref = responseRef(response)
  51. return ref ? [`${route.method.toUpperCase()} ${route.path} ${status} ${componentName(ref)}`] : []
  52. }),
  53. )
  54. .filter((entry) => entry.endsWith(" BadRequestError") || entry.endsWith(" NotFoundError"))
  55. expect(refs).toEqual([])
  56. })
  57. test("new /api endpoint errors cannot use built-in components without an explicit allowlist", () => {
  58. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  59. const builtInEndpointErrors = v2Operations(spec)
  60. .flatMap((route) =>
  61. Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
  62. if (status === "401") return []
  63. const ref = responseRef(response)
  64. if (!ref) return []
  65. const name = componentName(ref)
  66. return isBuiltInEndpointError(name) ? [`${route.method.toUpperCase()} ${route.path} ${status} ${name}`] : []
  67. }),
  68. )
  69. .sort()
  70. expect(builtInEndpointErrors).toEqual(allowedV2BuiltInEndpointErrors)
  71. })
  72. test("documents v2 provider and model catalog errors", () => {
  73. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  74. expect(componentName(responseRef(spec.paths["/api/provider"]?.get?.responses?.["503"]) ?? "")).toBe(
  75. "ServiceUnavailableError",
  76. )
  77. expect(componentName(responseRef(spec.paths["/api/model"]?.get?.responses?.["503"]) ?? "")).toBe(
  78. "ServiceUnavailableError",
  79. )
  80. expect(componentName(responseRef(spec.paths["/api/provider/{providerID}"]?.get?.responses?.["404"]) ?? "")).toBe(
  81. "ProviderNotFoundError",
  82. )
  83. expect(componentName(responseRef(spec.paths["/api/provider/{providerID}"]?.get?.responses?.["503"]) ?? "")).toBe(
  84. "ServiceUnavailableError",
  85. )
  86. })
  87. })