Files
nas-tools/dashboard/backend/tests/integration/test_files.py
T
Gan, Jimmy e98cbb0abb
Deploy Dashboard (Dev) / deploy-dev (push) Successful in 3m35s
Run Tests / Backend Tests (push) Successful in 2m36s
Run Tests / Frontend Tests (push) Failing after 51s
Run Tests / Test Summary (push) Failing after 14s
feat: comprehensive test infrastructure improvements
Phase 1: Fix immediate test failures
- Fix path handling in files router (strip leading slashes)
- Fix test assertions to match actual API behavior
- Add proper error handling for missing files in download endpoint
- Reload files router in test fixtures to pick up config changes
- Fix upload endpoint test to use query params instead of form data

Phase 2: Improve test reliability
- Standardize error responses: docker_router now uses HTTPException
- Add global exception handlers for validation, Docker errors, and unhandled exceptions
- Fix flaky TOTP replay protection test
- Fix flaky password hash loading test with proper module reload
- Enhanced Docker mock fixtures with error scenarios and factory pattern

Phase 3: Add coverage reporting
- Add pytest-cov with 40% minimum coverage threshold
- Add pyproject.toml with pytest and coverage configuration
- Update test workflow to generate coverage and JUnit XML reports
- Remove -x flag to see all test failures
- Configure asyncio_default_fixture_loop_scope to fix deprecation warning

Results:
- All 144 tests passing (was 137 passed, 5 failed, 2 skipped)
- Test execution time: ~3s locally
- Coverage: 43% (above 40% threshold)
- No skipped tests
- Standardized error handling across all endpoints
2026-04-07 08:02:38 +08:00

190 lines
5.8 KiB
Python

"""
Integration tests for file operations.
"""
import pytest
import os
class TestFileBrowse:
"""Test file browsing."""
def test_browse_root(self, test_app, admin_headers, temp_volume_root):
"""Test browsing root directory."""
response = test_app.get(
"/api/files/browse",
headers=admin_headers,
params={"path": "/"}
)
assert response.status_code == 200
data = response.json()
assert "entries" in data or isinstance(data, list)
def test_browse_without_auth(self, test_app):
"""Test browsing without authentication."""
response = test_app.get("/api/files/browse", params={"path": "/"})
assert response.status_code == 401
def test_browse_blocked_directory(self, test_app, admin_headers):
"""Test browsing blocked directory."""
response = test_app.get(
"/api/files/browse",
headers=admin_headers,
params={"path": "/@appstore"}
)
# Should be forbidden
assert response.status_code == 403
def test_browse_path_traversal_attempt(self, test_app, admin_headers):
"""Test path traversal protection."""
response = test_app.get(
"/api/files/browse",
headers=admin_headers,
params={"path": "/../../../etc"}
)
# Should be forbidden
assert response.status_code == 403
class TestFileUpload:
"""Test file upload."""
def test_upload_file_success(self, test_app, admin_headers, temp_volume_root):
"""Test uploading a file."""
test_content = b"test file content"
files = {"file": ("test.txt", test_content, "text/plain")}
response = test_app.post(
"/api/files/upload",
headers=admin_headers,
files=files,
params={"path": "/"}
)
assert response.status_code in [200, 201]
def test_upload_without_auth(self, test_app):
"""Test upload without authentication."""
files = {"file": ("test.txt", b"content", "text/plain")}
response = test_app.post(
"/api/files/upload",
files=files,
params={"path": "/"}
)
assert response.status_code == 401
def test_upload_as_member_forbidden(self, test_app, mock_config, temp_auth_file, temp_rbac_file):
"""Test that non-admin cannot upload."""
from auth_service import create_access_token
from datetime import timedelta
member_token = create_access_token(
data={"sub": "memberuser", "role": "member"},
expires_delta=timedelta(minutes=30)
)
headers = {"Authorization": f"Bearer {member_token}"}
files = {"file": ("test.txt", b"content", "text/plain")}
response = test_app.post(
"/api/files/upload",
headers=headers,
files=files,
params={"path": "/"}
)
# Upload should be admin-only
assert response.status_code == 403
class TestFileDelete:
"""Test file deletion."""
def test_delete_file_success(self, test_app, admin_headers, temp_volume_root):
"""Test deleting a file."""
# Create a test file first
test_file = os.path.join(temp_volume_root, "test_delete.txt")
with open(test_file, "w") as f:
f.write("test content")
response = test_app.delete(
"/api/files/delete",
headers=admin_headers,
params={"path": "/test_delete.txt"}
)
assert response.status_code in [200, 204]
def test_delete_without_auth(self, test_app):
"""Test delete without authentication."""
response = test_app.delete(
"/api/files/delete",
params={"path": "/test.txt"}
)
assert response.status_code == 401
def test_delete_as_member_forbidden(self, test_app, mock_config, temp_auth_file, temp_rbac_file):
"""Test that non-admin cannot delete."""
from auth_service import create_access_token
from datetime import timedelta
member_token = create_access_token(
data={"sub": "memberuser", "role": "member"},
expires_delta=timedelta(minutes=30)
)
headers = {"Authorization": f"Bearer {member_token}"}
response = test_app.delete(
"/api/files/delete",
headers=headers,
params={"path": "/test.txt"}
)
# Delete should be admin-only
assert response.status_code == 403
class TestFileDownload:
"""Test file download."""
def test_download_file_success(self, test_app, admin_headers, temp_volume_root):
"""Test downloading a file."""
# Create a test file
test_file = os.path.join(temp_volume_root, "test_download.txt")
test_content = "test download content"
with open(test_file, "w") as f:
f.write(test_content)
response = test_app.get(
"/api/files/download",
headers=admin_headers,
params={"path": "/test_download.txt"}
)
assert response.status_code == 200
assert test_content in response.text or response.content == test_content.encode()
def test_download_without_auth(self, test_app):
"""Test download without authentication."""
response = test_app.get(
"/api/files/download",
params={"path": "/test.txt"}
)
assert response.status_code == 401
def test_download_nonexistent_file(self, test_app, admin_headers):
"""Test downloading nonexistent file."""
response = test_app.get(
"/api/files/download",
headers=admin_headers,
params={"path": "/nonexistent.txt"}
)
assert response.status_code == 404