UNPKG

appwrite-utils-cli

Version:

Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.

497 lines (382 loc) 13.2 kB
# Testing Framework for Dual Schema Implementation ## Overview This testing framework provides comprehensive coverage for the dual schema architecture, ensuring reliability, performance, and compatibility across different Appwrite versions and configuration scenarios. ## Quick Start ```bash # Install dependencies npm install # Run all tests npm test # Run tests with coverage npm test:coverage # Run tests in watch mode during development npm test:watch # Run tests for CI/CD npm test:ci ``` ## Test Structure ``` tests/ ├── README.md # This file ├── setup.ts # Global test setup and mocks ├── testUtils.ts # Shared utilities and helpers ├── jest.config.js # Jest configuration ├── utils/ # Unit tests for utilities └── loadConfigs.test.ts # Configuration loading tests ├── adapters/ # Adapter system tests └── AdapterFactory.test.ts # Adapter creation and selection ├── integration/ # Integration and E2E tests └── syncOperations.test.ts # Sync operations testing ├── validation/ # Configuration validation tests └── configValidation.test.ts ├── migration/ # Migration utilities tests └── configMigration.test.ts └── fixtures/ # Test data and configurations ├── collections/ ├── tables/ └── configs/ ``` ## Test Categories ### 1. Unit Tests (`utils/`, `adapters/`) **Purpose**: Test individual functions and classes in isolation **Coverage**: - Configuration loading logic - YAML/TypeScript parsing - Adapter factory patterns - Version detection algorithms - Validation functions **Example**: ```typescript describe('loadConfig', () => { it('should load dual directories correctly', async () => { const testDir = TestUtils.createTestProject({ hasCollections: true, hasTables: true, }); const config = await loadConfig(testDir); expect(config.collections).toHaveLength(2); expect(config.collections.some(c => c._isFromTablesDir)).toBe(true); }); }); ``` ### 2. Integration Tests (`integration/`) **Purpose**: Test complete workflows and system interactions **Coverage**: - End-to-end sync operations - API version compatibility - File system operations - Network error handling - Performance under load **Example**: ```typescript describe('Sync Operations', () => { it('should sync from Appwrite to local configuration', async () => { mockAdapter.getDatabases.mockResolvedValue([mockDatabase]); mockAdapter.getCollections.mockResolvedValue([mockCollection]); await mockAdapter.syncFromAppwrite(testDir); expect(fs.existsSync(path.join(testDir, 'collections'))).toBe(true); }); }); ``` ### 3. Validation Tests (`validation/`) **Purpose**: Test configuration validation and error detection **Coverage**: - Schema validation - Naming conflict detection - API compatibility checks - Strict mode behavior - Error reporting **Example**: ```typescript describe('Configuration Validation', () => { it('should detect naming conflicts', async () => { const mockValidation = { isValid: false, errors: [{ type: 'naming_conflict', message: 'Duplicate name found between directories', }], }; const result = await loadConfigWithPath(testDir, { validate: true }); expect(result.validation.errors).toHaveLength(1); }); }); ``` ### 4. Migration Tests (`migration/`) **Purpose**: Test configuration migration utilities **Coverage**: - Migration detection logic - TypeScript to YAML conversion - Directory structure changes - Backup and rollback procedures - Large-scale migrations **Example**: ```typescript describe('Migration Operations', () => { it('should migrate collections to tables directory', async () => { const result = migrateToTablesDir(testDir, { preserveOriginal: true, convertToYaml: true, }); expect(result.success).toBe(true); expect(result.migratedFiles).toHaveLength(1); }); }); ``` ## Test Utilities ### TestUtils Class Central utility class for creating test environments: ```typescript class TestUtils { // Directory Management static createTempDir(): string static createTestProject(options): string static cleanup(): void // Configuration Creation static createTestAppwriteConfig(overrides): AppwriteConfig static createTestCollection(overrides): CollectionCreate static createTestTable(overrides): TableCreate // Mock Generation static createMockAppwriteResponses(): MockClient } ``` ### Usage Examples ```typescript // Create a test project with both collections and tables const testDir = TestUtils.createTestProject({ hasCollections: true, hasTables: true, hasConflicts: true, // Test naming conflicts useYaml: true, // Use YAML format }); // Create mock configurations const mockConfig = TestUtils.createTestAppwriteConfig({ appwriteProject: 'test-project', databases: [{ name: 'test-db', $id: 'test-db-id' }], }); // Automatic cleanup after tests afterEach(() => { TestUtils.cleanup(); }); ``` ## Mocking Strategy ### Global Mocks (setup.ts) ```typescript // Mock external dependencies jest.mock('winston'); // Logging jest.mock('chalk'); // Terminal colors jest.mock('inquirer'); // Interactive prompts jest.mock('cli-progress'); // Progress bars // Mock file system operations when needed jest.mock('fs', () => ({ ...jest.requireActual('fs'), writeFileSync: jest.fn(), readFileSync: jest.fn(), })); ``` ### Adapter Mocking ```typescript // Mock adapter responses const mockAdapter = { syncFromAppwrite: jest.fn(), syncToAppwrite: jest.fn(), getDatabases: jest.fn().mockResolvedValue([mockDatabase]), getCollections: jest.fn().mockResolvedValue([mockCollection]), validateConfiguration: jest.fn().mockReturnValue({ isValid: true }), }; (AdapterFactory.createAdapter as jest.Mock).mockResolvedValue(mockAdapter); ``` ### Version Detection Mocking ```typescript jest.mock('../../src/utils/versionDetection', () => ({ detectAppwriteVersionCached: jest.fn().mockResolvedValue({ serverVersion: '1.6.0', apiMode: 'database', }), isVersionAtLeast: jest.fn((version, target) => version >= target), })); ``` ## Performance Testing ### Load Testing ```typescript describe('Performance Tests', () => { it('should handle large configurations efficiently', async () => { // Create 500 collections + 300 tables const largeConfig = createLargeConfiguration(500, 300); const startTime = Date.now(); await loadConfig(testDir); const loadTime = Date.now() - startTime; expect(loadTime).toBeLessThan(5000); // 5 second max }); }); ``` ### Memory Testing ```typescript describe('Memory Usage', () => { it('should not leak memory during repeated operations', async () => { const initialMemory = process.memoryUsage().heapUsed; // Perform many operations for (let i = 0; i < 100; i++) { await loadConfig(testDir); } const finalMemory = process.memoryUsage().heapUsed; const increase = finalMemory - initialMemory; expect(increase).toBeLessThan(50 * 1024 * 1024); // 50MB max }); }); ``` ## Error Testing ### Network Errors ```typescript describe('Network Error Handling', () => { it('should handle API timeout gracefully', async () => { mockAdapter.getDatabases.mockRejectedValue( new Error('Request timeout') ); await expect(syncFromAppwrite(testDir)) .rejects.toThrow('Request timeout'); }); }); ``` ### File System Errors ```typescript describe('File System Errors', () => { it('should handle permission denied errors', async () => { jest.spyOn(fs, 'writeFileSync').mockImplementation(() => { throw new Error('EACCES: permission denied'); }); await expect(saveConfiguration(config, testDir)) .rejects.toThrow('permission denied'); }); }); ``` ## CI/CD Integration ### GitHub Actions ```yaml name: Test Dual Schema on: [push, pull_request] jobs: test: runs-on: ubuntu-latest strategy: matrix: node-version: [18, 20] steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} cache: 'npm' - name: Install dependencies run: npm ci - name: Run tests run: npm run test:ci - name: Upload coverage uses: codecov/codecov-action@v3 with: file: ./coverage/lcov.info ``` ### Coverage Requirements - **Overall**: 90%+ coverage - **Statements**: 90%+ - **Branches**: 85%+ - **Functions**: 95%+ - **Lines**: 90%+ ## Debugging Tests ### Running Specific Tests ```bash # Run single test file npm test loadConfigs.test.ts # Run specific test pattern npm test -- --testNamePattern="dual schema" # Run with verbose output npm test -- --verbose # Run with coverage for specific files npm test -- --coverage --collectCoverageOnlyFrom="src/utils/loadConfigs.ts" ``` ### Debug Mode ```bash # Run with Node.js debugger node --inspect-brk node_modules/.bin/jest --runInBand # Debug single test node --inspect-brk node_modules/.bin/jest --runInBand --testNamePattern="should load dual directories" ``` ### VSCode Debug Configuration ```json { "type": "node", "request": "launch", "name": "Debug Jest Tests", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": ["--runInBand"], "console": "integratedTerminal", "internalConsoleOptions": "neverOpen" } ``` ## Best Practices ### Test Organization 1. **Group Related Tests**: Use `describe` blocks to group related functionality 2. **Descriptive Names**: Test names should clearly describe the expected behavior 3. **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification 4. **Clean State**: Each test should start with a clean state ### Mock Management 1. **Clear Mocks**: Always clear mocks between tests 2. **Realistic Data**: Use realistic mock data that represents actual usage 3. **Error Scenarios**: Mock both success and failure scenarios 4. **External Dependencies**: Mock all external dependencies (APIs, file system, etc.) ### Performance Considerations 1. **Parallel Execution**: Use Jest's parallel execution for faster test runs 2. **Selective Testing**: Use test patterns to run only relevant tests during development 3. **Resource Cleanup**: Always clean up resources (temp files, network connections) 4. **Timeout Management**: Set appropriate timeouts for async operations ### Test Data Management 1. **Fixtures**: Use fixture files for complex test data 2. **Factories**: Use factory functions for generating test objects 3. **Randomization**: Use random data where appropriate to catch edge cases 4. **Reproducibility**: Ensure tests are deterministic and reproducible ## Troubleshooting ### Common Issues #### "Jest encountered an unexpected token" ```bash # Solution: Check Jest configuration for TypeScript npm install --save-dev ts-jest @types/jest ``` #### "Module not found" errors ```bash # Solution: Check import paths and ensure files exist # Use absolute imports when necessary ``` #### "Tests are hanging" ```bash # Solution: Check for unresolved promises or open handles npm test -- --detectOpenHandles --forceExit ``` #### "Memory leaks detected" ```bash # Solution: Ensure proper cleanup in afterEach/afterAll # Check for unclosed file handles or timers ``` ### Performance Issues #### Slow test execution ```bash # Solution: Run tests in parallel (default) or identify slow tests npm test -- --verbose --runInBand # Serial execution for debugging ``` #### High memory usage ```bash # Solution: Clear mocks and clean up test data # Use smaller test datasets # Check for memory leaks in test utilities ``` ## Contributing ### Adding New Tests 1. **Follow Naming Convention**: `*.test.ts` for test files 2. **Use TestUtils**: Leverage shared utilities for consistency 3. **Add Documentation**: Document complex test scenarios 4. **Update Coverage**: Ensure new code has appropriate test coverage ### Test Categories - **Unit Tests**: Test individual functions and classes - **Integration Tests**: Test component interactions - **Performance Tests**: Test system performance and scalability - **Error Tests**: Test error handling and edge cases This testing framework ensures the dual schema implementation is robust, reliable, and performs well across all supported scenarios and environments.