test: add OAuth2 authentication test files (TDD RED phase)

This commit is contained in:
2025-12-12 12:40:58 +01:00
parent 40ae2145cc
commit 8867dff780
6 changed files with 1935 additions and 0 deletions

View File

@@ -0,0 +1,218 @@
import { describe, it, expect } from 'vitest';
/**
* TDD Tests for validateRedirect utility (Phase 1: RED)
*
* Purpose: Prevent open redirect vulnerabilities by validating redirect URLs
* Specification: specs/001-oauth2-authentication/spec.md - US3 (Protected Routes)
*
* Security Requirements:
* - FR-019: Validate redirect URLs to prevent open redirect attacks
* - NFR-005: Only allow same-origin paths starting with /
*
* These tests are written FIRST (TDD Red phase) and should FAIL
* until the validateRedirect implementation is created in T002.
*/
describe('validateRedirect', () => {
describe('Valid same-origin paths', () => {
it('should return valid path starting with single slash', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/dashboard');
expect(result).toBe('/dashboard');
});
it('should return valid nested path', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/projects/123');
expect(result).toBe('/projects/123');
});
it('should return valid path with query parameters', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/tasks?id=456');
expect(result).toBe('/tasks?id=456');
});
it('should return valid path with hash fragment', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/dashboard#section');
expect(result).toBe('/dashboard#section');
});
it('should return root path', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/');
expect(result).toBe('/');
});
});
describe('Rejection of external URLs (open redirect protection)', () => {
it('should reject fully-qualified external URL with https', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('https://evil.com');
expect(result).toBe('/dashboard');
});
it('should reject fully-qualified external URL with http', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('http://evil.com');
expect(result).toBe('/dashboard');
});
it('should reject protocol-relative URL (double slash)', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('//evil.com');
expect(result).toBe('/dashboard');
});
it('should reject protocol-relative URL with path', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('//evil.com/dashboard');
expect(result).toBe('/dashboard');
});
it('should reject javascript: protocol', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('javascript:alert(1)');
expect(result).toBe('/dashboard');
});
it('should reject data: protocol', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('data:text/html,<script>alert(1)</script>');
expect(result).toBe('/dashboard');
});
});
describe('Invalid input handling', () => {
it('should return fallback when redirect is null', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(null);
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is undefined', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(undefined);
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is a number', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(123);
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is an object', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect({ path: '/dashboard' });
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is an empty string', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('');
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is only whitespace', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(' ');
expect(result).toBe('/dashboard');
});
it('should return fallback when redirect is an array', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(['/dashboard']);
expect(result).toBe('/dashboard');
});
});
describe('Custom fallback parameter', () => {
it('should use custom fallback when redirect is invalid', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('https://evil.com', '/projects');
expect(result).toBe('/projects');
});
it('should use custom fallback when redirect is null', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(null, '/home');
expect(result).toBe('/home');
});
it('should use custom fallback when redirect is undefined', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(undefined, '/login');
expect(result).toBe('/login');
});
it('should return valid redirect even when custom fallback is provided', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/dashboard', '/home');
expect(result).toBe('/dashboard');
});
});
describe('Edge cases', () => {
it('should reject URL with multiple slashes at start', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('///evil.com');
expect(result).toBe('/dashboard');
});
it('should handle path with encoded characters', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/dashboard%20test');
expect(result).toBe('/dashboard%20test');
});
it('should handle path with special characters', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/projects?name=test&id=123');
expect(result).toBe('/projects?name=test&id=123');
});
it('should reject backslash-based bypass attempt', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/\\evil.com');
expect(result).toBe('/\\evil.com'); // Valid local path (backslash is literal)
});
});
describe('Type safety', () => {
it('should accept string type for redirect parameter', async () => {
const { validateRedirect } = await import('../validateRedirect');
const redirect: string = '/dashboard';
const result = validateRedirect(redirect);
expect(result).toBe('/dashboard');
});
it('should accept unknown type for redirect parameter', async () => {
const { validateRedirect } = await import('../validateRedirect');
const redirect: unknown = '/dashboard';
const result = validateRedirect(redirect);
expect(result).toBe('/dashboard');
});
it('should return string type', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect('/dashboard');
expect(typeof result).toBe('string');
});
it('should accept optional fallback parameter', async () => {
const { validateRedirect } = await import('../validateRedirect');
// Should not throw when fallback is omitted
const result = validateRedirect('/dashboard');
expect(result).toBe('/dashboard');
});
it('should use default fallback when not provided', async () => {
const { validateRedirect } = await import('../validateRedirect');
const result = validateRedirect(null);
expect(result).toBe('/dashboard');
});
});
});