/** * E2E Tests für Newsletter Double Opt-In Flow * * Testet den kompletten Newsletter-Anmeldeprozess inkl. Rate-Limiting */ import { test, expect } from '@playwright/test' // Test tenant ID (porwoll.de) const TEST_TENANT_ID = 1 test.describe('Newsletter Subscribe API', () => { const subscribeEndpoint = '/api/newsletter/subscribe' test('POST /api/newsletter/subscribe requires email', async ({ request }) => { const response = await request.post(subscribeEndpoint, { data: { tenantId: TEST_TENANT_ID, }, }) // Handle rate limiting if (response.status() === 429) { return } expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) expect(data.message).toContain('E-Mail') }) test('POST /api/newsletter/subscribe validates email format', async ({ request }) => { const response = await request.post(subscribeEndpoint, { data: { email: 'not-an-email', tenantId: TEST_TENANT_ID, }, }) // Handle rate limiting if (response.status() === 429) { return } expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) expect(data.message).toContain('gültige E-Mail') }) test('POST /api/newsletter/subscribe requires tenant', async ({ request }) => { const response = await request.post(subscribeEndpoint, { data: { email: 'test@example.com', }, }) // Handle rate limiting if (response.status() === 429) { return } expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) expect(data.message).toContain('Tenant') }) test('POST /api/newsletter/subscribe accepts valid subscription', async ({ request }) => { // Use a unique email to avoid conflicts with existing data const uniqueEmail = `test-${Date.now()}@e2e-test.example` const response = await request.post(subscribeEndpoint, { data: { email: uniqueEmail, tenantId: TEST_TENANT_ID, firstName: 'E2E', lastName: 'Test', source: 'e2e-test', }, }) // Should succeed, indicate already subscribed, or be rate limited expect([200, 400, 429]).toContain(response.status()) // Only check response body if not rate limited if (response.status() !== 429) { const data = await response.json() expect(data).toHaveProperty('success') expect(data).toHaveProperty('message') if (data.success) { // New subscription expect(data.message).toContain('Bestätigungs') } } }) test('POST /api/newsletter/subscribe normalizes email to lowercase', async ({ request }) => { const uniqueEmail = `Test-Upper-${Date.now()}@E2E-TEST.Example` const response = await request.post(subscribeEndpoint, { data: { email: uniqueEmail, tenantId: TEST_TENANT_ID, }, }) // Request should be processed (email normalized internally) or rate limited expect([200, 400, 429]).toContain(response.status()) }) test('POST /api/newsletter/subscribe handles optional fields', async ({ request }) => { const uniqueEmail = `test-optional-${Date.now()}@e2e-test.example` const response = await request.post(subscribeEndpoint, { data: { email: uniqueEmail, tenantId: TEST_TENANT_ID, // Only required fields, no optional ones }, }) // Accept rate limiting as valid (429) expect([200, 400, 429]).toContain(response.status()) // Only check response structure if not rate limited if (response.status() !== 429) { const data = await response.json() expect(data).toHaveProperty('success') expect(data).toHaveProperty('message') } }) test('POST /api/newsletter/subscribe accepts source parameter', async ({ request }) => { const uniqueEmail = `test-source-${Date.now()}@e2e-test.example` const response = await request.post(subscribeEndpoint, { data: { email: uniqueEmail, tenantId: TEST_TENANT_ID, source: 'footer-form', }, }) // Accept rate limiting as valid (429) expect([200, 400, 429]).toContain(response.status()) }) }) test.describe('Newsletter Confirm API', () => { const confirmEndpoint = '/api/newsletter/confirm' test('GET /api/newsletter/confirm requires token', async ({ request }) => { const response = await request.get(confirmEndpoint) // Returns HTML page with error expect(response.ok()).toBe(true) const html = await response.text() expect(html).toContain('Ungültiger') }) test('GET /api/newsletter/confirm rejects invalid token', async ({ request }) => { const response = await request.get(`${confirmEndpoint}?token=invalid-token-12345`) expect(response.ok()).toBe(true) const html = await response.text() // Should show error page expect(html).toContain('Fehler') }) test('POST /api/newsletter/confirm requires token', async ({ request }) => { const response = await request.post(confirmEndpoint, { data: {}, }) expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) expect(data.message).toContain('Token') }) test('POST /api/newsletter/confirm rejects invalid token', async ({ request }) => { const response = await request.post(confirmEndpoint, { data: { token: 'invalid-token-67890', }, }) expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) }) test('GET /api/newsletter/confirm returns HTML response', async ({ request }) => { const response = await request.get(`${confirmEndpoint}?token=test`) expect(response.ok()).toBe(true) const contentType = response.headers()['content-type'] expect(contentType).toContain('text/html') }) }) test.describe('Newsletter Unsubscribe API', () => { const unsubscribeEndpoint = '/api/newsletter/unsubscribe' test('GET /api/newsletter/unsubscribe requires token', async ({ request }) => { const response = await request.get(unsubscribeEndpoint) expect(response.ok()).toBe(true) const html = await response.text() expect(html).toContain('Ungültiger') }) test('GET /api/newsletter/unsubscribe returns HTML response', async ({ request }) => { const response = await request.get(`${unsubscribeEndpoint}?token=test`) expect(response.ok()).toBe(true) const contentType = response.headers()['content-type'] expect(contentType).toContain('text/html') }) test('POST /api/newsletter/unsubscribe requires token', async ({ request }) => { const response = await request.post(unsubscribeEndpoint, { data: {}, }) expect(response.status()).toBe(400) const data = await response.json() expect(data.success).toBe(false) }) }) test.describe('Newsletter Rate Limiting', () => { test('Newsletter subscribe has rate limiting headers', async ({ request }) => { const response = await request.post('/api/newsletter/subscribe', { data: { email: 'ratelimit-test@example.com', tenantId: TEST_TENANT_ID, }, }) // Rate limit headers should be present on rate limited response if (response.status() === 429) { expect(response.headers()['retry-after']).toBeDefined() } }) }) test.describe('Newsletter Flow Integration', () => { test('Complete subscription flow structure', async ({ request }) => { // 1. Subscribe const uniqueEmail = `integration-${Date.now()}@e2e-test.example` const subscribeResponse = await request.post('/api/newsletter/subscribe', { data: { email: uniqueEmail, tenantId: TEST_TENANT_ID, firstName: 'Integration', lastName: 'Test', source: 'e2e-integration-test', }, }) expect([200, 400, 429]).toContain(subscribeResponse.status()) const subscribeData = await subscribeResponse.json() // If successful, should indicate confirmation email sent if (subscribeData.success) { expect(subscribeData.message).toContain('Bestätigungs') } // 2. Confirm endpoint should be accessible const confirmResponse = await request.get('/api/newsletter/confirm?token=dummy') expect(confirmResponse.ok()).toBe(true) // 3. Unsubscribe endpoint should be accessible const unsubscribeResponse = await request.get('/api/newsletter/unsubscribe?token=dummy') expect(unsubscribeResponse.ok()).toBe(true) }) test('Re-subscription handling', async ({ request }) => { const email = `resub-test-${Date.now()}@e2e-test.example` // First subscription const firstResponse = await request.post('/api/newsletter/subscribe', { data: { email: email, tenantId: TEST_TENANT_ID, }, }) if (firstResponse.status() === 429) { test.skip() // Rate limited, skip test return } // Second subscription with same email const secondResponse = await request.post('/api/newsletter/subscribe', { data: { email: email, tenantId: TEST_TENANT_ID, }, }) // Should handle gracefully (either success or already subscribed message) expect([200, 400, 429]).toContain(secondResponse.status()) const data = await secondResponse.json() expect(data).toHaveProperty('success') expect(data).toHaveProperty('message') }) })