Files
multitenetsaas/frontend/tests/integration/test_module_integration.test.tsx
AHMET YILMAZ b3fff546e9
Some checks failed
System Monitoring / Health Checks (push) Has been cancelled
System Monitoring / Performance Monitoring (push) Has been cancelled
System Monitoring / Database Monitoring (push) Has been cancelled
System Monitoring / Cache Monitoring (push) Has been cancelled
System Monitoring / Log Monitoring (push) Has been cancelled
System Monitoring / Resource Monitoring (push) Has been cancelled
System Monitoring / Uptime Monitoring (push) Has been cancelled
System Monitoring / Backup Monitoring (push) Has been cancelled
System Monitoring / Security Monitoring (push) Has been cancelled
System Monitoring / Monitoring Dashboard (push) Has been cancelled
System Monitoring / Alerting (push) Has been cancelled
Security Scanning / Dependency Scanning (push) Has been cancelled
Security Scanning / Code Security Scanning (push) Has been cancelled
Security Scanning / Secrets Scanning (push) Has been cancelled
Security Scanning / Container Security Scanning (push) Has been cancelled
Security Scanning / Compliance Checking (push) Has been cancelled
Security Scanning / Security Dashboard (push) Has been cancelled
Security Scanning / Security Remediation (push) Has been cancelled
project initialization
2025-10-05 02:37:33 +08:00

687 lines
22 KiB
TypeScript

