mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 16:14: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>
323 lines
11 KiB
TypeScript
323 lines
11 KiB
TypeScript
/**
|
|
* Tenant Access Control Unit Tests
|
|
*
|
|
* Tests for the tenant access control functions in src/lib/tenantAccess.ts
|
|
* Covers: getTenantIdFromHost, tenantScopedPublicRead, authenticatedOnly
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import type { PayloadRequest, Where } from 'payload'
|
|
import {
|
|
createSuperAdmin,
|
|
createTenantUser,
|
|
createTenantUserPrimitive,
|
|
createMockPayloadRequest,
|
|
createAnonymousRequest,
|
|
createMockTenant,
|
|
executeAccess,
|
|
hasFullAccess,
|
|
hasNoAccess,
|
|
hasFilteredAccess,
|
|
getTenantIdFromFilter,
|
|
TEST_TENANTS,
|
|
TEST_USERS,
|
|
} from '../../helpers/access-control-test-utils'
|
|
|
|
// ============================================================================
|
|
// Import the actual functions to test
|
|
// ============================================================================
|
|
|
|
import {
|
|
getTenantIdFromHost,
|
|
tenantScopedPublicRead,
|
|
authenticatedOnly,
|
|
} from '@/lib/tenantAccess'
|
|
|
|
// ============================================================================
|
|
// getTenantIdFromHost Tests
|
|
// ============================================================================
|
|
|
|
describe('getTenantIdFromHost', () => {
|
|
describe('Host Header Extraction', () => {
|
|
it('extracts tenant ID from valid domain', async () => {
|
|
const request = createAnonymousRequest('porwoll.de', [TEST_TENANTS.porwoll])
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBe(1)
|
|
})
|
|
|
|
it('extracts tenant ID with port in host', async () => {
|
|
const request = createAnonymousRequest('porwoll.de:3000', [TEST_TENANTS.porwoll])
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBe(1)
|
|
})
|
|
|
|
it('extracts tenant ID with www prefix', async () => {
|
|
const tenant = createMockTenant({
|
|
id: 1,
|
|
domains: [{ domain: 'porwoll.de' }],
|
|
})
|
|
const request = createAnonymousRequest('www.porwoll.de', [tenant])
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBe(1)
|
|
})
|
|
|
|
it('handles uppercase domain', async () => {
|
|
const request = createAnonymousRequest('PORWOLL.DE', [TEST_TENANTS.porwoll])
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBe(1)
|
|
})
|
|
|
|
it('returns null for missing host header', async () => {
|
|
const request = createMockPayloadRequest(null, { tenants: [TEST_TENANTS.porwoll] })
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBeNull()
|
|
})
|
|
|
|
it('returns null for unknown domain', async () => {
|
|
const request = createAnonymousRequest('unknown-domain.com', [TEST_TENANTS.porwoll])
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBeNull()
|
|
})
|
|
|
|
it('returns null for empty host header', async () => {
|
|
const request = createMockPayloadRequest(null, { host: '', tenants: [TEST_TENANTS.porwoll] })
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('Multiple Tenants', () => {
|
|
const allTenants = [TEST_TENANTS.porwoll, TEST_TENANTS.c2s, TEST_TENANTS.gunshin]
|
|
|
|
it('resolves correct tenant from multiple options', async () => {
|
|
const request = createAnonymousRequest('complexcaresolutions.de', allTenants)
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBe(4) // c2s tenant ID
|
|
})
|
|
|
|
it('resolves each tenant correctly', async () => {
|
|
const porwollReq = createAnonymousRequest('porwoll.de', allTenants)
|
|
const gunshinReq = createAnonymousRequest('gunshin.de', allTenants)
|
|
|
|
const porwollId = await getTenantIdFromHost(porwollReq as unknown as PayloadRequest)
|
|
const gunshinId = await getTenantIdFromHost(gunshinReq as unknown as PayloadRequest)
|
|
|
|
expect(porwollId).toBe(1)
|
|
expect(gunshinId).toBe(5)
|
|
})
|
|
})
|
|
|
|
describe('Error Handling', () => {
|
|
it('returns null when payload.find throws', async () => {
|
|
const request = createMockPayloadRequest(null, { host: 'test.com' })
|
|
request.payload.find = vi.fn().mockRejectedValue(new Error('Database error'))
|
|
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBeNull()
|
|
})
|
|
|
|
it('returns null for tenant without ID', async () => {
|
|
const request = createMockPayloadRequest(null, { host: 'test.com' })
|
|
request.payload.find = vi.fn().mockResolvedValue({
|
|
docs: [{ name: 'Test Tenant' }], // Missing ID
|
|
totalDocs: 1,
|
|
})
|
|
|
|
const tenantId = await getTenantIdFromHost(request as unknown as PayloadRequest)
|
|
|
|
expect(tenantId).toBeNull()
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// tenantScopedPublicRead Tests
|
|
// ============================================================================
|
|
|
|
describe('tenantScopedPublicRead', () => {
|
|
describe('Authenticated Users', () => {
|
|
it('grants full access to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('grants full access to regular authenticated user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('grants full access to multi-tenant user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.multiTenantUser)
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Anonymous Users', () => {
|
|
it('returns tenant filter for valid domain', async () => {
|
|
const request = createAnonymousRequest('porwoll.de', [TEST_TENANTS.porwoll])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
expect(getTenantIdFromFilter(result as Where)).toBe(1)
|
|
})
|
|
|
|
it('returns different tenant filter for different domain', async () => {
|
|
const request = createAnonymousRequest('complexcaresolutions.de', [TEST_TENANTS.c2s])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
expect(getTenantIdFromFilter(result as Where)).toBe(4)
|
|
})
|
|
|
|
it('denies access for unknown domain', async () => {
|
|
const request = createAnonymousRequest('malicious-site.com', [TEST_TENANTS.porwoll])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasNoAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('denies access when no host header', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(hasNoAccess(result)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Filter Structure', () => {
|
|
it('returns correct where clause structure', async () => {
|
|
const request = createAnonymousRequest('gunshin.de', [TEST_TENANTS.gunshin])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
expect(result).toEqual({
|
|
tenant: {
|
|
equals: 5,
|
|
},
|
|
})
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// authenticatedOnly Tests
|
|
// ============================================================================
|
|
|
|
describe('authenticatedOnly', () => {
|
|
describe('Grants Access', () => {
|
|
it('grants access to super admin', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('grants access to regular user', async () => {
|
|
const request = createMockPayloadRequest(TEST_USERS.porwollUser)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('grants access to user with minimal data', async () => {
|
|
const minimalUser = { id: 99, email: 'minimal@test.com' }
|
|
const request = createMockPayloadRequest(minimalUser)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
expect(result).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('Denies Access', () => {
|
|
it('denies access to anonymous user', async () => {
|
|
const request = createMockPayloadRequest(null)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
it('denies access when user is undefined', async () => {
|
|
const request = createMockPayloadRequest(undefined as unknown as null)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
expect(result).toBe(false)
|
|
})
|
|
})
|
|
})
|
|
|
|
// ============================================================================
|
|
// Edge Cases & Integration Scenarios
|
|
// ============================================================================
|
|
|
|
describe('Access Control Integration Scenarios', () => {
|
|
describe('Tenant Assignment Formats', () => {
|
|
it('handles tenant object format { tenant: { id } }', () => {
|
|
const user = createTenantUser([1, 4])
|
|
|
|
expect(user.tenants).toEqual([{ tenant: { id: 1 } }, { tenant: { id: 4 } }])
|
|
})
|
|
|
|
it('handles tenant primitive format { tenant: number }', () => {
|
|
const user = createTenantUserPrimitive([1, 4])
|
|
|
|
expect(user.tenants).toEqual([{ tenant: 1 }, { tenant: 4 }])
|
|
})
|
|
})
|
|
|
|
describe('Real-World Scenarios', () => {
|
|
it('public blog post access from tenant domain', async () => {
|
|
// Anonymous user visiting porwoll.de/blog
|
|
const request = createAnonymousRequest('porwoll.de', [TEST_TENANTS.porwoll])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
// Should only see porwoll.de posts
|
|
expect(hasFilteredAccess(result)).toBe(true)
|
|
expect(getTenantIdFromFilter(result as Where)).toBe(1)
|
|
})
|
|
|
|
it('admin editing posts from any tenant', async () => {
|
|
// Super admin in admin panel
|
|
const request = createMockPayloadRequest(TEST_USERS.superAdmin)
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
// Should see all posts
|
|
expect(hasFullAccess(result)).toBe(true)
|
|
})
|
|
|
|
it('tenant user creating content', async () => {
|
|
// User assigned to c2s tenant creating a post
|
|
const request = createMockPayloadRequest(TEST_USERS.c2sUser)
|
|
const result = await executeAccess(authenticatedOnly, request)
|
|
|
|
// Should be allowed to create
|
|
expect(result).toBe(true)
|
|
})
|
|
|
|
it('cross-origin attack prevention', async () => {
|
|
// Request from malicious site
|
|
const request = createAnonymousRequest('evil-site.com', [
|
|
TEST_TENANTS.porwoll,
|
|
TEST_TENANTS.c2s,
|
|
TEST_TENANTS.gunshin,
|
|
])
|
|
const result = await executeAccess(tenantScopedPublicRead, request)
|
|
|
|
// Should be denied
|
|
expect(hasNoAccess(result)).toBe(true)
|
|
})
|
|
})
|
|
})
|