mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 22:04:10 +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>
431 lines
13 KiB
TypeScript
431 lines
13 KiB
TypeScript
/**
|
|
* E2E Tests für News API
|
|
*
|
|
* Testet die dedizierte News-API mit Tenant-Isolation und Filtering
|
|
*/
|
|
|
|
import { test, expect } from '@playwright/test'
|
|
|
|
// Test tenant ID (porwoll.de)
|
|
const TEST_TENANT_ID = 1
|
|
|
|
test.describe('News API - List Endpoint', () => {
|
|
const newsEndpoint = '/api/news'
|
|
|
|
test('GET /api/news requires tenant parameter', async ({ request }) => {
|
|
const response = await request.get(newsEndpoint)
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('error')
|
|
expect(data.error).toContain('Tenant ID is required')
|
|
})
|
|
|
|
test('GET /api/news returns 400 for invalid tenant ID', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=invalid`)
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('error')
|
|
expect(data.error).toContain('Invalid tenant ID')
|
|
})
|
|
|
|
test('GET /api/news returns valid response structure with tenant', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
// Validate response structure
|
|
expect(data).toHaveProperty('docs')
|
|
expect(data).toHaveProperty('pagination')
|
|
expect(data).toHaveProperty('filters')
|
|
|
|
expect(Array.isArray(data.docs)).toBe(true)
|
|
expect(data.pagination).toHaveProperty('page')
|
|
expect(data.pagination).toHaveProperty('limit')
|
|
expect(data.pagination).toHaveProperty('totalPages')
|
|
expect(data.pagination).toHaveProperty('totalDocs')
|
|
expect(data.pagination).toHaveProperty('hasNextPage')
|
|
expect(data.pagination).toHaveProperty('hasPrevPage')
|
|
|
|
// Filters should contain tenant
|
|
expect(data.filters).toHaveProperty('tenant')
|
|
expect(data.filters.tenant).toBe(TEST_TENANT_ID)
|
|
})
|
|
|
|
test('GET /api/news validates type parameter', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&type=invalid`)
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('error')
|
|
expect(data.error).toContain('Invalid type')
|
|
})
|
|
|
|
test('GET /api/news accepts valid type parameter', async ({ request }) => {
|
|
const validTypes = ['news', 'press', 'announcement', 'blog']
|
|
|
|
for (const type of validTypes) {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&type=${type}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data.filters.type).toBe(type)
|
|
|
|
// All returned docs should match the type
|
|
for (const doc of data.docs) {
|
|
expect(doc.type).toBe(type)
|
|
}
|
|
}
|
|
})
|
|
|
|
test('GET /api/news supports multiple types', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&types=news,blog`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
// All docs should be either news or blog
|
|
for (const doc of data.docs) {
|
|
expect(['news', 'blog']).toContain(doc.type)
|
|
}
|
|
})
|
|
|
|
test('GET /api/news respects pagination parameters', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&page=1&limit=5`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data.pagination.page).toBe(1)
|
|
expect(data.pagination.limit).toBe(5)
|
|
expect(data.docs.length).toBeLessThanOrEqual(5)
|
|
})
|
|
|
|
test('GET /api/news enforces max limit', async ({ request }) => {
|
|
// Request more than max limit (50)
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&limit=100`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
// Should cap at max limit
|
|
expect(data.pagination.limit).toBeLessThanOrEqual(50)
|
|
})
|
|
|
|
test('GET /api/news includes categories when requested', async ({ request }) => {
|
|
const response = await request.get(
|
|
`${newsEndpoint}?tenant=${TEST_TENANT_ID}&includeCategories=true`
|
|
)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('categories')
|
|
expect(Array.isArray(data.categories)).toBe(true)
|
|
|
|
// Each category should have id, name, slug
|
|
for (const cat of data.categories) {
|
|
expect(cat).toHaveProperty('id')
|
|
expect(cat).toHaveProperty('name')
|
|
expect(cat).toHaveProperty('slug')
|
|
}
|
|
})
|
|
|
|
test('GET /api/news includes archive when requested', async ({ request }) => {
|
|
const response = await request.get(
|
|
`${newsEndpoint}?tenant=${TEST_TENANT_ID}&includeArchive=true`
|
|
)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('archive')
|
|
expect(Array.isArray(data.archive)).toBe(true)
|
|
|
|
// Each archive entry should have year, months, total
|
|
for (const entry of data.archive) {
|
|
expect(entry).toHaveProperty('year')
|
|
expect(entry).toHaveProperty('months')
|
|
expect(entry).toHaveProperty('total')
|
|
expect(Array.isArray(entry.months)).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('GET /api/news filters by featured', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&featured=true`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data.filters.featured).toBe(true)
|
|
|
|
// All docs should be featured
|
|
for (const doc of data.docs) {
|
|
expect(doc.isFeatured).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('GET /api/news validates year parameter', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&year=1900`)
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data.error).toContain('Invalid year')
|
|
})
|
|
|
|
test('GET /api/news validates month parameter', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&month=13`)
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data.error).toContain('Invalid month')
|
|
})
|
|
|
|
test('GET /api/news accepts valid locale parameter', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}&locale=en`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
expect(data.filters.locale).toBe('en')
|
|
})
|
|
|
|
test('GET /api/news doc items have correct structure', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
for (const doc of data.docs) {
|
|
expect(doc).toHaveProperty('id')
|
|
expect(doc).toHaveProperty('title')
|
|
expect(doc).toHaveProperty('slug')
|
|
expect(doc).toHaveProperty('type')
|
|
expect(doc).toHaveProperty('excerpt')
|
|
expect(doc).toHaveProperty('publishedAt')
|
|
expect(doc).toHaveProperty('isFeatured')
|
|
expect(doc).toHaveProperty('featuredImage')
|
|
expect(doc).toHaveProperty('categories')
|
|
expect(doc).toHaveProperty('seo')
|
|
|
|
// Categories should be an array
|
|
expect(Array.isArray(doc.categories)).toBe(true)
|
|
}
|
|
})
|
|
|
|
test('GET /api/news includes rate limit headers', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const headers = response.headers()
|
|
expect(headers['x-ratelimit-remaining']).toBeDefined()
|
|
})
|
|
|
|
test('GET /api/news includes cache headers', async ({ request }) => {
|
|
const response = await request.get(`${newsEndpoint}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const headers = response.headers()
|
|
expect(headers['cache-control']).toBeDefined()
|
|
expect(headers['cache-control']).toContain('max-age')
|
|
})
|
|
})
|
|
|
|
test.describe('News API - Detail Endpoint', () => {
|
|
test('GET /api/news/[slug] requires tenant parameter', async ({ request }) => {
|
|
const response = await request.get('/api/news/some-article')
|
|
|
|
expect(response.status()).toBe(400)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('error')
|
|
expect(data.error).toContain('Tenant ID is required')
|
|
})
|
|
|
|
test('GET /api/news/[slug] returns 404 for non-existent article', async ({ request }) => {
|
|
const response = await request.get(
|
|
`/api/news/non-existent-article-slug-12345?tenant=${TEST_TENANT_ID}`
|
|
)
|
|
|
|
expect(response.status()).toBe(404)
|
|
|
|
const data = await response.json()
|
|
expect(data).toHaveProperty('error')
|
|
expect(data.error).toContain('not found')
|
|
})
|
|
|
|
test('GET /api/news/[slug] returns valid article structure', async ({ request }) => {
|
|
// First get a list to find a valid slug
|
|
const listResponse = await request.get(`/api/news?tenant=${TEST_TENANT_ID}&limit=1`)
|
|
|
|
if (!listResponse.ok()) {
|
|
test.skip()
|
|
return
|
|
}
|
|
|
|
const listData = await listResponse.json()
|
|
|
|
if (listData.docs.length === 0) {
|
|
test.skip() // No articles to test with
|
|
return
|
|
}
|
|
|
|
const slug = listData.docs[0].slug
|
|
const response = await request.get(`/api/news/${slug}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
expect(data).toHaveProperty('article')
|
|
expect(data).toHaveProperty('locale')
|
|
|
|
const article = data.article
|
|
expect(article).toHaveProperty('id')
|
|
expect(article).toHaveProperty('title')
|
|
expect(article).toHaveProperty('slug')
|
|
expect(article).toHaveProperty('type')
|
|
expect(article).toHaveProperty('content')
|
|
expect(article).toHaveProperty('seo')
|
|
})
|
|
|
|
test('GET /api/news/[slug] includes related posts by default', async ({ request }) => {
|
|
// First get a list to find a valid slug
|
|
const listResponse = await request.get(`/api/news?tenant=${TEST_TENANT_ID}&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
|
|
const response = await request.get(`/api/news/${slug}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
// Related posts may or may not exist, but structure should be valid
|
|
if (data.relatedPosts) {
|
|
expect(Array.isArray(data.relatedPosts)).toBe(true)
|
|
|
|
for (const related of data.relatedPosts) {
|
|
expect(related).toHaveProperty('id')
|
|
expect(related).toHaveProperty('title')
|
|
expect(related).toHaveProperty('slug')
|
|
expect(related).toHaveProperty('type')
|
|
}
|
|
}
|
|
})
|
|
|
|
test('GET /api/news/[slug] includes navigation', async ({ request }) => {
|
|
// First get a list to find a valid slug
|
|
const listResponse = await request.get(`/api/news?tenant=${TEST_TENANT_ID}&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
|
|
const response = await request.get(`/api/news/${slug}?tenant=${TEST_TENANT_ID}`)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
expect(data).toHaveProperty('navigation')
|
|
expect(data.navigation).toHaveProperty('previous')
|
|
expect(data.navigation).toHaveProperty('next')
|
|
})
|
|
|
|
test('GET /api/news/[slug] can exclude related posts', async ({ request }) => {
|
|
const listResponse = await request.get(`/api/news?tenant=${TEST_TENANT_ID}&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
|
|
const response = await request.get(
|
|
`/api/news/${slug}?tenant=${TEST_TENANT_ID}&includeRelated=false`
|
|
)
|
|
|
|
expect(response.ok()).toBe(true)
|
|
|
|
const data = await response.json()
|
|
|
|
// Related posts should not be included
|
|
expect(data.relatedPosts).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
test.describe('News API - Tenant Isolation', () => {
|
|
test('Different tenants return isolated data', async ({ request }) => {
|
|
// Get news for tenant 1
|
|
const response1 = await request.get('/api/news?tenant=1')
|
|
// Get news for tenant 4 (C2S)
|
|
const response4 = await request.get('/api/news?tenant=4')
|
|
|
|
expect(response1.ok()).toBe(true)
|
|
expect(response4.ok()).toBe(true)
|
|
|
|
const data1 = await response1.json()
|
|
const data4 = await response4.json()
|
|
|
|
// Both should have valid structure
|
|
expect(data1).toHaveProperty('docs')
|
|
expect(data4).toHaveProperty('docs')
|
|
|
|
// Filter responses should reflect tenant
|
|
expect(data1.filters.tenant).toBe(1)
|
|
expect(data4.filters.tenant).toBe(4)
|
|
})
|
|
|
|
test('Cannot access news from non-existent tenant', 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 error
|
|
expect(data.docs).toEqual([])
|
|
expect(data.pagination.totalDocs).toBe(0)
|
|
})
|
|
})
|