Files
nas-tools/TESTING_IMPLEMENTATION_PLAN.md
T
Gan, Jimmy 8431920d26
Run Tests / Backend Tests (pull_request) Has been cancelled
Run Tests / Frontend Tests (pull_request) Has been cancelled
Run Tests / Test Summary (pull_request) Has been cancelled
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
2026-03-30 11:11:39 +08:00

504 lines
14 KiB
Markdown

# Testing Infrastructure Implementation Plan
## Overview
Add comprehensive unit and integration testing for the NAS Dashboard backend (Python/FastAPI) and frontend (Svelte 5). Currently, the codebase has zero test coverage and relies only on smoke tests and manual verification.
## Backend Testing Strategy
### 1. Testing Framework Setup
- **Framework**: pytest 8.x with pytest-asyncio for async tests
- **Coverage**: pytest-cov for coverage reporting
- **Mocking**: pytest-mock + unittest.mock
- **HTTP Testing**: httpx for TestClient (FastAPI built-in)
- **Fixtures**: Centralized fixtures for common test data
### 2. Test Structure
```
dashboard/backend/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── unit/
│ │ ├── test_auth.py # JWT, TOTP, password hashing
│ │ ├── test_rbac.py # Permission checks, role resolution
│ │ ├── test_config.py # IP parsing, env validation
│ │ └── test_utils.py # Path validation, encryption
│ └── integration/
│ ├── test_auth_flow.py # Login, refresh, logout flows
│ ├── test_docker.py # Container operations (mocked)
│ ├── test_files.py # File operations with permissions
│ ├── test_terminal.py # WebSocket terminal (mocked SSH)
│ ├── test_passkey.py # WebAuthn flows
│ └── test_security.py # Rate limiting, audit logging
```
### 3. Key Test Areas
#### Unit Tests (High Priority)
1. **auth.py** (~300 lines to test)
- JWT token generation/validation
- Token expiry and refresh logic
- TOTP generation/verification
- Password hashing/verification
- Token version management
- Encryption/decryption (Fernet + legacy fallback)
2. **rbac.py** (~200 lines to test)
- Role resolution (admin/member/viewer)
- Page access checks
- Sidebar link filtering
- User override logic
- Atomic JSON persistence
3. **config.py** (~150 lines to test)
- IP range parsing (LAN, Tailscale, trusted proxies)
- Environment variable validation
- Default value handling
4. **Path validation utilities**
- Traversal protection
- Symlink resolution
- Blocked directory checks
#### Integration Tests (High Priority)
1. **Authentication Flow** (routers/auth.py - 316 lines)
- POST /api/auth/login → TOTP required → token issued
- POST /api/auth/refresh → new tokens
- POST /api/auth/logout → token revoked
- Proxy auth with Remote-User header
- Rate limiting enforcement
2. **Docker Operations** (routers/docker_router.py - 37 lines)
- GET /api/docker/containers → list with health status
- POST /api/docker/containers/{id}/start → admin only
- GET /api/docker/containers/{id}/logs → tail limit
3. **File Operations** (routers/files.py - 128 lines)
- GET /api/files/browse → directory listing
- POST /api/files/upload → 100MB limit, admin only
- DELETE /api/files/delete → permission checks
4. **Terminal WebSocket** (routers/terminal.py - 220 lines)
- WebSocket /ws/terminal?host=nas → auth validation
- Session reservation (max 5 global, 5 per user)
- SSH connection lifecycle (mocked)
- Keepalive ping/pong
5. **Passkey/WebAuthn** (routers/passkey.py - 188 lines)
- Registration challenge generation
- Credential verification
- Credential management
### 4. Mocking Strategy
**Docker SDK** (docker.DockerClient):
```python
@pytest.fixture
def mock_docker_client(mocker):
client = mocker.Mock()
client.containers.list.return_value = [
mocker.Mock(id="abc123", name="test-container", status="running")
]
return client
```
**asyncssh** (SSH connections):
```python
@pytest.fixture
async def mock_ssh_connection(mocker):
conn = mocker.AsyncMock()
conn.create_process.return_value = mocker.AsyncMock()
mocker.patch('asyncssh.connect', return_value=conn)
return conn
```
**httpx** (External API calls):
```python
@pytest.fixture
def mock_httpx_client(mocker):
client = mocker.AsyncMock()
mocker.patch('httpx.AsyncClient', return_value=client)
return client
```
### 5. Test Fixtures (conftest.py)
```python
@pytest.fixture
def test_app():
"""FastAPI test client with overridden dependencies"""
from main import app
return TestClient(app)
@pytest.fixture
def valid_jwt_token():
"""Generate valid JWT for testing"""
return create_access_token({"sub": "testuser", "role": "admin"})
@pytest.fixture
def mock_rbac_data():
"""Sample RBAC configuration"""
return {
"role_defaults": {"admin": {"pages": "*", "sidebar_links": "*"}},
"user_overrides": {}
}
@pytest.fixture
def temp_auth_file(tmp_path):
"""Temporary auth.json for testing"""
auth_file = tmp_path / "auth.json"
auth_file.write_text('{"totp_secrets": {}, "passkey_credentials": {}}')
return auth_file
```
### 6. Coverage Goals
- **Target**: 80%+ overall coverage
- **Critical paths**: 95%+ (auth, RBAC, path validation)
- **Routers**: 70%+ (focus on happy path + error cases)
### 7. CI Integration
Add to `.gitea/workflows/test.yml`:
```yaml
name: Backend Tests
on: [push, pull_request]
jobs:
test:
runs-on: nas-runner
steps:
- uses: actions/checkout@v3
- name: Run pytest
run: |
cd dashboard/backend
pip install -r requirements.txt -r requirements-dev.txt
pytest --cov=. --cov-report=term --cov-report=html
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: dashboard/backend/htmlcov/
```
---
## Frontend Testing Strategy
### 1. Testing Framework Setup
- **Framework**: Vitest 2.x (Vite-native, fast)
- **Component Testing**: @testing-library/svelte for Svelte 5
- **Mocking**: vi.mock() for modules, vi.fn() for functions
- **Coverage**: @vitest/coverage-v8
### 2. Test Structure
```
dashboard/frontend/
├── tests/
│ ├── setup.js # Global test setup
│ ├── unit/
│ │ ├── api.test.js # API client, token management
│ │ └── voice.test.js # VoiceSession class
│ ├── components/
│ │ ├── Sidebar.test.js # Navigation, drag-drop, RBAC
│ │ └── App.test.js # Routing, auth flow
│ └── integration/
│ ├── Login.test.js # Passkey + password flows
│ ├── Terminal.test.js # WebSocket, reconnection
│ ├── Docker.test.js # Container management
│ └── Files.test.js # File browser operations
```
### 3. Key Test Areas
#### Unit Tests
1. **api.js** (~200 lines)
- Token storage/retrieval
- Auto-refresh on 401
- Error handling
- Request methods (get, post, put, del)
2. **voice.js** (~150 lines)
- VoiceSession state management
- STT/TTS integration (mocked)
- Language switching
- Talk mode handling
#### Component Tests
1. **App.svelte** (~400 lines)
- Initial auth check
- Page routing via query params
- Theme toggle persistence
- User info loading
2. **Sidebar.svelte** (~800 lines)
- Category rendering
- Drag-drop reordering
- Role-based link filtering
- Collapse state persistence
3. **Login.svelte** (~230 lines)
- Passkey authentication flow
- Password + TOTP flow
- Error message display
- Form validation
4. **Terminal.svelte** (~652 lines)
- WebSocket connection lifecycle
- Tab management
- Reconnection with exponential backoff
- Auth failure recovery
- Image upload (drag-drop/paste)
5. **Docker.svelte** (~133 lines)
- Container list rendering
- Start/stop/restart actions
- Log modal display
6. **Files.svelte** (~151 lines)
- Directory navigation
- File upload
- Download/delete actions
### 4. Mocking Strategy
**Fetch API**:
```javascript
beforeEach(() => {
global.fetch = vi.fn();
});
test('login success', async () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ access_token: 'test-token' })
});
// test logic
});
```
**WebSocket**:
```javascript
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = WebSocket.CONNECTING;
setTimeout(() => {
this.readyState = WebSocket.OPEN;
this.onopen?.();
}, 0);
}
send(data) { /* mock */ }
close() { /* mock */ }
}
global.WebSocket = MockWebSocket;
```
**Web Speech API**:
```javascript
global.SpeechRecognition = vi.fn(() => ({
start: vi.fn(),
stop: vi.fn(),
addEventListener: vi.fn()
}));
global.speechSynthesis = {
speak: vi.fn(),
cancel: vi.fn()
};
```
**xterm.js**:
```javascript
vi.mock('@xterm/xterm', () => ({
Terminal: vi.fn(() => ({
open: vi.fn(),
write: vi.fn(),
onData: vi.fn(),
dispose: vi.fn()
}))
}));
```
### 5. Test Configuration (vitest.config.js)
```javascript
import { defineConfig } from 'vitest/config';
import { svelte } from '@sveltejs/vite-plugin-svelte';
export default defineConfig({
plugins: [svelte({ hot: !process.env.VITEST })],
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'],
exclude: ['node_modules/', 'tests/', 'dist/']
}
}
});
```
### 6. Coverage Goals
- **Target**: 70%+ overall coverage
- **Critical components**: 80%+ (Terminal, Login, App)
- **API client**: 90%+ (core infrastructure)
### 7. CI Integration
Add to `.gitea/workflows/test.yml`:
```yaml
frontend-tests:
runs-on: nas-runner
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: |
cd dashboard/frontend
npm ci
- name: Run tests
run: |
cd dashboard/frontend
npm run test -- --coverage
- name: Upload coverage
if: always()
uses: actions/upload-artifact@v3
with:
name: frontend-coverage
path: dashboard/frontend/coverage/
```
---
## Implementation Phases
### Phase 1: Backend Foundation (Priority: High)
1. Install pytest, pytest-asyncio, pytest-cov, pytest-mock
2. Create test directory structure
3. Write conftest.py with shared fixtures
4. Implement unit tests for auth.py (JWT, TOTP, encryption)
5. Implement unit tests for rbac.py (permissions, role resolution)
6. Implement unit tests for config.py (IP parsing)
**Estimated effort**: 4-6 hours
**Files created**: 6-8 test files, conftest.py, requirements-dev.txt
### Phase 2: Backend Integration Tests (Priority: High)
1. Mock Docker SDK for container tests
2. Mock asyncssh for terminal tests
3. Mock httpx for external API tests
4. Implement integration tests for auth flow (login, refresh, logout)
5. Implement integration tests for Docker operations
6. Implement integration tests for file operations
7. Implement integration tests for terminal WebSocket
**Estimated effort**: 6-8 hours
**Files created**: 7-10 integration test files
### Phase 3: Frontend Foundation (Priority: Medium)
1. Install vitest, @testing-library/svelte, @vitest/coverage-v8, jsdom
2. Create vitest.config.js
3. Create test directory structure and setup.js
4. Implement unit tests for api.js (token management, auto-refresh)
5. Implement unit tests for voice.js (VoiceSession)
**Estimated effort**: 3-4 hours
**Files created**: vitest.config.js, setup.js, 2-3 test files
### Phase 4: Frontend Component Tests (Priority: Medium)
1. Mock fetch, WebSocket, Web Speech API, xterm.js
2. Implement component tests for App.svelte (routing, auth)
3. Implement component tests for Login.svelte (passkey, password flows)
4. Implement component tests for Sidebar.svelte (drag-drop, RBAC)
5. Implement component tests for Terminal.svelte (WebSocket, reconnection)
6. Implement component tests for Docker.svelte (actions, logs)
7. Implement component tests for Files.svelte (browse, upload)
**Estimated effort**: 8-10 hours
**Files created**: 6-8 component test files
### Phase 5: CI Integration (Priority: Medium)
1. Create .gitea/workflows/test.yml
2. Configure pytest in CI with coverage reporting
3. Configure vitest in CI with coverage reporting
4. Add coverage badges to README (optional)
5. Configure coverage thresholds (fail if below target)
**Estimated effort**: 2-3 hours
**Files created**: 1 workflow file
### Phase 6: Documentation & Maintenance (Priority: Low)
1. Add TESTING.md with instructions for running tests
2. Document mocking patterns and fixtures
3. Add pre-commit hook for running tests (optional)
4. Set up coverage monitoring
**Estimated effort**: 1-2 hours
**Files created**: TESTING.md
---
## Dependencies to Add
### Backend (requirements-dev.txt)
```
pytest==8.3.4
pytest-asyncio==0.24.0
pytest-cov==6.0.0
pytest-mock==3.14.0
```
### Frontend (package.json devDependencies)
```json
{
"vitest": "^2.1.8",
"@testing-library/svelte": "^5.2.3",
"@vitest/coverage-v8": "^2.1.8",
"jsdom": "^25.0.1"
}
```
---
## Success Criteria
1. **Backend**: 80%+ test coverage with all critical paths tested
2. **Frontend**: 70%+ test coverage with key components tested
3. **CI**: Tests run automatically on push/PR with coverage reports
4. **Documentation**: Clear instructions for running and writing tests
5. **Maintainability**: Fixtures and mocks are reusable and well-documented
---
## Risks & Mitigations
**Risk**: Tests may be slow due to Docker/SSH mocking complexity
**Mitigation**: Use lightweight mocks, avoid actual Docker/SSH connections
**Risk**: Frontend tests may be brittle due to Svelte 5 runes
**Mitigation**: Use @testing-library/svelte best practices, test behavior not implementation
**Risk**: CI runner may lack resources for parallel test execution
**Mitigation**: Run tests sequentially if needed, optimize test fixtures
**Risk**: Existing code may need refactoring for testability
**Mitigation**: Minimal refactoring, focus on testing current behavior first
---
## Open Questions
1. Should we add E2E tests with Playwright in addition to unit/integration tests?
- **Recommendation**: Start with unit/integration, add E2E later if needed
2. Should we enforce coverage thresholds in CI (fail build if below target)?
- **Recommendation**: Yes, but start with lenient thresholds (60%) and increase gradually
3. Should we test all 13 routers or focus on high-risk areas first?
- **Recommendation**: Focus on auth, docker, terminal, files first (80% of risk)
4. Should we mock the file system or use temporary directories?
- **Recommendation**: Use pytest's tmp_path fixture for real file operations
5. Should we test WebSocket reconnection logic in detail?
- **Recommendation**: Yes, it's complex and critical (Terminal.svelte has 652 lines)