Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions apps/cloud/src/auth/api-keys.test-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Effect, Layer } from "effect";

import { ApiKeyService } from "./api-keys";
import { ApiKeyManagementError } from "./api-key-errors";

export const ApiKeyServiceTestLayer = Layer.succeed(ApiKeyService)({
validate: () => Effect.succeed(null),
listUserKeys: () => Effect.succeed([]),
createUserKey: () => Effect.fail(new ApiKeyManagementError({ cause: "unstubbed" })),
revokeUserKey: () => Effect.void,
});
90 changes: 90 additions & 0 deletions apps/cloud/src/auth/cloud-auth-api.test-context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { requestHandler } from "@tanstack/react-start/server";
import { Context, Effect, Layer } from "effect";
import { FetchHttpClient, HttpRouter, HttpServer } from "effect/unstable/http";
import { HttpApiBuilder, HttpApiClient } from "effect/unstable/httpapi";

import {
AutumnTestLayer,
makeAutumnTestState,
type AutumnTestState,
} from "../services/autumn.test-layer";
import { ApiKeyServiceTestLayer } from "./api-keys.test-layer";
import {
makeUserStoreTestState,
UserStoreTestLayer,
type UserStoreTestState,
} from "./context.test-layer";
import { CloudAuthPublicHandlers, CloudSessionAuthHandlers, NonProtectedApi } from "./handlers";
import {
makeSessionTestContext,
SessionAuthTestLayer,
type SessionTestContext,
} from "./middleware.test-layer";
import { makeWorkOSTestState, WorkOSTestLayer, type WorkOSTestState } from "./workos.test-layer";

const TEST_BASE_URL = "http://test.local";
const SESSION_COOKIE = "wos-session=test_session";

export type CloudAuthApiTestState = {
readonly workos: WorkOSTestState;
readonly autumn: AutumnTestState;
readonly userStore: UserStoreTestState;
readonly session: SessionTestContext;
};

export const makeCloudAuthApiTestState = (
overrides: Partial<CloudAuthApiTestState> = {},
): CloudAuthApiTestState => ({
workos: makeWorkOSTestState(),
autumn: makeAutumnTestState(),
userStore: makeUserStoreTestState(),
session: makeSessionTestContext(),
...overrides,
});

const makeCloudAuthApiTestClient = (state: CloudAuthApiTestState) => {
const ApiLive = HttpApiBuilder.layer(NonProtectedApi).pipe(
Layer.provide(Layer.mergeAll(CloudAuthPublicHandlers, CloudSessionAuthHandlers)),
Layer.provide(SessionAuthTestLayer(state.session)),
Layer.provideMerge(WorkOSTestLayer(state.workos)),
Layer.provideMerge(AutumnTestLayer(state.autumn)),
Layer.provideMerge(UserStoreTestLayer(state.userStore)),
Layer.provideMerge(ApiKeyServiceTestLayer),
Layer.provideMerge(HttpServer.layerServices),
Layer.provideMerge(Layer.succeed(HttpRouter.RouterConfig)({ maxParamLength: 1000 })),
);

const handler = HttpRouter.toWebHandler(ApiLive, { disableLogger: true }).handler;
const startHandler = requestHandler((request) => handler(request, undefined));
const fetchViaHandler: typeof globalThis.fetch = async (input, init) => {
const request = input instanceof Request ? input : new Request(input, init);
request.headers.set("cookie", SESSION_COOKIE);
return await startHandler(request, undefined);
};
const clientLayer = FetchHttpClient.layer.pipe(
Layer.provide(Layer.succeed(FetchHttpClient.Fetch)(fetchViaHandler)),
);

return HttpApiClient.make(NonProtectedApi, { baseUrl: TEST_BASE_URL }).pipe(
Effect.provide(clientLayer),
);
};

export type CloudAuthApiTestClient = Effect.Success<ReturnType<typeof makeCloudAuthApiTestClient>>;

export class CloudAuthApiTestContext extends Context.Service<
CloudAuthApiTestContext,
{
readonly state: CloudAuthApiTestState;
readonly client: CloudAuthApiTestClient;
}
>()("CloudAuthApiTestContext") {}

export const CloudAuthApiTestContextLayer = (state = makeCloudAuthApiTestState()) =>
Layer.effect(
CloudAuthApiTestContext,
Effect.gen(function* () {
const client = yield* makeCloudAuthApiTestClient(state);
return { state, client };
}),
);
41 changes: 41 additions & 0 deletions apps/cloud/src/auth/context.test-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Effect, Layer } from "effect";

import { UserStoreService } from "./context";

type UserStore = Parameters<Parameters<UserStoreService["Service"]["use"]>[0]>[0];

export type UserStoreTestState = {
readonly upsertedOrganizations: Array<{ readonly id: string; readonly name: string }>;
};

export const makeUserStoreTestState = (
overrides: Partial<UserStoreTestState> = {},
): UserStoreTestState => ({
upsertedOrganizations: [],
...overrides,
});

const testDate = () => new Date("2026-01-01T00:00:00.000Z");

const makeUserStoreTestService = (state: UserStoreTestState): UserStoreService["Service"] => {
const store = {
ensureAccount: async (id: string) => ({ id, createdAt: testDate() }),
getAccount: async (id: string) => ({ id, createdAt: testDate() }),
upsertOrganization: async (org: { readonly id: string; readonly name: string }) => {
state.upsertedOrganizations.push(org);
return { ...org, createdAt: testDate() };
},
getOrganization: async (id: string) => ({
id,
name: id,
createdAt: testDate(),
}),
} satisfies UserStore;

return {
use: (fn) => Effect.promise(() => fn(store)),
};
};

