bilhej/frontend/src/__tests__/ResetPasswordPage.spec.ts
Joakim Mörling 86fb946e33
All checks were successful
CI / Lint, type check, unit tests, coverage (push) Successful in 2m2s
CI / E2E browser tests (push) Successful in 1m55s
Add password reset, logged-in change password, and Mailpit email dev/E2E.
Operators can fix prod admin passwords without email via Byt lösenord;
end users can use forgot-password when SMTP is configured. Local and CI
use Mailpit to capture outbound mail and verify reset links end-to-end.

- Backend: V8 password_reset_tokens, PasswordResetService, EmailService,
  POST /api/auth/forgot-password, reset-password, change-password
- Optional testToken in forgot-password response (docker profile only, for E2E)
- Frontend: ForgotPasswordPage, ResetPasswordPage, ChangePasswordPage,
  routes, login link, header Byt lösenord
- Mailpit (ghcr.io/axllent/mailpit:v1.28) in docker-compose + e2e stack
- E2E: password-reset.spec.ts + Mailpit API helper tests SMTP delivery
- Separate dev/e2e Docker image names to avoid overwriting bilhej-frontend
- Docs: README email section, production-email-checklist, .env.example
- Unit/integration tests for reset, change password, and Vitest page specs

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 18:05:15 +02:00

122 lines
3.7 KiB
TypeScript

import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'
import { mount } from '@vue/test-utils'
import { createRouter, createMemoryHistory } from 'vue-router'
import ResetPasswordPage from '@/pages/ResetPasswordPage.vue'
function mockFetchResponse(status: number, body: unknown) {
return Promise.resolve({
ok: status >= 200 && status < 300,
status,
json: () => Promise.resolve(body),
})
}
function createTestRouter() {
return createRouter({
history: createMemoryHistory(),
routes: [
{
path: '/aterstall-losenord',
name: 'reset-password',
component: ResetPasswordPage,
},
{
path: '/logga-in',
name: 'login',
component: { template: '<div>Login</div>' },
},
{
path: '/glomt-losenord',
name: 'forgot-password',
component: { template: '<div>Forgot</div>' },
},
],
})
}
async function mountPage(initialPath: string) {
const router = createTestRouter()
await router.push(initialPath)
await router.isReady()
return {
router,
wrapper: mount(ResetPasswordPage, {
global: { plugins: [router] },
}),
}
}
describe('ResetPasswordPage', () => {
beforeEach(() => {
vi.useFakeTimers()
globalThis.fetch = vi.fn()
vi.mocked(globalThis.fetch).mockResolvedValue(
mockFetchResponse(200, {
message: 'Lösenordet har uppdaterats. Du kan nu logga in.',
}),
)
})
afterEach(() => {
vi.useRealTimers()
})
it('shows error when token query is missing', async () => {
const { wrapper } = await mountPage('/aterstall-losenord')
await vi.waitFor(() => {
expect(wrapper.text()).toContain(
'Återställningslänken saknar en giltig kod.',
)
})
})
it('shows password min length hint', async () => {
const { wrapper } = await mountPage('/aterstall-losenord?token=abc')
await wrapper.find('#password').setValue('short')
expect(wrapper.text()).toContain('Lösenordet måste vara minst 8 tecken')
})
it('shows mismatch hint when confirm password differs', async () => {
const { wrapper } = await mountPage('/aterstall-losenord?token=abc')
await wrapper.find('#password').setValue('password1234')
await wrapper.find('#confirmPassword').setValue('different1234')
expect(wrapper.text()).toContain('Lösenorden matchar inte')
})
it('shows success and navigates to login after reset', async () => {
const { wrapper, router } = await mountPage('/aterstall-losenord?token=abc')
await wrapper.find('#password').setValue('newpassword123')
await wrapper.find('#confirmPassword').setValue('newpassword123')
await wrapper.find('form').trigger('submit.prevent')
await vi.waitFor(() => {
expect(wrapper.text()).toContain(
'Lösenordet har uppdaterats. Du kan nu logga in.',
)
})
await vi.advanceTimersByTimeAsync(2000)
expect(router.currentRoute.value.path).toBe('/logga-in')
})
it('shows invalid token message from backend', async () => {
vi.mocked(globalThis.fetch).mockResolvedValue(
mockFetchResponse(400, {
message: 'Återställningslänken är ogiltig eller har gått ut',
}),
)
const { wrapper } = await mountPage('/aterstall-losenord?token=bad')
await wrapper.find('#password').setValue('newpassword123')
await wrapper.find('#confirmPassword').setValue('newpassword123')
await wrapper.find('form').trigger('submit.prevent')
await vi.waitFor(() => {
expect(wrapper.text()).toContain(
'Återställningslänken är ogiltig eller har gått ut',
)
expect(wrapper.find('a[href="/glomt-losenord"]').text()).toContain(
'Begär ny länk',
)
})
})
})