feat: harden content workflows and staging smoke tests
CI — Test & Build / 🧪 Run Tests & Generate Reports (push) Waiting to run
CI — Test & Build / 🏗️ Build Docker Image (push) Blocked by required conditions
CI — Test & Build / 🐳 Docker integration & API E2E (push) Blocked by required conditions
CI — Test & Build / 🌐 Staging Playwright smoke (push) Waiting to run

This commit is contained in:
Do Siki
2026-08-17 16:08:57 +02:00
parent 35e341d5b2
commit 0047e1b441
28 changed files with 1208 additions and 360 deletions
+20 -9
View File
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from 'next/server'
import { getCollection } from '@/lib/mongodb'
interface ContactFormData {
name: string
@@ -8,6 +9,16 @@ interface ContactFormData {
gdprConsent: boolean
}
interface ContactSubmission {
name: string
email: string
subject: string
message: string
gdprConsent: true
status: 'new'
createdAt: Date
}
// Simple spam protection - rate limiting by IP
const rateLimitMap = new Map<string, { count: number; timestamp: number }>()
const RATE_LIMIT_WINDOW = 60 * 1000 // 1 minute
@@ -93,20 +104,20 @@ export async function POST(request: NextRequest) {
)
}
// Log the contact form submission (in production, this would be sent via email or saved to database)
console.log('Contact form submission:', {
const submissions = await getCollection<ContactSubmission>('contact_submissions')
const submission: ContactSubmission = {
...sanitizedData,
timestamp: new Date().toISOString(),
ip: ip
})
// TODO: In production, implement actual email sending
// For now, we'll just simulate success
gdprConsent: true,
status: 'new',
createdAt: new Date(),
}
const result = await submissions.insertOne(submission)
return NextResponse.json(
{
message: 'Üzenet sikeresen elküldve!',
timestamp: new Date().toISOString()
timestamp: submission.createdAt.toISOString(),
submissionId: result.insertedId.toHexString()
},
{ status: 200 }
)
@@ -2,6 +2,9 @@
* Unit tests for Contact API route
* These tests focus on testing the business logic without complex mocking
*/
import { POST } from './route'
const mockInsertOne = jest.fn()
// Mock the logger to avoid complex setup
jest.mock('@/lib/logger', () => ({
@@ -12,7 +15,57 @@ jest.mock('@/lib/logger', () => ({
})
}))
jest.mock('@/lib/mongodb', () => ({
getCollection: jest.fn(async () => ({ insertOne: mockInsertOne })),
}))
describe('/api/contact Unit Tests', () => {
beforeEach(() => {
mockInsertOne.mockReset()
})
describe('MongoDB persistence', () => {
const validData = {
name: 'Test User',
email: 'test@example.com',
subject: 'Test Subject',
message: 'This is a test message with enough content',
gdprConsent: true,
}
it('persists a valid submission before reporting success', async () => {
mockInsertOne.mockResolvedValue({ insertedId: { toHexString: () => 'submission-123' } })
const request = {
headers: new Headers({ 'x-forwarded-for': 'persistence-success' }),
json: async () => validData,
} as any
const response = await POST(request)
const body = await response.json()
expect(response.status).toBe(200)
expect(body.submissionId).toBe('submission-123')
expect(mockInsertOne).toHaveBeenCalledWith(expect.objectContaining({
...validData,
status: 'new',
createdAt: expect.any(Date),
}))
})
it('returns a server error when persistence fails', async () => {
mockInsertOne.mockRejectedValue(new Error('MongoDB unavailable'))
const request = {
headers: new Headers({ 'x-forwarded-for': 'persistence-failure' }),
json: async () => validData,
} as any
const response = await POST(request)
expect(response.status).toBe(500)
expect(mockInsertOne).toHaveBeenCalledTimes(1)
})
})
// TC-001: Email Format Validation Test (ZEE-48)
describe('Input validation logic', () => {
it('should validate required fields', () => {
+1 -1
View File
@@ -145,7 +145,7 @@ export default function Home() {
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto">
{pageContent.services.items.map((service: any, index: number) => (
{pageContent.services.items.map((service, index) => (
<div
key={service.id}
className="group card hover-lift"
+1 -1
View File
@@ -32,7 +32,7 @@ export default function ServicesPage() {
{/* Services Grid */}
<section className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{content.pages.home.services.items.map((service: any) => (
{content.pages.home.services.items.map((service) => (
<div key={service.id} className="bg-white rounded-xl shadow-sm border border-gray-200 p-8 hover:shadow-md transition-shadow">
<div className="w-16 h-16 bg-blue-100 rounded-lg flex items-center justify-center mb-6">
<span className="text-2xl">{service.icon}</span>