mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 17:24:12 +00:00
Payload CMS 3.68.4 doesn't officially support Next.js 16 yet. Reverting to 15.5.9 restores full compatibility. Changes: - Revert next: 16.0.10 → 15.5.9 - Revert eslint-config-next: 16.0.10 → 15.5.9 - Revert proxy.ts → middleware.ts (Next.js 15 convention) - Restore eslint config in next.config.mjs - Remove turbopack config (not needed for Next.js 15) Test fixes (TypeScript errors): - Fix MockPayloadRequest interface (remove PayloadRequest extension) - Add Where type imports to access control tests - Fix headers type casting in rate-limiter tests - Fix localization type guard in i18n tests - Add type property to post creation in search tests - Fix nodemailer mock typing in email tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
231 lines
6.3 KiB
TypeScript
231 lines
6.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
import type { Payload } from 'payload'
|
|
import type { Tenant } from '@/payload-types'
|
|
|
|
const mockSendMail = vi.fn(async () => ({ messageId: 'mocked-id' }))
|
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
const mockCreateTransport = vi.fn((_options?: unknown) => ({ sendMail: mockSendMail }))
|
|
|
|
vi.mock('nodemailer', () => ({
|
|
__esModule: true,
|
|
default: {
|
|
createTransport: (options: unknown) => mockCreateTransport(options),
|
|
},
|
|
}))
|
|
|
|
import {
|
|
sendTenantEmail,
|
|
invalidateTenantEmailCache,
|
|
invalidateGlobalEmailCache,
|
|
} from '@/lib/email/tenant-email-service'
|
|
|
|
describe('tenant email service', () => {
|
|
let payload: Payload
|
|
let mockFindByID: ReturnType<typeof vi.fn>
|
|
let mockCreate: ReturnType<typeof vi.fn>
|
|
|
|
beforeEach(() => {
|
|
mockSendMail.mockClear()
|
|
mockCreateTransport.mockClear()
|
|
mockFindByID = vi.fn()
|
|
mockCreate = vi.fn().mockResolvedValue({ id: 1 })
|
|
|
|
payload = {
|
|
findByID: mockFindByID,
|
|
create: mockCreate,
|
|
update: vi.fn().mockResolvedValue({}),
|
|
} as unknown as Payload
|
|
|
|
process.env.SMTP_HOST = 'smtp.global.test'
|
|
process.env.SMTP_PORT = '587'
|
|
process.env.SMTP_SECURE = 'false'
|
|
process.env.SMTP_USER = 'global-user'
|
|
process.env.SMTP_PASS = 'global-pass'
|
|
process.env.SMTP_FROM_ADDRESS = 'noreply@example.com'
|
|
|
|
invalidateGlobalEmailCache()
|
|
})
|
|
|
|
afterEach(() => {
|
|
// Restore NODE_ENV after each test
|
|
delete process.env.EMAIL_DELIVERY_DISABLED
|
|
})
|
|
|
|
describe('with EMAIL_DELIVERY_DISABLED=false (production mode)', () => {
|
|
beforeEach(() => {
|
|
// Override test environment to simulate production
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
vi.stubEnv('EMAIL_DELIVERY_DISABLED', 'false')
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('falls back to global SMTP configuration when tenant has no custom SMTP', async () => {
|
|
const tenant = {
|
|
id: 1,
|
|
slug: 'tenant-a',
|
|
name: 'Tenant A',
|
|
email: {
|
|
useCustomSmtp: false,
|
|
},
|
|
} as Tenant
|
|
|
|
mockFindByID.mockResolvedValue(tenant)
|
|
|
|
const result = await sendTenantEmail(payload, tenant.id, {
|
|
to: 'user@example.com',
|
|
subject: 'Test',
|
|
text: 'Hello from test',
|
|
})
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(mockCreateTransport).toHaveBeenCalledTimes(1)
|
|
expect(mockCreateTransport).toHaveBeenCalledWith({
|
|
host: 'smtp.global.test',
|
|
port: 587,
|
|
secure: false,
|
|
auth: {
|
|
user: 'global-user',
|
|
pass: 'global-pass',
|
|
},
|
|
})
|
|
expect(mockSendMail).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
from: '"Tenant A" <noreply@example.com>',
|
|
to: 'user@example.com',
|
|
}),
|
|
)
|
|
})
|
|
|
|
it('uses cached tenant-specific transporter and re-creates it after invalidation', async () => {
|
|
const tenant = {
|
|
id: 42,
|
|
slug: 'tenant-b',
|
|
name: 'Tenant B',
|
|
email: {
|
|
useCustomSmtp: true,
|
|
fromAddress: 'info@tenant-b.de',
|
|
fromName: 'Tenant B',
|
|
smtp: {
|
|
host: 'smtp.tenant-b.de',
|
|
port: 465,
|
|
secure: true,
|
|
user: 'tenant-user',
|
|
pass: 'tenant-pass',
|
|
},
|
|
},
|
|
} as Tenant
|
|
|
|
mockFindByID.mockResolvedValue(tenant)
|
|
|
|
await sendTenantEmail(payload, tenant.id, {
|
|
to: 'recipient@example.com',
|
|
subject: 'Hi',
|
|
text: 'First email',
|
|
})
|
|
|
|
expect(mockCreateTransport).toHaveBeenCalledTimes(1)
|
|
expect(mockCreateTransport).toHaveBeenCalledWith({
|
|
host: 'smtp.tenant-b.de',
|
|
port: 465,
|
|
secure: true,
|
|
auth: {
|
|
user: 'tenant-user',
|
|
pass: 'tenant-pass',
|
|
},
|
|
})
|
|
|
|
mockCreateTransport.mockClear()
|
|
|
|
await sendTenantEmail(payload, tenant.id, {
|
|
to: 'recipient@example.com',
|
|
subject: 'Hi again',
|
|
text: 'Second email',
|
|
})
|
|
|
|
expect(mockCreateTransport).not.toHaveBeenCalled()
|
|
|
|
invalidateTenantEmailCache(tenant.id)
|
|
|
|
await sendTenantEmail(payload, tenant.id, {
|
|
to: 'recipient@example.com',
|
|
subject: 'After invalidation',
|
|
text: 'Third email',
|
|
})
|
|
|
|
expect(mockCreateTransport).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('with EMAIL_DELIVERY_DISABLED=true (test mode)', () => {
|
|
it('skips SMTP delivery and returns synthetic message ID', async () => {
|
|
// NODE_ENV=test is default in vitest, which disables email delivery
|
|
const tenant = {
|
|
id: 1,
|
|
slug: 'tenant-a',
|
|
name: 'Tenant A',
|
|
email: {
|
|
useCustomSmtp: false,
|
|
},
|
|
} as Tenant
|
|
|
|
mockFindByID.mockResolvedValue(tenant)
|
|
|
|
const result = await sendTenantEmail(payload, tenant.id, {
|
|
to: 'user@example.com',
|
|
subject: 'Test',
|
|
text: 'Hello from test',
|
|
})
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.messageId).toMatch(/^test-message-\d+$/)
|
|
// SMTP should NOT be called in test mode
|
|
expect(mockCreateTransport).not.toHaveBeenCalled()
|
|
expect(mockSendMail).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('creates email log even when delivery is disabled', async () => {
|
|
const tenant = {
|
|
id: 1,
|
|
slug: 'tenant-a',
|
|
name: 'Tenant A',
|
|
email: {
|
|
useCustomSmtp: false,
|
|
},
|
|
} as Tenant
|
|
|
|
mockFindByID.mockResolvedValue(tenant)
|
|
|
|
await sendTenantEmail(payload, tenant.id, {
|
|
to: 'user@example.com',
|
|
subject: 'Test',
|
|
text: 'Hello from test',
|
|
})
|
|
|
|
// Email log should still be created
|
|
expect(mockCreate).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
collection: 'email-logs',
|
|
data: expect.objectContaining({
|
|
to: 'user@example.com',
|
|
subject: 'Test',
|
|
status: 'pending',
|
|
}),
|
|
}),
|
|
)
|
|
})
|
|
})
|
|
|
|
describe('cache invalidation', () => {
|
|
it('invalidates tenant-specific cache', () => {
|
|
// This is a simple function that clears cache - just verify it doesn't throw
|
|
expect(() => invalidateTenantEmailCache(42)).not.toThrow()
|
|
})
|
|
|
|
it('invalidates global cache', () => {
|
|
expect(() => invalidateGlobalEmailCache()).not.toThrow()
|
|
})
|
|
})
|
|
})
|