| 1 | /** |
| 2 | * Tests `HttpApi` implementations through an in-memory generated client. |
| 3 | * |
| 4 | * The helpers use the same request encoding, routing, response encoding, and |
| 5 | * client decoding as the normal `HttpApiBuilder` and `HttpApiClient` pipeline, |
| 6 | * but they do not start an HTTP server. This is useful for focused handler |
| 7 | * tests, schema round trips, middleware behavior, and typed client calls. |
| 8 | * |
| 9 | * @stability unstable |
| 10 | * @since 4.0.0 |
| 11 | */ |
| 12 | import * as Context from "../Context.ts" |
| 13 | import * as Effect from "../Effect.ts" |
| 14 | import type { FileSystem } from "../FileSystem.ts" |
| 15 | import type { Generator } from "../http/Etag.ts" |
| 16 | import * as HttpClient from "../http/HttpClient.ts" |
| 17 | import type { HttpPlatform } from "../http/HttpPlatform.ts" |
| 18 | import * as HttpRouter from "../http/HttpRouter.ts" |
| 19 | import * as HttpServerRequest from "../http/HttpServerRequest.ts" |
| 20 | import * as HttpServerResponse from "../http/HttpServerResponse.ts" |
| 21 | import * as preResponseHandler from "../http/internal/preResponseHandler.ts" |
| 22 | import * as Layer from "../Layer.ts" |
| 23 | import type { Path } from "../Path.ts" |
| 24 | import type { Scope } from "../Scope.ts" |
| 25 | import type * as HttpApi from "./HttpApi.ts" |
| 26 | import type { HandlerRuntime } from "./HttpApiBuilder.ts" |
| 27 | import * as HttpApiBuilder from "./HttpApiBuilder.ts" |
| 28 | import * as HttpApiClient from "./HttpApiClient.ts" |
| 29 | import type * as HttpApiEndpoint from "./HttpApiEndpoint.ts" |
| 30 | import type * as HttpApiGroup from "./HttpApiGroup.ts" |
| 31 | |
| 32 | /** |
| 33 | * Creates an in-memory client for testing selected groups of an `HttpApi`. |
| 34 | * |
| 35 | * **Details** |
| 36 | * |
| 37 | * Handlers for the selected groups are taken from the environment; unselected |
| 38 | * groups are wired with placeholder handlers that fail if called. |
| 39 | * |
| 40 | * @stability unstable |
| 41 | * @category testing |
| 42 | * @since 4.0.0 |
| 43 | */ |
| 44 | export const groups = Effect.fnUntraced(function*< |
| 45 | ApiId extends string, |
| 46 | Groups extends HttpApiGroup.Constraint, |
| 47 | const Identifiers extends ReadonlyArray<HttpApiGroup.Identifier<Groups>>, |
| 48 | SelectedGroups extends HttpApiGroup.Constraint = HttpApiGroup.WithIdentifier<Groups, Identifiers[number]> |
| 49 | >( |
| 50 | api: HttpApi.HttpApi<ApiId, Groups>, |
| 51 | groupIdentifiers: Identifiers, |
| 52 | options?: { |
| 53 | readonly baseUrl?: string | URL | undefined |
| 54 | } |
| 55 | ): Effect.fn.Return< |
| 56 | HttpApiClient.Client<Groups>, |
| 57 | never, |
| 58 | | HttpApiGroup.ToService<ApiId, SelectedGroups> |
| 59 | | HttpApiGroup.MiddlewareClient<Groups> |
| 60 | | HttpApiEndpoint.Middleware<HttpApiGroup.Endpoints<Groups>> |
| 61 | | FileSystem |
| 62 | | Generator |
| 63 | | HttpPlatform |
| 64 | | Path |
| 65 | | Scope |
| 66 | > { |
| 67 | let context = yield* Effect.context<HttpApiGroup.ToService<ApiId, SelectedGroups>>() |
| 68 | |
| 69 | const groups = api.groups as unknown as Record<string, HttpApiGroup.Top> |
| 70 | for (const identifier in groups) { |
| 71 | const group = groups[identifier] |
| 72 | if (groupIdentifiers.includes(identifier as any)) { |
| 73 | continue |
| 74 | } |
| 75 | const handlers = new Map<string, HandlerRuntime>() |
| 76 | const routes: Array<HttpRouter.Route<any, any>> = [] |
| 77 | for (const endpointIdentifier in group.endpoints) { |
| 78 | const endpoint = group.endpoints[endpointIdentifier] |
| 79 | const handler: HandlerRuntime = { |
| 80 | endpoint: endpoint as any, |
| 81 | handler: () => Effect.die(new Error(`Unhandled endpoint: ${endpointIdentifier}`)), |
| 82 | isRaw: false, |
| 83 | uninterruptible: false |
| 84 | } |
| 85 | handlers.set(endpointIdentifier, handler) |
| 86 | routes.push(HttpApiBuilder.handlerToRoute(api, group as any, handler, context)) |
| 87 | } |
| 88 | context = Context.add(context, group as any, { handlers, routes }) |
| 89 | } |
| 90 | |
| 91 | const layer: Layer.Layer< |
| 92 | never, |
| 93 | never, |
| 94 | | FileSystem |
| 95 | | Generator |
| 96 | | HttpPlatform |
| 97 | | HttpRouter.HttpRouter |
| 98 | | Path |
| 99 | > = HttpApiBuilder.layer(api).pipe( |
| 100 | Layer.provide(Layer.succeedContext(context)) |
| 101 | ) as any |
| 102 | const handler = yield* HttpRouter.toHttpEffect(layer) |
| 103 | const httpClient = HttpClient.make(Effect.fnUntraced(function*(request) { |
| 104 | const serverRequest = HttpServerRequest.fromClientRequest(request) |
| 105 | const response = yield* handler.pipe( |
| 106 | Effect.flatMap((response) => { |
| 107 | const preResponse = preResponseHandler.requestPreResponseHandlers.get(serverRequest.source) |
| 108 | return preResponse === undefined ? Effect.succeed(response) : preResponse(serverRequest, response) |
| 109 | }), |
| 110 | Effect.provideService(HttpServerRequest.HttpServerRequest, serverRequest), |
| 111 | Effect.orDie |
| 112 | ) |
| 113 | return HttpServerResponse.toClientResponse(response, { request }) |
| 114 | }, Effect.scoped)) |
| 115 | |
| 116 | return yield* HttpApiClient.makeWith(api, { |
| 117 | httpClient, |
| 118 | baseUrl: options?.baseUrl ?? "http://localhost:3000" |
| 119 | }) |
| 120 | }) |