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", () => {
@@ -263,11 +266,11 @@ describeE2E("admin", () => {
).rejects.toThrow("No session or API key");
});
});
});
});
// ===== admin.users.list =====
// ===== admin.users.list =====
describe("admin.users.list", () => {
describe("admin.users.list", () => {
test("returns all users", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -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,18 +333,20 @@ 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);
expect(users[0]?.email).toBe("onlyadmin@example.com");
});
});
});
});
// ===== admin.users.get =====
// ===== admin.users.get =====
describe("admin.users.get", () => {
describe("admin.users.get", () => {
test("returns user by email", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -439,12 +448,12 @@ describe("admin.users.get", () => {
expect(user.hasPassword).toBe(false);
});
});
});
});
// ===== admin.users.create =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
// ===== admin.users.create =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
describe("admin.users.create", () => {
describe("admin.users.create", () => {
afterAll(async () => {
// Clean up all test data
await truncateAllTables(getSharedDb());
@@ -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,16 +650,19 @@ 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");
});
});
});
// ===== admin.users.update =====
// ===== admin.users.update =====
describe("admin.users.update", () => {
describe("admin.users.update", () => {
test("grants superuser status", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -811,11 +830,11 @@ describe("admin.users.update", () => {
).rejects.toThrow("User not found");
});
});
});
});
// ===== admin.users.confirmEmail =====
// ===== admin.users.confirmEmail =====
describe("admin.users.confirmEmail", () => {
describe("admin.users.confirmEmail", () => {
test("confirms user email", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -917,11 +936,11 @@ describe("admin.users.confirmEmail", () => {
expect(result.success).toBe(true);
});
});
});
});
// ===== admin.orgs.list =====
// ===== admin.orgs.list =====
describe("admin.orgs.list", () => {
describe("admin.orgs.list", () => {
test("returns all organizations", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -982,11 +1001,11 @@ describe("admin.orgs.list", () => {
expect(orgs).toHaveLength(0);
});
});
});
});
// ===== admin.orgs.get =====
// ===== admin.orgs.get =====
describe("admin.orgs.get", () => {
describe("admin.orgs.get", () => {
test("returns organization by slug", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -1029,25 +1048,27 @@ describe("admin.orgs.get", () => {
).rejects.toThrow("Organization not found");
});
});
});
});
// ===== admin.orgs.create =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
// ===== admin.orgs.create =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
describe("admin.orgs.create", () => {
describe("admin.orgs.create", () => {
afterAll(async () => {
await truncateAllTables(getSharedDb());
});
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);
@@ -1163,11 +1186,11 @@ describe("admin.orgs.create", () => {
),
).rejects.toThrow("Organization with this slug already exists");
});
});
});
// ===== admin.orgs.update =====
// ===== admin.orgs.update =====
describe("admin.orgs.update", () => {
describe("admin.orgs.update", () => {
test("updates display name", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -1344,31 +1367,38 @@ describe("admin.orgs.update", () => {
).rejects.toThrow("Organization not found");
});
});
});
});
// ===== admin.orgs.delete =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
// ===== admin.orgs.delete =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
describe("admin.orgs.delete", () => {
describe("admin.orgs.delete", () => {
afterAll(async () => {
await truncateAllTables(getSharedDb());
});
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`,
@@ -1428,11 +1458,11 @@ describe("admin.orgs.delete", () => {
call(router.admin.orgs.delete, { slug: "nonexistent" }, { context }),
).rejects.toThrow("Organization not found");
});
});
});
// ===== admin.orgs.listSites =====
// ===== admin.orgs.listSites =====
describe("admin.orgs.listSites", () => {
describe("admin.orgs.listSites", () => {
test("returns sites for organization", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -1514,23 +1544,27 @@ 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");
});
});
});
});
// ===== admin.orgs.addSite =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
// ===== admin.orgs.addSite =====
// NOTE: These tests don't use withTestTransaction because the procedure uses db.transaction() internally
describe("admin.orgs.addSite", () => {
describe("admin.orgs.addSite", () => {
afterAll(async () => {
await truncateAllTables(getSharedDb());
});
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`,
@@ -1627,11 +1661,11 @@ describe("admin.orgs.addSite", () => {
),
).rejects.toThrow("Site with this domain already exists");
});
});
});
// ===== admin.orgs.removeSite =====
// ===== admin.orgs.removeSite =====
describe("admin.orgs.removeSite", () => {
describe("admin.orgs.removeSite", () => {
test("removes site from organization", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -1727,11 +1761,11 @@ describe("admin.orgs.removeSite", () => {
).rejects.toThrow("Site not found");
});
});
});
});
// ===== admin.auth.completeLogin =====
// ===== admin.auth.completeLogin =====
describe("admin.auth.completeLogin", () => {
describe("admin.auth.completeLogin", () => {
test("completes pending login request", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const admin = await createTestUser(db, {
@@ -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,12 +1932,16 @@ 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);
});
});
});
});
}); // Close describeE2E("admin")

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,23 +1670,35 @@ 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");
});
});
});
});
describe("me.apiTokens.create", () => {
describe("me.apiTokens.create", () => {
test("creates token for superuser with trusted session", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, {
@@ -1731,9 +1763,9 @@ describe("me.apiTokens.create", () => {
).rejects.toThrow("Creating API tokens requires a trusted session");
});
});
});
});
describe("me.apiTokens.delete", () => {
describe("me.apiTokens.delete", () => {
test("deletes own token", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const user = await createTestUser(db, {
@@ -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");
});
});
@@ -1810,24 +1862,29 @@ describe("me.apiTokens.delete", () => {
).rejects.toThrow("API token not found");
});
});
});
});
// =============================================================================
// me.invites tests
// =============================================================================
// =============================================================================
// me.invites tests
// =============================================================================
describe("me.invites.list", () => {
describe("me.invites.list", () => {
test("returns empty list when email not verified", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const inviter = await createTestUser(db, {
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,14 +1969,16 @@ 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);
});
});
});
});
describe("me.invites.get", () => {
describe("me.invites.get", () => {
test("returns invite details", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const inviter = await createTestUser(db, {
@@ -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,
@@ -2010,12 +2081,12 @@ describe("me.invites.get", () => {
).rejects.toThrow("Invitation not found or expired");
});
});
});
});
describe("me.invites.accept", () => {
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();
@@ -2163,9 +2256,9 @@ describe("me.invites.accept", () => {
).rejects.toThrow("Invitation not found or expired");
});
});
});
});
describe("me.invites.decline", () => {
describe("me.invites.decline", () => {
test("declines invite and deletes it", async () => {
await withTestTransaction(getSharedDb(), async (db) => {
const inviter = await createTestUser(db, {
@@ -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

File diff suppressed because it is too large Load Diff

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/