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:
@@ -0,0 +1 @@
|
||||
# Integration tests
|
||||
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Integration tests for authentication flow.
|
||||
"""
|
||||
import pytest
|
||||
import pyotp
|
||||
from fastapi import status
|
||||
from auth import get_password_hash, save_totp_secret, save_password_hash
|
||||
|
||||
|
||||
class TestLoginFlow:
|
||||
"""Test login endpoint."""
|
||||
|
||||
def test_login_success_no_2fa(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test successful login without 2FA."""
|
||||
# Set up password
|
||||
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
|
||||
data = response.json()
|
||||
assert data["user"] == "testadmin"
|
||||
assert data["role"] == "admin"
|
||||
assert data["has_2fa"] is False
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
assert data["token_type"] == "bearer"
|
||||
|
||||
def test_login_success_with_2fa(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
||||
"""Test successful login with 2FA."""
|
||||
password = "test_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
save_totp_secret(sample_totp_secret)
|
||||
|
||||
totp = pyotp.TOTP(sample_totp_secret)
|
||||
valid_code = totp.now()
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "testadmin", "password": password, "totp_code": valid_code}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["user"] == "testadmin"
|
||||
assert data["has_2fa"] is True
|
||||
|
||||
def test_login_wrong_password(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test login with wrong password."""
|
||||
password = "correct_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "testadmin", "password": "wrong_password", "totp_code": ""}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid credentials" in response.json()["detail"]
|
||||
|
||||
def test_login_wrong_username(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test login with wrong username."""
|
||||
password = "test_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "wronguser", "password": password, "totp_code": ""}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_login_missing_2fa_code(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
||||
"""Test login with 2FA enabled but no code provided."""
|
||||
password = "test_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
save_totp_secret(sample_totp_secret)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "testadmin", "password": password, "totp_code": ""}
|
||||
)
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "2FA code required" in response.json()["detail"]
|
||||
|
||||
def test_login_invalid_2fa_code(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
||||
"""Test login with invalid 2FA code."""
|
||||
password = "test_password"
|
||||
hashed = get_password_hash(password)
|
||||
save_password_hash(hashed)
|
||||
save_totp_secret(sample_totp_secret)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/login",
|
||||
json={"username": "testadmin", "password": password, "totp_code": "000000"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_login_sets_cookies(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test that login sets auth cookies."""
|
||||
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
|
||||
# Check that cookies are set
|
||||
assert "nas_access_token" in response.cookies
|
||||
assert "nas_refresh_token" in response.cookies
|
||||
|
||||
|
||||
class TestRefreshFlow:
|
||||
"""Test token refresh endpoint."""
|
||||
|
||||
def test_refresh_with_valid_token(self, test_app, mock_config, temp_auth_file, valid_refresh_token):
|
||||
"""Test refreshing with valid refresh token."""
|
||||
response = test_app.post(
|
||||
"/api/auth/refresh",
|
||||
json={"refresh_token": valid_refresh_token}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
assert "refresh_token" in data
|
||||
assert data["access_token"] != valid_refresh_token
|
||||
|
||||
def test_refresh_with_cookie(self, test_app, mock_config, temp_auth_file, valid_refresh_token):
|
||||
"""Test refreshing with refresh token in cookie."""
|
||||
test_app.cookies.set("nas_refresh_token", valid_refresh_token)
|
||||
|
||||
response = test_app.post("/api/auth/refresh", json={})
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "access_token" in data
|
||||
|
||||
def test_refresh_with_invalid_token(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test refreshing with invalid token."""
|
||||
response = test_app.post(
|
||||
"/api/auth/refresh",
|
||||
json={"refresh_token": "invalid.token.here"}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_refresh_with_expired_token(self, test_app, mock_config, temp_auth_file, expired_access_token):
|
||||
"""Test refreshing with expired token."""
|
||||
response = test_app.post(
|
||||
"/api/auth/refresh",
|
||||
json={"refresh_token": expired_access_token}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_refresh_missing_token(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test refresh without providing token."""
|
||||
response = test_app.post("/api/auth/refresh", json={})
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Missing refresh token" in response.json()["detail"]
|
||||
|
||||
def test_refresh_with_access_token_fails(self, test_app, mock_config, temp_auth_file, valid_access_token):
|
||||
"""Test that access token cannot be used for refresh."""
|
||||
response = test_app.post(
|
||||
"/api/auth/refresh",
|
||||
json={"refresh_token": valid_access_token}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
assert "Invalid token type" in response.json()["detail"]
|
||||
|
||||
|
||||
class TestLogoutFlow:
|
||||
"""Test logout endpoint."""
|
||||
|
||||
def test_logout_success(self, test_app, mock_config, temp_auth_file, admin_headers):
|
||||
"""Test successful logout."""
|
||||
response = test_app.post("/api/auth/logout", headers=admin_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["message"] == "Logged out successfully"
|
||||
|
||||
def test_logout_clears_cookies(self, test_app, mock_config, temp_auth_file, admin_headers):
|
||||
"""Test that logout clears auth cookies."""
|
||||
response = test_app.post("/api/auth/logout", headers=admin_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
# Cookies should be deleted (max-age=0 or expires in past)
|
||||
# TestClient doesn't fully simulate cookie deletion, but we can check response
|
||||
|
||||
def test_logout_without_auth(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test logout without authentication."""
|
||||
response = test_app.post("/api/auth/logout")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestUserInfo:
|
||||
"""Test user info endpoint."""
|
||||
|
||||
def test_get_user_info(self, test_app, mock_config, temp_auth_file, temp_rbac_file, admin_headers):
|
||||
"""Test getting current user info."""
|
||||
response = test_app.get("/api/auth/me", headers=admin_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["username"] == "testuser"
|
||||
assert data["role"] == "admin"
|
||||
assert "pages" in data
|
||||
assert "sidebar_links" in data
|
||||
|
||||
def test_get_user_info_without_auth(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test getting user info without authentication."""
|
||||
response = test_app.get("/api/auth/me")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestProxyAuth:
|
||||
"""Test proxy authentication endpoint."""
|
||||
|
||||
def test_proxy_auth_from_trusted_source(self, test_app, mock_config, temp_auth_file, temp_rbac_file):
|
||||
"""Test proxy auth from trusted proxy."""
|
||||
response = test_app.get(
|
||||
"/api/auth/proxy",
|
||||
headers={
|
||||
"Remote-User": "proxyuser",
|
||||
"Remote-Groups": "dashboard_admin"
|
||||
}
|
||||
)
|
||||
|
||||
# Note: This test may need adjustment based on actual proxy auth implementation
|
||||
# The test client's default host may not be in trusted proxies
|
||||
assert response.status_code in [200, 403]
|
||||
|
||||
def test_proxy_auth_from_untrusted_source(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test proxy auth from untrusted source."""
|
||||
# Mock request from untrusted IP
|
||||
response = test_app.get(
|
||||
"/api/auth/proxy",
|
||||
headers={
|
||||
"Remote-User": "proxyuser",
|
||||
"Remote-Groups": "dashboard_admin"
|
||||
}
|
||||
)
|
||||
|
||||
# Should reject untrusted proxy
|
||||
assert response.status_code == 403
|
||||
|
||||
|
||||
class TestPasswordChange:
|
||||
"""Test password change endpoint."""
|
||||
|
||||
def test_change_password_success(self, test_app, mock_config, temp_auth_file, admin_headers):
|
||||
"""Test successful password change."""
|
||||
old_password = "old_password"
|
||||
new_password = "new_password_123"
|
||||
|
||||
# Set initial password
|
||||
hashed = get_password_hash(old_password)
|
||||
save_password_hash(hashed)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/change-password",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"old_password": old_password,
|
||||
"new_password": new_password
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_change_password_wrong_old_password(self, test_app, mock_config, temp_auth_file, admin_headers):
|
||||
"""Test password change with wrong old password."""
|
||||
old_password = "old_password"
|
||||
hashed = get_password_hash(old_password)
|
||||
save_password_hash(hashed)
|
||||
|
||||
response = test_app.post(
|
||||
"/api/auth/change-password",
|
||||
headers=admin_headers,
|
||||
json={
|
||||
"old_password": "wrong_password",
|
||||
"new_password": "new_password_123"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_change_password_without_auth(self, test_app, mock_config, temp_auth_file):
|
||||
"""Test password change without authentication."""
|
||||
response = test_app.post(
|
||||
"/api/auth/change-password",
|
||||
json={
|
||||
"old_password": "old",
|
||||
"new_password": "new"
|
||||
}
|
||||
)
|
||||
|
||||
assert response.status_code == 401
|
||||
@@ -0,0 +1,181 @@
|
||||
"""
|
||||
Integration tests for Docker operations.
|
||||
"""
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
|
||||
class TestDockerContainerList:
|
||||
"""Test listing Docker containers."""
|
||||
|
||||
def test_list_containers_success(self, test_app, admin_headers):
|
||||
"""Test listing containers successfully."""
|
||||
response = test_app.get("/api/docker/containers", headers=admin_headers)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert isinstance(data, list)
|
||||
if len(data) > 0:
|
||||
container = data[0]
|
||||
assert "id" in container
|
||||
assert "name" in container
|
||||
assert "status" in container
|
||||
|
||||
def test_list_containers_without_auth(self, test_app):
|
||||
"""Test listing containers without authentication."""
|
||||
response = test_app.get("/api/docker/containers")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestDockerContainerActions:
|
||||
"""Test Docker container start/stop/restart actions."""
|
||||
|
||||
def test_start_container_success(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test starting a container."""
|
||||
container_id = "abc123"
|
||||
|
||||
response = test_app.post(
|
||||
f"/api/docker/containers/{container_id}/start",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "message" in data or "status" in data
|
||||
|
||||
def test_stop_container_success(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test stopping a container."""
|
||||
container_id = "abc123"
|
||||
|
||||
response = test_app.post(
|
||||
f"/api/docker/containers/{container_id}/stop",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_restart_container_success(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test restarting a container."""
|
||||
container_id = "abc123"
|
||||
|
||||
response = test_app.post(
|
||||
f"/api/docker/containers/{container_id}/restart",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_container_action_without_auth(self, test_app):
|
||||
"""Test container action without authentication."""
|
||||
response = test_app.post("/api/docker/containers/abc123/start")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_container_action_invalid_action(self, test_app, admin_headers):
|
||||
"""Test container action with invalid action."""
|
||||
response = test_app.post(
|
||||
"/api/docker/containers/abc123/invalid_action",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
class TestDockerContainerLogs:
|
||||
"""Test Docker container logs retrieval."""
|
||||
|
||||
def test_get_container_logs_success(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test getting container logs."""
|
||||
container_id = "abc123"
|
||||
|
||||
response = test_app.get(
|
||||
f"/api/docker/containers/{container_id}/logs",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "logs" in data or isinstance(data, str)
|
||||
|
||||
def test_get_container_logs_with_tail(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test getting container logs with tail limit."""
|
||||
container_id = "abc123"
|
||||
|
||||
response = test_app.get(
|
||||
f"/api/docker/containers/{container_id}/logs?tail=100",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_get_container_logs_without_auth(self, test_app):
|
||||
"""Test getting logs without authentication."""
|
||||
response = test_app.get("/api/docker/containers/abc123/logs")
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
def test_get_container_logs_nonexistent(self, test_app, admin_headers, mock_docker_client):
|
||||
"""Test getting logs for nonexistent container."""
|
||||
# Mock container not found
|
||||
mock_docker_client.containers.get.side_effect = Exception("Container not found")
|
||||
|
||||
response = test_app.get(
|
||||
"/api/docker/containers/nonexistent/logs",
|
||||
headers=admin_headers
|
||||
)
|
||||
|
||||
# Should handle error gracefully
|
||||
assert response.status_code in [404, 500]
|
||||
|
||||
|
||||
class TestDockerPermissions:
|
||||
"""Test Docker operation permissions."""
|
||||
|
||||
@pytest.fixture
|
||||
def member_token(self, mock_config, temp_auth_file, temp_rbac_file):
|
||||
"""Create token for member role."""
|
||||
from auth import create_access_token
|
||||
from datetime import timedelta
|
||||
return create_access_token(
|
||||
data={"sub": "memberuser", "role": "member"},
|
||||
expires_delta=timedelta(minutes=30)
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def member_headers(self, member_token):
|
||||
"""Headers with member access token."""
|
||||
return {"Authorization": f"Bearer {member_token}"}
|
||||
|
||||
def test_member_can_list_containers(self, test_app, member_headers):
|
||||
"""Test that member can list containers."""
|
||||
response = test_app.get("/api/docker/containers", headers=member_headers)
|
||||
|
||||
# Members should be able to view containers
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_member_cannot_start_container(self, test_app, member_headers):
|
||||
"""Test that member cannot start containers (admin only)."""
|
||||
response = test_app.post(
|
||||
"/api/docker/containers/abc123/start",
|
||||
headers=member_headers
|
||||
)
|
||||
|
||||
# Should be forbidden for non-admin
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_viewer_can_list_containers(self, test_app, mock_config, temp_auth_file, temp_rbac_file):
|
||||
"""Test that viewer can list containers if they have docker page access."""
|
||||
from auth import create_access_token
|
||||
from datetime import timedelta
|
||||
|
||||
viewer_token = create_access_token(
|
||||
data={"sub": "vieweruser", "role": "viewer"},
|
||||
expires_delta=timedelta(minutes=30)
|
||||
)
|
||||
headers = {"Authorization": f"Bearer {viewer_token}"}
|
||||
|
||||
response = test_app.get("/api/docker/containers", headers=headers)
|
||||
|
||||
# Viewer may not have docker page access by default
|
||||
assert response.status_code in [200, 403]
|
||||
@@ -0,0 +1,189 @@
|
||||
"""
|
||||
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 "files" 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,
|
||||
data={"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,
|
||||
data={"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 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,
|
||||
data={"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 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
|
||||
Reference in New Issue
Block a user