1import { assert, describe, it } from "@effect/vitest"
2import * as Crypto from "effect/Crypto"
3import * as Effect from "effect/Effect"
4import * as TestClock from "effect/testing/TestClock"
5
6const testCrypto = Crypto.make({
7 randomBytes: (size) =>
8 size === 7 ? Uint8Array.of(0x18, 0, 0, 0, 0, 0, 0) : Uint8Array.from({ length: size }, (_, i) => i),
9 digest: (algorithm, data) => Effect.succeed(Uint8Array.of(data.length, algorithm.length))
10})
11
12const makeCrypto = (value: bigint) =>
13 Crypto.make({
14 randomBytes: () =>
15 Uint8Array.of(
16 Number((value >> 48n) & 0x3fn),
17 Number((value >> 40n) & 0xffn),
18 Number((value >> 32n) & 0xffn),
19 Number((value >> 24n) & 0xffn),
20 Number((value >> 16n) & 0xffn),
21 Number((value >> 8n) & 0xffn),
22 Number(value & 0xffn)
23 ),
24 digest: (_algorithm, data) => Effect.succeed(data)
25 })
26
27describe("Crypto", () => {
28 it("uses the module path for its type ID", () => {
29 assert.strictEqual(
30 (testCrypto as unknown as Record<string, unknown>)["~effect/Crypto"],
31 "~effect/Crypto"
32 )
33 })
34
35 it("supports string literal digest algorithms", () => {
36 const algorithm: Crypto.DigestAlgorithm = "SHA-256"
37 assert.strictEqual(algorithm, "SHA-256")
38 })
39
40 it.effect("randomBytes delegates to the service", () =>
41 Effect.gen(function*() {
42 const crypto = yield* Crypto.Crypto
43 const bytes = yield* crypto.randomBytes(4)
44 assert.deepStrictEqual(bytes, Uint8Array.of(0, 1, 2, 3))
45 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
46
47 it.effect("random generators delegate to the service", () =>
48 Effect.gen(function*() {
49 const crypto = yield* Crypto.Crypto
50 const random = yield* crypto.random
51 const randomInt = yield* crypto.randomInt
52 const randomBoolean = yield* crypto.randomBoolean
53 const randomBetween = yield* crypto.randomBetween(10, 20)
54 const randomBetweenDecimalBounds = yield* crypto.randomBetween(10.5, 20.5)
55 const randomIntBetween = yield* crypto.randomIntBetween(1, 6)
56 const randomShuffle = yield* crypto.randomShuffle([1, 2, 3])
57
58 assert.strictEqual(random, 0.75)
59 assert.strictEqual(randomInt, -2251799813685247)
60 assert.strictEqual(randomBoolean, true)
61 assert.strictEqual(randomBetween, 17.5)
62 assert.strictEqual(randomBetweenDecimalBounds, 18)
63 assert.strictEqual(randomIntBetween, 5)
64 assert.deepStrictEqual(randomShuffle, [1, 2, 3])
65 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
66
67 it.effect("randomBetween excludes the upper bound when the result rounds up", () =>
68 Effect.gen(function*() {
69 const value = yield* makeCrypto((1n << 53n) - 2n).randomBetween(10, 20)
70
71 assert.isAtLeast(value, 10)
72 assert.isBelow(value, 20)
73 assert.strictEqual(value, 20 - 2 ** -48)
74 }))
75
76 it("maps adjacent random values to adjacent safe integers", () => {
77 assert.strictEqual(makeCrypto(0n).nextIntUnsafe(), Number.MIN_SAFE_INTEGER)
78 assert.strictEqual(makeCrypto(1n).nextIntUnsafe(), Number.MIN_SAFE_INTEGER + 1)
79 assert.strictEqual(makeCrypto(1n << 53n).nextIntUnsafe(), 1)
80 assert.strictEqual(makeCrypto((1n << 54n) - 2n).nextIntUnsafe(), Number.MAX_SAFE_INTEGER)
81 })
82
83 it.effect("randomIntBetween excludes the upper bound in half-open ranges", () =>
84 Effect.gen(function*() {
85 const crypto = yield* Crypto.Crypto
86 const value = yield* crypto.randomIntBetween(1, 6, { halfOpen: true })
87 assert.strictEqual(value, 5)
88 }).pipe(Effect.provideService(
89 Crypto.Crypto,
90 Crypto.make({
91 randomBytes: (size) => new Uint8Array(size).fill(0xff),
92 digest: (_algorithm, data) => Effect.succeed(data)
93 })
94 )))
95
96 it.effect("randomUUIDv4 formats UUID bytes from randomBytes", () =>
97 Effect.gen(function*() {
98 const crypto = yield* Crypto.Crypto
99 const uuid = yield* crypto.randomUUIDv4
100 assert.strictEqual(uuid, "00010203-0405-4607-8809-0a0b0c0d0e0f")
101 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
102
103 it.effect("randomUUIDv7 formats UUID bytes with the Clock timestamp", () =>
104 Effect.gen(function*() {
105 yield* TestClock.setTime(0x0123456789ab)
106 const crypto = yield* Crypto.Crypto
107 const uuid = yield* crypto.randomUUIDv7
108 assert.strictEqual(uuid, "01234567-89ab-7607-8809-0a0b0c0d0e0f")
109 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
110
111 it.effect("randomULID encodes the Clock timestamp and random bytes", () =>
112 Effect.gen(function*() {
113 yield* TestClock.setTime(0x0123456789ab)
114 const crypto = yield* Crypto.Crypto
115 const ulid = yield* crypto.randomULID
116
117 assert.strictEqual(ulid, "014D2PF2DB000G40R40M30E209")
118 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
119
120 it.effect("randomULID clamps an overflowing Clock timestamp", () =>
121 Effect.gen(function*() {
122 yield* TestClock.setTime(2 ** 48)
123 const crypto = yield* Crypto.Crypto
124 const ulid = yield* crypto.randomULID
125
126 assert.strictEqual(ulid, "7ZZZZZZZZZ000G40R40M30E209")
127 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
128
129 it.effect("digest delegates to the service", () =>
130 Effect.gen(function*() {
131 const crypto = yield* Crypto.Crypto
132 const digest = yield* crypto.digest("SHA-256", Uint8Array.of(1, 2, 3))
133 assert.deepStrictEqual(digest, Uint8Array.of(3, "SHA-256".length))
134 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
135
136 it.effect("can access a provided custom Crypto service", () =>
137 Effect.gen(function*() {
138 const crypto = yield* Crypto.Crypto
139 const bytes = yield* crypto.randomBytes(1)
140 assert.deepStrictEqual(bytes, Uint8Array.of(0))
141 }).pipe(Effect.provideService(Crypto.Crypto, testCrypto)))
142})