/**
* Frontend Integration Tests - Module Integration
*
* Tests for module-specific integration:
* - Module switching and data isolation
* - Cross-module data sharing
* - Module-specific permissions
* - Module loading performance
* - Error handling across modules
*
* Author: Claude
*/
import React from 'react';
import { render, screen, fireEvent, waitFor, act } from '@testing-library/react';
import '@testing-library/jest-dom';
import { BrowserRouter, Routes, Route, useNavigate } from 'react-router-dom';
import userEvent from '@testing-library/user-event';
// Mock services
const mockModuleService = {
getModules: jest.fn(),
activateModule: jest.fn(),
deactivateModule: jest.fn(),
getModuleData: jest.fn(),
validateModuleAccess: jest.fn(),
};
const mockPermissionService = {
checkPermission: jest.fn(),
getUserPermissions: jest.fn(),
};
const mockDataService = {
getTenantData: jest.fn(),
getSharedData: jest.fn(),
updateSharedData: jest.fn(),
};
// Mock components for different modules
const ModuleSwitcher = ({ currentModule, onModuleChange, modules }) => (
<div data-testid="module-switcher">
<div data-testid="current-module">{currentModule}</div>
<div data-testid="available-modules">
{modules.map((module) => (
<button
key={module.id}
data-testid={`module-${module.code}`}
onClick={() => onModuleChange(module.code)}
disabled={!module.enabled}
>
{module.name}
</button>
))}
</div>
</div>
);
const RetailModule = ({ tenantId, userData }) => (
<div data-testid="retail-module">
<h2>Retail Management</h2>
<div data-testid="tenant-data">{tenantId}</div>
<div data-testid="user-data">{userData?.name}</div>
<button data-testid="add-product-btn">Add Product</button>
<button data-testid="view-sales-btn">View Sales</button>
<div data-testid="retail-stats">
<div>Total Products: 150</div>
<div>Today's Sales: RM 2,450</div>
</div>
</div>
);
const HealthcareModule = ({ tenantId, userData }) => (
<div data-testid="healthcare-module">
<h2>Healthcare Management</h2>
<div data-testid="tenant-data">{tenantId}</div>
<div data-testid="user-data">{userData?.name}</div>
<button data-testid="add-patient-btn">Add Patient</button>
<button data-testid="schedule-appointment-btn">Schedule Appointment</button>
<div data-testid="healthcare-stats">
<div>Total Patients: 250</div>
<div>Today's Appointments: 12</div>
</div>
</div>
);
const EducationModule = ({ tenantId, userData }) => (
<div data-testid="education-module">
<h2>Education Management</h2>
<div data-testid="tenant-data">{tenantId}</div>
<div data-testid="user-data">{userData?.name}</div>
<button data-testid="add-student-btn">Add Student</button>
<button data-testid="manage-classes-btn">Manage Classes</button>
<div data-testid="education-stats">
<div>Total Students: 500</div>
<div>Active Classes: 25</div>
</div>
</div>
);
const LogisticsModule = ({ tenantId, userData }) => (
<div data-testid="logistics-module">
<h2>Logistics Management</h2>
<div data-testid="tenant-data">{tenantId}</div>
<div data-testid="user-data">{userData?.name}</div>
<button data-testid="add-shipment-btn">Add Shipment</button>
<button data-testid="track-vehicles-btn">Track Vehicles</button>
<div data-testid="logistics-stats">
<div>Active Shipments: 45</div>
<div>Vehicle Fleet: 12</div>
</div>
</div>
);
const BeautyModule = ({ tenantId, userData }) => (
<div data-testid="beauty-module">
<h2>Beauty Management</h2>
<div data-testid="tenant-data">{tenantId}</div>
<div data-testid="user-data">{userData?.name}</div>
<button data-testid="add-client-btn">Add Client</button>
<button data-testid="manage-services-btn">Manage Services</button>
<div data-testid="beauty-stats">
<div>Total Clients: 180</div>
<div>Active Services: 35</div>
</div>
</div>
);
const SharedDataPanel = ({ sharedData, onUpdateSharedData }) => (
<div data-testid="shared-data-panel">
<h3>Shared Data</h3>
<div data-testid="shared-business-name">{sharedData?.businessName}</div>
<div data-testid="shared-contact-email">{sharedData?.contactEmail}</div>
<button data-testid="update-shared-data-btn" onClick={onUpdateSharedData}>
Update Shared Data
</button>
</div>
);
const PermissionAlert = ({ hasPermission, requiredPermission }) => (
<div data-testid="permission-alert">
{hasPermission ? (
<div data-testid="permission-granted">Access granted for {requiredPermission}</div>
) : (
<div data-testid="permission-denied">Access denied for {requiredPermission}</div>
)}
</div>
);
const LoadingSpinner = () => (
<div data-testid="loading-spinner">Loading module...</div>
);
const ErrorBoundary = ({ children, onError }) => {
const [hasError, setHasError] = React.useState(false);
const [error, setError] = React.useState(null);
React.useEffect(() => {
const handleError = (event) => {
setHasError(true);
setError(event.error);
onError?.(event.error);
};
window.addEventListener('error', handleError);
return () => window.removeEventListener('error', handleError);
}, [onError]);
if (hasError) {
return (
<div data-testid="error-boundary">
<h2>Something went wrong</h2>
<div data-testid="error-message">{error?.message}</div>
<button onClick={() => setHasError(false)}>Try Again</button>
</div>
);
}
return <>{children}</>;
};
// Test app with module routing
const ModuleApp = () => {
const [currentModule, setCurrentModule] = React.useState('retail');
const [modules, setModules] = React.useState([]);
const [sharedData, setSharedData] = React.useState(null);
const [loading, setLoading] = React.useState(true);
const [error, setError] = React.useState(null);
const navigate = useNavigate();
React.useEffect(() => {
loadModules();
loadSharedData();
}, []);
const loadModules = async () => {
try {
setLoading(true);
const response = await mockModuleService.getModules();
setModules(response.data);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
const loadSharedData = async () => {
try {
const response = await mockDataService.getSharedData();
setSharedData(response.data);
} catch (err) {
console.error('Failed to load shared data:', err);
}
};
const handleModuleChange = async (moduleCode) => {
try {
await mockModuleService.validateModuleAccess(moduleCode);
setCurrentModule(moduleCode);
navigate(`/${moduleCode}`);
} catch (err) {
setError(err);
}
};
const handleUpdateSharedData = async () => {
try {
const updatedData = { ...sharedData, businessName: 'Updated Business Name' };
await mockDataService.updateSharedData(updatedData);
setSharedData(updatedData);
} catch (err) {
setError(err);
}
};
if (loading) {
return <LoadingSpinner />;
}
if (error) {
return (
<div data-testid="error-container">
<div data-testid="error-message">{error.message}</div>
<button onClick={() => setError(null)}>Retry</button>
</div>
);
}
return (
<div data-testid="module-app">
<ModuleSwitcher
currentModule={currentModule}
onModuleChange={handleModuleChange}
modules={modules}
/>
<SharedDataPanel
sharedData={sharedData}
onUpdateSharedData={handleUpdateSharedData}
/>
<Routes>
<Route path="/retail" element={
<ErrorBoundary onError={setError}>
<RetailModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
</ErrorBoundary>
} />
<Route path="/healthcare" element={
<ErrorBoundary onError={setError}>
<HealthcareModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
</ErrorBoundary>
} />
<Route path="/education" element={
<ErrorBoundary onError={setError}>
<EducationModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
</ErrorBoundary>
} />
<Route path="/logistics" element={
<ErrorBoundary onError={setError}>
<LogisticsModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
</ErrorBoundary>
} />
<Route path="/beauty" element={
<ErrorBoundary onError={setError}>
<BeautyModule tenantId="tenant-001" userData={{ name: 'Test User' }} />
</ErrorBoundary>
} />
</Routes>
</div>
);
};
describe('Module Integration Tests', () => {
const user = userEvent.setup();
beforeEach(() => {
jest.clearAllMocks();
// Mock default responses
mockModuleService.getModules.mockResolvedValue({
data: [
{ id: 1, code: 'retail', name: 'Retail Management', enabled: true },
{ id: 2, code: 'healthcare', name: 'Healthcare Management', enabled: true },
{ id: 3, code: 'education', name: 'Education Management', enabled: true },
{ id: 4, code: 'logistics', name: 'Logistics Management', enabled: true },
{ id: 5, code: 'beauty', name: 'Beauty Management', enabled: true },
]
});
mockDataService.getSharedData.mockResolvedValue({
data: {
businessName: 'Test Business Sdn Bhd',
contactEmail: 'contact@test.com',
address: '123 Test Street',
phone: '+60123456789'
}
});
mockModuleService.validateModuleAccess.mockResolvedValue(true);
mockPermissionService.checkPermission.mockResolvedValue(true);
});
describe('Module Switching', () => {
test('successfully switches between modules', async () => {
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
// Wait for initial load
await waitFor(() => {
expect(screen.getByTestId('module-app')).toBeInTheDocument();
});
// Should start with retail module
expect(screen.getByTestId('current-module')).toHaveTextContent('retail');
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
// Switch to healthcare module
const healthcareButton = screen.getByTestId('module-healthcare');
await user.click(healthcareButton);
await waitFor(() => {
expect(screen.getByTestId('current-module')).toHaveTextContent('healthcare');
expect(screen.getByTestId('healthcare-module')).toBeInTheDocument();
expect(screen.queryByTestId('retail-module')).not.toBeInTheDocument();
});
// Verify module access validation was called
expect(mockModuleService.validateModuleAccess).toHaveBeenCalledWith('healthcare');
});
test('maintains shared data across modules', async () => {
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('shared-data-panel')).toBeInTheDocument();
});
// Check shared data in retail module
expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Test Business Sdn Bhd');
expect(screen.getByTestId('shared-contact-email')).toHaveTextContent('contact@test.com');
// Switch to healthcare module
const healthcareButton = screen.getByTestId('module-healthcare');
await user.click(healthcareButton);
await waitFor(() => {
expect(screen.getByTestId('healthcare-module')).toBeInTheDocument();
});
// Shared data should still be available
expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Test Business Sdn Bhd');
expect(screen.getByTestId('shared-contact-email')).toHaveTextContent('contact@test.com');
});
test('handles module loading states', async () => {
// Mock slow loading
mockModuleService.getModules.mockImplementationOnce(() =>
new Promise(resolve => setTimeout(resolve, 1000))
);
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
// Should show loading spinner initially
expect(screen.getByTestId('loading-spinner')).toBeInTheDocument();
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByTestId('loading-spinner')).not.toBeInTheDocument();
expect(screen.getByTestId('module-app')).toBeInTheDocument();
});
});
});
describe('Module-Specific Features', () => {
test.each([
{ module: 'retail', button: 'add-product-btn', stats: 'retail-stats' },
{ module: 'healthcare', button: 'add-patient-btn', stats: 'healthcare-stats' },
{ module: 'education', button: 'add-student-btn', stats: 'education-stats' },
{ module: 'logistics', button: 'add-shipment-btn', stats: 'logistics-stats' },
{ module: 'beauty', button: 'add-client-btn', stats: 'beauty-stats' },
])('loads $module module with correct features', async ({ module, button, stats }) => {
render(
<BrowserRouter initialEntries={['/${module}']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId(`${module}-module`)).toBeInTheDocument();
});
// Check module-specific buttons
expect(screen.getByTestId(button)).toBeInTheDocument();
// Check module-specific stats
expect(screen.getByTestId(stats)).toBeInTheDocument();
// Check tenant data isolation
expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001');
// Check user data sharing
expect(screen.getByTestId('user-data')).toHaveTextContent('Test User');
});
});
describe('Permission Management', () => {
test('validates module access permissions', async () => {
// Mock permission denied for logistics module
mockModuleService.validateModuleAccess.mockImplementationOnce((moduleCode) => {
if (moduleCode === 'logistics') {
return Promise.reject(new Error('Access denied'));
}
return Promise.resolve(true);
});
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Try to access logistics module
const logisticsButton = screen.getByTestId('module-logistics');
await user.click(logisticsButton);
await waitFor(() => {
expect(screen.getByTestId('error-container')).toBeInTheDocument();
expect(screen.getByTestId('error-message')).toHaveTextContent('Access denied');
});
// Should not switch to logistics module
expect(screen.queryByTestId('logistics-module')).not.toBeInTheDocument();
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
test('checks feature-level permissions', async () => {
mockPermissionService.checkPermission.mockImplementation((permission) => {
const deniedPermissions = ['delete_products', 'manage_patients'];
return Promise.resolve(!deniedPermissions.includes(permission));
});
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Test retail permissions
expect(await mockPermissionService.checkPermission('view_products')).resolves.toBe(true);
expect(await mockPermissionService.checkPermission('delete_products')).resolves.toBe(false);
});
});
describe('Data Isolation and Sharing', () => {
test('maintains tenant data isolation', async () => {
// Mock different tenant data for different modules
mockDataService.getTenantData.mockImplementation((module) => {
const tenantData = {
retail: { id: 'tenant-retail', name: 'Retail Business' },
healthcare: { id: 'tenant-healthcare', name: 'Healthcare Business' },
};
return Promise.resolve({ data: tenantData[module] });
});
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Check retail tenant data
expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001');
// Switch to healthcare module
const healthcareButton = screen.getByTestId('module-healthcare');
await user.click(healthcareButton);
await waitFor(() => {
expect(screen.getByTestId('healthcare-module')).toBeInTheDocument();
});
// Should still use the same tenant (data isolation at tenant level)
expect(screen.getByTestId('tenant-data')).toHaveTextContent('tenant-001');
});
test('updates shared data across modules', async () => {
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('shared-data-panel')).toBeInTheDocument();
});
// Update shared data
const updateButton = screen.getByTestId('update-shared-data-btn');
await user.click(updateButton);
await waitFor(() => {
expect(mockDataService.updateSharedData).toHaveBeenCalled();
expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Updated Business Name');
});
// Switch to another module
const healthcareButton = screen.getByTestId('module-healthcare');
await user.click(healthcareButton);
await waitFor(() => {
expect(screen.getByTestId('healthcare-module')).toBeInTheDocument();
});
// Updated shared data should be available
expect(screen.getByTestId('shared-business-name')).toHaveTextContent('Updated Business Name');
});
});
describe('Error Handling', () => {
test('handles module loading errors gracefully', async () => {
mockModuleService.getModules.mockRejectedValueOnce(new Error('Failed to load modules'));
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('error-container')).toBeInTheDocument();
expect(screen.getByTestId('error-message')).toHaveTextContent('Failed to load modules');
});
});
test('handles individual module errors', async () => {
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Simulate module error
const error = new Error('Module crashed');
fireEvent.error(window, error);
await waitFor(() => {
expect(screen.getByTestId('error-boundary')).toBeInTheDocument();
expect(screen.getByTestId('error-message')).toHaveTextContent('Module crashed');
});
});
test('recovers from errors with retry', async () => {
mockModuleService.getModules.mockRejectedValueOnce(new Error('Network error'));
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('error-container')).toBeInTheDocument();
});
// Mock successful retry
mockModuleService.getModules.mockResolvedValueOnce({
data: [
{ id: 1, code: 'retail', name: 'Retail Management', enabled: true },
]
});
// Click retry button
const retryButton = screen.getByText('Retry');
await user.click(retryButton);
await waitFor(() => {
expect(screen.getByTestId('module-app')).toBeInTheDocument();
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
});
});
describe('Performance Optimization', () => {
test('lazy loads modules on demand', async () => {
const mockModuleLoad = jest.fn();
// Mock dynamic import
jest.mock('react-lazy', () => ({
lazy: (importFn) => {
mockModuleLoad();
return importFn();
}
}));
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Module should be loaded on demand
expect(mockModuleLoad).toHaveBeenCalled();
});
test('caches loaded modules', async () => {
render(
<BrowserRouter initialEntries={['/retail']}>
<ModuleApp />
</BrowserRouter>
);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Switch away and back to retail module
const healthcareButton = screen.getByTestId('module-healthcare');
await user.click(healthcareButton);
await waitFor(() => {
expect(screen.getByTestId('healthcare-module')).toBeInTheDocument();
});
const retailButton = screen.getByTestId('module-retail');
await user.click(retailButton);
await waitFor(() => {
expect(screen.getByTestId('retail-module')).toBeInTheDocument();
});
// Should not reload modules (check call count)
expect(mockModuleService.getModules).toHaveBeenCalledTimes(1);
});
});
});