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:
@@ -0,0 +1,201 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { isRedirect, isActionFailure } from '@sveltejs/kit';
|
||||
import { createFormRequest, createUrl } from '$lib/test-utils';
|
||||
|
||||
const { resetPassword } = vi.hoisted(() => ({
|
||||
resetPassword: vi.fn()
|
||||
}));
|
||||
|
||||
const { findFirst } = vi.hoisted(() => ({
|
||||
findFirst: vi.fn()
|
||||
}));
|
||||
|
||||
vi.mock('$lib/db', () => ({
|
||||
prisma: {
|
||||
verification: {
|
||||
findFirst
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
vi.mock('$lib/auth', () => ({
|
||||
auth: {
|
||||
api: {
|
||||
resetPassword
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
import { load, actions } from './+page.server';
|
||||
|
||||
describe('reset-password page server load', () => {
|
||||
beforeEach(() => {
|
||||
findFirst.mockReset();
|
||||
});
|
||||
|
||||
it('redirects to / when token is missing', async () => {
|
||||
const url = createUrl('/reset-password');
|
||||
|
||||
const err = await Promise.resolve(load({ url } as any)).catch((e: unknown) => e);
|
||||
expect(isRedirect(err)).toBe(true);
|
||||
if (isRedirect(err)) {
|
||||
expect(err.location).toBe('/');
|
||||
}
|
||||
expect(findFirst).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirects to / when verification record is not found', async () => {
|
||||
findFirst.mockResolvedValue(null);
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'invalid-token' });
|
||||
|
||||
const err = await Promise.resolve(load({ url } as any)).catch((e: unknown) => e);
|
||||
expect(isRedirect(err)).toBe(true);
|
||||
if (isRedirect(err)) {
|
||||
expect(err.location).toBe('/');
|
||||
}
|
||||
expect(findFirst).toHaveBeenCalledWith({
|
||||
where: {
|
||||
value: 'invalid-token',
|
||||
expiresAt: { gt: expect.any(Date) }
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it('returns undefined when token is valid (verification found)', async () => {
|
||||
findFirst.mockResolvedValue({ id: 'v-1', value: 'valid-token' });
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'valid-token' });
|
||||
|
||||
const result = await load({ url } as any);
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
it('redirects to / when verification is expired', async () => {
|
||||
findFirst.mockResolvedValue(null);
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'expired-token' });
|
||||
|
||||
const err = await Promise.resolve(load({ url } as any)).catch((e: unknown) => e);
|
||||
expect(isRedirect(err)).toBe(true);
|
||||
if (isRedirect(err)) {
|
||||
expect(err.location).toBe('/');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('reset-password page server action', () => {
|
||||
beforeEach(() => {
|
||||
resetPassword.mockReset();
|
||||
});
|
||||
|
||||
const invokeDefault = (url: URL, request: Request) =>
|
||||
actions.default({ request, url } as any);
|
||||
|
||||
it('returns fail(400) when token is missing', async () => {
|
||||
const url = createUrl('/reset-password');
|
||||
|
||||
const result = await invokeDefault(url, createFormRequest({}));
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Invalid or missing reset token.' });
|
||||
}
|
||||
expect(resetPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns fail(400) when password is too short', async () => {
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const result = await invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: 'short', password2: 'short' })
|
||||
);
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Password must be at least 8 characters.' });
|
||||
}
|
||||
expect(resetPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns fail(400) when password is empty', async () => {
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const result = await invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: '', password2: '' })
|
||||
);
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Password must be at least 8 characters.' });
|
||||
}
|
||||
expect(resetPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns fail(400) when passwords do not match', async () => {
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const result = await invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: 'longenough1', password2: 'longenough2' })
|
||||
);
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Passwords do not match.' });
|
||||
}
|
||||
expect(resetPassword).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns fail(400) when resetPassword throws', async () => {
|
||||
resetPassword.mockRejectedValue(new Error('Token expired'));
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const result = await invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
|
||||
);
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Token expired' });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns fail(400) with default message when non-Error is thrown', async () => {
|
||||
resetPassword.mockRejectedValue(null);
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const result = await invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
|
||||
);
|
||||
expect(isActionFailure(result)).toBe(true);
|
||||
if (isActionFailure(result)) {
|
||||
expect(result.data).toEqual({ error: 'Failed to reset password.' });
|
||||
}
|
||||
});
|
||||
|
||||
it('redirects to /login on successful reset', async () => {
|
||||
resetPassword.mockResolvedValue(undefined);
|
||||
|
||||
const url = createUrl('/reset-password', { token: 'token-1' });
|
||||
|
||||
const err = await Promise.resolve(
|
||||
invokeDefault(
|
||||
url,
|
||||
createFormRequest({ password: 'longenough1', password2: 'longenough1' })
|
||||
)
|
||||
).catch((e: unknown) => e);
|
||||
expect(isRedirect(err)).toBe(true);
|
||||
if (isRedirect(err)) {
|
||||
expect(err.status).toBe(303);
|
||||
expect(err.location).toBe('/login');
|
||||
}
|
||||
expect(resetPassword).toHaveBeenCalledWith({
|
||||
body: {
|
||||
newPassword: 'longenough1',
|
||||
token: 'token-1'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user