* Fix BetterAuth field mapping and add owner-prefixed team names - Fix auth.server.ts: use camelCase Drizzle field names for BetterAuth adapter (was snake_case, causing user inserts to fail); add generateId: "uuid" so PostgreSQL UUID columns accept generated IDs - Add prependOwnerToTeamName / stripOwnerFromTeamName utilities - Invite join, league creation, and settings assign/remove all now manage the owner-prefix on team names atomically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Complete BetterAuth migration: auth flows, rename, and cleanup - Add /forgot-password and /reset-password pages (full password reset flow) - Add 'Forgot password?' link on login page - Fix register.tsx: add explicit window.location fallback after signUp - Rename commissionerAuditLog.actorClerkId → actorUserId (migration 0084) and update all references in model, routes, components, and tests - Rewrite docs/agents/auth.md to document BetterAuth (removes all Clerk refs) - Update test fixtures and schema comments to remove Clerk ID references; use UUID-format IDs throughout test data - Update docs/agents/domain-models.md ownerId description Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Fix BetterAuth ID generation, clean up review issues - Set generateId: false so BetterAuth omits id from inserts; add $defaultFn(crypto.randomUUID) to accounts/sessions/verifications so Drizzle fills it in (fixes null id constraint violation on signup) - Move renameTeam to static import; rename before removeTeamOwner so a failed rename leaves owner intact rather than leaving a stale prefix - Clarify stripOwnerFromTeamName toTitleCase assumption in comment - Annotate reset-password token fallback to explain loader guarantee Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
115 lines
3.6 KiB
TypeScript
115 lines
3.6 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
vi.mock("~/models/user", () => ({
|
|
isUserAdmin: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("~/database/context", () => ({
|
|
database: vi.fn(),
|
|
}));
|
|
|
|
import { isCommissioner, hasCommissionerRecord } from "../commissioner";
|
|
import { isUserAdmin } from "~/models/user";
|
|
import { database } from "~/database/context";
|
|
|
|
const LEAGUE_ID = "league-1";
|
|
const USER_ID = "user-uuid-1";
|
|
|
|
function makeMockDb(commissionerRow: object | null) {
|
|
return {
|
|
query: {
|
|
commissioners: {
|
|
findFirst: vi.fn().mockResolvedValue(commissionerRow),
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe("isCommissioner", () => {
|
|
it("returns true for a site admin without a commissioner record", async () => {
|
|
vi.mocked(isUserAdmin).mockResolvedValue(true);
|
|
vi.mocked(database).mockReturnValue(makeMockDb(null) as never);
|
|
|
|
const result = await isCommissioner(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns true for a user with a commissioner record", async () => {
|
|
vi.mocked(isUserAdmin).mockResolvedValue(false);
|
|
vi.mocked(database).mockReturnValue(
|
|
makeMockDb({ id: "comm-1", leagueId: LEAGUE_ID, userId: USER_ID }) as never
|
|
);
|
|
|
|
const result = await isCommissioner(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns true for a site admin who also has a commissioner record", async () => {
|
|
vi.mocked(isUserAdmin).mockResolvedValue(true);
|
|
vi.mocked(database).mockReturnValue(
|
|
makeMockDb({ id: "comm-1", leagueId: LEAGUE_ID, userId: USER_ID }) as never
|
|
);
|
|
|
|
const result = await isCommissioner(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false for a non-admin user without a commissioner record", async () => {
|
|
vi.mocked(isUserAdmin).mockResolvedValue(false);
|
|
vi.mocked(database).mockReturnValue(makeMockDb(null) as never);
|
|
|
|
const result = await isCommissioner(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("runs the admin check and DB query in parallel", async () => {
|
|
const order: string[] = [];
|
|
vi.mocked(isUserAdmin).mockImplementation(async () => {
|
|
order.push("admin");
|
|
return false;
|
|
});
|
|
const db = makeMockDb(null);
|
|
db.query.commissioners.findFirst = vi.fn().mockImplementation(async () => {
|
|
order.push("db");
|
|
return null;
|
|
});
|
|
vi.mocked(database).mockReturnValue(db as never);
|
|
|
|
await isCommissioner(LEAGUE_ID, USER_ID);
|
|
|
|
// Both should have been called; order is non-deterministic for parallel calls
|
|
expect(order).toContain("admin");
|
|
expect(order).toContain("db");
|
|
});
|
|
});
|
|
|
|
describe("hasCommissionerRecord", () => {
|
|
it("returns true when a commissioner record exists", async () => {
|
|
vi.mocked(database).mockReturnValue(
|
|
makeMockDb({ id: "comm-1", leagueId: LEAGUE_ID, userId: USER_ID }) as never
|
|
);
|
|
|
|
const result = await hasCommissionerRecord(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(true);
|
|
});
|
|
|
|
it("returns false when no commissioner record exists", async () => {
|
|
vi.mocked(database).mockReturnValue(makeMockDb(null) as never);
|
|
|
|
const result = await hasCommissionerRecord(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(false);
|
|
});
|
|
|
|
it("returns false for a site admin without a commissioner record", async () => {
|
|
// hasCommissionerRecord must NOT consult the admin flag
|
|
vi.mocked(database).mockReturnValue(makeMockDb(null) as never);
|
|
|
|
const result = await hasCommissionerRecord(LEAGUE_ID, USER_ID);
|
|
expect(result).toBe(false);
|
|
expect(isUserAdmin).not.toHaveBeenCalled();
|
|
});
|
|
});
|