From 056b7108978e70612176c23991916f678d947f38 Mon Sep 17 00:00:00 2001 From: Milan Suk Date: Wed, 7 Aug 2024 11:33:49 +0200 Subject: [PATCH] add `{Bun,Node}HttpServer.layerTest` for testing http servers (#3409) --- .changeset/cyan-mugs-shout.md | 20 ++ .changeset/happy-glasses-boil.md | 19 ++ .changeset/wicked-dragons-remain.md | 5 + packages/platform-bun/docgen.json | 4 +- packages/platform-bun/src/BunHttpServer.ts | 18 ++ .../platform-bun/src/internal/httpServer.ts | 12 ++ .../test-bun/BunHttpServer.test.ts | 18 ++ packages/platform-node/docgen.json | 29 ++- packages/platform-node/src/NodeHttpServer.ts | 28 +++ .../platform-node/src/internal/httpServer.ts | 8 + .../platform-node/test/HttpServer.test.ts | 182 ++++++++---------- packages/platform/src/HttpServer.ts | 10 + packages/platform/src/internal/httpServer.ts | 17 ++ 13 files changed, 257 insertions(+), 113 deletions(-) create mode 100644 .changeset/cyan-mugs-shout.md create mode 100644 .changeset/happy-glasses-boil.md create mode 100644 .changeset/wicked-dragons-remain.md create mode 100644 packages/platform-bun/test-bun/BunHttpServer.test.ts diff --git a/.changeset/cyan-mugs-shout.md b/.changeset/cyan-mugs-shout.md new file mode 100644 index 0000000000..323f85f84d --- /dev/null +++ b/.changeset/cyan-mugs-shout.md @@ -0,0 +1,20 @@ +--- +"@effect/platform-node": patch +--- + +Add `NodeHttpServer.layerTest`. + +```ts +import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" +import { NodeHttpServer } from "@effect/platform-node" +import { expect, it } from "@effect/vitest" +import { Effect } from "effect" + +it.scoped("test", () => + Effect.gen(function* () { + yield* HttpServer.serveEffect(HttpRouter.empty) + const response = yield* HttpClientRequest.get("/") + expect(response.status, 404) + }).pipe(Effect.provide(NodeHttpServer.layerTest)) +) +``` diff --git a/.changeset/happy-glasses-boil.md b/.changeset/happy-glasses-boil.md new file mode 100644 index 0000000000..0a0d16040a --- /dev/null +++ b/.changeset/happy-glasses-boil.md @@ -0,0 +1,19 @@ +--- +"@effect/platform-bun": patch +--- + +Add `BunHttpServer.layerTest`. + +```ts +import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" +import { BunHttpServer } from "@effect/platform-bun" +import { expect, it } from "bun:test" +import { Effect } from "effect" + +it("test", () => + Effect.gen(function* (_) { + yield* HttpServer.serveEffect(HttpRouter.empty) + const response = yield* HttpClientRequest.get("/non-existing") + expect(response.status).toEqual(404) + }).pipe( Effect.provide(BunHttpServer.layerTest), Effect.scoped, Effect.runPromise)) +``` diff --git a/.changeset/wicked-dragons-remain.md b/.changeset/wicked-dragons-remain.md new file mode 100644 index 0000000000..551a466bcc --- /dev/null +++ b/.changeset/wicked-dragons-remain.md @@ -0,0 +1,5 @@ +--- +"@effect/platform": patch +--- + +Add `HttpClient.layerTest`. diff --git a/packages/platform-bun/docgen.json b/packages/platform-bun/docgen.json index f980a32f52..eb4f9983fa 100644 --- a/packages/platform-bun/docgen.json +++ b/packages/platform-bun/docgen.json @@ -1,6 +1,4 @@ { "$schema": "../../node_modules/@effect/docgen/schema.json", - "exclude": [ - "src/internal/**/*.ts" - ] + "exclude": ["src/internal/**/*.ts"] } diff --git a/packages/platform-bun/src/BunHttpServer.ts b/packages/platform-bun/src/BunHttpServer.ts index 925a2fb972..99e8b54239 100644 --- a/packages/platform-bun/src/BunHttpServer.ts +++ b/packages/platform-bun/src/BunHttpServer.ts @@ -2,8 +2,10 @@ * @since 1.0.0 */ import type * as Etag from "@effect/platform/Etag" +import type * as HttpClient from "@effect/platform/HttpClient" import type * as Platform from "@effect/platform/HttpPlatform" import type * as Server from "@effect/platform/HttpServer" +import type * as HttpServerError from "@effect/platform/HttpServerError" import type { ServeOptions } from "bun" import type * as Config from "effect/Config" import type * as ConfigError from "effect/ConfigError" @@ -36,6 +38,22 @@ export const layer: ( options: Omit ) => Layer.Layer = internal.layer +/** + * Layer starting a server on a random port and producing an `HttpClient` + * with prepended url of the running http server. + * + * @since 1.0.0 + * @category layers + */ +export const layerTest: Layer.Layer< + | HttpClient.HttpClient.Default + | Server.HttpServer + | Platform.HttpPlatform + | Etag.Generator + | BunContext.BunContext, + HttpServerError.ServeError +> = internal.layerTest + /** * @since 1.0.0 * @category layers diff --git a/packages/platform-bun/src/internal/httpServer.ts b/packages/platform-bun/src/internal/httpServer.ts index 6e0bfa70f6..3f086f3f40 100644 --- a/packages/platform-bun/src/internal/httpServer.ts +++ b/packages/platform-bun/src/internal/httpServer.ts @@ -5,6 +5,7 @@ import * as Cookies from "@effect/platform/Cookies" import type * as FileSystem from "@effect/platform/FileSystem" import * as Headers from "@effect/platform/Headers" import * as App from "@effect/platform/HttpApp" +import * as HttpClient from "@effect/platform/HttpClient" import * as IncomingMessage from "@effect/platform/HttpIncomingMessage" import type { HttpMethod } from "@effect/platform/HttpMethod" import * as Server from "@effect/platform/HttpServer" @@ -177,6 +178,17 @@ export const layer = ( BunContext.layer ) +/** @internal */ +export const layerTest = Server.layerTestClient.pipe( + Layer.provide(Layer.succeed( + HttpClient.HttpClient, + HttpClient.fetch.pipe( + HttpClient.transformResponse(HttpClient.withFetchOptions({ keepalive: false })) + ) + )), + Layer.provideMerge(layer({ port: 0 })) +) + /** @internal */ export const layerConfig = ( options: Config.Config.Wrap> diff --git a/packages/platform-bun/test-bun/BunHttpServer.test.ts b/packages/platform-bun/test-bun/BunHttpServer.test.ts new file mode 100644 index 0000000000..f536ebc78c --- /dev/null +++ b/packages/platform-bun/test-bun/BunHttpServer.test.ts @@ -0,0 +1,18 @@ +import { HttpClientRequest, HttpRouter, HttpServer, HttpServerResponse } from "@effect/platform" +import { BunHttpServer } from "@effect/platform-bun" +import { expect, it } from "bun:test" +import { Effect } from "effect" + +it("BunHttpTest", () => + Effect.gen(function*(_) { + yield* HttpRouter.empty.pipe( + HttpRouter.get("/", HttpServerResponse.text("Hello, World!")), + HttpServer.serveEffect() + ) + const response1 = yield* HttpClientRequest.get("/") + expect(response1.status).toEqual(200) + expect(yield* response1.text).toEqual("Hello, World!") + + const response2 = yield* HttpClientRequest.get("/non-existing") + expect(response2.status).toEqual(404) + }).pipe(Effect.provide(BunHttpServer.layerTest), Effect.scoped, Effect.runPromise)) diff --git a/packages/platform-node/docgen.json b/packages/platform-node/docgen.json index f980a32f52..7eb6dd18b3 100644 --- a/packages/platform-node/docgen.json +++ b/packages/platform-node/docgen.json @@ -1,6 +1,29 @@ { "$schema": "../../node_modules/@effect/docgen/schema.json", - "exclude": [ - "src/internal/**/*.ts" - ] + "exclude": ["src/internal/**/*.ts"], + "examplesCompilerOptions": { + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "moduleResolution": "Bundler", + "module": "ES2022", + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "paths": { + "effect": ["../../../effect/src/index.js"], + "effect/*": ["../../../effect/src/*.js"], + "@effect/platform": ["../../../platform/src/index.js"], + "@effect/platform/*": ["../../../platform/src/*.js"], + "@effect/platform-node": ["../../../platform-node/src/index.js"], + "@effect/platform-node/*": ["../../../platform-node/src/*.js"], + "@effect/platform-node-shared": [ + "../../../platform-node-shared/src/index.js" + ], + "@effect/platform-node-shared/*": [ + "../../../platform-node-shared/src/*.js" + ], + "@effect/schema": ["../../../schema/src/index.js"], + "@effect/schema/*": ["../../../schema/src/*.js"] + } + } } diff --git a/packages/platform-node/src/NodeHttpServer.ts b/packages/platform-node/src/NodeHttpServer.ts index ea1975a4d3..c57f2e452e 100644 --- a/packages/platform-node/src/NodeHttpServer.ts +++ b/packages/platform-node/src/NodeHttpServer.ts @@ -3,6 +3,7 @@ */ import type * as Etag from "@effect/platform/Etag" import type * as App from "@effect/platform/HttpApp" +import type * as HttpClient from "@effect/platform/HttpClient" import type * as Middleware from "@effect/platform/HttpMiddleware" import type * as Platform from "@effect/platform/HttpPlatform" import type * as Server from "@effect/platform/HttpServer" @@ -84,3 +85,30 @@ export const layerConfig: ( Platform.HttpPlatform | Etag.Generator | NodeContext.NodeContext | Server.HttpServer, ConfigError.ConfigError | ServeError > = internal.layerConfig + +/** + * Layer starting a server on a random port and producing an `HttpClient` + * with prepended url of the running http server. + * + * @example + * import { HttpClientRequest, HttpRouter, HttpServer } from "@effect/platform" + * import { NodeHttpServer } from "@effect/platform-node" + * import { Effect } from "effect" + * + * Effect.gen(function*() { + * yield* HttpServer.serveEffect(HttpRouter.empty) + * const response = yield* HttpClientRequest.get("/") + * assert.strictEqual(response.status, 404) + * }).pipe(Effect.provide(NodeHttpServer.layerTest)) + * + * @since 1.0.0 + * @category layers + */ +export const layerTest: Layer.Layer< + | HttpClient.HttpClient.Default + | Server.HttpServer + | Platform.HttpPlatform + | Etag.Generator + | NodeContext.NodeContext, + ServeError +> = internal.layerTest diff --git a/packages/platform-node/src/internal/httpServer.ts b/packages/platform-node/src/internal/httpServer.ts index bdeae5bc28..b8720a6c16 100644 --- a/packages/platform-node/src/internal/httpServer.ts +++ b/packages/platform-node/src/internal/httpServer.ts @@ -31,6 +31,7 @@ import { Readable } from "node:stream" import { pipeline } from "node:stream/promises" import * as WS from "ws" import * as NodeContext from "../NodeContext.js" +import * as NodeHttpClient from "../NodeHttpClient.js" import * as NodeSink from "../NodeSink.js" import { HttpIncomingMessageImpl } from "./httpIncomingMessage.js" import * as internalPlatform from "./httpPlatform.js" @@ -329,6 +330,13 @@ export const layer = ( NodeContext.layer ) +/** @internal */ +export const layerTest = Server.layerTestClient.pipe( + Layer.provide(NodeHttpClient.layerWithoutAgent), + Layer.provide(NodeHttpClient.makeAgentLayer({ keepAlive: false })), + Layer.provideMerge(layer(Http.createServer, { port: 0 })) +) + /** @internal */ export const layerConfig = ( evaluate: LazyArg, diff --git a/packages/platform-node/test/HttpServer.test.ts b/packages/platform-node/test/HttpServer.test.ts index 1698799562..ddd3840b85 100644 --- a/packages/platform-node/test/HttpServer.test.ts +++ b/packages/platform-node/test/HttpServer.test.ts @@ -1,6 +1,5 @@ import { Cookies, - type Etag, HttpBody, HttpClient, HttpClientRequest, @@ -16,38 +15,15 @@ import { Multipart, UrlParams } from "@effect/platform" -import { NodeContext, NodeEtag, NodeHttpClient, NodeHttpServer } from "@effect/platform-node" +import { NodeHttpServer } from "@effect/platform-node" import * as Schema from "@effect/schema/Schema" import { assert, describe, expect, it } from "@effect/vitest" import { Deferred, Duration, Fiber, Stream } from "effect" import * as Effect from "effect/Effect" -import * as Layer from "effect/Layer" import * as Option from "effect/Option" import * as Tracer from "effect/Tracer" -import { createServer } from "http" import * as Buffer from "node:buffer" -const ServerLive = NodeHttpServer.layer(createServer, { port: 0 }) -const EnvLive = Layer.mergeAll( - NodeContext.layer, - NodeEtag.layer, - ServerLive, - NodeHttpClient.layerWithoutAgent -).pipe( - Layer.provide(NodeHttpClient.makeAgentLayer({ keepAlive: false })) -) -const runPromise = ( - effect: Effect.Effect< - A, - E, - | NodeContext.NodeContext - | Etag.Generator - | HttpServer.HttpServer - | HttpPlatform.HttpPlatform - | HttpClient.HttpClient.Default - > -) => Effect.runPromise(Effect.provide(effect, EnvLive)) - const Todo = Schema.Struct({ id: Schema.Number, title: Schema.String @@ -57,23 +33,15 @@ const IdParams = Schema.Struct({ }) const todoResponse = HttpServerResponse.schemaJson(Todo) -const makeClient = Effect.map( - Effect.all([HttpServer.HttpServer, HttpClient.HttpClient]), - ([server, client]) => - HttpClient.mapRequest( - client, - HttpClientRequest.prependUrl(`http://127.0.0.1:${(server.address as HttpServer.TcpAddress).port}`) - ) -) const makeTodoClient = Effect.map( - makeClient, + HttpClient.HttpClient, HttpClient.mapEffectScoped( HttpClientResponse.schemaBodyJson(Todo) ) ) describe("HttpServer", () => { - it("schema", () => + it.scoped("schema", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -89,9 +57,9 @@ describe("HttpServer", () => { const client = yield* _(makeTodoClient) const todo = yield* _(client(HttpClientRequest.get("/todos/1"))) expect(todo).toEqual({ id: 1, title: "test" }) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("formData", () => + it.scoped("formData", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -110,7 +78,7 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") const result = yield* _( @@ -118,9 +86,9 @@ describe("HttpServer", () => { HttpClientResponse.json ) expect(result).toEqual({ ok: true }) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyForm", () => + it.scoped("schemaBodyForm", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -139,7 +107,7 @@ describe("HttpServer", () => { Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("file", new Blob(["test"], { type: "text/plain" }), "test.txt") formData.append("test", "test") @@ -148,9 +116,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(204) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("formData withMaxFileSize", () => + it.scoped("formData withMaxFileSize", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -169,7 +137,7 @@ describe("HttpServer", () => { HttpServer.serveEffect(), Multipart.withMaxFileSize(Option.some(100)) ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() const data = new Uint8Array(1000) formData.append("file", new Blob([data], { type: "text/plain" }), "test.txt") @@ -178,9 +146,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(413) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("formData withMaxFieldSize", () => + it.scoped("formData withMaxFieldSize", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -199,7 +167,7 @@ describe("HttpServer", () => { HttpServer.serveEffect(), Multipart.withMaxFieldSize(100) ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() const data = new Uint8Array(1000).fill(1) formData.append("file", new TextDecoder().decode(data)) @@ -208,9 +176,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(413) - }).pipe(Effect.scoped, Effect.tapErrorCause(Effect.log), runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("mount", () => + it.scoped("mount", () => Effect.gen(function*(_) { const child = HttpRouter.empty.pipe( HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), @@ -221,14 +189,14 @@ describe("HttpServer", () => { HttpRouter.mount("/child", child), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const todo = yield* _(client(HttpClientRequest.get("/child/1")), Effect.flatMap((_) => _.text), Effect.scoped) expect(todo).toEqual("/1") const root = yield* _(client(HttpClientRequest.get("/child")), Effect.flatMap((_) => _.text), Effect.scoped) expect(root).toEqual("/") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("mountApp", () => + it.scoped("mountApp", () => Effect.gen(function*(_) { const child = HttpRouter.empty.pipe( HttpRouter.get("/", Effect.map(HttpServerRequest.HttpServerRequest, (_) => HttpServerResponse.text(_.url))), @@ -239,14 +207,14 @@ describe("HttpServer", () => { HttpRouter.mountApp("/child", child), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const todo = yield* _(client(HttpClientRequest.get("/child/1")), HttpClientResponse.text) expect(todo).toEqual("/1") const root = yield* _(client(HttpClientRequest.get("/child")), HttpClientResponse.text) expect(root).toEqual("/") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("mountApp/includePrefix", () => + it.scoped("mountApp/includePrefix", () => Effect.gen(function*(_) { const child = HttpRouter.empty.pipe( HttpRouter.get( @@ -263,14 +231,14 @@ describe("HttpServer", () => { HttpRouter.mountApp("/child", child, { includePrefix: true }), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const todo = yield* _(client(HttpClientRequest.get("/child/1")), HttpClientResponse.text) expect(todo).toEqual("/child/1") const root = yield* _(client(HttpClientRequest.get("/child")), HttpClientResponse.text) expect(root).toEqual("/child") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("file", () => + it.scoped("file", () => Effect.gen(function*(_) { yield* _( yield* _( @@ -293,7 +261,7 @@ describe("HttpServer", () => { Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const res = yield* _(client(HttpClientRequest.get("/")), Effect.scoped) expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") @@ -301,9 +269,9 @@ describe("HttpServer", () => { expect(res.headers.etag).toEqual("\"etag\"") const text = yield* _(res.text) expect(text.trim()).toEqual("lorem ipsum dolar sit amet") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("fileWeb", () => + it.scoped("fileWeb", () => Effect.gen(function*(_) { const now = new Date() const file = new Buffer.File([new TextEncoder().encode("test")], "test.txt", { @@ -325,7 +293,7 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const res = yield* _(client(HttpClientRequest.get("/")), Effect.scoped) expect(res.status).toEqual(200) expect(res.headers["content-type"]).toEqual("text/plain") @@ -334,9 +302,9 @@ describe("HttpServer", () => { expect(res.headers.etag).toEqual("W/\"etag\"") const text = yield* _(res.text) expect(text.trim()).toEqual("test") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyUrlParams", () => + it.scoped("schemaBodyUrlParams", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -360,9 +328,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(todo).toEqual({ id: 1, title: "test" }) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyUrlParams error", () => + it.scoped("schemaBodyUrlParams error", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -379,16 +347,16 @@ describe("HttpServer", () => { HttpRouter.catchTag("ParseError", (error) => HttpServerResponse.unsafeJson({ error }, { status: 400 })), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const response = yield* _( HttpClientRequest.get("/todos"), client, Effect.scoped ) expect(response.status).toEqual(400) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyFormJson", () => + it.scoped("schemaBodyFormJson", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -407,7 +375,7 @@ describe("HttpServer", () => { Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append("json", JSON.stringify({ test: "content" })) const response = yield* _( @@ -415,9 +383,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(204) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyFormJson file", () => + it.scoped("schemaBodyFormJson file", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -436,7 +404,7 @@ describe("HttpServer", () => { Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const formData = new FormData() formData.append( "json", @@ -448,9 +416,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(204) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("schemaBodyFormJson url encoded", () => + it.scoped("schemaBodyFormJson url encoded", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -469,7 +437,7 @@ describe("HttpServer", () => { Effect.tapErrorCause(Effect.logError), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const response = yield* _( client( HttpClientRequest.post("/upload", { @@ -481,9 +449,9 @@ describe("HttpServer", () => { Effect.scoped ) expect(response.status).toEqual(204) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("tracing", () => + it.scoped("tracing", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -496,7 +464,7 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const requestSpan = yield* _(Effect.makeSpan("client request")) const body = yield* _( client(HttpClientRequest.get("/")), @@ -517,9 +485,9 @@ describe("HttpServer", () => { Effect.repeatN(2) ) expect((body as any).parent.value.spanId).toEqual(requestSpan.spanId) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("client abort", () => + it.scopedLive("client abort", () => Effect.gen(function*(_) { const latch = yield* _(Deferred.make()) yield* _( @@ -528,16 +496,16 @@ describe("HttpServer", () => { Effect.interruptible, HttpServer.serveEffect((app) => Effect.onExit(app, (exit) => Deferred.complete(latch, exit))) ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const fiber = yield* _(client(HttpClientRequest.get("/")), Effect.scoped, Effect.fork) yield* _(Effect.sleep(100)) yield* _(Fiber.interrupt(fiber)) const cause = yield* _(Deferred.await(latch), Effect.sandbox, Effect.flip) const [response] = HttpServerError.causeResponseStripped(cause) expect(response.status).toEqual(499) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("multiplex", () => + it.scoped("multiplex", () => Effect.gen(function*(_) { yield* _( HttpMultiplex.empty, @@ -546,7 +514,7 @@ describe("HttpServer", () => { HttpMultiplex.hostRegex(/^c\.example/, HttpServerResponse.text("C")), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient expect( yield* _( client( @@ -587,9 +555,9 @@ describe("HttpServer", () => { HttpClientResponse.text ) ).toEqual("C") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("html", () => + it.scoped("html", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -604,16 +572,16 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const home = yield* _(HttpClientRequest.get("/home"), client, HttpClientResponse.text) expect(home).toEqual("") const about = yield* _(HttpClientRequest.get("/about"), client, HttpClientResponse.text) expect(about).toEqual("") const stream = yield* _(HttpClientRequest.get("/stream"), client, HttpClientResponse.text) expect(stream).toEqual("123hello") - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("setCookie", () => + it.scoped("setCookie", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -635,7 +603,7 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const res = yield* _(HttpClientRequest.get("/home"), client, Effect.scoped) assert.deepStrictEqual( res.cookies.toJSON(), @@ -653,9 +621,9 @@ describe("HttpServer", () => { }) }).toJSON() ) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("uninterruptible routes", () => + it.scopedLive("uninterruptible routes", () => Effect.gen(function*(_) { yield* _( HttpRouter.empty, @@ -670,21 +638,21 @@ describe("HttpServer", () => { ), HttpServer.serveEffect() ) - const client = yield* _(makeClient) + const client = yield* HttpClient.HttpClient const res = yield* _(HttpClientRequest.get("/home"), client, Effect.scoped) assert.strictEqual(res.status, 204) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) describe("HttpServerRespondable", () => { - it("error/RouteNotFound", () => + it.scoped("error/RouteNotFound", () => Effect.gen(function*() { yield* HttpRouter.empty.pipe(HttpServer.serveEffect()) - const client = yield* makeClient + const client = yield* HttpClient.HttpClient const res = yield* HttpClientRequest.get("/home").pipe(client, Effect.scoped) assert.strictEqual(res.status, 404) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("error/schema", () => + it.scoped("error/schema", () => Effect.gen(function*() { class CustomError extends Schema.TaggedError()("CustomError", { name: Schema.String @@ -697,14 +665,14 @@ describe("HttpServer", () => { HttpRouter.get("/home", new CustomError({ name: "test" })), HttpServer.serveEffect() ) - const client = yield* makeClient + const client = yield* HttpClient.HttpClient const res = yield* HttpClientRequest.get("/home").pipe(client) assert.strictEqual(res.status, 599) const err = yield* HttpClientResponse.schemaBodyJson(CustomError)(res) assert.deepStrictEqual(err, new CustomError({ name: "test" })) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) - it("respondable schema", () => + it.scoped("respondable schema", () => Effect.gen(function*() { class User extends Schema.Class("User")({ name: Schema.String @@ -717,20 +685,20 @@ describe("HttpServer", () => { HttpRouter.get("/user", Effect.succeed(new User({ name: "test" }))), HttpServer.serveEffect() ) - const client = yield* makeClient + const client = yield* HttpClient.HttpClient const res = yield* HttpClientRequest.get("/user").pipe(client, HttpClientResponse.schemaBodyJsonScoped(User)) assert.deepStrictEqual(res, new User({ name: "test" })) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) }) - it("bad middleware responds with 500", () => + it.scoped("bad middleware responds with 500", () => Effect.gen(function*() { yield* HttpRouter.empty.pipe( HttpRouter.get("/", HttpServerResponse.empty()), HttpServer.serveEffect(() => Effect.fail("boom")) ) - const client = yield* makeClient + const client = yield* HttpClient.HttpClient const res = yield* HttpClientRequest.get("/").pipe(client) assert.deepStrictEqual(res.status, 500) - }).pipe(Effect.scoped, runPromise)) + }).pipe(Effect.provide(NodeHttpServer.layerTest))) }) diff --git a/packages/platform/src/HttpServer.ts b/packages/platform/src/HttpServer.ts index 1a254da9d2..9a5e155d5e 100644 --- a/packages/platform/src/HttpServer.ts +++ b/packages/platform/src/HttpServer.ts @@ -6,6 +6,7 @@ import type * as Effect from "effect/Effect" import type * as Layer from "effect/Layer" import type * as Scope from "effect/Scope" import type * as App from "./HttpApp.js" +import type * as Client from "./HttpClient.js" import type * as Middleware from "./HttpMiddleware.js" import type * as ServerRequest from "./HttpServerRequest.js" import * as internal from "./internal/httpServer.js" @@ -193,3 +194,12 @@ export const logAddress: Effect.Effect = internal.logAd */ export const withLogAddress: (layer: Layer.Layer) => Layer.Layer> = internal.withLogAddress + +/** + * Layer producing an `HttpClient` with prepended url of the running http server. + * + * @since 1.0.0 + * @category layers + */ +export const layerTestClient: Layer.Layer = + internal.layerTestClient diff --git a/packages/platform/src/internal/httpServer.ts b/packages/platform/src/internal/httpServer.ts index 6ee829e394..402faa13a6 100644 --- a/packages/platform/src/internal/httpServer.ts +++ b/packages/platform/src/internal/httpServer.ts @@ -4,6 +4,8 @@ import { dual } from "effect/Function" import * as Layer from "effect/Layer" import type * as Scope from "effect/Scope" import type * as App from "../HttpApp.js" +import * as Client from "../HttpClient.js" +import * as ClientRequest from "../HttpClientRequest.js" import type * as Middleware from "../HttpMiddleware.js" import type * as Server from "../HttpServer.js" import type * as ServerRequest from "../HttpServerRequest.js" @@ -164,3 +166,18 @@ export const withLogAddress = ( Layer.effectDiscard(logAddress).pipe( Layer.provideMerge(layer) ) + +/** @internal */ +export const makeTestClient = addressWith((address) => + Effect.flatMap(Client.HttpClient, (client) => { + if (address._tag === "UnixAddress") { + return Effect.die(new Error("HttpServer.layerTestClient: UnixAddress not supported")) + } + const host = address.hostname === "0.0.0.0" ? "127.0.0.1" : address.hostname + const url = `http://${host}:${address.port}` + return Effect.succeed(Client.mapRequest(client, ClientRequest.prependUrl(url))) + }) +) + +/** @internal */ +export const layerTestClient = Layer.effect(Client.HttpClient, makeTestClient)