test: add unit tests for all auth backend logic

- Register: edge cases for non-Error throws, error message assertions,
  and verify signUpEmail was called with correct payload
- Forgot-password: empty/missing email, Error/non-Error throws
- Reset-password: load (missing/expired/valid token) and action (short
  password, mismatch, throw, success redirect)
- Account setup: load (unauthorized, returns user), action (redirect,
  falsy create, throws, isRedirect passthrough)
- Email-verification: correct URL construction, sendMail delegation,
  throw propagation
- Hooks: sets/omits locals, passes building/auth to svelteKitHandler
- Fix Svelte 5  warning on setup page by using  with
  one-time guard instead of top-level prop read
This commit is contained in:
NeilClack
2026-07-07 09:22:56 -05:00
parent 1e391c9252
commit 6ff8e878a4
7 changed files with 670 additions and 5 deletions
@@ -18,10 +18,8 @@
let { data }: PageProps = $props();
const userFullName = data.user?.name;
let firstName = $state(splitName(userFullName).first);
let lastName = $state(splitName(userFullName).last);
let phoneNumber = $state('');
let firstName = $state('');
let lastName = $state('');
function splitName(fullName: string | null | undefined): { first: string; last: string } {
if (!fullName) return { first: '', last: '' };
@@ -29,6 +27,15 @@
return { first: parts[0] ?? '', last: parts.slice(1).join(' ') };
}
$effect(() => {
if (data.user?.name && !firstName) {
const names = splitName(data.user.name);
firstName = names.first;
lastName = names.last;
}
});
let phoneNumber = $state('');
function handlePhoneInput(event: Event) {
const input = event.target as HTMLInputElement;
phoneNumber = clientFormatPhoneNumber(input.value);
@@ -0,0 +1,133 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { isHttpError, isRedirect, isActionFailure, redirect } from '@sveltejs/kit';
import { createLocals, mockUser, createFormRequest } from '$lib/test-utils';
import { serverFormatPhoneNumber } from '$lib/utils';
const { create, findUnique } = vi.hoisted(() => ({
create: vi.fn(),
findUnique: vi.fn()
}));
vi.mock('$lib/db', () => ({
prisma: {
user: {
findUnique
},
profile: {
create
}
}
}));
vi.mock('$lib/utils', () => ({
serverFormatPhoneNumber: vi.fn((val: string) => `formatted-${val}`)
}));
import { load, actions } from './+page.server';
const invokeDefault = (request: Request) => actions.default({ request } as any);
describe('account setup page server load', () => {
beforeEach(() => {
create.mockReset();
});
it('throws 403 when user is not logged in', async () => {
const err = await Promise.resolve(
load({ locals: createLocals() } as any)
).catch((e: unknown) => e);
expect(isHttpError(err, 403)).toBe(true);
});
it('returns user data when logged in', async () => {
const user = mockUser({ id: 'user-1', name: 'Jane Doe' });
findUnique.mockResolvedValue(user);
const result = await load({ locals: createLocals(user as any) } as any);
expect(result).toEqual({ user });
});
});
describe('account setup page server action', () => {
beforeEach(() => {
create.mockReset();
});
it('redirects to /dashboard on successful profile creation', async () => {
create.mockResolvedValue({ id: 'profile-1', userId: 'user-1' });
const request = createFormRequest({
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-0100'
});
const err = await Promise.resolve(invokeDefault(request)).catch((e: unknown) => e);
expect(isRedirect(err)).toBe(true);
if (isRedirect(err)) {
expect(err.status).toBe(303);
expect(err.location).toBe('/dashboard');
}
expect(serverFormatPhoneNumber).toHaveBeenCalledWith('555-0100');
expect(create).toHaveBeenCalledWith({
data: {
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: 'formatted-555-0100'
}
});
});
it('returns fail(400) when profile.create returns falsy', async () => {
create.mockResolvedValue(null);
const request = createFormRequest({
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-0100'
});
const result = await invokeDefault(request);
expect(isActionFailure(result)).toBe(true);
});
it('throws 400 error when profile.create throws', async () => {
create.mockRejectedValue(new Error('db error'));
const request = createFormRequest({
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-0100'
});
const err = await Promise.resolve(invokeDefault(request)).catch((e: unknown) => e);
expect(isHttpError(err, 400)).toBe(true);
if (isHttpError(err)) {
expect(err.body.message).toBe('bad request');
}
});
it('re-throws redirect errors from profile.create', async () => {
let redirectError: unknown;
try {
redirect(303, '/dashboard');
} catch (e) {
redirectError = e;
}
create.mockRejectedValue(redirectError);
const request = createFormRequest({
userId: 'user-1',
firstName: 'Jane',
lastName: 'Doe',
phoneNumber: '555-0100'
});
const err = await Promise.resolve(invokeDefault(request)).catch((e: unknown) => e);
expect(err).toBe(redirectError);
});
});