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
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:
@@ -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', () => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user