1import { assert, describe, it } from "@effect/vitest"
2import { Effect, Redacted } from "effect"
3import * as Base64 from "effect/encoding/Base64"
4import { HttpClientRequest, HttpServerRequest } from "effect/http"
5import { HttpApiBuilder, HttpApiSecurity } from "effect/http-api"
6
7const decode = <Security extends HttpApiSecurity.HttpApiSecurity>(authorization: string, security: Security) =>
8 HttpApiBuilder.securityDecode(security).pipe(
9 Effect.provideService(
10 HttpServerRequest.HttpServerRequest,
11 HttpServerRequest.fromWeb(new Request("http://localhost/", { headers: { authorization } }))
12 ),
13 Effect.provideService(HttpServerRequest.ParsedSearchParams, {})
14 )
15
16describe("HttpApiSecurity", () => {
17 describe("securityDecode", () => {
18 it.effect("decodes a bearer token without a leading space", () =>
19 Effect.gen(function*() {
20 const token = "abc123"
21 const { headers } = HttpClientRequest.get("http://localhost/").pipe(
22 HttpClientRequest.bearerToken(token)
23 )
24 const credential = yield* HttpApiBuilder.securityDecode(HttpApiSecurity.bearer).pipe(
25 Effect.provideService(
26 HttpServerRequest.HttpServerRequest,
27 HttpServerRequest.fromWeb(new Request("http://localhost/", { headers }))
28 ),
29 Effect.provideService(HttpServerRequest.ParsedSearchParams, {})
30 )
31
32 assert.strictEqual(Redacted.value(credential), token)
33 }))
34
35 it.effect("decodes a custom http scheme without a leading space", () =>
36 Effect.gen(function*() {
37 const credential = yield* decode("Token abc123", HttpApiSecurity.http({ scheme: "Token" }))
38
39 assert.strictEqual(Redacted.value(credential), "abc123")
40 }))
41
42 it.effect("matches HTTP schemes case-insensitively", () =>
43 Effect.gen(function*() {
44 const credential = yield* decode("bEaReR abc123", HttpApiSecurity.bearer)
45
46 assert.strictEqual(Redacted.value(credential), "abc123")
47 }))
48
49 it.effect("accepts multiple spaces before HTTP credentials", () =>
50 Effect.gen(function*() {
51 const credential = yield* decode("Bearer abc123", HttpApiSecurity.bearer)
52
53 assert.strictEqual(Redacted.value(credential), "abc123")
54 }))
55
56 it.effect("rejects mismatched and malformed HTTP schemes", () =>
57 Effect.gen(function*() {
58 const mismatched = yield* decode("Basic abc123", HttpApiSecurity.bearer)
59 const malformed = yield* decode("Bearerabc123", HttpApiSecurity.bearer)
60
61 assert.strictEqual(Redacted.value(mismatched), "")
62 assert.strictEqual(Redacted.value(malformed), "")
63 }))
64
65 it.effect("decodes Basic credentials using the first colon separator", () =>
66 Effect.gen(function*() {
67 const encoded = Base64.encode("alice:secret:with:colons")
68 const credential = yield* decode(`Basic ${encoded}`, HttpApiSecurity.basic)
69
70 assert.strictEqual(credential.username, "alice")
71 assert.strictEqual(Redacted.value(credential.password), "secret:with:colons")
72 }))
73
74 it.effect("rejects Basic credentials from a different scheme", () =>
75 Effect.gen(function*() {
76 const encoded = Base64.encode("alice:secret")
77 const credential = yield* decode(`Bearer ${encoded}`, HttpApiSecurity.basic)
78
79 assert.strictEqual(credential.username, "")
80 assert.strictEqual(Redacted.value(credential.password), "")
81 }))
82 })
83})