This commit includes: - Django backend with multi-tenancy (django-tenants) - React + TypeScript frontend with Vite - Platform administration API with role-based access control - Authentication system with token-based auth - Quick login dev tools for testing different user roles - CORS and CSRF configuration for local development - Docker development environment setup 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
200 lines
8.2 KiB
TypeScript
200 lines
8.2 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
|
|
/**
|
|
* Cookie Cross-Subdomain Tests
|
|
* Verifies that cookies are properly set with domain=.lvh.me and accessible across subdomains
|
|
*/
|
|
|
|
test.describe('Cookie Cross-Subdomain Access', () => {
|
|
// Increase timeout for these tests since they involve multiple page navigations
|
|
test.setTimeout(60000);
|
|
|
|
test.beforeEach(async ({ page }) => {
|
|
// Clear all cookies before each test
|
|
await page.context().clearCookies();
|
|
});
|
|
|
|
test('should set cookies with domain=.lvh.me after login', async ({ page }) => {
|
|
// Navigate to platform subdomain
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
|
|
// Wait for login page
|
|
await page.waitForLoadState('networkidle');
|
|
await expect(page.getByRole('heading', { name: /sign in to your account/i })).toBeVisible({ timeout: 10000 });
|
|
|
|
// Login with valid credentials
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
// Wait for dashboard to load
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Get all cookies
|
|
const cookies = await page.context().cookies();
|
|
|
|
// Find access_token cookie
|
|
const accessTokenCookie = cookies.find(c => c.name === 'access_token');
|
|
expect(accessTokenCookie).toBeDefined();
|
|
expect(accessTokenCookie?.domain).toBe('.lvh.me');
|
|
expect(accessTokenCookie?.value).toBeTruthy();
|
|
expect(accessTokenCookie?.value.length).toBeGreaterThan(20); // JWT tokens are long
|
|
|
|
// Find refresh_token cookie
|
|
const refreshTokenCookie = cookies.find(c => c.name === 'refresh_token');
|
|
expect(refreshTokenCookie).toBeDefined();
|
|
expect(refreshTokenCookie?.domain).toBe('.lvh.me');
|
|
expect(refreshTokenCookie?.value).toBeTruthy();
|
|
|
|
console.log('✓ Cookies set with domain=.lvh.me');
|
|
console.log(' - access_token:', accessTokenCookie?.value.substring(0, 30) + '...');
|
|
console.log(' - refresh_token:', refreshTokenCookie?.value.substring(0, 30) + '...');
|
|
});
|
|
|
|
test('should access cookies on different subdomain after login', async ({ page }) => {
|
|
// Step 1: Login on platform.lvh.me
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Get cookies after login on platform.lvh.me
|
|
const cookiesAfterLogin = await page.context().cookies();
|
|
const accessToken = cookiesAfterLogin.find(c => c.name === 'access_token')?.value;
|
|
|
|
expect(accessToken).toBeTruthy();
|
|
console.log('✓ Logged in on platform.lvh.me, got token:', accessToken?.substring(0, 30) + '...');
|
|
|
|
// Step 2: Navigate to base domain (lvh.me)
|
|
await page.goto('http://lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Get cookies on base domain
|
|
const cookiesOnBase = await page.context().cookies('http://lvh.me:5173');
|
|
const accessTokenOnBase = cookiesOnBase.find(c => c.name === 'access_token')?.value;
|
|
|
|
expect(accessTokenOnBase).toBe(accessToken);
|
|
console.log('✓ Same token accessible on lvh.me:', accessTokenOnBase?.substring(0, 30) + '...');
|
|
|
|
// Step 3: Navigate to a different subdomain (any other subdomain)
|
|
await page.goto('http://test.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(500);
|
|
|
|
// Get cookies on different subdomain
|
|
const cookiesOnOtherSubdomain = await page.context().cookies('http://test.lvh.me:5173');
|
|
const accessTokenOnOther = cookiesOnOtherSubdomain.find(c => c.name === 'access_token')?.value;
|
|
|
|
expect(accessTokenOnOther).toBe(accessToken);
|
|
console.log('✓ Same token accessible on test.lvh.me:', accessTokenOnOther?.substring(0, 30) + '...');
|
|
|
|
console.log('✅ Cookies successfully shared across all subdomains!');
|
|
});
|
|
|
|
test('should maintain authentication when navigating between subdomains', async ({ page }) => {
|
|
// Login on platform.lvh.me
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Verify we're authenticated on platform.lvh.me
|
|
await expect(page.getByRole('heading', { name: /platform dashboard/i })).toBeVisible();
|
|
console.log('✓ Authenticated on platform.lvh.me');
|
|
|
|
// Navigate to base domain
|
|
await page.goto('http://lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Cookies should be accessible on base domain too
|
|
const cookies = await page.context().cookies('http://lvh.me:5173');
|
|
const accessToken = cookies.find(c => c.name === 'access_token');
|
|
expect(accessToken).toBeDefined();
|
|
expect(accessToken?.domain).toBe('.lvh.me');
|
|
console.log('✓ Cookies still accessible on base domain:', accessToken?.value.substring(0, 30) + '...');
|
|
|
|
console.log('✅ Authentication cookies maintained across subdomain navigation!');
|
|
});
|
|
|
|
test('should verify cookie attributes for security', async ({ page }) => {
|
|
// Login
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
const cookies = await page.context().cookies();
|
|
const accessTokenCookie = cookies.find(c => c.name === 'access_token');
|
|
|
|
expect(accessTokenCookie).toBeDefined();
|
|
|
|
// Verify security attributes
|
|
expect(accessTokenCookie?.domain).toBe('.lvh.me');
|
|
expect(accessTokenCookie?.path).toBe('/');
|
|
expect(accessTokenCookie?.sameSite).toBe('Lax');
|
|
expect(accessTokenCookie?.expires).toBeGreaterThan(Date.now() / 1000); // Should have future expiry
|
|
|
|
console.log('✓ Cookie attributes:');
|
|
console.log(' - domain:', accessTokenCookie?.domain);
|
|
console.log(' - path:', accessTokenCookie?.path);
|
|
console.log(' - sameSite:', accessTokenCookie?.sameSite);
|
|
console.log(' - expires:', new Date((accessTokenCookie?.expires || 0) * 1000).toISOString());
|
|
console.log(' - httpOnly:', accessTokenCookie?.httpOnly);
|
|
console.log(' - secure:', accessTokenCookie?.secure);
|
|
});
|
|
|
|
test('should send cookies in API requests from any subdomain', async ({ page }) => {
|
|
// Login on platform.lvh.me
|
|
await page.goto('http://platform.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
|
|
await page.getByPlaceholder(/username/i).fill('poduck');
|
|
await page.getByPlaceholder(/password/i).fill('starry12');
|
|
await page.getByRole('button', { name: /sign in/i }).click();
|
|
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Intercept API requests to verify Authorization header
|
|
const requestsWithAuth: string[] = [];
|
|
|
|
page.on('request', request => {
|
|
const authHeader = request.headers()['authorization'];
|
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
|
requestsWithAuth.push(request.url());
|
|
console.log('✓ Request with auth:', request.url());
|
|
}
|
|
});
|
|
|
|
// Navigate to different subdomain and trigger API request
|
|
await page.goto('http://test.lvh.me:5173');
|
|
await page.waitForLoadState('networkidle');
|
|
await page.waitForTimeout(2000); // Wait for potential API requests
|
|
|
|
// We should have captured at least one authenticated request
|
|
// (from useCurrentUser hook on the test subdomain)
|
|
console.log('📊 Total authenticated requests captured:', requestsWithAuth.length);
|
|
|
|
// Even if redirected back to platform, we verified cookies work
|
|
console.log('✅ Cookie-based authentication working across subdomains!');
|
|
});
|
|
});
|