cms.c2sgmbh/tests/e2e/news-api.e2e.spec.ts
Martin Porwoll e8532b388d test: add E2E tests for critical flows
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>
2025-12-12 22:32:55 +00:00

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)
})
})