export const UserStoreTestLayer = (state: UserStoreTestState) =>
Layer.succeed(UserStoreService)(makeUserStoreTestService(state));
23 changes: 23 additions & 0 deletions apps/cloud/src/auth/middleware.test-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Effect, Layer } from "effect";

import { SessionAuth, SessionContext, type Session } from "./middleware";

export type SessionTestContext = Session;

export const makeSessionTestContext = (
overrides: Partial<SessionTestContext> = {},
): SessionTestContext => ({
accountId: "user_1",
email: "test@example.com",
name: "Test User",
avatarUrl: null,
organizationId: "org_existing_1",
sealedSession: "test_session",
refreshedSession: null,
...overrides,
});

export const SessionAuthTestLayer = (session: Session = makeSessionTestContext()) =>
Layer.succeed(SessionAuth)({
cookie: (httpEffect) => Effect.provideService(httpEffect, SessionContext, session),
});
122 changes: 122 additions & 0 deletions apps/cloud/src/auth/workos.test-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { Data, Effect, Layer } from "effect";
import type { Organization, OrganizationMembership, OrganizationRole } from "@workos-inc/node";

import { WorkOSAuth, type WorkOSCollectedList } from "./workos";

export type WorkOSTestState = {
readonly memberships: readonly OrganizationMembership[];
readonly createdOrganizations: Array<{ readonly id: string; readonly name: string }>;
readonly createdMemberships: Array<{
readonly organizationId: string;
readonly userId: string;
readonly roleSlug: string | undefined;
}>;
};

export class UnstubbedWorkOSMethod extends Data.TaggedError("UnstubbedWorkOSMethod")<{
readonly method: string;
}> {}

export const makeWorkOSTestState = (overrides: Partial<WorkOSTestState> = {}): WorkOSTestState => ({
memberships: [],
createdOrganizations: [],
createdMemberships: [],
...overrides,
});

export const WorkOSTestRole: OrganizationRole = {
object: "role",
id: "role_admin",
name: "Admin",
slug: "admin",
description: null,
permissions: [],
resourceTypeSlug: "organization",
type: "OrganizationRole",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};

export const makeWorkOSTestOrganization = (id: string, name = id): Organization => ({
object: "organization",
id,
name,
allowProfilesOutsideOrganization: false,
domains: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
externalId: null,
metadata: {},
});

export const makeWorkOSTestMembership = (
organizationId: string,
status: OrganizationMembership["status"],
) =>
({
object: "organization_membership",
id: `membership_${organizationId}`,
organizationId,
organizationName: organizationId,
status,
userId: "user_1",
role: WorkOSTestRole,
directoryManaged: false,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
customAttributes: {},
}) satisfies OrganizationMembership;

const collected = <A>(data: readonly A[]): WorkOSCollectedList<A> => ({
object: "list",
data: [...data],
listMetadata: {
before: null,
after: null,
},
});

const makeWorkOSTestService = (state: WorkOSTestState): WorkOSAuth["Service"] => {
const nextOrgId = "org_created";
const service: Partial<WorkOSAuth["Service"]> = {
listUserMemberships: () => Effect.succeed(collected(state.memberships)),
createOrganization: (name) =>
Effect.sync(() => {
const org = makeWorkOSTestOrganization(nextOrgId, name);
state.createdOrganizations.push({ id: org.id, name: org.name });
return org;
}),
createMembership: (organizationId, userId, roleSlug) =>
Effect.sync(() => {
state.createdMemberships.push({ organizationId, userId, roleSlug });
return makeWorkOSTestMembership(organizationId, "active");
}),
refreshSession: (_sealedSession, organizationId) => Effect.succeed(`session_${organizationId}`),
authenticateSealedSession: (sealedSession) =>
Effect.succeed({
userId: "user_1",
email: "test@example.com",
firstName: "Test",
lastName: "User",
avatarUrl: null,
organizationId: sealedSession.replace("session_", ""),
sessionId: "session_id",
refreshedSession: undefined,
}),
};

return new Proxy(service as WorkOSAuth["Service"], {
get: (target, prop) => {
if (prop in target) return target[prop as keyof typeof target];
return () =>
Effect.fail(
new UnstubbedWorkOSMethod({
method: typeof prop === "string" ? prop : (prop.description ?? "symbol"),
}),
);
},
});
};

export const WorkOSTestLayer = (state: WorkOSTestState) =>
Layer.succeed(WorkOSAuth)(makeWorkOSTestService(state));
36 changes: 36 additions & 0 deletions apps/cloud/src/services/autumn.test-layer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Effect, Layer } from "effect";
import type { Autumn } from "autumn-js";

import { AutumnService, type IAutumnService } from "./autumn";

export type AutumnTestSubscriptionSummary = {
readonly planId?: string | null;
readonly status?: string | null;
};

export type AutumnTestState = {
readonly subscriptionsByOrgId: Readonly<Record<string, readonly AutumnTestSubscriptionSummary[]>>;
};

export const makeAutumnTestState = (overrides: Partial<AutumnTestState> = {}): AutumnTestState => ({
subscriptionsByOrgId: {},
...overrides,
});

const makeAutumnTestService = (state: AutumnTestState): IAutumnService => {
const fakeClient = {
customers: {
getOrCreate: async ({ customerId }: { readonly customerId: string }) => ({
subscriptions: state.subscriptionsByOrgId[customerId] ?? [],
}),
},
} as Autumn;

return {
use: (fn) => Effect.promise(() => fn(fakeClient)),
trackExecution: () => Effect.void,
};
};

export const AutumnTestLayer = (state: AutumnTestState) =>
Layer.succeed(AutumnService)(makeAutumnTestService(state));
Loading