1import { describe, it } from "@effect/vitest"
2import { assertNone, assertSome, assertTrue, deepStrictEqual, doesNotThrow, strictEqual } from "@effect/vitest/utils"
3import { Redacted, Schema } from "effect"
4import { Headers } from "effect/http"
5import { assertSuccess } from "../utils/assert.ts"
6
7describe("Headers", () => {
8 describe("Schema.Headers", () => {
9 it("serializer annotation", () => {
10 const _Accept = Schema.toIso(Schema.Headers).at("Accept")
11 const headers = Headers.fromRecordUnsafe({
12 "Accept": "application/json, text/plain, */*",
13 "Cache-Control": "no-cache"
14 })
15 assertSuccess(_Accept.getResult(headers), "application/json, text/plain, */*")
16 assertSuccess(
17 _Accept.replaceResult("application/json", headers),
18 Headers.fromRecordUnsafe({
19 "accept": "application/json",
20 "cache-control": "no-cache"
21 })
22 )
23 })
24 })
25
26 it("does not expose inspectable prototype methods during for..in iteration", () => {
27 const headers = Headers.fromInput({ foo: "bar" })
28 const keys: Array<string> = []
29
30 for (const key in headers) {
31 keys.push(key)
32 }
33
34 deepStrictEqual(keys, ["foo"])
35 })
36
37 it("remove", () => {
38 const headers = Headers.fromInput({ foo: "bar", baz: "qux", hello: "world" })
39 const result = Headers.remove(headers, "baz")
40 deepStrictEqual({ ...result }, { foo: "bar", hello: "world" })
41 })
42
43 it("removeMany deletes multiple headers", () => {
44 const headers = Headers.fromInput({ foo: "bar", baz: "qux", hello: "world" })
45 const result = Headers.removeMany(headers, ["baz", "hello"])
46 deepStrictEqual({ ...result }, { foo: "bar" })
47 })
48
49 it("removeMany normalizes keys to lowercase", () => {
50 const headers = Headers.fromInput({ "Content-Type": "text/plain", "X-Custom": "value", keep: "me" })
51 const result = Headers.removeMany(headers, ["Content-Type", "X-CUSTOM"])
52 deepStrictEqual({ ...result }, { keep: "me" })
53 })
54
55 it("isRedactedName compares string patterns case-insensitively", () => {
56 strictEqual(Headers.isRedactedName("Authorization", ["authorization"]), true)
57 })
58
59 it("works with for..in based headers polyfills", () => {
60 const effectHeaders = Headers.fromInput({ foo: "bar" })
61 const nativeHeaders = new globalThis.Headers()
62
63 doesNotThrow(() => {
64 for (const key in effectHeaders) {
65 nativeHeaders.append(key, effectHeaders[key])
66 }
67 })
68
69 strictEqual(nativeHeaders.get("foo"), "bar")
70 })
71
72 it("get returns Option", () => {
73 const headers = Headers.fromInput({ foo: "bar" })
74 assertSome(Headers.get(headers, "foo"), "bar")
75 assertNone(Headers.get(headers, "missing"))
76 })
77
78 it("redacts each header independently with a global pattern", () => {
79 const result = Headers.redact(
80 Headers.fromInput({ "x-secret-one": "one", "x-secret-two": "two" }),
81 /^x-secret-/g
82 )
83
84 assertTrue(Redacted.isRedacted(result["x-secret-one"]))
85 assertTrue(Redacted.isRedacted(result["x-secret-two"]))
86 })
87
88 it("checks each header independently with a sticky pattern", () => {
89 const pattern = /^x-secret-/y
90
91 assertTrue(Headers.isRedactedName("x-secret-one", [pattern]))
92 assertTrue(Headers.isRedactedName("x-secret-two", [pattern]))
93 })
94
95 it("preserves caller-owned stateful pattern cursors", () => {
96 const redactPattern = /^x-secret/g
97 const namePattern = /^x-secret/y
98 redactPattern.lastIndex = 3
99 namePattern.lastIndex = 4
100
101 Headers.redact(Headers.fromInput({ "x-secret": "value" }), redactPattern)
102 Headers.isRedactedName("x-secret", [namePattern])
103
104 strictEqual(redactPattern.lastIndex, 3)
105 strictEqual(namePattern.lastIndex, 4)
106 })
107})