e98cbb0abb
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
190 lines
5.8 KiB
Python
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
|