210 lines
6.3 KiB
TypeScript
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
|
|
})
|
|
)
|
|
})
|
|
})
|