- Remove unused biome suppression comment in completions.ts - Remove unnecessary if condition in execute-bootstrap.test.ts - Add eslint-disable comments for any type assertions in client.test.ts - Add eslint-disable comments for expect().rejects patterns - Fix template literal number expression with toString() - Fix error handling in test-db.ts to avoid object stringify Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1955 lines
59 KiB
TypeScript
1955 lines
59 KiB
TypeScript
/**
|
|
* End-to-end tests for Admin procedures (superuser operations)
|
|
*
|
|
* These tests use a real PostgreSQL database to test:
|
|
*
|
|
* Users:
|
|
* - admin.users.list - list all users
|
|
* - admin.users.get - get user by email
|
|
* - admin.users.create - create passwordless user with optional org
|
|
* - admin.users.update - update user properties (e.g., isSuperuser)
|
|
* - admin.users.confirmEmail - confirm a user's email
|
|
*
|
|
* Organizations:
|
|
* - admin.orgs.list - list all organizations
|
|
* - admin.orgs.get - get organization by slug
|
|
* - admin.orgs.create - create organization with owner
|
|
* - admin.orgs.update - update organization properties
|
|
* - admin.orgs.delete - delete organization and related records
|
|
* - admin.orgs.listSites - list sites for organization
|
|
* - admin.orgs.addSite - add site to organization
|
|
* - admin.orgs.removeSite - remove site from organization
|
|
*
|
|
* Auth:
|
|
* - admin.auth.completeLogin - complete pending login request
|
|
*/
|
|
|
|
import type { Database } from "@reviq/db-schema";
|
|
import type { Kysely } from "kysely";
|
|
import type { APIContext } from "../../context.js";
|
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
import { call } from "@orpc/server";
|
|
import { createLoggingEmailClient } from "@reviq/emails";
|
|
import {
|
|
createTestUser,
|
|
describeE2E,
|
|
getSharedDb,
|
|
initTestDb,
|
|
TEST_RP,
|
|
truncateAllTables,
|
|
uniqueTestId,
|
|
withTestTransaction,
|
|
} from "@reviq/test-helpers";
|
|
import { router } from "../../router.js";
|
|
import { COOKIE_NAMES } from "../../utils/cookies.js";
|
|
import { hashToken } from "../../utils/crypto.js";
|
|
|
|
/** Session expiry duration: 24 hours in milliseconds */
|
|
const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|
|
|
/** Login request expiry: 15 minutes in milliseconds */
|
|
const LOGIN_REQUEST_EXPIRY_MS = 15 * 60 * 1000;
|
|
|
|
/**
|
|
* Create an API context with optional authentication
|
|
*/
|
|
function createAPIContext(
|
|
db: Kysely<Database>,
|
|
options?: {
|
|
sessionToken?: string;
|
|
},
|
|
): APIContext {
|
|
const reqHeaders = new Headers();
|
|
const cookies: string[] = [];
|
|
|
|
if (options?.sessionToken) {
|
|
cookies.push(`${COOKIE_NAMES.SESSION_TOKEN}=${options.sessionToken}`);
|
|
}
|
|
if (cookies.length > 0) {
|
|
reqHeaders.set("cookie", cookies.join("; "));
|
|
}
|
|
|
|
return {
|
|
db,
|
|
origin: TEST_RP.origin,
|
|
allowedOrigins: [...TEST_RP.allowedOrigins],
|
|
rpName: TEST_RP.rpName,
|
|
reqHeaders,
|
|
resHeaders: new Headers(),
|
|
email: {
|
|
client: createLoggingEmailClient(),
|
|
fromAddress: "test@example.com",
|
|
baseUrl: TEST_RP.origin,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Create a real session in the database and return the token
|
|
*/
|
|
async function createSession(
|
|
db: Kysely<Database>,
|
|
userId: number,
|
|
): Promise<{ token: string; sessionId: number }> {
|
|
const token = `test-session-${uniqueTestId()}`;
|
|
const tokenHashValue = await hashToken(token);
|
|
const expiresAt = new Date(Date.now() + SESSION_EXPIRY_MS);
|
|
|
|
const result = await db
|
|
.insertInto("sessions")
|
|
.values({
|
|
user_id: userId,
|
|
token_hash: tokenHashValue,
|
|
ip_address: "127.0.0.1",
|
|
user_agent: "test-agent",
|
|
expires_at: expiresAt,
|
|
trusted_mode: false,
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return { token, sessionId: Number(result.id) };
|
|
}
|
|
|
|
/**
|
|
* Create an organization in the database
|
|
*/
|
|
async function createOrg(
|
|
db: Kysely<Database>,
|
|
options?: {
|
|
slug?: string;
|
|
displayName?: string;
|
|
logoUrl?: string;
|
|
},
|
|
): Promise<{ id: number; slug: string }> {
|
|
const slug = options?.slug ?? `org-${uniqueTestId()}`;
|
|
|
|
const result = await db
|
|
.insertInto("orgs")
|
|
.values({
|
|
slug,
|
|
display_name: options?.displayName ?? "Test Organization",
|
|
logo_url: options?.logoUrl ?? null,
|
|
})
|
|
.returning(["id", "slug"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Add a user as a member of an organization
|
|
*/
|
|
async function addOrgMember(
|
|
db: Kysely<Database>,
|
|
orgId: number,
|
|
userId: number,
|
|
role: "owner" | "admin" | "member" = "member",
|
|
): Promise<void> {
|
|
await db
|
|
.insertInto("org_members")
|
|
.values({
|
|
org_id: orgId,
|
|
user_id: userId,
|
|
role,
|
|
})
|
|
.execute();
|
|
}
|
|
|
|
/**
|
|
* Create a site for an organization
|
|
*/
|
|
async function createSite(
|
|
db: Kysely<Database>,
|
|
orgId: number,
|
|
domain: string,
|
|
): Promise<{ id: number; domain: string }> {
|
|
const result = await db
|
|
.insertInto("org_sites")
|
|
.values({
|
|
org_id: orgId,
|
|
domain,
|
|
})
|
|
.returning(["id", "domain"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a login request in the database
|
|
*/
|
|
async function createLoginRequest(
|
|
db: Kysely<Database>,
|
|
userId: number,
|
|
email: string,
|
|
options?: {
|
|
completedAt?: Date | null;
|
|
expiresAt?: Date;
|
|
},
|
|
): Promise<{ id: number; token: string }> {
|
|
const token = `login-${uniqueTestId()}`;
|
|
const expiresAt =
|
|
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
|
|
|
|
const result = await db
|
|
.insertInto("login_requests")
|
|
.values({
|
|
user_id: userId,
|
|
email,
|
|
token,
|
|
device_fingerprint: "test-fingerprint",
|
|
expires_at: expiresAt,
|
|
completed_at: options?.completedAt ?? null,
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return { id: Number(result.id), token };
|
|
}
|
|
|
|
/**
|
|
* Create an organization invite
|
|
*/
|
|
async function createOrgInvite(
|
|
db: Kysely<Database>,
|
|
orgId: number,
|
|
email: string,
|
|
invitedBy: number,
|
|
): Promise<{ id: number }> {
|
|
const token = `invite-${uniqueTestId()}`;
|
|
const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000); // 7 days
|
|
|
|
const result = await db
|
|
.insertInto("org_invites")
|
|
.values({
|
|
org_id: orgId,
|
|
email,
|
|
role: "member",
|
|
invited_by: invitedBy,
|
|
token,
|
|
expires_at: expiresAt,
|
|
})
|
|
.returning("id")
|
|
.executeTakeFirstOrThrow();
|
|
|
|
return { id: result.id };
|
|
}
|
|
|
|
describeE2E("admin", () => {
|
|
beforeAll(async () => {
|
|
await initTestDb();
|
|
// Ensure clean slate in case other test files left data behind
|
|
await truncateAllTables(getSharedDb());
|
|
});
|
|
|
|
// ===== Authorization Tests =====
|
|
|
|
describe("admin authorization", () => {
|
|
test("rejects non-superuser for admin.users.list", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const user = await createTestUser(db, {
|
|
email: "regular@example.com",
|
|
isSuperuser: false,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, user.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(router.admin.users.list, undefined, { context }),
|
|
).rejects.toThrow("Superuser access required");
|
|
});
|
|
});
|
|
|
|
test("rejects unauthenticated request for admin.orgs.list", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const context = createAPIContext(db);
|
|
|
|
await expect(
|
|
call(router.admin.orgs.list, undefined, { context }),
|
|
).rejects.toThrow("No session or API key");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.users.list =====
|
|
|
|
describe("admin.users.list", () => {
|
|
test("returns all users", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: "user1@example.com" });
|
|
await createTestUser(db, { email: "user2@example.com" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const users = await call(router.admin.users.list, undefined, {
|
|
context,
|
|
});
|
|
|
|
expect(users.length).toBe(3);
|
|
const emails = users.map((u) => u.email).sort();
|
|
expect(emails).toContain("admin@example.com");
|
|
expect(emails).toContain("user1@example.com");
|
|
expect(emails).toContain("user2@example.com");
|
|
});
|
|
});
|
|
|
|
test("returns users with correct fields", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
displayName: "Admin User",
|
|
fullName: "Admin Full Name",
|
|
emailVerifiedAt: new Date(),
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const users = await call(router.admin.users.list, undefined, {
|
|
context,
|
|
});
|
|
|
|
const adminUser = users.find((u) => u.email === "admin@example.com");
|
|
expect(adminUser).toBeDefined();
|
|
expect(adminUser?.displayName).toBe("Admin User");
|
|
expect(adminUser?.fullName).toBe("Admin Full Name");
|
|
expect(adminUser?.emailVerified).toBe(true);
|
|
expect(adminUser?.isSuperuser).toBe(true);
|
|
expect(adminUser?.needsSetup).toBe(false);
|
|
});
|
|
});
|
|
|
|
test("returns empty array when no users", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
// Create only the admin user
|
|
const admin = await createTestUser(db, {
|
|
email: "onlyadmin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const users = await call(router.admin.users.list, undefined, {
|
|
context,
|
|
});
|
|
|
|
// Only the admin user exists
|
|
expect(users.length).toBe(1);
|
|
expect(users[0]?.email).toBe("onlyadmin@example.com");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.users.get =====
|
|
|
|
describe("admin.users.get", () => {
|
|
test("returns user by email", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, {
|
|
email: "target@example.com",
|
|
displayName: "Target User",
|
|
fullName: "Target Full",
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const user = await call(
|
|
router.admin.users.get,
|
|
{ email: "target@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(user.email).toBe("target@example.com");
|
|
expect(user.displayName).toBe("Target User");
|
|
expect(user.fullName).toBe("Target Full");
|
|
});
|
|
});
|
|
|
|
test("normalizes email to lowercase", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: "test@example.com" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const user = await call(
|
|
router.admin.users.get,
|
|
{ email: "TEST@EXAMPLE.COM" },
|
|
{ context },
|
|
);
|
|
|
|
expect(user.email).toBe("test@example.com");
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent user", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.get,
|
|
{ email: "nonexistent@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
});
|
|
|
|
test("returns correct hasPassword and needsSetup flags", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
// User without display name (needs setup)
|
|
await createTestUser(db, {
|
|
email: "nosetup@example.com",
|
|
displayName: undefined,
|
|
});
|
|
|
|
// Set display_name to null
|
|
await db
|
|
.updateTable("users")
|
|
.set({ display_name: null })
|
|
.where("email", "=", "nosetup@example.com")
|
|
.execute();
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const user = await call(
|
|
router.admin.users.get,
|
|
{ email: "nosetup@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(user.needsSetup).toBe(true);
|
|
expect(user.hasPassword).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.users.create =====
|
|
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
|
|
|
|
describe("admin.users.create", () => {
|
|
afterAll(async () => {
|
|
// Clean up all test data
|
|
await truncateAllTables(getSharedDb());
|
|
});
|
|
|
|
test("creates passwordless user", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.users.create,
|
|
{ email: `newuser-${uniqueId}@example.com` },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify user was created
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", `newuser-${uniqueId}@example.com`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(user).toBeDefined();
|
|
expect(user?.password_hash).toBeNull();
|
|
});
|
|
|
|
test("creates user with name", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.create,
|
|
{ email: `named-${uniqueId}@example.com`, name: "Named User" },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", `named-${uniqueId}@example.com`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(user?.display_name).toBe("Named User");
|
|
});
|
|
|
|
test("creates user and adds to organization as member", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: `test-org-${uniqueId}` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.create,
|
|
{
|
|
email: `orguser-${uniqueId}@example.com`,
|
|
orgSlug: `test-org-${uniqueId}`,
|
|
},
|
|
{ context },
|
|
);
|
|
|
|
// Verify org membership
|
|
const membership = await db
|
|
.selectFrom("org_members")
|
|
.innerJoin("users", "users.id", "org_members.user_id")
|
|
.where("users.email", "=", `orguser-${uniqueId}@example.com`)
|
|
.where("org_members.org_id", "=", org.id)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(membership).toBeDefined();
|
|
expect(membership?.role).toBe("member");
|
|
});
|
|
|
|
test("creates user and adds to organization with custom role", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: `test-org-${uniqueId}` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.create,
|
|
{
|
|
email: `adminuser-${uniqueId}@example.com`,
|
|
orgSlug: `test-org-${uniqueId}`,
|
|
orgRole: "admin",
|
|
},
|
|
{ context },
|
|
);
|
|
|
|
const membership = await db
|
|
.selectFrom("org_members")
|
|
.innerJoin("users", "users.id", "org_members.user_id")
|
|
.where("users.email", "=", `adminuser-${uniqueId}@example.com`)
|
|
.where("org_members.org_id", "=", org.id)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(membership?.role).toBe("admin");
|
|
});
|
|
|
|
test("normalizes email to lowercase", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.create,
|
|
{ email: `UPPERCASE-${uniqueId}@EXAMPLE.COM` },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", `uppercase-${uniqueId}@example.com`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(user).toBeDefined();
|
|
});
|
|
|
|
test("throws CONFLICT for duplicate email", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: `existing-${uniqueId}@example.com` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.create,
|
|
{ email: `existing-${uniqueId}@example.com` },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User with this email already exists");
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent org", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.create,
|
|
{
|
|
email: `newuser-${uniqueId}@example.com`,
|
|
orgSlug: "nonexistent-org",
|
|
},
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
|
|
// ===== admin.users.update =====
|
|
|
|
describe("admin.users.update", () => {
|
|
test("grants superuser status", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, {
|
|
email: "regular@example.com",
|
|
isSuperuser: false,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.update,
|
|
{ email: "regular@example.com", isSuperuser: true },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", "regular@example.com")
|
|
.select(["is_superuser"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(user.is_superuser).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("revokes superuser status from another user", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, {
|
|
email: "otheradmin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.update,
|
|
{ email: "otheradmin@example.com", isSuperuser: false },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", "otheradmin@example.com")
|
|
.select(["is_superuser"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(user.is_superuser).toBe(false);
|
|
});
|
|
});
|
|
|
|
test("prevents self-demotion", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.update,
|
|
{ email: "admin@example.com", isSuperuser: false },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Cannot remove your own superuser status");
|
|
});
|
|
});
|
|
|
|
test("normalizes email to lowercase", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: "target@example.com" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.update,
|
|
{ email: "TARGET@EXAMPLE.COM", isSuperuser: true },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", "target@example.com")
|
|
.select(["is_superuser"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(user.is_superuser).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent user", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.update,
|
|
{ email: "nonexistent@example.com", isSuperuser: true },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
});
|
|
|
|
test("returns success for no-op update (no fields to update)", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: "target@example.com" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.users.update,
|
|
{ email: "target@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for no-op update on non-existent user", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.update,
|
|
{ email: "nonexistent@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.users.confirmEmail =====
|
|
|
|
describe("admin.users.confirmEmail", () => {
|
|
test("confirms user email", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, {
|
|
email: "unverified@example.com",
|
|
emailVerifiedAt: undefined,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.confirmEmail,
|
|
{ email: "unverified@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", "unverified@example.com")
|
|
.select(["email_verified_at"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(user.email_verified_at).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
test("normalizes email to lowercase", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: "test@example.com" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.users.confirmEmail,
|
|
{ email: "TEST@EXAMPLE.COM" },
|
|
{ context },
|
|
);
|
|
|
|
const user = await db
|
|
.selectFrom("users")
|
|
.where("email", "=", "test@example.com")
|
|
.select(["email_verified_at"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(user.email_verified_at).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent user", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.users.confirmEmail,
|
|
{ email: "nonexistent@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
});
|
|
|
|
test("succeeds for already verified user (idempotent)", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, {
|
|
email: "verified@example.com",
|
|
emailVerifiedAt: new Date(),
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
// Should not throw
|
|
const result = await call(
|
|
router.admin.users.confirmEmail,
|
|
{ email: "verified@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.list =====
|
|
|
|
describe("admin.orgs.list", () => {
|
|
test("returns all organizations", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "org-one", displayName: "Org One" });
|
|
await createOrg(db, { slug: "org-two", displayName: "Org Two" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const orgs = await call(router.admin.orgs.list, undefined, { context });
|
|
|
|
expect(orgs.length).toBe(2);
|
|
const slugs = orgs.map((o) => o.slug).sort();
|
|
expect(slugs).toEqual(["org-one", "org-two"]);
|
|
});
|
|
});
|
|
|
|
test("returns organizations with correct fields", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, {
|
|
slug: "test-org",
|
|
displayName: "Test Org",
|
|
logoUrl: "https://example.com/logo.png",
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const orgs = await call(router.admin.orgs.list, undefined, { context });
|
|
|
|
const org = orgs.find((o) => o.slug === "test-org");
|
|
expect(org).toBeDefined();
|
|
expect(org?.displayName).toBe("Test Org");
|
|
expect(org?.logoUrl).toBe("https://example.com/logo.png");
|
|
expect(org?.createdAt).toBeInstanceOf(Date);
|
|
});
|
|
});
|
|
|
|
test("returns empty array when no organizations", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const orgs = await call(router.admin.orgs.list, undefined, { context });
|
|
|
|
expect(orgs).toHaveLength(0);
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.get =====
|
|
|
|
describe("admin.orgs.get", () => {
|
|
test("returns organization by slug", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, {
|
|
slug: "target-org",
|
|
displayName: "Target Organization",
|
|
logoUrl: "https://example.com/logo.png",
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const org = await call(
|
|
router.admin.orgs.get,
|
|
{ slug: "target-org" },
|
|
{ context },
|
|
);
|
|
|
|
expect(org.slug).toBe("target-org");
|
|
expect(org.displayName).toBe("Target Organization");
|
|
expect(org.logoUrl).toBe("https://example.com/logo.png");
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(router.admin.orgs.get, { slug: "nonexistent" }, { context }),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.create =====
|
|
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
|
|
|
|
describe("admin.orgs.create", () => {
|
|
afterAll(async () => {
|
|
await truncateAllTables(getSharedDb());
|
|
});
|
|
|
|
test("creates organization with owner", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const owner = await createTestUser(db, {
|
|
email: `owner-${uniqueId}@example.com`,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.create,
|
|
{
|
|
slug: `new-org-${uniqueId}`,
|
|
displayName: "New Organization",
|
|
ownerEmail: `owner-${uniqueId}@example.com`,
|
|
},
|
|
{ context },
|
|
);
|
|
|
|
expect(result.slug).toBe(`new-org-${uniqueId}`);
|
|
|
|
// Verify org was created
|
|
const org = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", `new-org-${uniqueId}`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(org).toBeDefined();
|
|
expect(org?.display_name).toBe("New Organization");
|
|
|
|
// Verify owner membership
|
|
if (org) {
|
|
const membership = await db
|
|
.selectFrom("org_members")
|
|
.where("org_id", "=", org.id)
|
|
.where("user_id", "=", owner.id)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(membership).toBeDefined();
|
|
expect(membership?.role).toBe("owner");
|
|
}
|
|
});
|
|
|
|
test("normalizes owner email to lowercase", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.create,
|
|
{
|
|
slug: `new-org-${uniqueId}`,
|
|
displayName: "New Organization",
|
|
ownerEmail: `OWNER-${uniqueId}@EXAMPLE.COM`,
|
|
},
|
|
{ context },
|
|
);
|
|
|
|
expect(result.slug).toBe(`new-org-${uniqueId}`);
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent owner", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.create,
|
|
{
|
|
slug: `new-org-${uniqueId}`,
|
|
displayName: "New Organization",
|
|
ownerEmail: "nonexistent@example.com",
|
|
},
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("User not found");
|
|
});
|
|
|
|
test("throws CONFLICT for duplicate slug", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const owner = await createTestUser(db, {
|
|
email: `owner-${uniqueId}@example.com`,
|
|
});
|
|
await createOrg(db, { slug: `existing-org-${uniqueId}` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.create,
|
|
{
|
|
slug: `existing-org-${uniqueId}`,
|
|
displayName: "New Organization",
|
|
ownerEmail: owner.email,
|
|
},
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization with this slug already exists");
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.update =====
|
|
|
|
describe("admin.orgs.update", () => {
|
|
test("updates display name", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "test-org", displayName: "Old Name" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.orgs.update,
|
|
{ slug: "test-org", displayName: "New Name" },
|
|
{ context },
|
|
);
|
|
|
|
const org = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", "test-org")
|
|
.select(["display_name"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(org.display_name).toBe("New Name");
|
|
});
|
|
});
|
|
|
|
test("updates logo URL", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "test-org" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.orgs.update,
|
|
{ slug: "test-org", logoUrl: "https://example.com/newlogo.png" },
|
|
{ context },
|
|
);
|
|
|
|
const org = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", "test-org")
|
|
.select(["logo_url"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(org.logo_url).toBe("https://example.com/newlogo.png");
|
|
});
|
|
});
|
|
|
|
test("clears logo URL with empty string", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, {
|
|
slug: "test-org",
|
|
logoUrl: "https://example.com/logo.png",
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.orgs.update,
|
|
{ slug: "test-org", logoUrl: "" },
|
|
{ context },
|
|
);
|
|
|
|
const org = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", "test-org")
|
|
.select(["logo_url"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(org.logo_url).toBeNull();
|
|
});
|
|
});
|
|
|
|
test("updates multiple fields at once", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, {
|
|
slug: "test-org",
|
|
displayName: "Old",
|
|
logoUrl: undefined,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.orgs.update,
|
|
{
|
|
slug: "test-org",
|
|
displayName: "New Name",
|
|
logoUrl: "https://example.com/logo.png",
|
|
},
|
|
{ context },
|
|
);
|
|
|
|
const org = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", "test-org")
|
|
.select(["display_name", "logo_url"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(org.display_name).toBe("New Name");
|
|
expect(org.logo_url).toBe("https://example.com/logo.png");
|
|
});
|
|
});
|
|
|
|
test("returns success for no-op update", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "test-org" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.update,
|
|
{ slug: "test-org" },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for no-op on non-existent org", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(router.admin.orgs.update, { slug: "nonexistent" }, { context }),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.update,
|
|
{ slug: "nonexistent", displayName: "Test" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.delete =====
|
|
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
|
|
|
|
describe("admin.orgs.delete", () => {
|
|
afterAll(async () => {
|
|
await truncateAllTables(getSharedDb());
|
|
});
|
|
|
|
test("deletes organization and related records", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const member = await createTestUser(db, {
|
|
email: `member-${uniqueId}@example.com`,
|
|
});
|
|
const org = await createOrg(db, { slug: `delete-me-${uniqueId}` });
|
|
|
|
// Create related records
|
|
await addOrgMember(db, org.id, member.id, "owner");
|
|
await createSite(db, org.id, `example-${uniqueId}.com`);
|
|
await createOrgInvite(
|
|
db,
|
|
org.id,
|
|
`invite-${uniqueId}@example.com`,
|
|
admin.id,
|
|
);
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.delete,
|
|
{ slug: `delete-me-${uniqueId}` },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify org is deleted
|
|
const deletedOrg = await db
|
|
.selectFrom("orgs")
|
|
.where("slug", "=", `delete-me-${uniqueId}`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
expect(deletedOrg).toBeUndefined();
|
|
|
|
// Verify related records are deleted
|
|
const members = await db
|
|
.selectFrom("org_members")
|
|
.where("org_id", "=", org.id)
|
|
.selectAll()
|
|
.execute();
|
|
expect(members).toHaveLength(0);
|
|
|
|
const sites = await db
|
|
.selectFrom("org_sites")
|
|
.where("org_id", "=", org.id)
|
|
.selectAll()
|
|
.execute();
|
|
expect(sites).toHaveLength(0);
|
|
|
|
const invites = await db
|
|
.selectFrom("org_invites")
|
|
.where("org_id", "=", org.id)
|
|
.selectAll()
|
|
.execute();
|
|
expect(invites).toHaveLength(0);
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(router.admin.orgs.delete, { slug: "nonexistent" }, { context }),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.listSites =====
|
|
|
|
describe("admin.orgs.listSites", () => {
|
|
test("returns sites for organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: "test-org" });
|
|
await createSite(db, org.id, "example.com");
|
|
await createSite(db, org.id, "test.com");
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const sites = await call(
|
|
router.admin.orgs.listSites,
|
|
{ slug: "test-org" },
|
|
{ context },
|
|
);
|
|
|
|
expect(sites.length).toBe(2);
|
|
const domains = sites.map((s) => s.domain).sort();
|
|
expect(domains).toEqual(["example.com", "test.com"]);
|
|
});
|
|
});
|
|
|
|
test("returns empty array when no sites", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "empty-org" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const sites = await call(
|
|
router.admin.orgs.listSites,
|
|
{ slug: "empty-org" },
|
|
{ context },
|
|
);
|
|
|
|
expect(sites).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
test("returns sites with correct fields", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: "test-org" });
|
|
await createSite(db, org.id, "example.com");
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const sites = await call(
|
|
router.admin.orgs.listSites,
|
|
{ slug: "test-org" },
|
|
{ context },
|
|
);
|
|
|
|
expect(sites[0]?.id).toBeDefined();
|
|
expect(sites[0]?.domain).toBe("example.com");
|
|
expect(sites[0]?.createdAt).toBeInstanceOf(Date);
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.listSites,
|
|
{ slug: "nonexistent" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.addSite =====
|
|
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
|
|
|
|
describe("admin.orgs.addSite", () => {
|
|
afterAll(async () => {
|
|
await truncateAllTables(getSharedDb());
|
|
});
|
|
|
|
test("adds site to organization", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: `test-org-${uniqueId}` });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.addSite,
|
|
{ slug: `test-org-${uniqueId}`, domain: `newsite-${uniqueId}.com` },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify site was created
|
|
const site = await db
|
|
.selectFrom("org_sites")
|
|
.where("org_id", "=", org.id)
|
|
.where("domain", "=", `newsite-${uniqueId}.com`)
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(site).toBeDefined();
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.addSite,
|
|
{ slug: "nonexistent", domain: `test-${uniqueId}.com` },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
|
|
test("throws CONFLICT for duplicate domain", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: `test-org-${uniqueId}` });
|
|
await createSite(db, org.id, `existing-${uniqueId}.com`);
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.addSite,
|
|
{ slug: `test-org-${uniqueId}`, domain: `existing-${uniqueId}.com` },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Site with this domain already exists");
|
|
});
|
|
|
|
test("throws CONFLICT for domain in another organization", async () => {
|
|
const db = getSharedDb();
|
|
const uniqueId = uniqueTestId();
|
|
|
|
const admin = await createTestUser(db, {
|
|
email: `admin-${uniqueId}@example.com`,
|
|
isSuperuser: true,
|
|
});
|
|
const org1 = await createOrg(db, { slug: `org-one-${uniqueId}` });
|
|
await createOrg(db, { slug: `org-two-${uniqueId}` });
|
|
await createSite(db, org1.id, `shared-${uniqueId}.com`);
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.addSite,
|
|
{ slug: `org-two-${uniqueId}`, domain: `shared-${uniqueId}.com` },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Site with this domain already exists");
|
|
});
|
|
});
|
|
|
|
// ===== admin.orgs.removeSite =====
|
|
|
|
describe("admin.orgs.removeSite", () => {
|
|
test("removes site from organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const org = await createOrg(db, { slug: "test-org" });
|
|
await createSite(db, org.id, "remove-me.com");
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.orgs.removeSite,
|
|
{ slug: "test-org", domain: "remove-me.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify site was removed
|
|
const site = await db
|
|
.selectFrom("org_sites")
|
|
.where("org_id", "=", org.id)
|
|
.where("domain", "=", "remove-me.com")
|
|
.selectAll()
|
|
.executeTakeFirst();
|
|
|
|
expect(site).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.removeSite,
|
|
{ slug: "nonexistent", domain: "test.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Organization not found");
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for non-existent site", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
await createOrg(db, { slug: "test-org" });
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.removeSite,
|
|
{ slug: "test-org", domain: "nonexistent.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Site not found");
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for site in another organization", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const org1 = await createOrg(db, { slug: "org-one" });
|
|
await createOrg(db, { slug: "org-two" });
|
|
await createSite(db, org1.id, "org1-site.com");
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.orgs.removeSite,
|
|
{ slug: "org-two", domain: "org1-site.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Site not found");
|
|
});
|
|
});
|
|
});
|
|
|
|
// ===== admin.auth.completeLogin =====
|
|
|
|
describe("admin.auth.completeLogin", () => {
|
|
test("completes pending login request", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const user = await createTestUser(db, { email: "user@example.com" });
|
|
const loginRequest = await createLoginRequest(
|
|
db,
|
|
user.id,
|
|
"user@example.com",
|
|
);
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
const result = await call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "user@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
expect(result.success).toBe(true);
|
|
|
|
// Verify login request was completed
|
|
const request = await db
|
|
.selectFrom("login_requests")
|
|
.where("id", "=", loginRequest.id.toString())
|
|
.select(["completed_at"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(request.completed_at).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
test("normalizes email to lowercase", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const user = await createTestUser(db, { email: "user@example.com" });
|
|
const loginRequest = await createLoginRequest(
|
|
db,
|
|
user.id,
|
|
"user@example.com",
|
|
);
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "USER@EXAMPLE.COM" },
|
|
{ context },
|
|
);
|
|
|
|
const request = await db
|
|
.selectFrom("login_requests")
|
|
.where("id", "=", loginRequest.id.toString())
|
|
.select(["completed_at"])
|
|
.executeTakeFirstOrThrow();
|
|
|
|
expect(request.completed_at).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
test("throws NOT_FOUND for no login request", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "noRequest@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("No login request found");
|
|
});
|
|
});
|
|
|
|
test("throws BAD_REQUEST for already completed request", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const user = await createTestUser(db, { email: "user@example.com" });
|
|
await createLoginRequest(db, user.id, "user@example.com", {
|
|
completedAt: new Date(),
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "user@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Login request already completed");
|
|
});
|
|
});
|
|
|
|
test("throws BAD_REQUEST for expired request", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const user = await createTestUser(db, { email: "user@example.com" });
|
|
await createLoginRequest(db, user.id, "user@example.com", {
|
|
expiresAt: new Date(Date.now() - 1000), // Expired
|
|
});
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await expect(
|
|
call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "user@example.com" },
|
|
{ context },
|
|
),
|
|
).rejects.toThrow("Login request expired");
|
|
});
|
|
});
|
|
|
|
test("completes most recent login request", async () => {
|
|
await withTestTransaction(getSharedDb(), async (db) => {
|
|
const admin = await createTestUser(db, {
|
|
email: "admin@example.com",
|
|
isSuperuser: true,
|
|
});
|
|
const user = await createTestUser(db, { email: "user@example.com" });
|
|
|
|
// Create two login requests
|
|
await createLoginRequest(db, user.id, "user@example.com");
|
|
await createLoginRequest(db, user.id, "user@example.com");
|
|
|
|
const { token: sessionToken } = await createSession(db, admin.id);
|
|
const context = createAPIContext(db, { sessionToken });
|
|
|
|
await call(
|
|
router.admin.auth.completeLogin,
|
|
{ email: "user@example.com" },
|
|
{ context },
|
|
);
|
|
|
|
// Check that exactly one login request was completed
|
|
// (Note: both requests have the same created_at in transaction, so ORDER BY is non-deterministic)
|
|
const allRequests = await db
|
|
.selectFrom("login_requests")
|
|
.where("email", "=", "user@example.com")
|
|
.select(["id", "completed_at"])
|
|
.execute();
|
|
|
|
expect(allRequests.length).toBe(2);
|
|
|
|
const completedCount = allRequests.filter(
|
|
(r) => r.completed_at !== null,
|
|
).length;
|
|
expect(completedCount).toBe(1);
|
|
|
|
const uncompletedCount = allRequests.filter(
|
|
(r) => r.completed_at === null,
|
|
).length;
|
|
expect(uncompletedCount).toBe(1);
|
|
});
|
|
});
|
|
});
|
|
}); // Close describeE2E("admin")
|