feat: add comprehensive unit and integration testing infrastructure
- Backend: pytest with unit tests (auth, rbac, config) and integration tests (auth flow, docker, files) - Frontend: vitest with unit tests (api client) and component tests (login) - CI: Gitea Actions workflow for automated testing with coverage reports - Documentation: TESTING.md guide with setup, usage, and best practices - Coverage goals: 80%+ backend, 70%+ frontend
This commit is contained in:
+295
@@ -0,0 +1,295 @@
|
||||
# Testing Guide
|
||||
|
||||
This document describes how to run and write tests for the NAS Dashboard project.
|
||||
|
||||
## Overview
|
||||
|
||||
The project has comprehensive test coverage for both backend (Python/FastAPI) and frontend (Svelte 5):
|
||||
|
||||
- **Backend**: pytest with unit and integration tests
|
||||
- **Frontend**: Vitest with unit and component tests
|
||||
- **CI**: Automated testing via Gitea Actions
|
||||
|
||||
## Backend Testing
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd dashboard/backend
|
||||
pip install -r requirements.txt
|
||||
pip install -r requirements-dev.txt
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run with coverage
|
||||
pytest --cov=. --cov-report=term --cov-report=html
|
||||
|
||||
# Run only unit tests
|
||||
pytest tests/unit/ -v
|
||||
|
||||
# Run only integration tests
|
||||
pytest tests/integration/ -v
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/unit/test_auth.py -v
|
||||
|
||||
# Run specific test
|
||||
pytest tests/unit/test_auth.py::TestPasswordHashing::test_hash_and_verify_password -v
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
dashboard/backend/tests/
|
||||
├── conftest.py # Shared fixtures
|
||||
├── unit/
|
||||
│ ├── test_auth.py # JWT, TOTP, password hashing
|
||||
│ ├── test_rbac.py # Role-based access control
|
||||
│ └── test_config.py # IP parsing, env validation
|
||||
└── integration/
|
||||
├── test_auth_flow.py # Login, refresh, logout flows
|
||||
├── test_docker.py # Container operations
|
||||
└── test_files.py # File operations
|
||||
```
|
||||
|
||||
### Key Fixtures
|
||||
|
||||
- `mock_config`: Mocked configuration with test values
|
||||
- `temp_auth_file`: Temporary auth.json for testing
|
||||
- `temp_rbac_file`: Temporary rbac.json for testing
|
||||
- `valid_access_token`: Valid JWT access token
|
||||
- `admin_headers`: Headers with admin authentication
|
||||
- `mock_docker_client`: Mocked Docker SDK client
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Example unit test:
|
||||
|
||||
```python
|
||||
def test_hash_and_verify_password(self):
|
||||
"""Test that password hashing and verification works."""
|
||||
password = "test_password_123"
|
||||
hashed = get_password_hash(password)
|
||||
|
||||
assert hashed != password
|
||||
assert verify_password(password, hashed)
|
||||
```
|
||||
|
||||
Example integration test:
|
||||
|
||||
```python
|
||||
def test_login_success_no_2fa(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test successful login without 2FA."""
|
||||
password = "test_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "testadmin", "password": password, "totp_code": ""}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["user"] == "testadmin"
|
||||
```
|
||||
|
||||
## Frontend Testing
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
cd dashboard/frontend
|
||||
npm install
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
npm test
|
||||
|
||||
# Run with coverage
|
||||
npm run test:coverage
|
||||
|
||||
# Run in watch mode
|
||||
npm test -- --watch
|
||||
|
||||
# Run specific test file
|
||||
npm test -- tests/unit/api.test.js
|
||||
|
||||
# Run with UI
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
### Test Structure
|
||||
|
||||
```
|
||||
dashboard/frontend/tests/
|
||||
├── setup.js # Global test setup
|
||||
├── unit/
|
||||
│ └── api.test.js # API client tests
|
||||
├── components/
|
||||
│ └── Login.test.js # Login component tests
|
||||
└── integration/
|
||||
└── (future tests)
|
||||
```
|
||||
|
||||
### Mocked APIs
|
||||
|
||||
The test setup (`tests/setup.js`) provides mocks for:
|
||||
|
||||
- `fetch` - HTTP requests
|
||||
- `WebSocket` - WebSocket connections
|
||||
- `SpeechRecognition` / `speechSynthesis` - Web Speech API
|
||||
- `navigator.credentials` - WebAuthn
|
||||
- `localStorage` - Local storage
|
||||
- `ResizeObserver` - Resize observer
|
||||
- `MutationObserver` - Mutation observer
|
||||
|
||||
### Writing Tests
|
||||
|
||||
Example unit test:
|
||||
|
||||
```javascript
|
||||
it('should set and get token', () => {
|
||||
setToken('test-token-123');
|
||||
expect(getToken()).toBe('test-token-123');
|
||||
});
|
||||
```
|
||||
|
||||
Example component test:
|
||||
|
||||
```javascript
|
||||
it('should render login form', () => {
|
||||
render(Login);
|
||||
|
||||
expect(screen.getByLabelText(/username/i)).toBeTruthy();
|
||||
expect(screen.getByLabelText(/password/i)).toBeTruthy();
|
||||
});
|
||||
```
|
||||
|
||||
## Coverage Goals
|
||||
|
||||
- **Backend**: 80%+ overall, 95%+ for critical paths (auth, RBAC)
|
||||
- **Frontend**: 70%+ overall, 80%+ for critical components
|
||||
|
||||
## CI Integration
|
||||
|
||||
Tests run automatically on:
|
||||
- Push to `main` branch
|
||||
- Pull requests to `main`
|
||||
|
||||
The CI workflow (`.gitea/workflows/test.yml`) runs:
|
||||
1. Backend unit tests
|
||||
2. Backend integration tests
|
||||
3. Frontend tests with coverage
|
||||
4. Uploads coverage reports as artifacts
|
||||
|
||||
### Viewing CI Results
|
||||
|
||||
1. Go to the Gitea repository
|
||||
2. Click on "Actions" tab
|
||||
3. Select the workflow run
|
||||
4. Download coverage artifacts
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Backend Tests
|
||||
|
||||
**Issue**: `ModuleNotFoundError: No module named 'fastapi'`
|
||||
**Solution**: Install dependencies: `pip install -r requirements.txt -r requirements-dev.txt`
|
||||
|
||||
**Issue**: `RuntimeError: SECRET_KEY must be set`
|
||||
**Solution**: Tests use mocked config, ensure `conftest.py` is loaded
|
||||
|
||||
**Issue**: Permission denied on temp files
|
||||
**Solution**: Check that `/tmp` is writable
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
**Issue**: `Cannot find module '@testing-library/svelte'`
|
||||
**Solution**: Install dependencies: `npm install`
|
||||
|
||||
**Issue**: `ReferenceError: WebSocket is not defined`
|
||||
**Solution**: Ensure `tests/setup.js` is loaded (check `vitest.config.js`)
|
||||
|
||||
**Issue**: Component tests fail with Svelte 5 runes
|
||||
**Solution**: Use `@testing-library/svelte` v5.2.3+ which supports Svelte 5
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Backend
|
||||
|
||||
1. **Use fixtures**: Leverage shared fixtures from `conftest.py`
|
||||
2. **Mock external services**: Always mock Docker, SSH, HTTP clients
|
||||
3. **Test edge cases**: Invalid inputs, expired tokens, permission errors
|
||||
4. **Atomic tests**: Each test should be independent
|
||||
5. **Clear test names**: Use descriptive names that explain what's being tested
|
||||
|
||||
### Frontend
|
||||
|
||||
1. **Test behavior, not implementation**: Focus on user interactions
|
||||
2. **Mock API calls**: Use `vi.fn()` to mock fetch responses
|
||||
3. **Test accessibility**: Use `getByRole`, `getByLabelText` from testing-library
|
||||
4. **Avoid testing internal state**: Test what the user sees
|
||||
5. **Keep tests simple**: One assertion per test when possible
|
||||
|
||||
## Debugging Tests
|
||||
|
||||
### Backend
|
||||
|
||||
```bash
|
||||
# Run with verbose output
|
||||
pytest -vv
|
||||
|
||||
# Run with print statements visible
|
||||
pytest -s
|
||||
|
||||
# Run with debugger on failure
|
||||
pytest --pdb
|
||||
|
||||
# Run specific test with full output
|
||||
pytest tests/unit/test_auth.py::TestPasswordHashing -vv -s
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```bash
|
||||
# Run with verbose output
|
||||
npm test -- --reporter=verbose
|
||||
|
||||
# Run in UI mode for debugging
|
||||
npm run test:ui
|
||||
|
||||
# Run single test file in watch mode
|
||||
npm test -- tests/unit/api.test.js --watch
|
||||
```
|
||||
|
||||
## Adding New Tests
|
||||
|
||||
### Backend
|
||||
|
||||
1. Create test file in appropriate directory (`tests/unit/` or `tests/integration/`)
|
||||
2. Import necessary fixtures from `conftest.py`
|
||||
3. Write test classes and methods
|
||||
4. Run tests to verify
|
||||
|
||||
### Frontend
|
||||
|
||||
1. Create test file in appropriate directory (`tests/unit/`, `tests/components/`, or `tests/integration/`)
|
||||
2. Import necessary mocks and utilities
|
||||
3. Write test cases using `describe` and `it`
|
||||
4. Run tests to verify
|
||||
|
||||
## Resources
|
||||
|
||||
- [pytest documentation](https://docs.pytest.org/)
|
||||
- [Vitest documentation](https://vitest.dev/)
|
||||
- [Testing Library documentation](https://testing-library.com/)
|
||||
- [FastAPI testing guide](https://fastapi.tiangolo.com/tutorial/testing/)
|
||||
- [Svelte testing guide](https://svelte.dev/docs/testing)
|
||||
Reference in New Issue
Block a user