| 1 | import { assert, describe, it } from "@effect/vitest" |
| 2 | import * as Crypto from "effect/Crypto" |
| 3 | import * as Effect from "effect/Effect" |
| 4 | import * as TestClock from "effect/testing/TestClock" |
| 5 | |
| 6 | const 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 | |
| 12 | const 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 | |
| 27 | describe("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 | }) |