Merge branch 'feature/nextjs-16-upgrade' - TypeScript and test fixes

Merged changes:
- Reverted Next.js 16 upgrade (Payload CMS 3.68.4 incompatible)
- Fixed all TypeScript errors in test files
- ESLint and typecheck now pass

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-15 10:10:58 +00:00
commit 91d00b016e
11 changed files with 42 additions and 29 deletions

View file

@ -16,7 +16,7 @@ const nextConfig = {
workerThreads: false, workerThreads: false,
cpus: 1, cpus: 1,
}, },
// Your Next.js config here // Webpack configuration for TypeScript/ESM compatibility
webpack: (webpackConfig) => { webpack: (webpackConfig) => {
webpackConfig.resolve.extensionAlias = { webpackConfig.resolve.extensionAlias = {
'.cjs': ['.cts', '.cjs'], '.cjs': ['.cts', '.cjs'],

View file

@ -73,7 +73,7 @@
"vitest": "4.0.15" "vitest": "4.0.15"
}, },
"engines": { "engines": {
"node": "^18.20.2 || >=20.9.0", "node": ">=20.9.0",
"pnpm": "^9 || ^10" "pnpm": "^9 || ^10"
}, },
"pnpm": { "pnpm": {

View file

@ -26,8 +26,10 @@ export interface MockTenant {
domains?: Array<{ domain: string }> domains?: Array<{ domain: string }>
} }
export interface MockPayloadRequest extends Partial<PayloadRequest> { // Note: Not extending PayloadRequest to allow flexible mock types for testing
export interface MockPayloadRequest {
user?: MockUser | null user?: MockUser | null
// Allow both Headers and plain object for testing different header formats
headers: Headers | Record<string, string | string[] | undefined> headers: Headers | Record<string, string | string[] | undefined>
payload: { payload: {
find: ReturnType<typeof vi.fn> find: ReturnType<typeof vi.fn>
@ -126,10 +128,10 @@ export function createMockPayloadRequest(
tenants?: MockTenant[] tenants?: MockTenant[]
} = {}, } = {},
): MockPayloadRequest { ): MockPayloadRequest {
const headers: Record<string, string | string[] | undefined> = {} const headers = new Headers()
if (options.host) { if (options.host) {
headers['host'] = options.host headers.set('host', options.host)
} }
// Mock payload.find to resolve tenant from host // Mock payload.find to resolve tenant from host
@ -306,9 +308,11 @@ export async function executeAccess(
data?: Record<string, unknown> data?: Record<string, unknown>
} = {}, } = {},
): Promise<AccessResult> { ): Promise<AccessResult> {
// Convert string ID to number if needed (Payload access functions expect number | undefined)
const numericId = typeof options.id === 'string' ? parseInt(options.id, 10) : options.id
const result = await accessFn({ const result = await accessFn({
req: request as unknown as PayloadRequest, req: request as unknown as PayloadRequest,
id: options.id, id: numericId,
data: options.data, data: options.data,
}) })

View file

@ -3,12 +3,13 @@ import type { Payload } from 'payload'
import type { Tenant } from '@/payload-types' import type { Tenant } from '@/payload-types'
const mockSendMail = vi.fn(async () => ({ messageId: 'mocked-id' })) const mockSendMail = vi.fn(async () => ({ messageId: 'mocked-id' }))
const mockCreateTransport = vi.fn(() => ({ sendMail: mockSendMail })) // eslint-disable-next-line @typescript-eslint/no-unused-vars
const mockCreateTransport = vi.fn((_options?: unknown) => ({ sendMail: mockSendMail }))
vi.mock('nodemailer', () => ({ vi.mock('nodemailer', () => ({
__esModule: true, __esModule: true,
default: { default: {
createTransport: (...args: unknown[]) => mockCreateTransport(...args), createTransport: (options: unknown) => mockCreateTransport(options),
}, },
})) }))

View file

@ -75,8 +75,13 @@ describe('Payload Localization Integration', () => {
it('payload config has localization enabled', async () => { it('payload config has localization enabled', async () => {
const payloadConfig = await config const payloadConfig = await config
expect(payloadConfig.localization).toBeDefined() expect(payloadConfig.localization).toBeDefined()
expect(payloadConfig.localization?.locales).toBeDefined() expect(payloadConfig.localization).not.toBe(false)
expect(payloadConfig.localization?.defaultLocale).toBe('de') // Type guard for localization config
const localization = payloadConfig.localization
if (localization && typeof localization === 'object') {
expect(localization.locales).toBeDefined()
expect(localization.defaultLocale).toBe('de')
}
}) })
it('payload config has i18n enabled', async () => { it('payload config has i18n enabled', async () => {

View file

@ -242,10 +242,12 @@ describe('Search API Integration', () => {
try { try {
const post = await payload.create({ const post = await payload.create({
collection: 'posts', collection: 'posts',
draft: false,
data: { data: {
title: 'Searchable Test Post Title', title: 'Searchable Test Post Title',
slug: `searchable-test-post-${Date.now()}`, slug: `searchable-test-post-${Date.now()}`,
excerpt: 'This is a searchable excerpt for testing', excerpt: 'This is a searchable excerpt for testing',
type: 'blog',
status: 'published', status: 'published',
publishedAt: new Date().toISOString(), publishedAt: new Date().toISOString(),
tenant: testTenantId, tenant: testTenantId,

View file

@ -9,7 +9,7 @@
*/ */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import type { Access, PayloadRequest } from 'payload' import type { Access, PayloadRequest, Where } from 'payload'
import { import {
createSuperAdmin, createSuperAdmin,
createTenantUser, createTenantUser,
@ -122,7 +122,7 @@ describe('EmailLogs Collection Access', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds).toContain(1) // porwoll tenant ID expect(tenantIds).toContain(1) // porwoll tenant ID
}) })
@ -131,7 +131,7 @@ describe('EmailLogs Collection Access', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds).toEqual(expect.arrayContaining([1, 4, 5])) expect(tenantIds).toEqual(expect.arrayContaining([1, 4, 5]))
}) })
@ -141,7 +141,7 @@ describe('EmailLogs Collection Access', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds).toContain(1) expect(tenantIds).toContain(1)
expect(tenantIds).toContain(4) expect(tenantIds).toContain(4)
}) })
@ -159,7 +159,7 @@ describe('EmailLogs Collection Access', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds).toEqual([]) expect(tenantIds).toEqual([])
}) })
}) })
@ -377,7 +377,7 @@ describe('Access Control Edge Cases', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds).toHaveLength(0) expect(tenantIds).toHaveLength(0)
}) })
@ -428,7 +428,7 @@ describe('Access Control Edge Cases', () => {
const result = await executeAccess(emailLogsAccess.read, request) const result = await executeAccess(emailLogsAccess.read, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
const tenantIds = getTenantIdsFromInFilter(result as Record<string, unknown>) const tenantIds = getTenantIdsFromInFilter(result as Where)
expect(tenantIds.sort()).toEqual([1, 2, 3]) expect(tenantIds.sort()).toEqual([1, 2, 3])
}) })
}) })

