zerobyte/app/server/lib/auth/middlewares/__tests__/convert-legacy-user.test.ts
Nico 7a3932f969
feat: OIDC (#564)
* feat: oidc

feat: organization switcher

refactor: org context

feat: invitations

GLM

* feat: link current account

* refactor: own page for sso registration

* feat: per-user account management

* refactor: code style

* refactor: user existing check

* refactor: restrict provider configuration to super admins only

* refactor: cleanup / pr review

* chore: fix lint issues

* chore: pr feedbacks

* test(e2e): automated tests for OIDC

* fix: check url first for sso provider identification

* fix: prevent oidc provider to be named "credential"
2026-02-27 23:13:54 +01:00

274 lines
7.9 KiB
TypeScript

import { test, describe, beforeEach, expect } from "bun:test";
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", () => {
beforeEach(async () => {
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 () => {
const hashedPassword = await Bun.password.hash("correct-password");
// 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: hashedPassword,
});
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(hashedPassword);
});
test("should migrate legacy user with existing organization membership", async () => {
const password = "correct-password";
const hashedPassword = await Bun.password.hash(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: hashedPassword,
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";
const hashedPassword = await Bun.password.hash(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: hashedPassword,
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 hashedPassword = await Bun.password.hash(password);
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-user",
email: "legacy@test.com",
name: "Legacy User",
passwordHash: hashedPassword,
});
// 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 hashedPassword = await Bun.password.hash(password);
const userId = crypto.randomUUID();
await db.insert(usersTable).values({
id: userId,
username: "legacy-user",
email: "legacy@test.com",
name: "Legacy User",
passwordHash: hashedPassword,
});
// 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();
});
});