zerobyte/app/server/lib/auth/middlewares/__tests__/convert-legacy-user.test.ts
Nico 4305057185
test: move test runner from Bun to Vitest (#727)
* chore: migrate to vitest

* test: speed up some suites by sharing sessions and mocking expensive non-tested actions

* test: refactor some tests to verify behavior instead of implementation details

* chore: fix linting issues
2026-04-01 20:05:54 +02:00

279 lines
7.9 KiB
TypeScript

import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("better-auth/crypto", () => ({
hashPassword: vi.fn(async () => "test-account-password-hash"),
}));
import { convertLegacyUserOnFirstLogin } from "../convert-legacy-user";
import { db } from "~/server/db/db";
import { usersTable, account, organization, member } from "~/server/db/schema";
import type { AuthMiddlewareContext } from "~/server/lib/auth";
describe("convertLegacyUserOnFirstLogin", () => {
const legacyPasswordHash = "legacy-password-hash";
const verifyPassword = vi.spyOn(Bun.password, "verify");
beforeEach(async () => {
verifyPassword.mockReset();
verifyPassword.mockImplementation(async (password) => password === "correct-password");
await db.delete(member);
await db.delete(account);
await db.delete(organization);
await db.delete(usersTable);
});
const createContext = (path: string, body: Record<string, string>) => ({ path, body }) as AuthMiddlewareContext;
test("should return early for non-sign-in paths", async () => {
const ctx = createContext("/sign-up", { username: "test", password: "test" });
const result = await convertLegacyUserOnFirstLogin(ctx);
expect(result).toBeUndefined();
});
test("should do nothing when no legacy user exists", async () => {
await db.insert(usersTable).values({
id: crypto.randomUUID(),
username: "existing-user",
email: "existing@test.com",
name: "Existing User",
passwordHash: null,
});
const ctx = createContext("/sign-in/username", {
username: "existing-user",
password: "password123",
});
const result = await convertLegacyUserOnFirstLogin(ctx);
expect(result).toBeUndefined();
// Verify user still exists with no account
const user = await db.query.usersTable.findFirst({
where: { username: "existing-user" },
});
expect(user).toBeDefined();
expect(user?.passwordHash).toBeNull();
});
test("should throw UnauthorizedError for invalid password", async () => {
// Create a legacy user with a hashed password
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-user",
email: "legacy@test.com",
name: "Legacy User",
passwordHash: legacyPasswordHash,
});
const ctx = createContext("/sign-in/username", {
username: "legacy-user",
password: "wrong-password",
});
await expect(convertLegacyUserOnFirstLogin(ctx)).rejects.toThrow("Invalid credentials");
// Verify user still exists (not migrated)
const user = await db.query.usersTable.findFirst({
where: { username: "legacy-user" },
});
expect(user).toBeDefined();
expect(user?.passwordHash).toBe(legacyPasswordHash);
});
test("should migrate legacy user with existing organization membership", async () => {
const password = "correct-password";
// Create legacy user
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-with-org",
email: "legacy-org@test.com",
name: "Legacy With Org",
passwordHash: legacyPasswordHash,
role: "admin",
});
// Create organization and membership
const orgId = crypto.randomUUID();
await db.insert(organization).values({
id: orgId,
name: "Legacy Org",
slug: "legacy-org",
createdAt: new Date(),
});
const membershipId = crypto.randomUUID();
await db.insert(member).values({
id: membershipId,
userId: userId,
organizationId: orgId,
role: "owner",
createdAt: new Date(),
});
const ctx = createContext("/sign-in/username", {
username: "legacy-with-org",
password,
});
await convertLegacyUserOnFirstLogin(ctx);
// Verify old user is deleted
const oldUser = await db.query.usersTable.findFirst({
where: { id: userId },
});
expect(oldUser).toBeUndefined();
// Verify new user exists
const newUser = await db.query.usersTable.findFirst({
where: { username: "legacy-with-org" },
});
expect(newUser).toBeDefined();
expect(newUser?.email).toBe("legacy-org@test.com");
expect(newUser?.name).toBe("Legacy With Org");
expect(newUser?.role).toBe("admin");
expect(newUser?.passwordHash).toBeNull();
expect(newUser?.id).not.toBe(userId);
// Verify account was created
const userAccount = await db.query.account.findFirst({
where: { userId: newUser?.id },
});
expect(userAccount).toBeDefined();
expect(userAccount?.providerId).toBe("credential");
expect(userAccount?.accountId).toBe("legacy-with-org");
expect(userAccount?.password).toBeDefined();
// Verify membership was migrated
const memberships = await db.query.member.findMany({
where: { userId: newUser?.id },
});
expect(memberships.length).toBe(1);
expect(memberships[0].organizationId).toBe(orgId);
expect(memberships[0].role).toBe("owner");
});
test("should migrate legacy user and create new organization when no membership exists", async () => {
const password = "correct-password";
// Create legacy user without organization membership
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-no-org",
email: "legacy-noorg@test.com",
name: "Legacy No Org",
passwordHash: legacyPasswordHash,
hasDownloadedResticPassword: true,
});
const ctx = createContext("/sign-in/username", {
username: "legacy-no-org",
password,
});
await convertLegacyUserOnFirstLogin(ctx);
// Verify old user is deleted
const oldUser = await db.query.usersTable.findFirst({
where: { id: userId },
});
expect(oldUser).toBeUndefined();
// Verify new user exists
const newUser = await db.query.usersTable.findFirst({
where: { username: "legacy-no-org" },
});
expect(newUser).toBeDefined();
expect(newUser?.email).toBe("legacy-noorg@test.com");
expect(newUser?.hasDownloadedResticPassword).toBe(true);
expect(newUser?.role).toBe("admin");
// Verify account was created
const userAccount = await db.query.account.findFirst({
where: { userId: newUser?.id },
});
expect(userAccount).toBeDefined();
// Verify new organization was created
const memberships = await db.query.member.findMany({
where: { userId: newUser?.id },
});
expect(memberships.length).toBe(1);
expect(memberships[0].role).toBe("owner");
const org = await db.query.organization.findFirst({
where: { id: memberships[0].organizationId },
});
expect(org).toBeDefined();
expect(org?.name).toBe("Legacy No Org's Workspace");
expect(org?.metadata).toBeDefined();
});
test("should be case-insensitive for username", async () => {
const password = "correct-password";
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-user",
email: "legacy@test.com",
name: "Legacy User",
passwordHash: legacyPasswordHash,
});
// Try login with uppercase username
const ctx = createContext("/sign-in/username", {
username: "LEGACY-USER",
password,
});
await convertLegacyUserOnFirstLogin(ctx);
// Verify migration happened
const oldUser = await db.query.usersTable.findFirst({
where: { id: userId },
});
expect(oldUser).toBeUndefined();
const newUser = await db.query.usersTable.findFirst({
where: { username: "legacy-user" },
});
expect(newUser).toBeDefined();
});
test("should trim whitespace from username", async () => {
const password = "correct-password";
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-user",
email: "legacy@test.com",
name: "Legacy User",
passwordHash: legacyPasswordHash,
});
// Try login with whitespace
const ctx = createContext("/sign-in/username", {
username: " legacy-user ",
password,
});
await convertLegacyUserOnFirstLogin(ctx);
// Verify migration happened
const oldUser = await db.query.usersTable.findFirst({
where: { id: userId },
});
expect(oldUser).toBeUndefined();
const newUser = await db.query.usersTable.findFirst({
where: { username: "legacy-user" },
});
expect(newUser).toBeDefined();
});
});