1/// <reference types="node" />
2
3import * as NodeCrypto from "node:crypto";
4
5import { vi } from "vite-plus/test";
6import { assert, describe, it } from "@effect/vitest";
7import { ManagedRelay } from "@t3tools/client-runtime/relay";
8import * as Effect from "effect/Effect";
9import * as Layer from "effect/Layer";
10import { FetchHttpClient } from "effect/http";
11
12import * as Dpop from "./dpop";
13import * as ManagedRelayLayer from "./managedRelayLayer";
14
15vi.mock("expo-crypto", () => ({
16 CryptoDigestAlgorithm: { SHA256: "SHA-256" },
17 getRandomBytes: (byteCount: number) => new Uint8Array(NodeCrypto.randomBytes(byteCount)),
18 getRandomBytesAsync: (byteCount: number) =>
19 Promise.resolve(new Uint8Array(NodeCrypto.randomBytes(byteCount))),
20 digest: (algorithm: string, data: Uint8Array) =>
21 Promise.resolve(new Uint8Array(NodeCrypto.createHash(algorithm).update(data).digest()).buffer),
22}));
23
24const secureStore = new Map<string, string>();
25let failNextRead = false;
26vi.mock("expo-secure-store", () => ({
27 getItemAsync: (key: string) => {
28 if (failNextRead) {
29 failNextRead = false;
30 return Promise.reject(new Error("keychain locked"));
31 }
32 return Promise.resolve(secureStore.get(key) ?? null);
33 },
34 setItemAsync: (key: string, value: string) => {
35 secureStore.set(key, value);
36 return Promise.resolve();
37 },
38 deleteItemAsync: (key: string) => {
39 secureStore.delete(key);
40 return Promise.resolve();
41 },
42}));
43
44describe("managed relay DPoP signer", () => {
45 it.effect("loads the proof key again after a failed read", () =>
46 Effect.gen(function* () {
47 const signer = yield* ManagedRelay.ManagedRelayDpopSigner;
48 failNextRead = true;
49 const failed = yield* Effect.flip(signer.thumbprint);
50 assert.equal(failed._tag, "ManagedRelayDpopKeyLoadError");
51
52 // The failure was not kept: the next request reads the key store again.
53 const thumbprint = yield* signer.thumbprint;
54 assert.equal(yield* signer.thumbprint, thumbprint);
55 }).pipe(
56 Effect.provide(
57 ManagedRelayLayer.layer("https://relay.example.test").pipe(
58 Layer.provide(Layer.mergeAll(FetchHttpClient.layer, Dpop.layer)),
59 ),
60 ),
61 ),
62 );
63});