mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +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>
559 lines
20 KiB
TypeScript
559 lines
20 KiB
TypeScript
/**
|
|
* Collection Access Control Unit Tests
|
|
*
|
|
* Tests for access control patterns used in various collections.
|
|
* Covers: Super Admin access, Tenant-scoped access, WORM patterns
|
|
*
|
|
* IMPORTANT: These tests import the actual access functions from @/lib/access
|
|
* to ensure regressions in live collections are detected.
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import type { Access, PayloadRequest, Where } from 'payload'
|
|
import {
|
|
createSuperAdmin,
|
|
createTenantUser,
|
|
createTenantUserPrimitive,
|
|
createMockPayloadRequest,
|
|
executeAccess,
|
|
hasFullAccess,
|
|
hasNoAccess,
|
|
hasFilteredAccess,
|
|
getTenantIdsFromInFilter,
|
|
TEST_USERS,
|
|
} from '../../helpers/access-control-test-utils'
|
|
|
|
// ============================================================================
|
|
// Import REAL access functions from centralized library
|
|
// ============================================================================
|
|
|
|
import {
|
|
superAdminOnly,
|
|
denyAll,
|
|
auditLogsAccess,
|
|
emailLogsReadAccess,
|
|
emailLogsAccess,
|
|
pagesReadAccess,
|
|
pagesWriteAccess,
|
|
pagesAccess,
|
|
createApiKeyAccess,
|
|
consentLogsCreateAccess,
|
|
} from '@/lib/access'
|
|
|
|
// ============================================================================
|
|
// AuditLogs Access Tests
|
|
// ============================================================================
|
|
|
|
describe('AuditLogs Collection Access', () => {
|
|
describe('Read Access', () => {
|
|
it('grants access to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies access to regular tenant user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies access to multi-tenant user without super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.multiTenantUser)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies access to anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('WORM Pattern (Write-Once-Read-Many)', () => {
|
|
it('denies create for everyone including super admin', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const userReq = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const anonReq = createMockPayloadRequest(null)
|
|
|
|
expect(await executeAccess(auditLogsAccess.create, superAdminReq)).toBe(false)
|
|
expect(await executeAccess(auditLogsAccess.create, userReq)).toBe(false)
|
|
expect(await executeAccess(auditLogsAccess.create, anonReq)).toBe(false)
|
|
})
|
|
|
|
it('denies update for everyone including super admin', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const userReq = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
|
|
expect(await executeAccess(auditLogsAccess.update, superAdminReq)).toBe(false)
|
|
expect(await executeAccess(auditLogsAccess.update, userReq)).toBe(false)
|
|
})
|
|
|
|
it('denies delete for everyone including super admin', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const userReq = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
|
|
expect(await executeAccess(auditLogsAccess.delete, superAdminReq)).toBe(false)
|
|
expect(await executeAccess(auditLogsAccess.delete, userReq)).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// EmailLogs Access Tests
|
|
// ============================================================================
|
|
|
|
describe('EmailLogs Collection Access', () => {
|
|
describe('Read Access', () => {
|
|
it('grants full access to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('filters by tenant for regular user with object format', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds).toContain(1) // porwoll tenant ID
|
|
})
|
|
|
|
it('filters by multiple tenants for multi-tenant user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.multiTenantUser)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds).toEqual(expect.arrayContaining([1, 4, 5]))
|
|
})
|
|
|
|
it('handles primitive tenant format', async () => {
|
|
const user = createTenantUserPrimitive([1, 4])
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds).toContain(1)
|
|
expect(tenantIds).toContain(4)
|
|
})
|
|
|
|
it('denies access to anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasNoAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('returns empty tenant filter for user with no tenants', async () => {
|
|
const userNoTenants = createTenantUser([])
|
|
const request = createMockPayloadRequest(userNoTenants)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds).toEqual([])
|
|
})
|
|
})
|
|
|
|
describe('Create/Update Access', () => {
|
|
it('denies create for everyone (system-generated only)', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const userReq = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
|
|
expect(await executeAccess(emailLogsAccess.create, superAdminReq)).toBe(false)
|
|
expect(await executeAccess(emailLogsAccess.create, userReq)).toBe(false)
|
|
})
|
|
|
|
it('denies update for everyone', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
|
|
expect(await executeAccess(emailLogsAccess.update, superAdminReq)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Delete Access', () => {
|
|
it('grants delete to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(emailLogsAccess.delete, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies delete to regular user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(emailLogsAccess.delete, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies delete to anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(emailLogsAccess.delete, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Pages Status-Based Access Tests
|
|
// ============================================================================
|
|
|
|
describe('Pages Collection Access', () => {
|
|
describe('Read Access', () => {
|
|
it('grants full access to authenticated user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(pagesAccess.read, request)
|
|
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('filters by published status for anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(pagesAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
expect(result).toEqual({ status: { equals: 'published' } })
|
|
})
|
|
})
|
|
|
|
describe('Write Access', () => {
|
|
it('grants write to authenticated user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(pagesAccess.create, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies write to anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(pagesAccess.create, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// ConsentLogs API Key Access Tests
|
|
// ============================================================================
|
|
|
|
describe('ConsentLogs Collection Access', () => {
|
|
const originalEnv = process.env.CONSENT_LOGGING_API_KEY
|
|
|
|
beforeEach(() => {
|
|
process.env.CONSENT_LOGGING_API_KEY = 'test-consent-api-key'
|
|
})
|
|
|
|
afterEach(() => {
|
|
if (originalEnv !== undefined) {
|
|
process.env.CONSENT_LOGGING_API_KEY = originalEnv
|
|
} else {
|
|
delete process.env.CONSENT_LOGGING_API_KEY
|
|
}
|
|
})
|
|
|
|
describe('API Key Access', () => {
|
|
it('grants create with valid API key (Record headers)', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'test-consent-api-key' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('grants create with valid API key (Headers object)', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const headers = new Headers()
|
|
headers.set('x-api-key', 'test-consent-api-key')
|
|
request.headers = headers
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies create with invalid API key', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'wrong-api-key' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies create with missing API key', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = {}
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies create with array API key header', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': ['key1', 'key2'] }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('trims whitespace from API key', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': ' test-consent-api-key ' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies create when env var not set', async () => {
|
|
delete process.env.CONSENT_LOGGING_API_KEY
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'test-consent-api-key' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies create with empty API key', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': '' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies create with whitespace-only API key', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': ' ' }
|
|
|
|
const result = await executeAccess(consentLogsCreateAccess, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Edge Cases & Security Scenarios
|
|
// ============================================================================
|
|
|
|
describe('Access Control Edge Cases', () => {
|
|
describe('User Object Variations', () => {
|
|
it('handles user without isSuperAdmin property', async () => {
|
|
const user = { id: 1, email: 'test@test.com' } // No isSuperAdmin
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('handles user with isSuperAdmin: false explicitly', async () => {
|
|
const user = { id: 1, email: 'test@test.com', isSuperAdmin: false }
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('handles user with empty tenants array', async () => {
|
|
const user = createTenantUser([])
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds).toHaveLength(0)
|
|
})
|
|
|
|
it('handles user without tenants property', async () => {
|
|
const user = { id: 1, email: 'test@test.com', isSuperAdmin: false }
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Privilege Escalation Prevention', () => {
|
|
it('prevents non-super-admin from accessing audit logs', async () => {
|
|
// Even with many tenants, should not see audit logs
|
|
const userManyTenants = createTenantUser([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
|
|
const request = createMockPayloadRequest(userManyTenants)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('prevents falsified isSuperAdmin claim without proper structure', async () => {
|
|
// User trying to fake super admin by setting string instead of boolean
|
|
const fakeAdmin = { id: 1, email: 'fake@test.com', isSuperAdmin: 'true' as unknown as boolean }
|
|
const request = createMockPayloadRequest(fakeAdmin)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
// String 'true' is truthy, but proper implementation should use Boolean()
|
|
// This test documents current behavior
|
|
expect(result).toBe(true) // Note: This shows Boolean('true') = true
|
|
})
|
|
})
|
|
|
|
describe('Tenant ID Extraction', () => {
|
|
it('correctly extracts IDs from mixed tenant formats', async () => {
|
|
const mixedUser = {
|
|
id: 1,
|
|
email: 'mixed@test.com',
|
|
isSuperAdmin: false,
|
|
tenants: [
|
|
{ tenant: { id: 1 } }, // Object format
|
|
{ tenant: 2 }, // Primitive format
|
|
{ tenant: { id: 3 } }, // Object format
|
|
],
|
|
}
|
|
const request = createMockPayloadRequest(mixedUser)
|
|
const result = await executeAccess(emailLogsAccess.read, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
const tenantIds = getTenantIdsFromInFilter(result as Where)
|
|
expect(tenantIds.sort()).toEqual([1, 2, 3])
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Access Pattern Verification Tests
|
|
// ============================================================================
|
|
|
|
describe('Access Pattern Consistency', () => {
|
|
const allUsers = [
|
|
{ name: 'Super Admin', user: TEST_USERS.superAdmin },
|
|
{ name: 'Porwoll User', user: TEST_USERS.porwollUser },
|
|
{ name: 'Multi-Tenant User', user: TEST_USERS.multiTenantUser },
|
|
{ name: 'Anonymous', user: null },
|
|
]
|
|
|
|
describe('WORM Collections (AuditLogs, EmailLogs Create/Update)', () => {
|
|
it.each(allUsers)('$name cannot create audit logs', async ({ user }) => {
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.create, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it.each(allUsers)('$name cannot update audit logs', async ({ user }) => {
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.update, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it.each(allUsers)('$name cannot delete audit logs', async ({ user }) => {
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.delete, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('Super Admin Only Collections (AuditLogs Read)', () => {
|
|
it('only super admin has read access', async () => {
|
|
for (const { name, user } of allUsers) {
|
|
const request = createMockPayloadRequest(user)
|
|
const result = await executeAccess(auditLogsAccess.read, request)
|
|
|
|
if (user?.isSuperAdmin) {
|
|
expect(result).toBe(true)
|
|
} else {
|
|
expect(result).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// createApiKeyAccess Factory Tests
|
|
// ============================================================================
|
|
|
|
describe('createApiKeyAccess Factory', () => {
|
|
const testEnvKey = 'TEST_API_KEY_FOR_UNIT_TESTS'
|
|
|
|
beforeEach(() => {
|
|
process.env[testEnvKey] = 'my-secret-api-key'
|
|
})
|
|
|
|
afterEach(() => {
|
|
delete process.env[testEnvKey]
|
|
})
|
|
|
|
it('creates access function that validates against env var', async () => {
|
|
const accessFn = createApiKeyAccess(testEnvKey)
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'my-secret-api-key' }
|
|
|
|
const result = await executeAccess(accessFn, request)
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('creates access function that rejects wrong key', async () => {
|
|
const accessFn = createApiKeyAccess(testEnvKey)
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'wrong-key' }
|
|
|
|
const result = await executeAccess(accessFn, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('returns false when env var is not set', async () => {
|
|
delete process.env[testEnvKey]
|
|
const accessFn = createApiKeyAccess(testEnvKey)
|
|
const request = createMockPayloadRequest(null)
|
|
request.headers = { 'x-api-key': 'my-secret-api-key' }
|
|
|
|
const result = await executeAccess(accessFn, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Standalone Function Tests
|
|
// ============================================================================
|
|
|
|
describe('Standalone Access Functions', () => {
|
|
describe('superAdminOnly', () => {
|
|
it('grants access to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(superAdminOnly, request)
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('denies access to regular user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(superAdminOnly, request)
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('denyAll', () => {
|
|
it('denies everyone including super admin', async () => {
|
|
const superAdminReq = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const userReq = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const anonReq = createMockPayloadRequest(null)
|
|
|
|
expect(await executeAccess(denyAll, superAdminReq)).toBe(false)
|
|
expect(await executeAccess(denyAll, userReq)).toBe(false)
|
|
expect(await executeAccess(denyAll, anonReq)).toBe(false)
|
|
})
|
|
})
|
|
})
|