Files
zopu-code/repos/effect-smol/packages/effect/test/unstable/httpapi/HttpApiEndpoint.test.ts

210 lines
6.3 KiB
TypeScript

import { assert, describe, it } from "@effect/vitest"
import { Schema } from "effect"
import { HttpApiEndpoint, HttpApiSchema } from "effect/unstable/httpapi"
const Events = Schema.Struct({
event: Schema.Literal("user.created"),
data: Schema.String
})
const StreamError = Schema.Struct({ reason: Schema.String })
const sse = () => HttpApiSchema.StreamSse({ events: Events, error: StreamError })
describe("HttpApiEndpoint", () => {
it("stores the supplied identifier", () => {
const endpoint = HttpApiEndpoint.get("getUser", "/users/:id")
assert.strictEqual(endpoint.identifier, "getUser")
})
it("can be extended as a class", () => {
const endpoint = HttpApiEndpoint.get("getUser", "/users/:id")
class GetUser extends endpoint {}
assert.strictEqual(typeof endpoint, "function")
assert.strictEqual(GetUser.name, "GetUser")
assert.strictEqual(GetUser.identifier, "getUser")
assert.isTrue(HttpApiEndpoint.isHttpApiEndpoint(GetUser))
const prefixed = GetUser.prefix("/v1")
assert.strictEqual(prefixed.identifier, "getUser")
assert.strictEqual(prefixed.path, "/v1/users/:id")
})
})
describe("HttpApiEndpoint payload schemas", () => {
it("normalizes payload map keys while preserving the declared content type", () => {
const contentType = "Application/Vnd.Effect+JSON; Charset=UTF-8"
const endpoint = HttpApiEndpoint.post("create", "/", {
payload: Schema.Struct({ name: Schema.String }).pipe(HttpApiSchema.asJson({ contentType }))
})
const entry = endpoint.payload.get("application/vnd.effect+json")
assert.isDefined(entry)
assert.strictEqual(entry.encoding.contentType, contentType)
})
it("rejects incompatible encodings for equivalent content types", () => {
const JsonPayload = Schema.Struct({ name: Schema.String }).pipe(
HttpApiSchema.asJson({ contentType: "Application/Vnd.Effect+Data; charset=utf-8" })
)
const TextPayload = Schema.String.pipe(
HttpApiSchema.asText({ contentType: "application/vnd.effect+data" })
)
assert.throws(
() => HttpApiEndpoint.post("create", "/", { payload: [JsonPayload, TextPayload] }),
/Multiple payload encodings/
)
})
})
describe("HttpApiEndpoint streaming success schemas", () => {
it("GET endpoint accepts StreamSse success", () => {
const stream = sse()
const endpoint = HttpApiEndpoint.get("events", "/events", {
success: stream
})
assert.isTrue(endpoint.success.has(stream))
})
it("GET endpoint accepts StreamUint8Array success", () => {
const stream = HttpApiSchema.StreamUint8Array()
const endpoint = HttpApiEndpoint.get("download", "/download", {
success: stream
})
assert.isTrue(endpoint.success.has(stream))
})
it("streaming schema in error throws during endpoint construction", () => {
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
error: sse() as any
})
)
})
it("HEAD with streaming success throws", () => {
assert.throws(() =>
HttpApiEndpoint.head("events", "/events", {
success: sse()
})
)
})
it("streaming success mixed with NoContent at the same status throws", () => {
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
success: [
sse(),
HttpApiSchema.NoContent.pipe(HttpApiSchema.status(200))
]
})
)
})
it("streaming success mixed with a buffered success at the same status is allowed for distinct content types", () => {
const stream = sse()
const endpoint = HttpApiEndpoint.get("events", "/events", {
success: [
stream,
Schema.Struct({ ok: Schema.Boolean })
]
})
assert.isTrue(endpoint.success.has(stream))
})
it("streaming success mixed with a buffered success at the same content type throws", () => {
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
success: [
HttpApiSchema.StreamSse({ contentType: "application/json", events: Events, error: StreamError }),
Schema.Struct({ ok: Schema.Boolean })
]
})
)
})
it("streaming success mixed with a buffered success at content types differing only by parameters throws", () => {
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
success: [
HttpApiSchema.StreamSse({
contentType: "application/json; charset=utf-8",
events: Events,
error: StreamError
}),
Schema.Struct({ ok: Schema.Boolean })
]
})
)
})
it("streaming success mixed with a buffered success at distinct statuses is allowed", () => {
const stream = HttpApiSchema.status(206)(sse())
const endpoint = HttpApiEndpoint.get("events", "/events", {
success: [
stream,
Schema.Struct({ ok: Schema.Boolean })
]
})
assert.isTrue(endpoint.success.has(stream))
})
it("streaming success mixed with NoContent at distinct statuses is allowed", () => {
const stream = HttpApiSchema.status(200)(sse())
const endpoint = HttpApiEndpoint.get("events", "/events", {
success: [
stream,
HttpApiSchema.NoContent
]
})
assert.isTrue(endpoint.success.has(stream))
})
it("two streaming successes for the same status throw", () => {
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
success: [
sse(),
HttpApiSchema.StreamUint8Array({ contentType: "application/custom-stream" })
]
})
)
})
it("two streaming successes for distinct statuses are allowed", () => {
const stream = HttpApiSchema.status(206)(sse())
const bytes = HttpApiSchema.status(200)(
HttpApiSchema.StreamUint8Array({ contentType: "application/custom-stream" })
)
const endpoint = HttpApiEndpoint.get("events", "/events", {
success: [stream, bytes]
})
assert.isTrue(endpoint.success.has(stream))
assert.isTrue(endpoint.success.has(bytes))
})
it("statically detectable SSE reserved failure event name throws", () => {
const stream = HttpApiSchema.StreamSse({
events: Schema.Struct({
event: Schema.Literal("effect/httpapi/stream/failure"),
data: Schema.String
}),
error: StreamError
})
assert.throws(() =>
HttpApiEndpoint.get("events", "/events", {
success: stream
})
)
})
})