Fix lint errors and add ast-grep rule for countAll

- Fix template literal expressions: wrap Date.now() in String()
- Add missing afterAll import in admin.test.ts
- Fix countOwners to use countAll() without misleading <number> type
- Add ast-grep rule to prevent countAll<number>() usage
- Fix formatting issues from merge conflict resolution

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
igm
2026-01-12 13:40:06 +08:00
parent 92f7e1df09
commit 6fa4da1abb
8 changed files with 3866 additions and 3442 deletions

View File

@@ -0,0 +1,8 @@
id: no-countall-number
language: typescript
severity: error
message: "Don't use countAll<number>() - use countAll() instead. PostgreSQL COUNT returns bigint (string), so the type annotation is misleading."
note: "Use Number() to convert the result if you need a number type."
rule:
pattern: countAll<number>()
fix: countAll()

View File

@@ -27,7 +27,7 @@
import type { Database } from "@reviq/db-schema";
import type { Kysely } from "kysely";
import type { APIContext } from "../../context.js";
import { beforeAll, describe, expect, test } from "bun:test";
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { call } from "@orpc/server";
import {
createTestUser,
@@ -115,7 +115,9 @@ async function createOrg(
logoUrl?: string;
},
): Promise<{ id: number; slug: string }> {
const slug = options?.slug ?? `org-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`;
const slug =
options?.slug ??
`org-${String(Date.now())}-${String(Math.random()).slice(2, 8)}`;
const result = await db
.insertInto("orgs")
@@ -182,7 +184,8 @@ async function createLoginRequest(
},
): Promise<{ id: number; token: string }> {
const token = `login-${String(Date.now())}${String(Math.random())}`;
const expiresAt = options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
const expiresAt =
options?.expiresAt ?? new Date(Date.now() + LOGIN_REQUEST_EXPIRY_MS);
const result = await db
.insertInto("login_requests")
@@ -225,7 +228,7 @@ async function createOrgInvite(
.returning("id")
.executeTakeFirstOrThrow();
return { id: Number(result.id) };
return { id: result.id };
}
describeE2E("admin", () => {
@@ -280,7 +283,9 @@ describe("admin.users.list", () => {
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
const users = await call(router.admin.users.list, undefined, { context });
const users = await call(router.admin.users.list, undefined, {
context,
});
expect(users.length).toBe(3);
const emails = users.map((u) => u.email).sort();
@@ -303,7 +308,9 @@ describe("admin.users.list", () => {
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
const users = await call(router.admin.users.list, undefined, { context });
const users = await call(router.admin.users.list, undefined, {
context,
});
const adminUser = users.find((u) => u.email === "admin@example.com");
expect(adminUser).toBeDefined();
@@ -326,7 +333,9 @@ describe("admin.users.list", () => {
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
const users = await call(router.admin.users.list, undefined, { context });
const users = await call(router.admin.users.list, undefined, {
context,
});
// Only the admin user exists
expect(users.length).toBe(1);
@@ -452,7 +461,7 @@ describe("admin.users.create", () => {
test("creates passwordless user", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -483,7 +492,7 @@ describe("admin.users.create", () => {
test("creates user with name", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -510,7 +519,7 @@ describe("admin.users.create", () => {
test("creates user and adds to organization as member", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -523,7 +532,10 @@ describe("admin.users.create", () => {
await call(
router.admin.users.create,
{ email: `orguser-${uniqueId}@example.com`, orgSlug: `test-org-${uniqueId}` },
{
email: `orguser-${uniqueId}@example.com`,
orgSlug: `test-org-${uniqueId}`,
},
{ context },
);
@@ -542,7 +554,7 @@ describe("admin.users.create", () => {
test("creates user and adds to organization with custom role", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -555,7 +567,11 @@ describe("admin.users.create", () => {
await call(
router.admin.users.create,
{ email: `adminuser-${uniqueId}@example.com`, orgSlug: `test-org-${uniqueId}`, orgRole: "admin" },
{
email: `adminuser-${uniqueId}@example.com`,
orgSlug: `test-org-${uniqueId}`,
orgRole: "admin",
},
{ context },
);
@@ -572,7 +588,7 @@ describe("admin.users.create", () => {
test("normalizes email to lowercase", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -599,7 +615,7 @@ describe("admin.users.create", () => {
test("throws CONFLICT for duplicate email", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -621,7 +637,7 @@ describe("admin.users.create", () => {
test("throws NOT_FOUND for non-existent org", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -634,7 +650,10 @@ describe("admin.users.create", () => {
await expect(
call(
router.admin.users.create,
{ email: `newuser-${uniqueId}@example.com`, orgSlug: "nonexistent-org" },
{
email: `newuser-${uniqueId}@example.com`,
orgSlug: "nonexistent-org",
},
{ context },
),
).rejects.toThrow("Organization not found");
@@ -1041,13 +1060,15 @@ describe("admin.orgs.create", () => {
test("creates organization with owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
isSuperuser: true,
});
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
@@ -1077,7 +1098,7 @@ describe("admin.orgs.create", () => {
// Verify owner membership
const membership = await db
.selectFrom("org_members")
.where("org_id", "=", org!.id)
.where("org_id", "=", org?.id)
.where("user_id", "=", owner.id)
.selectAll()
.executeTakeFirst();
@@ -1088,7 +1109,7 @@ describe("admin.orgs.create", () => {
test("normalizes owner email to lowercase", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1114,7 +1135,7 @@ describe("admin.orgs.create", () => {
test("throws NOT_FOUND for non-existent owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1139,13 +1160,15 @@ describe("admin.orgs.create", () => {
test("throws CONFLICT for duplicate slug", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
isSuperuser: true,
});
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
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);
@@ -1356,19 +1379,26 @@ describe("admin.orgs.delete", () => {
test("deletes organization and related records", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
isSuperuser: true,
});
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
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);
await createOrgInvite(
db,
org.id,
`invite-${uniqueId}@example.com`,
admin.id,
);
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
@@ -1414,7 +1444,7 @@ describe("admin.orgs.delete", () => {
test("throws NOT_FOUND for non-existent organization", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1514,7 +1544,11 @@ describe("admin.orgs.listSites", () => {
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.admin.orgs.listSites, { slug: "nonexistent" }, { context }),
call(
router.admin.orgs.listSites,
{ slug: "nonexistent" },
{ context },
),
).rejects.toThrow("Organization not found");
});
});
@@ -1530,7 +1564,7 @@ describe("admin.orgs.addSite", () => {
test("adds site to organization", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1562,7 +1596,7 @@ describe("admin.orgs.addSite", () => {
test("throws NOT_FOUND for non-existent organization", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1583,7 +1617,7 @@ describe("admin.orgs.addSite", () => {
test("throws CONFLICT for duplicate domain", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1606,7 +1640,7 @@ describe("admin.orgs.addSite", () => {
test("throws CONFLICT for domain in another organization", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
@@ -1739,7 +1773,11 @@ describe("admin.auth.completeLogin", () => {
isSuperuser: true,
});
const user = await createTestUser(db, { email: "user@example.com" });
const loginRequest = await createLoginRequest(db, user.id, "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 });
@@ -1770,7 +1808,11 @@ describe("admin.auth.completeLogin", () => {
isSuperuser: true,
});
const user = await createTestUser(db, { email: "user@example.com" });
const loginRequest = await createLoginRequest(db, user.id, "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 });
@@ -1890,10 +1932,14 @@ describe("admin.auth.completeLogin", () => {
expect(allRequests.length).toBe(2);
const completedCount = allRequests.filter((r) => r.completed_at !== null).length;
const completedCount = allRequests.filter(
(r) => r.completed_at !== null,
).length;
expect(completedCount).toBe(1);
const uncompletedCount = allRequests.filter((r) => r.completed_at === null).length;
const uncompletedCount = allRequests.filter(
(r) => r.completed_at === null,
).length;
expect(uncompletedCount).toBe(1);
});
});

View File

@@ -224,7 +224,7 @@ describeE2E("me", () => {
const user = await createTestUser(db, { email: "expired@example.com" });
// Create an expired session
const token = `expired-session-${Date.now()}`;
const token = `expired-session-${String(Date.now())}`;
const tokenHashValue = await hashToken(token);
await db
.insertInto("sessions")
@@ -249,7 +249,7 @@ describeE2E("me", () => {
const user = await createTestUser(db, { email: "revoked@example.com" });
// Create a revoked session
const token = `revoked-session-${Date.now()}`;
const token = `revoked-session-${String(Date.now())}`;
const tokenHashValue = await hashToken(token);
await db
.insertInto("sessions")
@@ -1583,7 +1583,7 @@ async function createOrgInvite(
.returning("id")
.executeTakeFirstOrThrow();
return { id: Number(result.id) };
return { id: result.id };
}
describeE2E("me.apiTokens and me.invites", () => {
@@ -1598,7 +1598,9 @@ describeE2E("me.apiTokens and me.invites", () => {
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const tokens = await call(router.me.apiTokens.list, undefined, { context });
const tokens = await call(router.me.apiTokens.list, undefined, {
context,
});
expect(tokens).toHaveLength(0);
});
@@ -1619,15 +1621,27 @@ describeE2E("me.apiTokens and me.invites", () => {
await db
.insertInto("api_tokens")
.values([
{ user_id: user.id, token_hash: tokenHash1, name: "Token One", expires_at: expiresAt },
{ user_id: user.id, token_hash: tokenHash2, name: "Token Two", expires_at: expiresAt },
{
user_id: user.id,
token_hash: tokenHash1,
name: "Token One",
expires_at: expiresAt,
},
{
user_id: user.id,
token_hash: tokenHash2,
name: "Token Two",
expires_at: expiresAt,
},
])
.execute();
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const tokens = await call(router.me.apiTokens.list, undefined, { context });
const tokens = await call(router.me.apiTokens.list, undefined, {
context,
});
expect(tokens).toHaveLength(2);
const names = tokens.map((t) => t.name).sort();
@@ -1640,8 +1654,14 @@ describeE2E("me.apiTokens and me.invites", () => {
test("only returns current user tokens", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user1 = await createTestUser(db, { email: "user1@example.com", isSuperuser: true });
const user2 = await createTestUser(db, { email: "user2@example.com", isSuperuser: true });
const user1 = await createTestUser(db, {
email: "user1@example.com",
isSuperuser: true,
});
const user2 = await createTestUser(db, {
email: "user2@example.com",
isSuperuser: true,
});
const tokenHash1 = await hashToken("token1");
const tokenHash2 = await hashToken("token2");
@@ -1650,15 +1670,27 @@ describeE2E("me.apiTokens and me.invites", () => {
await db
.insertInto("api_tokens")
.values([
{ user_id: user1.id, token_hash: tokenHash1, name: "User1 Token", expires_at: expiresAt },
{ user_id: user2.id, token_hash: tokenHash2, name: "User2 Token", expires_at: expiresAt },
{
user_id: user1.id,
token_hash: tokenHash1,
name: "User1 Token",
expires_at: expiresAt,
},
{
user_id: user2.id,
token_hash: tokenHash2,
name: "User2 Token",
expires_at: expiresAt,
},
])
.execute();
const { token: sessionToken } = await createSession(db, user1.id);
const context = createAPIContext(db, { sessionToken });
const tokens = await call(router.me.apiTokens.list, undefined, { context });
const tokens = await call(router.me.apiTokens.list, undefined, {
context,
});
expect(tokens).toHaveLength(1);
expect(tokens[0].name).toBe("User1 Token");
@@ -1746,7 +1778,12 @@ describe("me.apiTokens.delete", () => {
const insertResult = await db
.insertInto("api_tokens")
.values({ user_id: user.id, token_hash: tokenHash, name: "To Delete", expires_at: expiresAt })
.values({
user_id: user.id,
token_hash: tokenHash,
name: "To Delete",
expires_at: expiresAt,
})
.returning("id")
.executeTakeFirstOrThrow();
@@ -1774,15 +1811,26 @@ describe("me.apiTokens.delete", () => {
test("cannot delete other user token", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user1 = await createTestUser(db, { email: "owner@example.com", isSuperuser: true });
const user2 = await createTestUser(db, { email: "other@example.com", isSuperuser: true });
const user1 = await createTestUser(db, {
email: "owner@example.com",
isSuperuser: true,
});
const user2 = await createTestUser(db, {
email: "other@example.com",
isSuperuser: true,
});
const tokenHash = await hashToken("other-token");
const expiresAt = new Date(Date.now() + API_TOKEN_EXPIRY_MS);
const insertResult = await db
.insertInto("api_tokens")
.values({ user_id: user1.id, token_hash: tokenHash, name: "User1 Token", expires_at: expiresAt })
.values({
user_id: user1.id,
token_hash: tokenHash,
name: "User1 Token",
expires_at: expiresAt,
})
.returning("id")
.executeTakeFirstOrThrow();
@@ -1790,7 +1838,11 @@ describe("me.apiTokens.delete", () => {
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.me.apiTokens.delete, { tokenId: Number(insertResult.id) }, { context }),
call(
router.me.apiTokens.delete,
{ tokenId: Number(insertResult.id) },
{ context },
),
).rejects.toThrow("API token not found");
});
});
@@ -1823,11 +1875,16 @@ describe("me.invites.list", () => {
email: "inviter@example.com",
emailVerifiedAt: new Date(),
});
const org = await createOrg(db, { slug: "test-org", displayName: "Test Org" });
const org = await createOrg(db, {
slug: "test-org",
displayName: "Test Org",
});
await addOrgMember(db, org.id, inviter.id, "owner");
// User without verified email
const user = await createTestUser(db, { email: "unverified@example.com" });
const user = await createTestUser(db, {
email: "unverified@example.com",
});
// Create an invite for the unverified user
await createOrgInvite(db, {
@@ -1839,7 +1896,9 @@ describe("me.invites.list", () => {
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const invites = await call(router.me.invites.list, undefined, { context });
const invites = await call(router.me.invites.list, undefined, {
context,
});
expect(invites).toHaveLength(0);
});
@@ -1852,7 +1911,10 @@ describe("me.invites.list", () => {
emailVerifiedAt: new Date(),
displayName: "Inviter Person",
});
const org = await createOrg(db, { slug: "invite-org", displayName: "Invite Org" });
const org = await createOrg(db, {
slug: "invite-org",
displayName: "Invite Org",
});
await addOrgMember(db, org.id, inviter.id, "owner");
const user = await createTestUser(db, {
@@ -1870,7 +1932,9 @@ describe("me.invites.list", () => {
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const invites = await call(router.me.invites.list, undefined, { context });
const invites = await call(router.me.invites.list, undefined, {
context,
});
expect(invites).toHaveLength(1);
expect(invites[0].org.slug).toBe("invite-org");
@@ -1905,7 +1969,9 @@ describe("me.invites.list", () => {
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const invites = await call(router.me.invites.list, undefined, { context });
const invites = await call(router.me.invites.list, undefined, {
context,
});
expect(invites).toHaveLength(0);
});
@@ -1920,7 +1986,10 @@ describe("me.invites.get", () => {
emailVerifiedAt: new Date(),
displayName: "The Inviter",
});
const org = await createOrg(db, { slug: "get-invite-org", displayName: "Get Invite Org" });
const org = await createOrg(db, {
slug: "get-invite-org",
displayName: "Get Invite Org",
});
await addOrgMember(db, org.id, inviter.id, "owner");
const user = await createTestUser(db, {
@@ -1960,7 +2029,9 @@ describe("me.invites.get", () => {
const org = await createOrg(db, { slug: "unverified-get-org" });
await addOrgMember(db, org.id, inviter.id, "owner");
const user = await createTestUser(db, { email: "unverified2@example.com" });
const user = await createTestUser(db, {
email: "unverified2@example.com",
});
const invite = await createOrgInvite(db, {
orgId: org.id,
@@ -2015,7 +2086,7 @@ describe("me.invites.get", () => {
describe("me.invites.accept", () => {
test("accepts invite and adds user to org", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const inviter = await createTestUser(db, {
email: `inviter-accept-${uniqueId}@example.com`,
@@ -2069,9 +2140,18 @@ describe("me.invites.accept", () => {
expect(inviteCheck).toBeUndefined();
} finally {
// Cleanup
await db.deleteFrom("org_members").where("org_id", "=", org.id).execute();
await db.deleteFrom("org_invites").where("org_id", "=", org.id).execute();
await db.deleteFrom("sessions").where("user_id", "=", user.id).execute();
await db
.deleteFrom("org_members")
.where("org_id", "=", org.id)
.execute();
await db
.deleteFrom("org_invites")
.where("org_id", "=", org.id)
.execute();
await db
.deleteFrom("sessions")
.where("user_id", "=", user.id)
.execute();
await db.deleteFrom("orgs").where("id", "=", org.id).execute();
await db.deleteFrom("users").where("id", "=", user.id).execute();
await db.deleteFrom("users").where("id", "=", inviter.id).execute();
@@ -2087,7 +2167,9 @@ describe("me.invites.accept", () => {
const org = await createOrg(db, { slug: "unverified-accept-org" });
await addOrgMember(db, org.id, inviter.id, "owner");
const user = await createTestUser(db, { email: "unverified3@example.com" });
const user = await createTestUser(db, {
email: "unverified3@example.com",
});
const invite = await createOrgInvite(db, {
orgId: org.id,
@@ -2106,13 +2188,15 @@ describe("me.invites.accept", () => {
test("returns error if already a member", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const inviter = await createTestUser(db, {
email: `inviter-already-${uniqueId}@example.com`,
emailVerifiedAt: new Date(),
});
const org = await createOrg(db, { slug: `already-member-org-${uniqueId}` });
const org = await createOrg(db, {
slug: `already-member-org-${uniqueId}`,
});
await addOrgMember(db, org.id, inviter.id, "owner");
const user = await createTestUser(db, {
@@ -2139,9 +2223,18 @@ describe("me.invites.accept", () => {
).rejects.toThrow("You are already a member of this organization");
} finally {
// Cleanup
await db.deleteFrom("org_members").where("org_id", "=", org.id).execute();
await db.deleteFrom("org_invites").where("org_id", "=", org.id).execute();
await db.deleteFrom("sessions").where("user_id", "=", user.id).execute();
await db
.deleteFrom("org_members")
.where("org_id", "=", org.id)
.execute();
await db
.deleteFrom("org_invites")
.where("org_id", "=", org.id)
.execute();
await db
.deleteFrom("sessions")
.where("user_id", "=", user.id)
.execute();
await db.deleteFrom("orgs").where("id", "=", org.id).execute();
await db.deleteFrom("users").where("id", "=", user.id).execute();
await db.deleteFrom("users").where("id", "=", inviter.id).execute();
@@ -2256,4 +2349,5 @@ describe("me.invites.decline", () => {
).rejects.toThrow("Invitation not found");
});
});
}); // Close describe for me.invites.decline
}); // Close describeE2E for me.apiTokens and me.invites

View File

@@ -11,6 +11,7 @@
import type { Database } from "@reviq/db-schema";
import type { Kysely } from "kysely";
import type { APIContext } from "../../context.js";
import { beforeAll, describe, expect, test } from "bun:test";
import { call } from "@orpc/server";
import {
@@ -19,10 +20,8 @@ import {
getSharedDb,
initTestDb,
TEST_RP,
truncateAllTables,
withTestTransaction,
} from "@reviq/test-helpers";
import type { APIContext } from "../../context.js";
import { router } from "../../router.js";
import { COOKIE_NAMES } from "../../utils/cookies.js";
import { hashToken } from "../../utils/crypto.js";
@@ -167,8 +166,11 @@ async function createOrgInvite(
expiresAt?: Date;
},
): Promise<{ id: number; token: string }> {
const token = options?.token ?? `invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const expiresAt = options?.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const token =
options?.token ??
`invite-${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const expiresAt =
options?.expiresAt ?? new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);
const result = await db
.insertInto("org_invites")
@@ -209,7 +211,11 @@ describeE2E("orgs", () => {
const context = createAPIContext(db);
await expect(
call(router.orgs.create, { slug: "test", displayName: "Test" }, { context }),
call(
router.orgs.create,
{ slug: "test", displayName: "Test" },
{ context },
),
).rejects.toThrow("No session or API key");
});
});
@@ -243,8 +249,14 @@ describe("orgs.list", () => {
test("returns orgs where user is a member", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, { email: "user@example.com" });
const org1 = await createOrg(db, { slug: "org-one", displayName: "Org One" });
const org2 = await createOrg(db, { slug: "org-two", displayName: "Org Two" });
const org1 = await createOrg(db, {
slug: "org-one",
displayName: "Org One",
});
const org2 = await createOrg(db, {
slug: "org-two",
displayName: "Org Two",
});
await addOrgMember(db, org1.id, user.id, "owner");
await addOrgMember(db, org2.id, user.id, "member");
@@ -262,7 +274,9 @@ describe("orgs.list", () => {
test("does not return orgs where user is not a member", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, { email: "user@example.com" });
const otherUser = await createTestUser(db, { email: "other@example.com" });
const otherUser = await createTestUser(db, {
email: "other@example.com",
});
const org1 = await createOrg(db, { slug: "my-org" });
const org2 = await createOrg(db, { slug: "other-org" });
await addOrgMember(db, org1.id, user.id, "owner");
@@ -305,9 +319,11 @@ describe("orgs.list", () => {
describe("orgs.create", () => {
test("creates org and makes user owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const user = await createTestUser(db, { email: `user-${uniqueId}@example.com` });
const user = await createTestUser(db, {
email: `user-${uniqueId}@example.com`,
});
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
@@ -333,9 +349,11 @@ describe("orgs.create", () => {
test("rejects duplicate slug", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const user = await createTestUser(db, { email: `user-${uniqueId}@example.com` });
const user = await createTestUser(db, {
email: `user-${uniqueId}@example.com`,
});
await createOrg(db, { slug: `existing-${uniqueId}` });
const { token: sessionToken } = await createSession(db, user.id);
@@ -367,7 +385,11 @@ describe("orgs.get", () => {
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
const result = await call(router.orgs.get, { slug: "test-org" }, { context });
const result = await call(
router.orgs.get,
{ slug: "test-org" },
{ context },
);
expect(result.slug).toBe("test-org");
expect(result.displayName).toBe("Test Org");
@@ -378,7 +400,9 @@ describe("orgs.get", () => {
test("rejects when user is not a member", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, { email: "user@example.com" });
const otherUser = await createTestUser(db, { email: "other@example.com" });
const otherUser = await createTestUser(db, {
email: "other@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, otherUser.id, "owner");
@@ -411,7 +435,10 @@ describe("orgs.update", () => {
test("updates display name when user is admin", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, { email: "user@example.com" });
const org = await createOrg(db, { slug: "test-org", displayName: "Old Name" });
const org = await createOrg(db, {
slug: "test-org",
displayName: "Old Name",
});
await addOrgMember(db, org.id, user.id, "admin");
const { token: sessionToken } = await createSession(db, user.id);
@@ -480,7 +507,9 @@ describe("orgs.update", () => {
test("rejects when user is not a member", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, { email: "user@example.com" });
const otherUser = await createTestUser(db, { email: "other@example.com" });
const otherUser = await createTestUser(db, {
email: "other@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, otherUser.id, "owner");
@@ -503,16 +532,22 @@ describe("orgs.update", () => {
describe("orgs.delete", () => {
test("deletes org when user is owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const user = await createTestUser(db, { email: `user-${uniqueId}@example.com` });
const user = await createTestUser(db, {
email: `user-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `delete-org-${uniqueId}` });
await addOrgMember(db, org.id, user.id, "owner");
const { token: sessionToken } = await createSession(db, user.id);
const context = createAPIContext(db, { sessionToken });
await call(router.orgs.delete, { slug: `delete-org-${uniqueId}` }, { context });
await call(
router.orgs.delete,
{ slug: `delete-org-${uniqueId}` },
{ context },
);
const deleted = await db
.selectFrom("orgs")
@@ -546,10 +581,14 @@ describe("orgs.delete", () => {
describe("orgs.leave", () => {
test("allows member to leave org", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `leave-org-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -557,7 +596,11 @@ describe("orgs.leave", () => {
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
await call(router.orgs.leave, { slug: `leave-org-${uniqueId}` }, { context });
await call(
router.orgs.leave,
{ slug: `leave-org-${uniqueId}` },
{ context },
);
const membership = await db
.selectFrom("org_members")
@@ -571,10 +614,14 @@ describe("orgs.leave", () => {
test("allows owner to leave when there are other owners", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com` });
const owner2 = await createTestUser(db, { email: `owner2-${uniqueId}@example.com` });
const owner1 = await createTestUser(db, {
email: `owner1-${uniqueId}@example.com`,
});
const owner2 = await createTestUser(db, {
email: `owner2-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `leave-org-${uniqueId}` });
await addOrgMember(db, org.id, owner1.id, "owner");
await addOrgMember(db, org.id, owner2.id, "owner");
@@ -582,7 +629,11 @@ describe("orgs.leave", () => {
const { token: sessionToken } = await createSession(db, owner1.id);
const context = createAPIContext(db, { sessionToken });
await call(router.orgs.leave, { slug: `leave-org-${uniqueId}` }, { context });
await call(
router.orgs.leave,
{ slug: `leave-org-${uniqueId}` },
{ context },
);
const membership = await db
.selectFrom("org_members")
@@ -596,9 +647,11 @@ describe("orgs.leave", () => {
test("prevents only owner from leaving", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `leave-only-owner-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
@@ -606,7 +659,11 @@ describe("orgs.leave", () => {
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.orgs.leave, { slug: `leave-only-owner-${uniqueId}` }, { context }),
call(
router.orgs.leave,
{ slug: `leave-only-owner-${uniqueId}` },
{ context },
),
).rejects.toThrow("Cannot leave as the only owner");
});
@@ -632,9 +689,18 @@ describe("orgs.leave", () => {
describe("orgs.members.list", () => {
test("returns all members of org", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com", displayName: "Owner" });
const admin = await createTestUser(db, { email: "admin@example.com", displayName: "Admin" });
const member = await createTestUser(db, { email: "member@example.com", displayName: "Member" });
const owner = await createTestUser(db, {
email: "owner@example.com",
displayName: "Owner",
});
const admin = await createTestUser(db, {
email: "admin@example.com",
displayName: "Admin",
});
const member = await createTestUser(db, {
email: "member@example.com",
displayName: "Member",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin.id, "admin");
@@ -643,23 +709,38 @@ describe("orgs.members.list", () => {
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
const members = await call(router.orgs.members.list, { slug: "test-org" }, { context });
const members = await call(
router.orgs.members.list,
{ slug: "test-org" },
{ context },
);
expect(members.length).toBe(3);
expect(members.map((m) => m.role).sort()).toEqual(["admin", "member", "owner"]);
expect(members.map((m) => m.role).sort()).toEqual([
"admin",
"member",
"owner",
]);
});
});
test("includes user details", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com", displayName: "Test Owner" });
const owner = await createTestUser(db, {
email: "owner@example.com",
displayName: "Test Owner",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
const { token: sessionToken } = await createSession(db, owner.id);
const context = createAPIContext(db, { sessionToken });
const members = await call(router.orgs.members.list, { slug: "test-org" }, { context });
const members = await call(
router.orgs.members.list,
{ slug: "test-org" },
{ context },
);
expect(members[0]?.email).toBe("owner@example.com");
expect(members[0]?.displayName).toBe("Test Owner");
@@ -689,10 +770,14 @@ describe("orgs.members.list", () => {
describe("orgs.members.updateRole", () => {
test("owner can promote member to admin", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `update-role-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -718,10 +803,14 @@ describe("orgs.members.updateRole", () => {
test("owner can promote member to owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `update-role-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -747,10 +836,14 @@ describe("orgs.members.updateRole", () => {
test("owner can demote owner to admin when multiple owners exist", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com` });
const owner2 = await createTestUser(db, { email: `owner2-${uniqueId}@example.com` });
const owner1 = await createTestUser(db, {
email: `owner1-${uniqueId}@example.com`,
});
const owner2 = await createTestUser(db, {
email: `owner2-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `update-role-${uniqueId}` });
await addOrgMember(db, org.id, owner1.id, "owner");
await addOrgMember(db, org.id, owner2.id, "owner");
@@ -776,9 +869,11 @@ describe("orgs.members.updateRole", () => {
test("prevents demoting the only owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `update-role-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
@@ -798,7 +893,9 @@ describe("orgs.members.updateRole", () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const admin = await createTestUser(db, { email: "admin@example.com" });
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin.id, "admin");
@@ -819,9 +916,11 @@ describe("orgs.members.updateRole", () => {
test("rejects when target member not found", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `update-role-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
@@ -843,10 +942,14 @@ describe("orgs.members.updateRole", () => {
describe("orgs.members.remove", () => {
test("owner can remove member", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `remove-member-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -872,10 +975,14 @@ describe("orgs.members.remove", () => {
test("owner can remove admin", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `remove-admin-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin.id, "admin");
@@ -901,10 +1008,14 @@ describe("orgs.members.remove", () => {
test("owner can remove other owner when multiple owners exist", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner1 = await createTestUser(db, { email: `owner1-${uniqueId}@example.com` });
const owner2 = await createTestUser(db, { email: `owner2-${uniqueId}@example.com` });
const owner1 = await createTestUser(db, {
email: `owner1-${uniqueId}@example.com`,
});
const owner2 = await createTestUser(db, {
email: `owner2-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `remove-owner-${uniqueId}` });
await addOrgMember(db, org.id, owner1.id, "owner");
await addOrgMember(db, org.id, owner2.id, "owner");
@@ -930,10 +1041,14 @@ describe("orgs.members.remove", () => {
test("prevents removing the only owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const org = await createOrg(db, { slug: `remove-only-owner-${uniqueId}` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, {
slug: `remove-only-owner-${uniqueId}`,
});
await addOrgMember(db, org.id, owner.id, "owner");
const { token: sessionToken } = await createSession(db, owner.id);
@@ -950,11 +1065,17 @@ describe("orgs.members.remove", () => {
test("admin can remove member", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `admin-remove-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin.id, "admin");
@@ -981,11 +1102,17 @@ describe("orgs.members.remove", () => {
test("admin cannot remove owner", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com` });
const org = await createOrg(db, { slug: `admin-no-remove-owner-${uniqueId}` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
});
const org = await createOrg(db, {
slug: `admin-no-remove-owner-${uniqueId}`,
});
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin.id, "admin");
@@ -1003,12 +1130,20 @@ describe("orgs.members.remove", () => {
test("admin cannot remove other admin", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const admin1 = await createTestUser(db, { email: `admin1-${uniqueId}@example.com` });
const admin2 = await createTestUser(db, { email: `admin2-${uniqueId}@example.com` });
const org = await createOrg(db, { slug: `admin-no-remove-admin-${uniqueId}` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const admin1 = await createTestUser(db, {
email: `admin1-${uniqueId}@example.com`,
});
const admin2 = await createTestUser(db, {
email: `admin2-${uniqueId}@example.com`,
});
const org = await createOrg(db, {
slug: `admin-no-remove-admin-${uniqueId}`,
});
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, admin1.id, "admin");
await addOrgMember(db, org.id, admin2.id, "admin");
@@ -1027,11 +1162,17 @@ describe("orgs.members.remove", () => {
test("member cannot remove anyone", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member1 = await createTestUser(db, { email: `member1-${uniqueId}@example.com` });
const member2 = await createTestUser(db, { email: `member2-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member1 = await createTestUser(db, {
email: `member1-${uniqueId}@example.com`,
});
const member2 = await createTestUser(db, {
email: `member2-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `member-no-remove-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member1.id, "member");
@@ -1051,9 +1192,11 @@ describe("orgs.members.remove", () => {
test("rejects when target member not found", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `remove-not-found-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
@@ -1075,19 +1218,33 @@ describe("orgs.members.remove", () => {
describe("orgs.invites.list", () => {
test("returns pending invites for org", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, { email: "admin@example.com", displayName: "Admin User" });
const admin = await createTestUser(db, {
email: "admin@example.com",
displayName: "Admin User",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, admin.id, "admin");
await createOrgInvite(db, org.id, "invite1@example.com", admin.id, { role: "member" });
await createOrgInvite(db, org.id, "invite2@example.com", admin.id, { role: "admin" });
await createOrgInvite(db, org.id, "invite1@example.com", admin.id, {
role: "member",
});
await createOrgInvite(db, org.id, "invite2@example.com", admin.id, {
role: "admin",
});
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
const invites = await call(router.orgs.invites.list, { slug: "test-org" }, { context });
const invites = await call(
router.orgs.invites.list,
{ slug: "test-org" },
{ context },
);
expect(invites.length).toBe(2);
expect(invites.map((i) => i.email).sort()).toEqual(["invite1@example.com", "invite2@example.com"]);
expect(invites.map((i) => i.email).sort()).toEqual([
"invite1@example.com",
"invite2@example.com",
]);
});
});
@@ -1104,7 +1261,11 @@ describe("orgs.invites.list", () => {
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
const invites = await call(router.orgs.invites.list, { slug: "test-org" }, { context });
const invites = await call(
router.orgs.invites.list,
{ slug: "test-org" },
{ context },
);
expect(invites.length).toBe(1);
expect(invites[0]?.email).toBe("active@example.com");
@@ -1114,7 +1275,9 @@ describe("orgs.invites.list", () => {
test("member cannot list invites", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -1134,9 +1297,11 @@ describe("orgs.invites.list", () => {
describe("orgs.invites.create", () => {
test("admin can create member invite", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com` });
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `invite-org-${uniqueId}` });
await addOrgMember(db, org.id, admin.id, "admin");
@@ -1145,7 +1310,11 @@ describe("orgs.invites.create", () => {
await call(
router.orgs.invites.create,
{ slug: `invite-org-${uniqueId}`, email: `new-${uniqueId}@example.com`, role: "member" },
{
slug: `invite-org-${uniqueId}`,
email: `new-${uniqueId}@example.com`,
role: "member",
},
{ context },
);
@@ -1162,9 +1331,11 @@ describe("orgs.invites.create", () => {
test("admin can create admin invite", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const admin = await createTestUser(db, { email: `admin-${uniqueId}@example.com` });
const admin = await createTestUser(db, {
email: `admin-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `invite-org-${uniqueId}` });
await addOrgMember(db, org.id, admin.id, "admin");
@@ -1173,7 +1344,11 @@ describe("orgs.invites.create", () => {
await call(
router.orgs.invites.create,
{ slug: `invite-org-${uniqueId}`, email: `new-${uniqueId}@example.com`, role: "admin" },
{
slug: `invite-org-${uniqueId}`,
email: `new-${uniqueId}@example.com`,
role: "admin",
},
{ context },
);
@@ -1210,9 +1385,11 @@ describe("orgs.invites.create", () => {
test("owner can create owner invite", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `invite-org-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
@@ -1221,7 +1398,11 @@ describe("orgs.invites.create", () => {
await call(
router.orgs.invites.create,
{ slug: `invite-org-${uniqueId}`, email: `new-${uniqueId}@example.com`, role: "owner" },
{
slug: `invite-org-${uniqueId}`,
email: `new-${uniqueId}@example.com`,
role: "owner",
},
{ context },
);
@@ -1238,7 +1419,9 @@ describe("orgs.invites.create", () => {
test("rejects invite for existing member", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -1279,7 +1462,9 @@ describe("orgs.invites.create", () => {
test("member cannot create invite", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
@@ -1306,7 +1491,12 @@ describe("orgs.invites.cancel", () => {
const admin = await createTestUser(db, { email: "admin@example.com" });
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, admin.id, "admin");
const invite = await createOrgInvite(db, org.id, "invited@example.com", admin.id);
const invite = await createOrgInvite(
db,
org.id,
"invited@example.com",
admin.id,
);
const { token: sessionToken } = await createSession(db, admin.id);
const context = createAPIContext(db, { sessionToken });
@@ -1349,11 +1539,18 @@ describe("orgs.invites.cancel", () => {
test("member cannot cancel invite", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
const invite = await createOrgInvite(db, org.id, "invited@example.com", owner.id);
const invite = await createOrgInvite(
db,
org.id,
"invited@example.com",
owner.id,
);
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
@@ -1374,18 +1571,32 @@ describe("orgs.invites.cancel", () => {
describe("orgs.invites.accept", () => {
test("accepts invite and adds user to org", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const invitee = await createTestUser(db, { email: `invitee-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const invitee = await createTestUser(db, {
email: `invitee-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `accept-org-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
const invite = await createOrgInvite(db, org.id, `invitee-${uniqueId}@example.com`, owner.id, { role: "admin" });
const invite = await createOrgInvite(
db,
org.id,
`invitee-${uniqueId}@example.com`,
owner.id,
{ role: "admin" },
);
const { token: sessionToken } = await createSession(db, invitee.id);
const context = createAPIContext(db, { sessionToken });
await call(router.orgs.invites.accept, { token: invite.token }, { context });
await call(
router.orgs.invites.accept,
{ token: invite.token },
{ context },
);
// Verify membership
const membership = await db
@@ -1410,18 +1621,30 @@ describe("orgs.invites.accept", () => {
test("rejects expired invite", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const invitee = await createTestUser(db, { email: "invitee@example.com" });
const invitee = await createTestUser(db, {
email: "invitee@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
const invite = await createOrgInvite(db, org.id, "invitee@example.com", owner.id, {
const invite = await createOrgInvite(
db,
org.id,
"invitee@example.com",
owner.id,
{
expiresAt: new Date(Date.now() - 1000), // expired
});
},
);
const { token: sessionToken } = await createSession(db, invitee.id);
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.orgs.invites.accept, { token: invite.token }, { context }),
call(
router.orgs.invites.accept,
{ token: invite.token },
{ context },
),
).rejects.toThrow("Invalid or expired invitation");
});
});
@@ -1434,7 +1657,11 @@ describe("orgs.invites.accept", () => {
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.orgs.invites.accept, { token: "invalid-token" }, { context }),
call(
router.orgs.invites.accept,
{ token: "invalid-token" },
{ context },
),
).rejects.toThrow("Invalid or expired invitation");
});
});
@@ -1442,31 +1669,55 @@ describe("orgs.invites.accept", () => {
test("rejects when email doesn't match", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const owner = await createTestUser(db, { email: "owner@example.com" });
const invitee = await createTestUser(db, { email: "invitee@example.com" });
const wrongUser = await createTestUser(db, { email: "wrong@example.com" });
const _invitee = await createTestUser(db, {
email: "invitee@example.com",
});
const wrongUser = await createTestUser(db, {
email: "wrong@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, owner.id, "owner");
const invite = await createOrgInvite(db, org.id, "invitee@example.com", owner.id);
const invite = await createOrgInvite(
db,
org.id,
"invitee@example.com",
owner.id,
);
const { token: sessionToken } = await createSession(db, wrongUser.id);
const context = createAPIContext(db, { sessionToken });
await expect(
call(router.orgs.invites.accept, { token: invite.token }, { context }),
).rejects.toThrow("This invitation was sent to a different email address");
call(
router.orgs.invites.accept,
{ token: invite.token },
{ context },
),
).rejects.toThrow(
"This invitation was sent to a different email address",
);
});
});
test("handles already a member gracefully", async () => {
const db = getSharedDb();
const uniqueId = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
const uniqueId = `${String(Date.now())}-${Math.random().toString(36).slice(2, 8)}`;
const owner = await createTestUser(db, { email: `owner-${uniqueId}@example.com` });
const member = await createTestUser(db, { email: `member-${uniqueId}@example.com` });
const owner = await createTestUser(db, {
email: `owner-${uniqueId}@example.com`,
});
const member = await createTestUser(db, {
email: `member-${uniqueId}@example.com`,
});
const org = await createOrg(db, { slug: `test-org-${uniqueId}` });
await addOrgMember(db, org.id, owner.id, "owner");
await addOrgMember(db, org.id, member.id, "member");
const invite = await createOrgInvite(db, org.id, `member-${uniqueId}@example.com`, owner.id);
const invite = await createOrgInvite(
db,
org.id,
`member-${uniqueId}@example.com`,
owner.id,
);
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
@@ -1491,7 +1742,9 @@ describe("orgs.invites.accept", () => {
describe("orgs.sites.list", () => {
test("returns sites for org", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, member.id, "member");
await createSite(db, org.id, "example.com");
@@ -1500,23 +1753,36 @@ describe("orgs.sites.list", () => {
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
const sites = await call(router.orgs.sites.list, { slug: "test-org" }, { context });
const sites = await call(
router.orgs.sites.list,
{ slug: "test-org" },
{ context },
);
expect(sites.length).toBe(2);
expect(sites.map((s) => s.domain).sort()).toEqual(["example.com", "test.com"]);
expect(sites.map((s) => s.domain).sort()).toEqual([
"example.com",
"test.com",
]);
});
});
test("returns empty array when no sites", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, member.id, "member");
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
const sites = await call(router.orgs.sites.list, { slug: "test-org" }, { context });
const sites = await call(
router.orgs.sites.list,
{ slug: "test-org" },
{ context },
);
expect(sites).toHaveLength(0);
});
@@ -1524,7 +1790,9 @@ describe("orgs.sites.list", () => {
test("returns site details including id and createdAt", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const member = await createTestUser(db, { email: "member@example.com" });
const member = await createTestUser(db, {
email: "member@example.com",
});
const org = await createOrg(db, { slug: "test-org" });
await addOrgMember(db, org.id, member.id, "member");
await createSite(db, org.id, "example.com");
@@ -1532,7 +1800,11 @@ describe("orgs.sites.list", () => {
const { token: sessionToken } = await createSession(db, member.id);
const context = createAPIContext(db, { sessionToken });
const sites = await call(router.orgs.sites.list, { slug: "test-org" }, { context });
const sites = await call(
router.orgs.sites.list,
{ slug: "test-org" },
{ context },
);
expect(sites[0]?.id).toBeDefined();
expect(sites[0]?.domain).toBe("example.com");

View File

@@ -68,7 +68,9 @@ export async function signupWithPassword(
// Handle duplicate email (unique constraint violation)
// Use generic error to prevent email enumeration
if (error instanceof Error && error.message.includes("users_email_key")) {
throw new ORPCError("BAD_REQUEST", { message: "Unable to create account" });
throw new ORPCError("BAD_REQUEST", {
message: "Unable to create account",
});
}
throw error;
}
@@ -209,7 +211,9 @@ export async function signupWithPasskey(
// Handle duplicate email (unique constraint violation)
// Use generic error to prevent email enumeration
if (error instanceof Error && error.message.includes("users_email_key")) {
throw new ORPCError("BAD_REQUEST", { message: "Unable to create account" });
throw new ORPCError("BAD_REQUEST", {
message: "Unable to create account",
});
}
throw error;
}

View File

@@ -115,11 +115,11 @@ export async function countOwners(
): Promise<number> {
const result = await db
.selectFrom("org_members")
.select((eb) => eb.fn.countAll<number>().as("count"))
.select((eb) => eb.fn.countAll().as("count"))
.where("org_id", "=", orgId)
.where("role", "=", "owner")
.executeTakeFirstOrThrow();
// PostgreSQL COUNT returns bigint which may be a string; ensure numeric comparison works
// PostgreSQL COUNT returns bigint (string), convert to number
return Number(result.count);
}

View File

@@ -1,4 +1,4 @@
\restrict 7omiXDURqmmr2m2jWDDMoltRzeUAT80fRWiPifpD7IpQGCLgxQNBFsA5uBgakPg
\restrict NNYnwssF6iMx0TXsk1nTprUEDwxna9uejAmsIiUlMLcPLlQlnnRVCusYtzweHXM
-- Dumped from database version 17.7
-- Dumped by pg_dump version 17.7
@@ -1084,7 +1084,7 @@ ALTER TABLE ONLY public.user_devices
-- PostgreSQL database dump complete
--
\unrestrict 7omiXDURqmmr2m2jWDDMoltRzeUAT80fRWiPifpD7IpQGCLgxQNBFsA5uBgakPg
\unrestrict NNYnwssF6iMx0TXsk1nTprUEDwxna9uejAmsIiUlMLcPLlQlnnRVCusYtzweHXM
--

View File

@@ -1,6 +1,6 @@
ruleDirs:
- /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/rules/
- .ast-grep/rules/
testConfigs:
- testDir: /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/rule-tests/
- testDir: .ast-grep/rule-tests/
utilDirs:
- /Users/igm/proj/reviq/publisher-dashboard/.ast-grep/utils/
- .ast-grep/utils/