/** * E2E Tests für Multi-Tenant Isolation * * Kritische Tests um sicherzustellen, dass Daten zwischen Tenants isoliert sind */ import { test, expect } from '@playwright/test' // Known tenant IDs const TENANT_PORWOLL = 1 const TENANT_C2S = 4 const TENANT_GUNSHIN = 5 test.describe('Tenant Isolation - Public APIs', () => { test.describe('News API Tenant Isolation', () => { test('News API requires tenant parameter', async ({ request }) => { const response = await request.get('/api/news') expect(response.status()).toBe(400) const data = await response.json() expect(data.error).toContain('Tenant ID is required') }) test('News API returns different data for different tenants', async ({ request }) => { const [response1, response4, response5] = await Promise.all([ request.get(`/api/news?tenant=${TENANT_PORWOLL}`), request.get(`/api/news?tenant=${TENANT_C2S}`), request.get(`/api/news?tenant=${TENANT_GUNSHIN}`), ]) expect(response1.ok()).toBe(true) expect(response4.ok()).toBe(true) expect(response5.ok()).toBe(true) const data1 = await response1.json() const data4 = await response4.json() const data5 = await response5.json() // Each response should reflect its tenant filter expect(data1.filters.tenant).toBe(TENANT_PORWOLL) expect(data4.filters.tenant).toBe(TENANT_C2S) expect(data5.filters.tenant).toBe(TENANT_GUNSHIN) }) test('News detail API enforces tenant isolation', async ({ request }) => { // Get a news item from tenant 1 const listResponse = await request.get(`/api/news?tenant=${TENANT_PORWOLL}&limit=1`) if (!listResponse.ok()) { test.skip() return } const listData = await listResponse.json() if (listData.docs.length === 0) { test.skip() return } const slug = listData.docs[0].slug // Try to access with wrong tenant - should return 404 const wrongTenantResponse = await request.get(`/api/news/${slug}?tenant=${TENANT_C2S}`) // Should not find the article (different tenant) expect(wrongTenantResponse.status()).toBe(404) // Same tenant should work const correctTenantResponse = await request.get(`/api/news/${slug}?tenant=${TENANT_PORWOLL}`) expect(correctTenantResponse.ok()).toBe(true) }) }) test.describe('Posts API Tenant Isolation', () => { test('Posts API filters by tenant when specified', async ({ request }) => { const response = await request.get(`/api/posts?tenant=${TENANT_PORWOLL}`) expect(response.ok()).toBe(true) const data = await response.json() expect(data.filters.tenant).toBe(TENANT_PORWOLL) }) test('Different tenants can have independent post counts', async ({ request }) => { const [response1, response4] = await Promise.all([ request.get(`/api/posts?tenant=${TENANT_PORWOLL}&limit=1`), request.get(`/api/posts?tenant=${TENANT_C2S}&limit=1`), ]) expect(response1.ok()).toBe(true) expect(response4.ok()).toBe(true) const data1 = await response1.json() const data4 = await response4.json() // Both should have valid pagination info expect(data1.pagination).toHaveProperty('totalDocs') expect(data4.pagination).toHaveProperty('totalDocs') // Counts can be different (independent data) expect(typeof data1.pagination.totalDocs).toBe('number') expect(typeof data4.pagination.totalDocs).toBe('number') }) }) test.describe('Newsletter Tenant Isolation', () => { test('Newsletter subscription requires tenant', async ({ request }) => { const response = await request.post('/api/newsletter/subscribe', { data: { email: 'isolation-test@example.com', }, }) expect(response.status()).toBe(400) const data = await response.json() expect(data.message).toContain('Tenant') }) test('Newsletter subscriptions are tenant-specific', async ({ request }) => { const uniqueEmail = `tenant-isolation-${Date.now()}@e2e-test.example` // Subscribe to tenant 1 const response1 = await request.post('/api/newsletter/subscribe', { data: { email: uniqueEmail, tenantId: TENANT_PORWOLL, }, }) // Should succeed or be rate limited expect([200, 400, 429]).toContain(response1.status()) // Subscribe same email to tenant 4 (should be separate) const response4 = await request.post('/api/newsletter/subscribe', { data: { email: uniqueEmail, tenantId: TENANT_C2S, }, }) // Both should be able to subscribe (separate per tenant) expect([200, 400, 429]).toContain(response4.status()) }) }) }) test.describe('Tenant Isolation - Protected APIs', () => { test('Tenants API requires authentication', async ({ request }) => { const response = await request.get('/api/tenants') expect([401, 403]).toContain(response.status()) }) test('Users API requires authentication', async ({ request }) => { const response = await request.get('/api/users') expect([401, 403]).toContain(response.status()) }) test('Media API requires authentication', async ({ request }) => { const response = await request.get('/api/media') expect([401, 403]).toContain(response.status()) }) test('Pages API requires authentication', async ({ request }) => { const response = await request.get('/api/pages') expect([401, 403]).toContain(response.status()) }) test('Categories API requires authentication', async ({ request }) => { const response = await request.get('/api/categories') expect([401, 403]).toContain(response.status()) }) }) test.describe('Tenant Data Leakage Prevention', () => { test('Cannot enumerate tenants without auth', async ({ request }) => { const response = await request.get('/api/tenants') // Should not expose tenant list without authentication expect([401, 403]).toContain(response.status()) }) test('Cannot access other tenant media without auth', async ({ request }) => { const response = await request.get('/api/media') expect([401, 403]).toContain(response.status()) }) test('Public endpoints do not leak tenant information', async ({ request }) => { const response = await request.get(`/api/news?tenant=${TENANT_PORWOLL}`) expect(response.ok()).toBe(true) const data = await response.json() // Response should not contain sensitive tenant data for (const doc of data.docs) { // Should not expose full tenant object, only ID reference at most if (doc.tenant) { expect(typeof doc.tenant).not.toBe('object') } } }) test('Error messages do not leak tenant information', async ({ request }) => { const response = await request.get('/api/news?tenant=99999') expect(response.ok()).toBe(true) const data = await response.json() // Should return empty results, not expose info about non-existent tenant expect(data.docs).toEqual([]) // Error message should not reveal tenant existence }) }) test.describe('Cross-Tenant Access Prevention', () => { test('Cannot access data from wrong tenant via news API', async ({ request }) => { // Assuming tenant 1 has some posts but tenant 99999 doesn't exist const validResponse = await request.get(`/api/news?tenant=${TENANT_PORWOLL}`) const invalidResponse = await request.get('/api/news?tenant=99999') expect(validResponse.ok()).toBe(true) expect(invalidResponse.ok()).toBe(true) // Returns empty, not error const validData = await validResponse.json() const invalidData = await invalidResponse.json() // Invalid tenant should return empty results expect(invalidData.docs).toEqual([]) expect(invalidData.pagination.totalDocs).toBe(0) }) test('Archive data is tenant-scoped', async ({ request }) => { const response = await request.get(`/api/news?tenant=${TENANT_PORWOLL}&includeArchive=true`) expect(response.ok()).toBe(true) const data = await response.json() // Archive should be present and scoped to tenant expect(data).toHaveProperty('archive') expect(data.filters.tenant).toBe(TENANT_PORWOLL) }) test('Categories are tenant-scoped', async ({ request }) => { const response = await request.get(`/api/news?tenant=${TENANT_PORWOLL}&includeCategories=true`) expect(response.ok()).toBe(true) const data = await response.json() // Categories should be present and scoped to tenant expect(data).toHaveProperty('categories') expect(Array.isArray(data.categories)).toBe(true) }) }) test.describe('Tenant Validation', () => { test('Rejects invalid tenant ID format', async ({ request }) => { const response = await request.get('/api/news?tenant=invalid') expect(response.status()).toBe(400) const data = await response.json() expect(data.error).toContain('Invalid tenant ID') }) test('Rejects negative tenant ID', async ({ request }) => { const response = await request.get('/api/news?tenant=-1') expect(response.status()).toBe(400) const data = await response.json() expect(data.error).toContain('Invalid tenant ID') }) test('Rejects zero tenant ID', async ({ request }) => { const response = await request.get('/api/news?tenant=0') expect(response.status()).toBe(400) const data = await response.json() expect(data.error).toContain('Invalid tenant ID') }) test('Rejects floating point tenant ID', async ({ request }) => { const response = await request.get('/api/news?tenant=1.5') // Should either reject or truncate to integer expect([200, 400]).toContain(response.status()) }) })