210 lines · 6.3 KB · TypeScript
RawDownload
import { describe, expect, it } from "@effect/vitest";import { Effect, Exit, Layer } from "effect"; import {  ApiKeyService,  ApiKeyValidationError,  makeCachedApiKeyValidate,  type ApiKeyOwner,} from "./api-keys";import { WorkOSClient, type WorkOSClientService } from "./workos"; const stubWorkOS = (overrides: Partial<WorkOSClientService>) =>  Layer.succeed(    WorkOSClient,    new Proxy({} as WorkOSClientService, {      get: (_target, prop) => {        if (prop in overrides) return overrides[prop as keyof WorkOSClientService];        return () => Effect.die(`unexpected WorkOSClient.${String(prop)} call`);      },    }),  ); const userOwner = (keyId: string): ApiKeyOwner => ({  scope: "user",  accountId: "user_123",  organizationId: "org_123",  keyId,}); describe("makeCachedApiKeyValidate", () => {  it.effect("serves a repeat validation from cache without a second upstream call", () =>    Effect.gen(function* () {      let calls = 0;      const cached = makeCachedApiKeyValidate(() =>        Effect.sync(() => {          calls += 1;          return userOwner("api_key_1");        }),      );       const first = yield* cached.validate("sk_test_abc");      const second = yield* cached.validate("sk_test_abc");       expect(calls).toBe(1);      expect(first).toEqual(userOwner("api_key_1"));      expect(second).toEqual(first);    }),  );   it.effect("re-validates once the TTL has expired", () =>    Effect.gen(function* () {      let calls = 0;      let nowMs = 0;      const cached = makeCachedApiKeyValidate(        () =>          Effect.sync(() => {            calls += 1;            return userOwner("api_key_1");          }),        { ttlMs: 1_000, now: () => nowMs },      );       yield* cached.validate("sk_test_abc");      nowMs = 999;      yield* cached.validate("sk_test_abc");      expect(calls).toBe(1);       nowMs = 1_000;      yield* cached.validate("sk_test_abc");      expect(calls).toBe(2);    }),  );   it.effect("never caches an invalid key", () =>    Effect.gen(function* () {      let calls = 0;      const cached = makeCachedApiKeyValidate(() =>        Effect.sync(() => {          calls += 1;          return null;        }),      );       const first = yield* cached.validate("sk_test_bad");      const second = yield* cached.validate("sk_test_bad");       expect(first).toBeNull();      expect(second).toBeNull();      expect(calls).toBe(2);      expect(cached.cacheKeys()).toHaveLength(0);    }),  );   it.effect("never caches an upstream failure", () =>    Effect.gen(function* () {      let calls = 0;      const cached = makeCachedApiKeyValidate(() =>        Effect.suspend(() => {          calls += 1;          return Effect.fail(new ApiKeyValidationError({ cause: "workos_down" }));        }),      );       const first = yield* Effect.exit(cached.validate("sk_test_abc"));      const second = yield* Effect.exit(cached.validate("sk_test_abc"));       expect(Exit.isFailure(first)).toBe(true);      expect(Exit.isFailure(second)).toBe(true);      expect(calls).toBe(2);      expect(cached.cacheKeys()).toHaveLength(0);    }),  );   it.effect("resets a saturated map of live entries rather than growing past the bound", () =>    Effect.gen(function* () {      const cached = makeCachedApiKeyValidate((value) => Effect.succeed(userOwner(value)), {        maxEntries: 3,      });       for (const value of ["sk_1", "sk_2", "sk_3"]) {        yield* cached.validate(value);      }      expect(cached.cacheKeys()).toHaveLength(3);       // Nothing is expired, so a fourth live entry resets the map instead of      // growing it (mirrors execution-gate's writeCache).      yield* cached.validate("sk_4");      expect(cached.cacheKeys()).toHaveLength(1);       yield* cached.validate("sk_5");      expect(cached.cacheKeys()).toHaveLength(2);    }),  );   it.effect("evicts expired entries before resetting a saturated map", () =>    Effect.gen(function* () {      let nowMs = 0;      const cached = makeCachedApiKeyValidate((value) => Effect.succeed(userOwner(value)), {        ttlMs: 1_000,        maxEntries: 3,        now: () => nowMs,      });       for (const value of ["sk_1", "sk_2", "sk_3"]) {        yield* cached.validate(value);      }      expect(cached.cacheKeys()).toHaveLength(3);       // Everything is expired: the sweep drops the stale entries and the new      // one fits without a full reset.      nowMs = 2_000;      yield* cached.validate("sk_4");      expect(cached.cacheKeys()).toHaveLength(1);    }),  );   it.effect("keys the cache by the SHA-256 digest, never the raw key value", () =>    Effect.gen(function* () {      const rawKey = "sk_live_super_secret_value";      const cached = makeCachedApiKeyValidate(() => Effect.succeed(userOwner("api_key_1")));       yield* cached.validate(rawKey);       const expectedDigest = yield* Effect.promise(async () => {        const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(rawKey));        return Array.from(new Uint8Array(digest), (byte) =>          byte.toString(16).padStart(2, "0"),        ).join("");      });      expect(cached.cacheKeys()).toEqual([expectedDigest]);      expect(cached.cacheKeys()).not.toContain(rawKey);    }),  );}); describe("ApiKeyService.WorkOS validation cache", () => {  it.effect("validates a repeated key through one upstream call per layer build", () =>    Effect.gen(function* () {      let calls = 0;      const layer = ApiKeyService.WorkOS.pipe(        Layer.provide(          stubWorkOS({            validateApiKey: () =>              Effect.sync(() => {                calls += 1;                return {                  apiKey: {                    id: "api_key_123",                    owner: { type: "user", id: "user_123", organizationId: "org_123" },                  },                };              }),          }),        ),      );       const { first, second } = yield* Effect.gen(function* () {        const apiKeys = yield* ApiKeyService;        return {          first: yield* apiKeys.validate("sk_test_abc"),          second: yield* apiKeys.validate("sk_test_abc"),        };      }).pipe(Effect.provide(layer));       expect(calls).toBe(1);      expect(first?.keyId).toBe("api_key_123");      expect(second).toEqual(first);    }),  );});