Unified · Split
| @@ -0,0 +1,11 @@ | |||
| 1 | import { Effect, Layer } from "effect"; | ||
| 2 | |||
| 3 | import { ApiKeyService } from "./api-keys"; | ||
| 4 | import { ApiKeyManagementError } from "./api-key-errors"; | ||
| 5 | |||
| 6 | export const ApiKeyServiceTestLayer = Layer.succeed(ApiKeyService)({ | ||
| 7 | validate: () => Effect.succeed(null), | ||
| 8 | listUserKeys: () => Effect.succeed([]), | ||
| 9 | createUserKey: () => Effect.fail(new ApiKeyManagementError({ cause: "unstubbed" })), | ||
| 10 | revokeUserKey: () => Effect.void, | ||
| 11 | }); | ||
| @@ -0,0 +1,90 @@ | |||
| 1 | import { requestHandler } from "@tanstack/react-start/server"; | ||
| 2 | import { Context, Effect, Layer } from "effect"; | ||
| 3 | import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http"; | ||
| 4 | import { HttpApiBuilder, HttpApiClient } from "effect/unstable/httpapi"; | ||
| 5 | |||
| 6 | import { | ||
| 7 | AutumnTestLayer, | ||
| 8 | makeAutumnTestState, | ||
| 9 | type AutumnTestState, | ||
| 10 | } from "../services/autumn.test-layer"; | ||
| 11 | import { ApiKeyServiceTestLayer } from "./api-keys.test-layer"; | ||
| 12 | import { | ||
| 13 | makeUserStoreTestState, | ||
| 14 | UserStoreTestLayer, | ||
| 15 | type UserStoreTestState, | ||
| 16 | } from "./context.test-layer"; | ||
| 17 | import { CloudAuthPublicHandlers, CloudSessionAuthHandlers, NonProtectedApi } from "./handlers"; | ||
| 18 | import { | ||
| 19 | makeSessionTestContext, | ||
| 20 | SessionAuthTestLayer, | ||
| 21 | type SessionTestContext, | ||
| 22 | } from "./middleware.test-layer"; | ||
| 23 | import { makeWorkOSTestState, WorkOSTestLayer, type WorkOSTestState } from "./workos.test-layer"; | ||
| 24 | |||
| 25 | const TEST_BASE_URL = "http://test.local"; | ||
| 26 | const SESSION_COOKIE = "wos-session=test_session"; | ||
| 27 | |||
| 28 | export type CloudAuthApiTestState = { | ||
| 29 | readonly workos: WorkOSTestState; | ||
| 30 | readonly autumn: AutumnTestState; | ||
| 31 | readonly userStore: UserStoreTestState; | ||
| 32 | readonly session: SessionTestContext; | ||
| 33 | }; | ||
| 34 | |||
| 35 | export const makeCloudAuthApiTestState = ( | ||
| 36 | overrides: Partial<CloudAuthApiTestState> = {}, | ||
| 37 | ): CloudAuthApiTestState => ({ | ||
| 38 | workos: makeWorkOSTestState(), | ||
| 39 | autumn: makeAutumnTestState(), | ||
| 40 | userStore: makeUserStoreTestState(), | ||
| 41 | session: makeSessionTestContext(), | ||
| 42 | ...overrides, | ||
| 43 | }); | ||
| 44 | |||
| 45 | const makeCloudAuthApiTestClient = (state: CloudAuthApiTestState) => { | ||
| 46 | const ApiLive = HttpApiBuilder.layer(NonProtectedApi).pipe( | ||
| 47 | Layer.provide(Layer.mergeAll(CloudAuthPublicHandlers, CloudSessionAuthHandlers)), | ||
| 48 | Layer.provide(SessionAuthTestLayer(state.session)), | ||
| 49 | Layer.provideMerge(WorkOSTestLayer(state.workos)), | ||
| 50 | Layer.provideMerge(AutumnTestLayer(state.autumn)), | ||
| 51 | Layer.provideMerge(UserStoreTestLayer(state.userStore)), | ||
| 52 | Layer.provideMerge(ApiKeyServiceTestLayer), | ||
| 53 | Layer.provideMerge(HttpServer.layerServices), | ||
| 54 | Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })), | ||
| 55 | ); | ||
| 56 | |||
| 57 | const handler = HttpRouter.toWebHandler(ApiLive, { disableLogger: true }).handler; | ||
| 58 | const startHandler = requestHandler((request) => handler(request, undefined)); | ||
| 59 | const fetchViaHandler: typeof globalThis.fetch = async (input, init) => { | ||
| 60 | const request = input instanceof Request ? input : new Request(input, init); | ||
| 61 | request.headers.set("cookie", SESSION_COOKIE); | ||
| 62 | return await startHandler(request, undefined); | ||
| 63 | }; | ||
| 64 | const clientLayer = FetchHttpClient.layer.pipe( | ||
| 65 | Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchViaHandler)), | ||
| 66 | ); | ||
| 67 | |||
| 68 | return HttpApiClient.make(NonProtectedApi, { baseUrl: TEST_BASE_URL }).pipe( | ||
| 69 | Effect.provide(clientLayer), | ||
| 70 | ); | ||
| 71 | }; | ||
| 72 | |||
| 73 | export type CloudAuthApiTestClient = Effect.Success<ReturnType<typeof makeCloudAuthApiTestClient>>; | ||
| 74 | |||
| 75 | export class CloudAuthApiTestContext extends Context.Service< | ||
| 76 | CloudAuthApiTestContext, | ||
| 77 | { | ||
| 78 | readonly state: CloudAuthApiTestState; | ||
| 79 | readonly client: CloudAuthApiTestClient; | ||
| 80 | } | ||
| 81 | >()("CloudAuthApiTestContext") {} | ||
| 82 | |||
| 83 | export const CloudAuthApiTestContextLayer = (state = makeCloudAuthApiTestState()) => | ||
| 84 | Layer.effect( | ||
| 85 | CloudAuthApiTestContext, | ||
| 86 | Effect.gen(function* () { | ||
| 87 | const client = yield* makeCloudAuthApiTestClient(state); | ||
| 88 | return { state, client }; | ||
| 89 | }), | ||
| 90 | ); | ||
| @@ -0,0 +1,41 @@ | |||
| 1 | import { Effect, Layer } from "effect"; | ||
| 2 | |||
| 3 | import { UserStoreService } from "./context"; | ||
| 4 | |||
| 5 | type UserStore = Parameters<Parameters<UserStoreService["Service"]["use"]>[0]>[0]; | ||
| 6 | |||
| 7 | export type UserStoreTestState = { | ||
| 8 | readonly upsertedOrganizations: Array<{ readonly id: string; readonly name: string }>; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export const makeUserStoreTestState = ( | ||
| 12 | overrides: Partial<UserStoreTestState> = {}, | ||
| 13 | ): UserStoreTestState => ({ | ||
| 14 | upsertedOrganizations: [], | ||
| 15 | ...overrides, | ||
| 16 | }); | ||
| 17 | |||
| 18 | const testDate = () => new Date("2026-01-01T00:00:00.000Z"); | ||
| 19 | |||
| 20 | const makeUserStoreTestService = (state: UserStoreTestState): UserStoreService["Service"] => { | ||
| 21 | const store = { | ||
| 22 | ensureAccount: async (id: string) => ({ id, createdAt: testDate() }), | ||
| 23 | getAccount: async (id: string) => ({ id, createdAt: testDate() }), | ||
| 24 | upsertOrganization: async (org: { readonly id: string; readonly name: string }) => { | ||
| 25 | state.upsertedOrganizations.push(org); | ||
| 26 | return { ...org, createdAt: testDate() }; | ||
| 27 | }, | ||
| 28 | getOrganization: async (id: string) => ({ | ||
| 29 | id, | ||
| 30 | name: id, | ||
| 31 | createdAt: testDate(), | ||
| 32 | }), | ||
| 33 | } satisfies UserStore; | ||
| 34 | |||
| 35 | return { | ||
| 36 | use: (fn) => Effect.promise(() => fn(store)), | ||
| 37 | }; | ||
| 38 | }; | ||
| 39 | |||
| 40 | export const UserStoreTestLayer = (state: UserStoreTestState) => | ||
| 41 | Layer.succeed(UserStoreService)(makeUserStoreTestService(state)); | ||
| @@ -0,0 +1,23 @@ | |||
| 1 | import { Effect, Layer } from "effect"; | ||
| 2 | |||
| 3 | import { SessionAuth, SessionContext, type Session } from "./middleware"; | ||
| 4 | |||
| 5 | export type SessionTestContext = Session; | ||
| 6 | |||
| 7 | export const makeSessionTestContext = ( | ||
| 8 | overrides: Partial<SessionTestContext> = {}, | ||
| 9 | ): SessionTestContext => ({ | ||
| 10 | accountId: "user_1", | ||
| 11 | email: "test@example.com", | ||
| 12 | name: "Test User", | ||
| 13 | avatarUrl: null, | ||
| 14 | organizationId: "org_existing_1", | ||
| 15 | sealedSession: "test_session", | ||
| 16 | refreshedSession: null, | ||
| 17 | ...overrides, | ||
| 18 | }); | ||
| 19 | |||
| 20 | export const SessionAuthTestLayer = (session: Session = makeSessionTestContext()) => | ||
| 21 | Layer.succeed(SessionAuth)({ | ||
| 22 | cookie: (httpEffect) => Effect.provideService(httpEffect, SessionContext, session), | ||
| 23 | }); | ||
| @@ -0,0 +1,122 @@ | |||
| 1 | import { Data, Effect, Layer } from "effect"; | ||
| 2 | import type { Organization, OrganizationMembership, OrganizationRole } from "@workos-inc/node"; | ||
| 3 | |||
| 4 | import { WorkOSAuth, type WorkOSCollectedList } from "./workos"; | ||
| 5 | |||
| 6 | export type WorkOSTestState = { | ||
| 7 | readonly memberships: readonly OrganizationMembership[]; | ||
| 8 | readonly createdOrganizations: Array<{ readonly id: string; readonly name: string }>; | ||
| 9 | readonly createdMemberships: Array<{ | ||
| 10 | readonly organizationId: string; | ||
| 11 | readonly userId: string; | ||
| 12 | readonly roleSlug: string | undefined; | ||
| 13 | }>; | ||
| 14 | }; | ||
| 15 | |||
| 16 | export class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{ | ||
| 17 | readonly method: string; | ||
| 18 | }> {} | ||
| 19 | |||
| 20 | export const makeWorkOSTestState = (overrides: Partial<WorkOSTestState> = {}): WorkOSTestState => ({ | ||
| 21 | memberships: [], | ||
| 22 | createdOrganizations: [], | ||
| 23 | createdMemberships: [], | ||
| 24 | ...overrides, | ||
| 25 | }); | ||
| 26 | |||
| 27 | export const WorkOSTestRole: OrganizationRole = { | ||
| 28 | object: "role", | ||
| 29 | id: "role_admin", | ||
| 30 | name: "Admin", | ||
| 31 | slug: "admin", | ||
| 32 | description: null, | ||
| 33 | permissions: [], | ||
| 34 | resourceTypeSlug: "organization", | ||
| 35 | type: "OrganizationRole", | ||
| 36 | createdAt: "2026-01-01T00:00:00.000Z", | ||
| 37 | updatedAt: "2026-01-01T00:00:00.000Z", | ||
| 38 | }; | ||
| 39 | |||
| 40 | export const makeWorkOSTestOrganization = (id: string, name = id): Organization => ({ | ||
| 41 | object: "organization", | ||
| 42 | id, | ||
| 43 | name, | ||
| 44 | allowProfilesOutsideOrganization: false, | ||
| 45 | domains: [], | ||
| 46 | createdAt: "2026-01-01T00:00:00.000Z", | ||
| 47 | updatedAt: "2026-01-01T00:00:00.000Z", | ||
| 48 | externalId: null, | ||
| 49 | metadata: {}, | ||
| 50 | }); | ||
| 51 | |||
| 52 | export const makeWorkOSTestMembership = ( | ||
| 53 | organizationId: string, | ||
| 54 | status: OrganizationMembership["status"], | ||
| 55 | ) => | ||
| 56 | ({ | ||
| 57 | object: "organization_membership", | ||
| 58 | id: `membership_${organizationId}`, | ||
| 59 | organizationId, | ||
| 60 | organizationName: organizationId, | ||
| 61 | status, | ||
| 62 | userId: "user_1", | ||
| 63 | role: WorkOSTestRole, | ||
| 64 | directoryManaged: false, | ||
| 65 | createdAt: "2026-01-01T00:00:00.000Z", | ||
| 66 | updatedAt: "2026-01-01T00:00:00.000Z", | ||
| 67 | customAttributes: {}, | ||
| 68 | }) satisfies OrganizationMembership; | ||
| 69 | |||
| 70 | const collected = <A>(data: readonly A[]): WorkOSCollectedList<A> => ({ | ||
| 71 | object: "list", | ||
| 72 | data: [...data], | ||
| 73 | listMetadata: { | ||
| 74 | before: null, | ||
| 75 | after: null, | ||
| 76 | }, | ||
| 77 | }); | ||
| 78 | |||
| 79 | const makeWorkOSTestService = (state: WorkOSTestState): WorkOSAuth["Service"] => { | ||
| 80 | const nextOrgId = "org_created"; | ||
| 81 | const service: Partial<WorkOSAuth["Service"]> = { | ||
| 82 | listUserMemberships: () => Effect.succeed(collected(state.memberships)), | ||
| 83 | createOrganization: (name) => | ||
| 84 | Effect.sync(() => { | ||
| 85 | const org = makeWorkOSTestOrganization(nextOrgId, name); | ||
| 86 | state.createdOrganizations.push({ id: org.id, name: org.name }); | ||
| 87 | return org; | ||
| 88 | }), | ||
| 89 | createMembership: (organizationId, userId, roleSlug) => | ||
| 90 | Effect.sync(() => { | ||
| 91 | state.createdMemberships.push({ organizationId, userId, roleSlug }); | ||
| 92 | return makeWorkOSTestMembership(organizationId, "active"); | ||
| 93 | }), | ||
| 94 | refreshSession: (_sealedSession, organizationId) => Effect.succeed(`session_${organizationId}`), | ||
| 95 | authenticateSealedSession: (sealedSession) => | ||
| 96 | Effect.succeed({ | ||
| 97 | userId: "user_1", | ||
| 98 | email: "test@example.com", | ||
| 99 | firstName: "Test", | ||
| 100 | lastName: "User", | ||
| 101 | avatarUrl: null, | ||
| 102 | organizationId: sealedSession.replace("session_", ""), | ||
| 103 | sessionId: "session_id", | ||
| 104 | refreshedSession: undefined, | ||
| 105 | }), | ||
| 106 | }; | ||
| 107 | |||
| 108 | return new Proxy(service as WorkOSAuth["Service"], { | ||
| 109 | get: (target, prop) => { | ||
| 110 | if (prop in target) return target[prop as keyof typeof target]; | ||
| 111 | return () => | ||
| 112 | Effect.fail( | ||
| 113 | new UnstubbedWorkOSMethod({ | ||
| 114 | method: typeof prop === "string" ? prop : (prop.description ?? "symbol"), | ||
| 115 | }), | ||
| 116 | ); | ||
| 117 | }, | ||
| 118 | }); | ||
| 119 | }; | ||
| 120 | |||
| 121 | export const WorkOSTestLayer = (state: WorkOSTestState) => | ||
| 122 | Layer.succeed(WorkOSAuth)(makeWorkOSTestService(state)); | ||
| @@ -0,0 +1,36 @@ | |||
| 1 | import { Effect, Layer } from "effect"; | ||
| 2 | import type { Autumn } from "autumn-js"; | ||
| 3 | |||
| 4 | import { AutumnService, type IAutumnService } from "./autumn"; | ||
| 5 | |||
| 6 | export type AutumnTestSubscriptionSummary = { | ||
| 7 | readonly planId?: string | null; | ||
| 8 | readonly status?: string | null; | ||
| 9 | }; | ||
| 10 | |||
| 11 | export type AutumnTestState = { | ||
| 12 | readonly subscriptionsByOrgId: Readonly<Record<string, readonly AutumnTestSubscriptionSummary[]>>; | ||
| 13 | }; | ||
| 14 | |||
| 15 | export const makeAutumnTestState = (overrides: Partial<AutumnTestState> = {}): AutumnTestState => ({ | ||
| 16 | subscriptionsByOrgId: {}, | ||
| 17 | ...overrides, | ||
| 18 | }); | ||
| 19 | |||
| 20 | const makeAutumnTestService = (state: AutumnTestState): IAutumnService => { | ||
| 21 | const fakeClient = { | ||
| 22 | customers: { | ||
| 23 | getOrCreate: async ({ customerId }: { readonly customerId: string }) => ({ | ||
| 24 | subscriptions: state.subscriptionsByOrgId[customerId] ?? [], | ||
| 25 | }), | ||
| 26 | }, | ||
| 27 | } as Autumn; | ||
| 28 | |||
| 29 | return { | ||
| 30 | use: (fn) => Effect.promise(() => fn(fakeClient)), | ||
| 31 | trackExecution: () => Effect.void, | ||
| 32 | }; | ||
| 33 | }; | ||
| 34 | |||
| 35 | export const AutumnTestLayer = (state: AutumnTestState) => | ||
| 36 | Layer.succeed(AutumnService)(makeAutumnTestService(state)); | ||