Files
hr-portal/frontend/services/__tests__/tenant.service.test.ts
Porsche Chen 360533393f feat: HR Portal - Complete Multi-Tenant System with Redis Session Storage
Major Features:
-  Multi-tenant architecture (tenant isolation)
-  Employee CRUD with lifecycle management (onboarding/offboarding)
-  Department tree structure with email domain management
-  Company info management (single-record editing)
-  System functions CRUD (permission management)
-  Email account management (multi-account per employee)
-  Keycloak SSO integration (auth.lab.taipei)
-  Redis session storage (10.1.0.254:6379)
  - Solves Cookie 4KB limitation
  - Cross-system session sharing
  - Sliding expiration (8 hours)
  - Automatic token refresh

Technical Stack:
Backend:
- FastAPI + SQLAlchemy
- PostgreSQL 16 (10.1.0.20:5433)
- Keycloak Admin API integration
- Docker Mailserver integration (SSH)
- Alembic migrations

Frontend:
- Next.js 14 (App Router)
- NextAuth 4 with Keycloak Provider
- Redis session storage (ioredis)
- Tailwind CSS

Infrastructure:
- Redis 7 (10.1.0.254:6379) - Session + Cache
- Keycloak 26.1.0 (auth.lab.taipei)
- Docker Mailserver (10.1.0.254)

Architecture Highlights:
- Session管理由 Keycloak + Redis 統一控制
- 支援多系統 (HR/WebMail/Calendar/Drive/Office) 共享 session
- Token 自動刷新,異質服務整合
- 未來可無縫遷移到雲端

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-23 20:12:43 +08:00

142 lines
3.7 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest'
import axios from 'axios'
import { tenantService } from '../tenant.service'
import type { Tenant, TenantUpdateRequest, TenantUpdateResponse } from '../../types/tenant'
// Mock axios
vi.mock('axios', () => ({
default: {
get: vi.fn(),
put: vi.fn(),
},
}))
const mockedAxios = axios as unknown as {
get: ReturnType<typeof vi.fn>
put: ReturnType<typeof vi.fn>
}
describe('TenantService', () => {
beforeEach(() => {
vi.clearAllMocks()
})
describe('getTenant', () => {
it('should fetch tenant information successfully', async () => {
// Arrange
const tenantId = 1
const mockTenant: Tenant = {
id: 1,
code: 'PWD',
name: 'Porsche World',
name_eng: 'Porsche World Co., Ltd.',
tax_id: '12345678',
prefix: 'PWD',
tel: '02-1234-5678',
add: '台北市信義區...',
url: 'https://porscheworld.tw',
plan_id: 'starter',
max_users: 50,
storage_quota_gb: 1000,
status: 'active',
is_sysmana: false,
is_active: true,
created_at: '2026-01-01T00:00:00',
updated_at: '2026-02-21T00:00:00',
}
mockedAxios.get.mockResolvedValueOnce({ data: mockTenant })
// Act
const result = await tenantService.getTenant(tenantId)
// Assert
expect(mockedAxios.get).toHaveBeenCalledWith(
`http://localhost:10181/api/v1/tenants/${tenantId}`
)
expect(result).toEqual(mockTenant)
})
it('should throw error when tenant not found', async () => {
// Arrange
const tenantId = 999
mockedAxios.get.mockRejectedValueOnce({
response: {
data: { detail: 'Tenant not found' },
status: 404,
},
})
// Act & Assert
await expect(tenantService.getTenant(tenantId)).rejects.toThrow()
})
})
describe('updateTenant', () => {
it('should update tenant information successfully', async () => {
// Arrange
const tenantId = 1
const updateData: TenantUpdateRequest = {
name: 'Porsche World Updated',
tax_id: '87654321',
tel: '02-8765-4321',
add: '台北市大安區...',
url: 'https://newporscheworld.tw',
}
const mockResponse: TenantUpdateResponse = {
message: 'Tenant updated successfully',
tenant: {
id: 1,
code: 'PWD',
name: 'Porsche World Updated',
name_eng: 'Porsche World Co., Ltd.',
tax_id: '87654321',
prefix: 'PWD',
tel: '02-8765-4321',
add: '台北市大安區...',
url: 'https://newporscheworld.tw',
plan_id: 'starter',
max_users: 50,
storage_quota_gb: 1000,
status: 'active',
is_sysmana: false,
is_active: true,
created_at: '2026-01-01T00:00:00',
updated_at: '2026-02-21T10:00:00',
},
}
mockedAxios.put.mockResolvedValueOnce({ data: mockResponse })
// Act
const result = await tenantService.updateTenant(tenantId, updateData)
// Assert
expect(mockedAxios.put).toHaveBeenCalledWith(
`http://localhost:10181/api/v1/tenants/${tenantId}`,
updateData
)
expect(result).toEqual(mockResponse)
})
it('should throw error when update fails', async () => {
// Arrange
const tenantId = 1
const updateData: TenantUpdateRequest = {
name: 'Test',
}
mockedAxios.put.mockRejectedValueOnce({
response: {
data: { detail: 'Update failed' },
status: 400,
},
})
// Act & Assert
await expect(tenantService.updateTenant(tenantId, updateData)).rejects.toThrow()
})
})
})