httpapi-public-openapi.test.ts 3.3 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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 = [
  18. "GET /api/session 400 effect_HttpApiError_BadRequest",
  19. "GET /api/session/{sessionID}/message 400 effect_HttpApiError_BadRequest",
  20. ]
  21. function v2Operations(spec: OpenApiSpec) {
  22. return Object.entries(spec.paths).flatMap(([path, item]) =>
  23. path.startsWith("/api/")
  24. ? methods.flatMap((method) => {
  25. const operation = item[method]
  26. return operation ? [{ method, path, operation }] : []
  27. })
  28. : [],
  29. )
  30. }
  31. function responseRef(response: OpenApiResponse | undefined) {
  32. return response?.content?.["application/json"]?.schema?.$ref
  33. }
  34. function componentName(ref: string) {
  35. return ref.replace("#/components/schemas/", "")
  36. }
  37. function isBuiltInEndpointError(name: string) {
  38. return name.startsWith("EffectHttpApiError") || name.startsWith("effect_HttpApiError_")
  39. }
  40. describe("PublicApi OpenAPI v2 errors", () => {
  41. test("preserves /api auth responses", () => {
  42. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  43. for (const route of v2Operations(spec)) {
  44. expect(route.operation.responses?.["401"], `${route.method.toUpperCase()} ${route.path}`).toBeDefined()
  45. expect(route.operation.security, `${route.method.toUpperCase()} ${route.path}`).toEqual([])
  46. }
  47. })
  48. test("does not rewrite /api endpoint errors to legacy error components", () => {
  49. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  50. const refs = v2Operations(spec)
  51. .flatMap((route) =>
  52. Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
  53. const ref = responseRef(response)
  54. return ref ? [`${route.method.toUpperCase()} ${route.path} ${status} ${componentName(ref)}`] : []
  55. }),
  56. )
  57. .filter((entry) => entry.includes("BadRequestError") || entry.includes("NotFoundError"))
  58. expect(refs).toEqual(["GET /api/provider/{providerID} 404 NotFoundError"])
  59. })
  60. test("new /api endpoint errors cannot use built-in components without an explicit allowlist", () => {
  61. const spec = OpenApi.fromApi(PublicApi) as OpenApiSpec
  62. const builtInEndpointErrors = v2Operations(spec)
  63. .flatMap((route) =>
  64. Object.entries(route.operation.responses ?? {}).flatMap(([status, response]) => {
  65. if (status === "401") return []
  66. const ref = responseRef(response)
  67. if (!ref) return []
  68. const name = componentName(ref)
  69. return isBuiltInEndpointError(name) ? [`${route.method.toUpperCase()} ${route.path} ${status} ${name}`] : []
  70. }),
  71. )
  72. .sort()
  73. expect(builtInEndpointErrors).toEqual(allowedV2BuiltInEndpointErrors)
  74. })
  75. })