191 lines · 7.5 KB · TypeScript
RawDownload
import * as NodeServices from "@effect/platform-node/NodeServices";import { expect, it } from "@effect/vitest";import { Duration, Effect, Layer } from "effect";import { TestClock } from "effect/testing"; import type { ServerConfigShape } from "../../config.ts";import { ServerConfig } from "../../config.ts";import { SqlitePersistenceMemory } from "../../persistence/Layers/Sqlite.ts";import { SessionCredentialService } from "../Services/SessionCredentialService.ts";import { ServerSecretStoreLive } from "./ServerSecretStore.ts";import { SessionCredentialServiceLive } from "./SessionCredentialService.ts"; const makeServerConfigLayer = (  overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,) =>  Layer.effect(    ServerConfig,    Effect.gen(function* () {      const config = yield* ServerConfig;      return {        ...config,        ...overrides,      } satisfies ServerConfigShape;    }),  ).pipe(Layer.provide(ServerConfig.layerTest(process.cwd(), { prefix: "t3-auth-session-test-" }))); const makeSessionCredentialLayer = (  overrides?: Partial<Pick<ServerConfigShape, "desktopBootstrapToken">>,) =>  SessionCredentialServiceLive.pipe(    Layer.provide(SqlitePersistenceMemory),    Layer.provide(ServerSecretStoreLive),    Layer.provide(makeServerConfigLayer(overrides)),  ); it.layer(NodeServices.layer)("SessionCredentialServiceLive", (it) => {  it.effect("issues and verifies signed browser session tokens", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const issued = yield* sessions.issue({        subject: "desktop-bootstrap",        role: "owner",        client: {          label: "Desktop app",          deviceType: "desktop",          os: "macOS",          browser: "Electron",          ipAddress: "127.0.0.1",        },      });      const verified = yield* sessions.verify(issued.token);       expect(verified.method).toBe("browser-session-cookie");      expect(verified.subject).toBe("desktop-bootstrap");      expect(verified.role).toBe("owner");      expect(verified.client.label).toBe("Desktop app");      expect(verified.client.browser).toBe("Electron");      expect(verified.expiresAt?.toString()).toBe(issued.expiresAt.toString());    }).pipe(Effect.provide(makeSessionCredentialLayer())),  );  it.effect("rejects malformed session tokens", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const error = yield* Effect.flip(sessions.verify("not-a-session-token"));       expect(error._tag).toBe("SessionCredentialError");      expect(error.message).toContain("Malformed session token");    }).pipe(Effect.provide(makeSessionCredentialLayer())),  );  it.effect("verifies session tokens against the Effect clock", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const issued = yield* sessions.issue({        method: "bearer-session-token",        subject: "test-clock",      });      const verified = yield* sessions.verify(issued.token);       expect(verified.method).toBe("bearer-session-token");      expect(verified.subject).toBe("test-clock");      expect(verified.role).toBe("client");    }).pipe(Effect.provide(Layer.merge(makeSessionCredentialLayer(), TestClock.layer()))),  );   it.effect("rejects websocket tokens once the parent session has expired", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const issued = yield* sessions.issue({        method: "bearer-session-token",        subject: "short-lived",        ttl: Duration.seconds(1),      });      const websocket = yield* sessions.issueWebSocketToken(issued.sessionId);       yield* TestClock.adjust(Duration.seconds(2));       const error = yield* Effect.flip(sessions.verifyWebSocketToken(websocket.token));      expect(error.message).toContain("expired");    }).pipe(Effect.provide(Layer.merge(makeSessionCredentialLayer(), TestClock.layer()))),  );   it.effect("lists active sessions, tracks connectivity, and revokes other sessions", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const owner = yield* sessions.issue({        subject: "desktop-bootstrap",        role: "owner",        client: {          label: "Desktop app",          deviceType: "desktop",          os: "macOS",          browser: "Electron",        },      });      const client = yield* sessions.issue({        subject: "one-time-token",        role: "client",        client: {          label: "Julius iPhone",          deviceType: "mobile",          os: "iOS",          browser: "Safari",          ipAddress: "192.168.1.88",        },      });       yield* sessions.markConnected(client.sessionId);      const beforeRevoke = yield* sessions.listActive();      const revokedCount = yield* sessions.revokeAllExcept(owner.sessionId);      const afterRevoke = yield* sessions.listActive();      const revokedClient = yield* Effect.flip(sessions.verify(client.token));       expect(beforeRevoke).toHaveLength(2);      expect(beforeRevoke.find((entry) => entry.sessionId === client.sessionId)?.connected).toBe(        true,      );      expect(beforeRevoke.find((entry) => entry.sessionId === client.sessionId)?.client.label).toBe(        "Julius iPhone",      );      expect(        beforeRevoke.find((entry) => entry.sessionId === owner.sessionId)?.client.deviceType,      ).toBe("desktop");      expect(revokedCount).toBe(1);      expect(afterRevoke).toHaveLength(1);      expect(afterRevoke[0]?.sessionId).toBe(owner.sessionId);      expect(revokedClient.message).toContain("revoked");    }).pipe(Effect.provide(makeSessionCredentialLayer())),  );   it.effect("persists lastConnectedAt on first connect and updates it after reconnect", () =>    Effect.gen(function* () {      const sessions = yield* SessionCredentialService;      const issued = yield* sessions.issue({        subject: "reconnect-test",        method: "bearer-session-token",      });       const beforeConnect = yield* sessions.listActive();      expect(beforeConnect[0]?.lastConnectedAt).toBeNull();       yield* TestClock.adjust(Duration.seconds(1));      yield* sessions.markConnected(issued.sessionId);      const firstConnect = yield* sessions.listActive();      const firstConnectedAt = firstConnect[0]?.lastConnectedAt;       expect(firstConnect[0]?.connected).toBe(true);      expect(firstConnectedAt).not.toBeNull();       yield* TestClock.adjust(Duration.seconds(1));      yield* sessions.markConnected(issued.sessionId);      const stillConnected = yield* sessions.listActive();       expect(stillConnected[0]?.lastConnectedAt?.toString()).toBe(firstConnectedAt?.toString());       yield* sessions.markDisconnected(issued.sessionId);      yield* sessions.markDisconnected(issued.sessionId);      const afterDisconnect = yield* sessions.listActive();       expect(afterDisconnect[0]?.connected).toBe(false);      expect(afterDisconnect[0]?.lastConnectedAt?.toString()).toBe(firstConnectedAt?.toString());       yield* TestClock.adjust(Duration.seconds(1));      yield* sessions.markConnected(issued.sessionId);      const afterReconnect = yield* sessions.listActive();       expect(afterReconnect[0]?.connected).toBe(true);      expect(afterReconnect[0]?.lastConnectedAt).not.toBeNull();      expect(afterReconnect[0]?.lastConnectedAt?.toString()).not.toBe(firstConnectedAt?.toString());    }).pipe(Effect.provide(Layer.merge(makeSessionCredentialLayer(), TestClock.layer()))),  );});