fix(invite): address review cycle 2 feedback
Some checks failed
CI / Unit & Component Tests (push) Failing after 2m32s
CI / Unit & Component Tests (pull_request) Failing after 2m31s
CI / OCR Service Tests (pull_request) Successful in 31s
CI / Backend Unit Tests (pull_request) Failing after 2m46s
CI / OCR Service Tests (push) Successful in 36s
CI / Backend Unit Tests (push) Failing after 2m43s

- Narrow isTrustedProxy to RFC 1918 172.16-31.x.x (was 172.x.x.x)
- Add @Valid/@NotBlank/@Email to RegisterRequest and @Valid to AuthController
- Add FK constraint on invite_token_group_ids.group_id → user_groups(id)
- Add back-to-login link and <main> landmark to register error state
- Add component test suite for register/+page.svelte (11 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-04-19 09:30:57 +02:00
parent 9fc4993fca
commit 88012a1193
6 changed files with 140 additions and 9 deletions

View File

@@ -25,7 +25,7 @@ let showPassword = $state(false);
<div class="flex min-h-screen flex-col bg-canvas">
<AuthHeader />
<div class="flex flex-1 items-center justify-center px-4 py-8">
<main class="flex flex-1 items-center justify-center px-4 py-8">
<div class="w-full max-w-sm">
<div class="mb-10 text-center">
<a href="/" class="inline-flex items-center" aria-label="Familienarchiv">
@@ -53,7 +53,13 @@ let showPassword = $state(false);
<h1 class="mb-2 font-sans text-sm font-bold tracking-widest text-ink uppercase">
{m.register_invalid_code()}
</h1>
<p class="font-serif text-sm text-ink-2">{m.register_invalid_code_desc()}</p>
<p class="mb-6 font-serif text-sm text-ink-2">{m.register_invalid_code_desc()}</p>
<a
href="/login"
class="font-sans text-xs font-bold tracking-widest text-brand-navy/60 uppercase transition-colors hover:text-brand-navy"
>
{m.forgot_password_back_to_login()}
</a>
</div>
{:else}
<div class="rounded-sm border border-line bg-surface p-8 shadow-sm">
@@ -185,7 +191,7 @@ let showPassword = $state(false);
</div>
{/if}
</div>
</div>
</main>
<div class="py-4 text-center">
<p class="font-sans text-xs tracking-widest text-ink-3 uppercase">Familienarchiv</p>

View File

@@ -0,0 +1,106 @@
import { afterEach, describe, expect, it } from 'vitest';
import { cleanup, render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import RegisterPage from './+page.svelte';
const tick = () => new Promise((r) => setTimeout(r, 0));
afterEach(cleanup);
const validData = {
code: 'ABCDE12345',
prefill: null,
codeError: null
};
const prefillData = {
code: 'ABCDE12345',
prefill: { firstName: 'Max', lastName: 'Muster', email: 'max@test.com' },
codeError: null
};
const errorData = {
code: null,
prefill: null,
codeError: 'INVITE_NOT_FOUND'
};
describe('Register page valid code', () => {
it('renders the heading', async () => {
render(RegisterPage, { data: validData });
await expect.element(page.getByRole('heading', { level: 1 })).toBeInTheDocument();
});
it('renders the email input', async () => {
render(RegisterPage, { data: validData });
await tick();
const input = document.querySelector<HTMLInputElement>('input[name="email"]');
expect(input).not.toBeNull();
expect(input?.type).toBe('email');
expect(input?.required).toBe(true);
});
it('renders the password input', async () => {
render(RegisterPage, { data: validData });
await tick();
const input = document.querySelector<HTMLInputElement>('input[name="password"]');
expect(input).not.toBeNull();
expect(input?.required).toBe(true);
});
it('renders the submit button', async () => {
render(RegisterPage, { data: validData });
await expect.element(page.getByRole('button', { name: 'Konto erstellen' })).toBeInTheDocument();
});
it('prefills fields from invite data', async () => {
render(RegisterPage, { data: prefillData });
await tick();
const email = document.querySelector<HTMLInputElement>('input[name="email"]');
const firstName = document.querySelector<HTMLInputElement>('input[name="firstName"]');
const lastName = document.querySelector<HTMLInputElement>('input[name="lastName"]');
expect(email?.value).toBe('max@test.com');
expect(firstName?.value).toBe('Max');
expect(lastName?.value).toBe('Muster');
});
it('has a hidden code input', async () => {
render(RegisterPage, { data: validData });
await tick();
const hidden = document.querySelector<HTMLInputElement>('input[name="code"][type="hidden"]');
expect(hidden).not.toBeNull();
expect(hidden?.value).toBe('ABCDE12345');
});
it('shows form error when action returns error', async () => {
render(RegisterPage, { data: validData, form: { error: 'INVITE_REVOKED' } });
await tick();
expect(document.querySelector('.text-red-600')).not.toBeNull();
});
it('has main landmark', async () => {
render(RegisterPage, { data: validData });
await tick();
expect(document.querySelector('main')).not.toBeNull();
});
});
describe('Register page error state', () => {
it('renders the error card when codeError is set', async () => {
render(RegisterPage, { data: errorData });
await tick();
expect(document.querySelector('form')).toBeNull();
});
it('shows a back-to-login link in error state', async () => {
render(RegisterPage, { data: errorData });
await expect.element(page.getByRole('link', { name: /login/i })).toBeInTheDocument();
});
it('back-to-login link points to /login', async () => {
render(RegisterPage, { data: errorData });
await tick();
const link = document.querySelector<HTMLAnchorElement>('a[href="/login"]');
expect(link).not.toBeNull();
});
});