View file

@ -6,7 +6,7 @@
*/ */
import { describe, it, expect, vi, beforeEach } from 'vitest' import { describe, it, expect, vi, beforeEach } from 'vitest'
import type { PayloadRequest } from 'payload' import type { PayloadRequest, Where } from 'payload'
import { import {
createSuperAdmin, createSuperAdmin,
createTenantUser, createTenantUser,
@ -173,7 +173,7 @@ describe('tenantScopedPublicRead', () => {
const result = await executeAccess(tenantScopedPublicRead, request) const result = await executeAccess(tenantScopedPublicRead, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
expect(getTenantIdFromFilter(result as Record<string, unknown>)).toBe(1) expect(getTenantIdFromFilter(result as Where)).toBe(1)
}) })
it('returns different tenant filter for different domain', async () => { it('returns different tenant filter for different domain', async () => {
@ -181,7 +181,7 @@ describe('tenantScopedPublicRead', () => {
const result = await executeAccess(tenantScopedPublicRead, request) const result = await executeAccess(tenantScopedPublicRead, request)
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
expect(getTenantIdFromFilter(result as Record<string, unknown>)).toBe(4) expect(getTenantIdFromFilter(result as Where)).toBe(4)
}) })
it('denies access for unknown domain', async () => { it('denies access for unknown domain', async () => {
@ -286,7 +286,7 @@ describe('Access Control Integration Scenarios', () => {
// Should only see porwoll.de posts // Should only see porwoll.de posts
expect(hasFilteredAccess(result)).toBe(true) expect(hasFilteredAccess(result)).toBe(true)
expect(getTenantIdFromFilter(result as Record<string, unknown>)).toBe(1) expect(getTenantIdFromFilter(result as Where)).toBe(1)
}) })
it('admin editing posts from any tenant', async () => { it('admin editing posts from any tenant', async () => {

View file

@ -288,7 +288,7 @@ describe('Data Masking', () => {
it('handles non-Error objects', () => { it('handles non-Error objects', () => {
const notAnError = { message: 'password=secret', code: 500 } const notAnError = { message: 'password=secret', code: 500 }
const masked = maskError(notAnError as Error) const masked = maskError(notAnError as unknown as Error)
expect(masked).toBeDefined() expect(masked).toBeDefined()
}) })

View file

@ -205,7 +205,7 @@ describe('Rate Limiter', () => {
resetIn: 45000, resetIn: 45000,
} }
const headers = rateLimitHeaders(result, 30) const headers = rateLimitHeaders(result, 30) as Record<string, string>
expect(headers['X-RateLimit-Limit']).toBe('30') expect(headers['X-RateLimit-Limit']).toBe('30')
expect(headers['X-RateLimit-Remaining']).toBe('25') expect(headers['X-RateLimit-Remaining']).toBe('25')
@ -220,7 +220,7 @@ describe('Rate Limiter', () => {
retryAfter: 30, retryAfter: 30,
} }
const headers = rateLimitHeaders(result, 10) const headers = rateLimitHeaders(result, 10) as Record<string, string>
expect(headers['Retry-After']).toBe('30') expect(headers['Retry-After']).toBe('30')
expect(headers['X-RateLimit-Remaining']).toBe('0') expect(headers['X-RateLimit-Remaining']).toBe('0')
@ -233,8 +233,8 @@ describe('Rate Limiter', () => {
resetIn: 60000, resetIn: 60000,
} }
const headers = rateLimitHeaders(result, 10) const headers = rateLimitHeaders(result, 10) as Record<string, string>
const resetValue = headers['X-RateLimit-Reset'] as string const resetValue = headers['X-RateLimit-Reset']
// The reset value should be a number (either timestamp or seconds) // The reset value should be a number (either timestamp or seconds)
expect(resetValue).toBeDefined() expect(resetValue).toBeDefined()

View file

@ -30,15 +30,16 @@
"./src/payload.config.ts" "./src/payload.config.ts"
] ]
}, },
"target": "ES2022", "target": "ES2022"
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
".next/types/**/*.ts" ".next/types/**/*.ts",
".next/dev/types/**/*.ts"
], ],
"exclude": [ "exclude": [
"node_modules" "node_modules"
], ]
} }