fix(auth): fix mock responses in tests and block open redirect in login

- Add response object to mockSuccess() in login and signup tests so
  response.headers.get() no longer throws
- Validate ?redirect= param: must start with / and not // to prevent
  redirecting users to external domains

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-02 18:48:48 +02:00
parent 0aa65214fc
commit 16f0feb8d5
3 changed files with 53 additions and 6 deletions

View File

@@ -31,8 +31,16 @@ describe('login form action', () => {
} as any;
}
function mockSuccess() {
return {
data: { data: { id: '123' } },
error: undefined,
response: { headers: { get: vi.fn().mockReturnValue(null) } }
};
}
it('calls POST /v1/auth/login with form data', async () => {
mockPost.mockResolvedValue({ data: { data: { id: '123' } }, error: undefined });
mockPost.mockResolvedValue(mockSuccess());
try {
await actions.default(createEvent({
@@ -52,7 +60,7 @@ describe('login form action', () => {
});
it('redirects to /planner on success by default', async () => {
mockPost.mockResolvedValue({ data: { data: { id: '123' } }, error: undefined });
mockPost.mockResolvedValue(mockSuccess());
try {
await actions.default(createEvent({
@@ -67,7 +75,7 @@ describe('login form action', () => {
});
it('redirects to ?redirect param when present', async () => {
mockPost.mockResolvedValue({ data: { data: { id: '123' } }, error: undefined });
mockPost.mockResolvedValue(mockSuccess());
try {
await actions.default(createEvent(
@@ -81,6 +89,36 @@ describe('login form action', () => {
}
});
it('falls back to /planner when ?redirect= is an absolute URL', async () => {
mockPost.mockResolvedValue(mockSuccess());
try {
await actions.default(createEvent(
{ email: 'sarah@example.com', password: 'password123' },
'?redirect=https%3A%2F%2Fevil.com'
));
expect.unreachable();
} catch (e: any) {
expect(e.status).toBe(303);
expect(e.location).toBe('/planner');
}
});
it('falls back to /planner when ?redirect= is a protocol-relative URL', async () => {
mockPost.mockResolvedValue(mockSuccess());
try {
await actions.default(createEvent(
{ email: 'sarah@example.com', password: 'password123' },
'?redirect=%2F%2Fevil.com'
));
expect.unreachable();
} catch (e: any) {
expect(e.status).toBe(303);
expect(e.location).toBe('/planner');
}
});
it('rejects empty email with validation error', async () => {
const result = await actions.default(createEvent({
email: '',