mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 15:04:14 +00:00
Comprehensive E2E test suite covering: - Authentication flow (login, CSRF, admin access) - News API (tenant isolation, filtering, pagination) - Newsletter Double Opt-In (subscribe, confirm, unsubscribe) - Form submission flow - Multi-tenant data isolation Tests validate: - Tenant parameter is required on public APIs - Cross-tenant data access is prevented - Rate limiting headers are present - API responses have correct structure - Error handling returns proper formats Updated Playwright config with: - CI-specific reporters (github, list) - Screenshot/video on failure - Improved timeouts 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
301 lines
9.6 KiB
TypeScript
301 lines
9.6 KiB
TypeScript
/**
|
|
* 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())
|
|
})
|
|
})
|