cms.c2sgmbh/tests/unit/access-control/collection-access.unit.spec.ts
Martin Porwoll 47d4825f77 revert: downgrade to Next.js 15.5.9 for Payload compatibility
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>
2025-12-15 10:07:39 +00:00

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)
})
})
})