test: 新增 admin-auth 完整測試套件

Unit Tests (30 tests):
- auth_service: LDAP 認證、管理員檢查
- page_registry: 頁面狀態管理、並發安全
- permissions: 權限檢查函數、裝飾器

Integration Tests (17 tests):
- 登入/登出路由
- 權限中介層
- Admin API
- Context Processor

E2E Tests (10 tests):
- 完整登入登出流程
- 頁面存取控制
- 頁面管理流程
- Portal 動態 tabs
- Session 持久性
- 安全性場景

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
beabigegg
2026-01-28 15:00:10 +08:00
parent b5d63fb87d
commit 468aa65adf
6 changed files with 1094 additions and 3 deletions

View File

@@ -72,10 +72,26 @@
- [x] 管理員登入後顯示名稱和「頁面管理」連結 - [x] 管理員登入後顯示名稱和「頁面管理」連結
- [x] Dev 頁面 tabs 對非管理員隱藏(使用 `can_view_page` 條件渲染) - [x] Dev 頁面 tabs 對非管理員隱藏(使用 `can_view_page` 條件渲染)
## 測試任務(延後) ## 測試任務
### Task 12-14: 單元測試與整合測試 ### Task 12: 單元測試
- [ ] 待後續補充 - [x] auth_service 測試LDAP 認證、管理員檢查)
- [x] page_registry 測試(頁面狀態讀寫、並發存取)
- [x] permissions 測試(權限檢查、裝飾器)
### Task 13: 整合測試
- [x] 登入/登出路由測試
- [x] 權限中介層測試released/dev 頁面存取)
- [x] Admin API 測試(頁面管理)
- [x] Context Processor 測試
### Task 14: E2E 測試
- [x] 完整登入登出流程
- [x] 頁面存取控制流程
- [x] 頁面管理流程
- [x] Portal 動態 tabs 顯示
- [x] Session 持久性
- [x] 安全性場景測試
## 部署任務 ## 部署任務

View File

@@ -0,0 +1,350 @@
# -*- coding: utf-8 -*-
"""End-to-end tests for admin authentication flow.
These tests simulate real user workflows through the admin authentication system.
Run with: pytest tests/e2e/test_admin_auth_e2e.py -v --run-integration
"""
import json
import pytest
from unittest.mock import patch, MagicMock
import tempfile
from pathlib import Path
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', 'src'))
import mes_dashboard.core.database as db
from mes_dashboard.app import create_app
from mes_dashboard.services import page_registry
@pytest.fixture
def temp_page_status(tmp_path):
"""Create temporary page status file."""
data_file = tmp_path / "page_status.json"
initial_data = {
"pages": [
{"route": "/", "name": "首頁", "status": "released"},
{"route": "/wip-overview", "name": "WIP 即時概況", "status": "released"},
{"route": "/wip-detail", "name": "WIP 明細", "status": "released"},
{"route": "/tables", "name": "表格總覽", "status": "dev"},
{"route": "/resource", "name": "機台狀態", "status": "dev"},
],
"api_public": True
}
data_file.write_text(json.dumps(initial_data, ensure_ascii=False), encoding="utf-8")
return data_file
@pytest.fixture
def app(temp_page_status):
"""Create application for testing."""
db._ENGINE = None
# Mock page registry
original_data_file = page_registry.DATA_FILE
original_cache = page_registry._cache
page_registry.DATA_FILE = temp_page_status
page_registry._cache = None
app = create_app('testing')
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
yield app
page_registry.DATA_FILE = original_data_file
page_registry._cache = original_cache
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
def mock_ldap_success(mail="ymirliu@panjit.com.tw"):
"""Helper to create mock for successful LDAP auth."""
mock_response = MagicMock()
mock_response.json.return_value = {
"success": True,
"user": {
"username": "92367",
"displayName": "Test Admin",
"mail": mail,
"department": "Test Department"
}
}
return mock_response
class TestFullLoginLogoutFlow:
"""E2E tests for complete login/logout flow."""
@patch('mes_dashboard.services.auth_service.requests.post')
def test_complete_admin_login_workflow(self, mock_post, client):
"""Test complete admin login workflow."""
mock_post.return_value = mock_ldap_success()
# 1. Access portal - should see login link
response = client.get("/")
assert response.status_code == 200
content = response.data.decode("utf-8")
assert "管理員登入" in content
# 2. Go to login page
response = client.get("/admin/login")
assert response.status_code == 200
# 3. Submit login form
response = client.post("/admin/login", data={
"username": "92367",
"password": "password123"
}, follow_redirects=True)
assert response.status_code == 200
content = response.data.decode("utf-8")
# Should see admin name and logout option
assert "Test Admin" in content or "登出" in content
# 4. Verify session has admin
with client.session_transaction() as sess:
assert "admin" in sess
assert sess["admin"]["mail"] == "ymirliu@panjit.com.tw"
# 5. Access admin pages
response = client.get("/admin/pages")
assert response.status_code == 200
# 6. Logout
response = client.get("/admin/logout", follow_redirects=True)
assert response.status_code == 200
# 7. Verify logged out
with client.session_transaction() as sess:
assert "admin" not in sess
# 8. Admin pages should redirect now
response = client.get("/admin/pages", follow_redirects=False)
assert response.status_code == 302
class TestPageAccessControlFlow:
"""E2E tests for page access control flow."""
def test_non_admin_cannot_access_dev_pages(self, client, temp_page_status):
"""Test non-admin users cannot access dev pages."""
# 1. Access released page - should work
response = client.get("/wip-overview")
assert response.status_code != 403
# 2. Access dev page - should get 403
response = client.get("/tables")
assert response.status_code == 403
content = response.data.decode("utf-8")
assert "開發中" in content or "403" in content
@patch('mes_dashboard.services.auth_service.requests.post')
def test_admin_can_access_all_pages(self, mock_post, client, temp_page_status):
"""Test admin users can access all pages."""
mock_post.return_value = mock_ldap_success()
# 1. Login as admin
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
# 2. Access released page - should work
response = client.get("/wip-overview")
assert response.status_code != 403
# 3. Access dev page - should work for admin
response = client.get("/tables")
assert response.status_code != 403
class TestPageManagementFlow:
"""E2E tests for page management flow."""
@patch('mes_dashboard.services.auth_service.requests.post')
def test_admin_can_change_page_status(self, mock_post, client, temp_page_status):
"""Test admin can change page status via management interface."""
mock_post.return_value = mock_ldap_success()
# 1. Login as admin
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
# 2. Get current pages list
response = client.get("/admin/api/pages")
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
# 3. Change /wip-overview from released to dev
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"status": "dev"}),
content_type="application/json"
)
assert response.status_code == 200
# 4. Verify change persisted
page_registry._cache = None
status = page_registry.get_page_status("/wip-overview")
assert status == "dev"
# 5. Logout
client.get("/admin/logout")
# 6. Now non-admin should get 403 on /wip-overview
response = client.get("/wip-overview")
assert response.status_code == 403
@patch('mes_dashboard.services.auth_service.requests.post')
def test_release_dev_page_makes_it_public(self, mock_post, client, temp_page_status):
"""Test releasing a dev page makes it publicly accessible."""
mock_post.return_value = mock_ldap_success()
# 1. Verify /tables is currently dev (403 for non-admin)
response = client.get("/tables")
assert response.status_code == 403
# 2. Login as admin
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
# 3. Release the page
response = client.put(
"/admin/api/pages/tables",
data=json.dumps({"status": "released"}),
content_type="application/json"
)
assert response.status_code == 200
# 4. Logout
client.get("/admin/logout")
# 5. Clear cache and verify non-admin can access
page_registry._cache = None
response = client.get("/tables")
assert response.status_code != 403
class TestPortalDynamicTabs:
"""E2E tests for dynamic portal tabs based on page status."""
def test_portal_hides_dev_tabs_for_non_admin(self, client, temp_page_status):
"""Test portal hides dev page tabs for non-admin users."""
response = client.get("/")
assert response.status_code == 200
content = response.data.decode("utf-8")
# Released pages should show
assert "WIP 即時概況" in content
# Dev pages should NOT show (tables and resource are dev)
# Note: This depends on the can_view_page implementation in portal.html
@patch('mes_dashboard.services.auth_service.requests.post')
def test_portal_shows_all_tabs_for_admin(self, mock_post, client, temp_page_status):
"""Test portal shows all tabs for admin users."""
mock_post.return_value = mock_ldap_success()
# Login as admin
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
response = client.get("/")
assert response.status_code == 200
content = response.data.decode("utf-8")
# Admin should see all pages
assert "WIP 即時概況" in content
class TestSessionPersistence:
"""E2E tests for session persistence."""
@patch('mes_dashboard.services.auth_service.requests.post')
def test_session_persists_across_requests(self, mock_post, client):
"""Test admin session persists across multiple requests."""
mock_post.return_value = mock_ldap_success()
# Login
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
# Make multiple requests
for _ in range(5):
response = client.get("/admin/pages")
assert response.status_code == 200
# Session should still be valid
with client.session_transaction() as sess:
assert "admin" in sess
class TestSecurityScenarios:
"""E2E tests for security scenarios."""
def test_cannot_access_admin_api_without_login(self, client):
"""Test admin APIs are protected."""
# Try to get pages without login
response = client.get("/admin/api/pages", follow_redirects=False)
assert response.status_code == 302
# Try to update page without login
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"status": "dev"}),
content_type="application/json",
follow_redirects=False
)
assert response.status_code == 302
@patch('mes_dashboard.services.auth_service.requests.post')
def test_non_admin_user_cannot_login(self, mock_post, client):
"""Test non-admin user cannot access admin features."""
# Mock LDAP success but with non-admin email
mock_response = MagicMock()
mock_response.json.return_value = {
"success": True,
"user": {
"username": "99999",
"displayName": "Regular User",
"mail": "regular@panjit.com.tw",
"department": "Test"
}
}
mock_post.return_value = mock_response
# Try to login
response = client.post("/admin/login", data={
"username": "99999",
"password": "password123"
})
# Should fail (show error, not redirect)
assert response.status_code == 200
content = response.data.decode("utf-8")
assert "管理員" in content or "error" in content.lower()
# Should NOT have admin session
with client.session_transaction() as sess:
assert "admin" not in sess
if __name__ == "__main__":
pytest.main([__file__, "-v"])

View File

@@ -0,0 +1,298 @@
# -*- coding: utf-8 -*-
"""Integration tests for authentication routes and permission middleware."""
import json
import pytest
from unittest.mock import patch, MagicMock
import tempfile
from pathlib import Path
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
import mes_dashboard.core.database as db
from mes_dashboard.app import create_app
from mes_dashboard.services import page_registry
@pytest.fixture
def temp_page_status(tmp_path):
"""Create temporary page status file."""
data_file = tmp_path / "page_status.json"
initial_data = {
"pages": [
{"route": "/", "name": "Portal", "status": "released"},
{"route": "/wip-overview", "name": "WIP Overview", "status": "released"},
{"route": "/dev-feature", "name": "Dev Feature", "status": "dev"},
],
"api_public": True
}
data_file.write_text(json.dumps(initial_data), encoding="utf-8")
return data_file
@pytest.fixture
def app(temp_page_status):
"""Create application for testing."""
db._ENGINE = None
# Mock page registry to use temp file
original_data_file = page_registry.DATA_FILE
original_cache = page_registry._cache
page_registry.DATA_FILE = temp_page_status
page_registry._cache = None
app = create_app('testing')
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
yield app
# Restore
page_registry.DATA_FILE = original_data_file
page_registry._cache = original_cache
@pytest.fixture
def client(app):
"""Create test client."""
return app.test_client()
class TestLoginRoute:
"""Tests for login route."""
def test_login_page_renders(self, client):
"""Test login page is accessible."""
response = client.get("/admin/login")
assert response.status_code == 200
assert "管理員登入" in response.data.decode("utf-8") or "login" in response.data.decode("utf-8").lower()
@patch('mes_dashboard.services.auth_service.requests.post')
def test_login_success(self, mock_post, client):
"""Test successful login."""
# Mock LDAP response
mock_response = MagicMock()
mock_response.json.return_value = {
"success": True,
"user": {
"username": "92367",
"displayName": "Admin User",
"mail": "ymirliu@panjit.com.tw",
"department": "Test Dept"
}
}
mock_post.return_value = mock_response
response = client.post("/admin/login", data={
"username": "92367",
"password": "password123"
}, follow_redirects=False)
# Should redirect after successful login
assert response.status_code == 302
# Check session contains admin
with client.session_transaction() as sess:
assert "admin" in sess
assert sess["admin"]["username"] == "92367"
@patch('mes_dashboard.services.auth_service.requests.post')
def test_login_invalid_credentials(self, mock_post, client):
"""Test login with invalid credentials."""
mock_response = MagicMock()
mock_response.json.return_value = {"success": False}
mock_post.return_value = mock_response
response = client.post("/admin/login", data={
"username": "wrong",
"password": "wrong"
})
assert response.status_code == 200
# Should show error message
assert "錯誤" in response.data.decode("utf-8") or "error" in response.data.decode("utf-8").lower()
@patch('mes_dashboard.services.auth_service.requests.post')
def test_login_non_admin_user(self, mock_post, client):
"""Test login with non-admin user."""
mock_response = MagicMock()
mock_response.json.return_value = {
"success": True,
"user": {
"username": "99999",
"displayName": "Regular User",
"mail": "regular@panjit.com.tw",
"department": "Test Dept"
}
}
mock_post.return_value = mock_response
response = client.post("/admin/login", data={
"username": "99999",
"password": "password123"
})
assert response.status_code == 200
# Should show non-admin error
content = response.data.decode("utf-8")
assert "管理員" in content or "admin" in content.lower()
def test_login_empty_credentials(self, client):
"""Test login with empty credentials."""
response = client.post("/admin/login", data={
"username": "",
"password": ""
})
assert response.status_code == 200
class TestLogoutRoute:
"""Tests for logout route."""
def test_logout(self, client):
"""Test logout clears session."""
# Login first
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.get("/admin/logout", follow_redirects=False)
assert response.status_code == 302
with client.session_transaction() as sess:
assert "admin" not in sess
class TestPermissionMiddleware:
"""Tests for permission middleware."""
def test_released_page_accessible_without_login(self, client):
"""Test released pages are accessible without login."""
response = client.get("/wip-overview")
# Should not be 403 (might be 200 or redirect)
assert response.status_code != 403
def test_dev_page_returns_403_without_login(self, client, temp_page_status):
"""Test dev pages return 403 for non-admin."""
# Add a dev route that exists in the app
# First update page status to have an existing route as dev
data = json.loads(temp_page_status.read_text())
data["pages"].append({"route": "/tables", "name": "Tables", "status": "dev"})
temp_page_status.write_text(json.dumps(data))
page_registry._cache = None
response = client.get("/tables")
assert response.status_code == 403
def test_dev_page_accessible_with_admin_login(self, client, temp_page_status):
"""Test dev pages are accessible for admin."""
# Update tables to dev
data = json.loads(temp_page_status.read_text())
data["pages"].append({"route": "/tables", "name": "Tables", "status": "dev"})
temp_page_status.write_text(json.dumps(data))
page_registry._cache = None
# Login as admin
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin", "mail": "admin@test.com"}
response = client.get("/tables")
assert response.status_code != 403
def test_admin_pages_redirect_without_login(self, client):
"""Test admin pages redirect to login without authentication."""
response = client.get("/admin/pages", follow_redirects=False)
assert response.status_code == 302
assert "/admin/login" in response.location
def test_admin_pages_accessible_with_login(self, client):
"""Test admin pages are accessible with login."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin", "mail": "admin@test.com"}
response = client.get("/admin/pages")
assert response.status_code == 200
class TestAdminAPI:
"""Tests for admin API endpoints."""
def test_get_pages_without_login(self, client):
"""Test get pages API requires login."""
response = client.get("/admin/api/pages")
# Should redirect
assert response.status_code == 302
def test_get_pages_with_login(self, client):
"""Test get pages API with login."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.get("/admin/api/pages")
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
assert "pages" in data
def test_update_page_status(self, client, temp_page_status):
"""Test updating page status via API."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"status": "dev"}),
content_type="application/json"
)
assert response.status_code == 200
data = json.loads(response.data)
assert data["success"] is True
# Verify status changed
page_registry._cache = None
assert page_registry.get_page_status("/wip-overview") == "dev"
def test_update_page_invalid_status(self, client):
"""Test updating page with invalid status."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.put(
"/admin/api/pages/wip-overview",
data=json.dumps({"status": "invalid"}),
content_type="application/json"
)
assert response.status_code == 400
class TestContextProcessor:
"""Tests for template context processor."""
def test_is_admin_in_context_when_logged_in(self, client):
"""Test is_admin is True in context when logged in."""
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin", "displayName": "Admin"}
response = client.get("/")
content = response.data.decode("utf-8")
# Should show admin-related content (logout link, etc.)
assert "登出" in content or "logout" in content.lower() or "Admin" in content
def test_is_admin_in_context_when_not_logged_in(self, client):
"""Test is_admin is False in context when not logged in."""
response = client.get("/")
content = response.data.decode("utf-8")
# Should show login link, not logout
assert "管理員登入" in content or "login" in content.lower()
if __name__ == "__main__":
pytest.main([__file__, "-v"])

131
tests/test_auth_service.py Normal file
View File

@@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
"""Unit tests for auth_service module."""
import pytest
from unittest.mock import patch, MagicMock
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from mes_dashboard.services import auth_service
class TestAuthenticate:
"""Tests for authenticate function."""
@patch('mes_dashboard.services.auth_service.requests.post')
def test_authenticate_success(self, mock_post):
"""Test successful authentication."""
mock_response = MagicMock()
mock_response.json.return_value = {
"success": True,
"user": {
"username": "92367",
"displayName": "Test User",
"mail": "test@panjit.com.tw",
"department": "Test Dept"
}
}
mock_post.return_value = mock_response
result = auth_service.authenticate("92367", "password123")
assert result is not None
assert result["username"] == "92367"
assert result["mail"] == "test@panjit.com.tw"
mock_post.assert_called_once()
@patch('mes_dashboard.services.auth_service.requests.post')
def test_authenticate_invalid_credentials(self, mock_post):
"""Test authentication with invalid credentials."""
mock_response = MagicMock()
mock_response.json.return_value = {"success": False}
mock_post.return_value = mock_response
result = auth_service.authenticate("wrong", "wrong")
assert result is None
@patch('mes_dashboard.services.auth_service.requests.post')
def test_authenticate_timeout(self, mock_post):
"""Test authentication timeout handling."""
import requests
mock_post.side_effect = requests.Timeout()
result = auth_service.authenticate("user", "pass")
assert result is None
@patch('mes_dashboard.services.auth_service.requests.post')
def test_authenticate_connection_error(self, mock_post):
"""Test authentication connection error handling."""
import requests
mock_post.side_effect = requests.ConnectionError()
result = auth_service.authenticate("user", "pass")
assert result is None
@patch('mes_dashboard.services.auth_service.requests.post')
def test_authenticate_invalid_json(self, mock_post):
"""Test authentication with invalid JSON response."""
mock_response = MagicMock()
mock_response.json.side_effect = ValueError("Invalid JSON")
mock_post.return_value = mock_response
result = auth_service.authenticate("user", "pass")
assert result is None
class TestIsAdmin:
"""Tests for is_admin function."""
def test_is_admin_with_admin_email(self):
"""Test admin check with admin email."""
# Save original ADMIN_EMAILS
original = auth_service.ADMIN_EMAILS
try:
auth_service.ADMIN_EMAILS = ["admin@panjit.com.tw"]
user = {"mail": "admin@panjit.com.tw"}
assert auth_service.is_admin(user) is True
finally:
auth_service.ADMIN_EMAILS = original
def test_is_admin_with_non_admin_email(self):
"""Test admin check with non-admin email."""
original = auth_service.ADMIN_EMAILS
try:
auth_service.ADMIN_EMAILS = ["admin@panjit.com.tw"]
user = {"mail": "user@panjit.com.tw"}
assert auth_service.is_admin(user) is False
finally:
auth_service.ADMIN_EMAILS = original
def test_is_admin_case_insensitive(self):
"""Test admin check is case insensitive."""
original = auth_service.ADMIN_EMAILS
try:
auth_service.ADMIN_EMAILS = ["admin@panjit.com.tw"]
user = {"mail": "ADMIN@PANJIT.COM.TW"}
assert auth_service.is_admin(user) is True
finally:
auth_service.ADMIN_EMAILS = original
def test_is_admin_with_missing_mail(self):
"""Test admin check with missing mail field."""
user = {}
assert auth_service.is_admin(user) is False
def test_is_admin_with_empty_mail(self):
"""Test admin check with empty mail field."""
user = {"mail": ""}
assert auth_service.is_admin(user) is False
if __name__ == "__main__":
pytest.main([__file__, "-v"])

194
tests/test_page_registry.py Normal file
View File

@@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
"""Unit tests for page_registry module."""
import json
import pytest
import tempfile
from pathlib import Path
from unittest.mock import patch
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from mes_dashboard.services import page_registry
@pytest.fixture
def temp_data_file(tmp_path):
"""Create a temporary data file for testing."""
data_file = tmp_path / "page_status.json"
initial_data = {
"pages": [
{"route": "/", "name": "Home", "status": "released"},
{"route": "/dev-page", "name": "Dev Page", "status": "dev"},
],
"api_public": True
}
data_file.write_text(json.dumps(initial_data), encoding="utf-8")
return data_file
@pytest.fixture
def mock_registry(temp_data_file):
"""Mock page_registry to use temp file."""
original_data_file = page_registry.DATA_FILE
original_cache = page_registry._cache
page_registry.DATA_FILE = temp_data_file
page_registry._cache = None # Clear cache
yield temp_data_file
# Restore original
page_registry.DATA_FILE = original_data_file
page_registry._cache = original_cache
class TestGetPageStatus:
"""Tests for get_page_status function."""
def test_get_released_page_status(self, mock_registry):
"""Test getting status of released page."""
status = page_registry.get_page_status("/")
assert status == "released"
def test_get_dev_page_status(self, mock_registry):
"""Test getting status of dev page."""
status = page_registry.get_page_status("/dev-page")
assert status == "dev"
def test_get_unregistered_page_status(self, mock_registry):
"""Test getting status of unregistered page returns None."""
status = page_registry.get_page_status("/not-registered")
assert status is None
class TestIsPageRegistered:
"""Tests for is_page_registered function."""
def test_registered_page(self, mock_registry):
"""Test checking registered page."""
assert page_registry.is_page_registered("/") is True
def test_unregistered_page(self, mock_registry):
"""Test checking unregistered page."""
assert page_registry.is_page_registered("/not-here") is False
class TestSetPageStatus:
"""Tests for set_page_status function."""
def test_update_existing_page(self, mock_registry):
"""Test updating existing page status."""
page_registry.set_page_status("/", "dev")
assert page_registry.get_page_status("/") == "dev"
def test_add_new_page(self, mock_registry):
"""Test adding new page."""
page_registry.set_page_status("/new-page", "released", "New Page")
assert page_registry.get_page_status("/new-page") == "released"
def test_invalid_status_raises_error(self, mock_registry):
"""Test setting invalid status raises ValueError."""
with pytest.raises(ValueError, match="Invalid status"):
page_registry.set_page_status("/", "invalid")
def test_update_page_name(self, mock_registry):
"""Test updating page name."""
page_registry.set_page_status("/", "released", "New Name")
pages = page_registry.get_all_pages()
home = next(p for p in pages if p["route"] == "/")
assert home["name"] == "New Name"
class TestGetAllPages:
"""Tests for get_all_pages function."""
def test_get_all_pages(self, mock_registry):
"""Test getting all pages."""
pages = page_registry.get_all_pages()
assert len(pages) == 2
routes = [p["route"] for p in pages]
assert "/" in routes
assert "/dev-page" in routes
class TestIsApiPublic:
"""Tests for is_api_public function."""
def test_api_public_true(self, mock_registry):
"""Test API public flag when true."""
assert page_registry.is_api_public() is True
def test_api_public_false(self, mock_registry, temp_data_file):
"""Test API public flag when false."""
data = json.loads(temp_data_file.read_text())
data["api_public"] = False
temp_data_file.write_text(json.dumps(data))
page_registry._cache = None # Clear cache
assert page_registry.is_api_public() is False
class TestReloadCache:
"""Tests for reload_cache function."""
def test_reload_cache(self, mock_registry, temp_data_file):
"""Test reloading cache from disk."""
# First load
assert page_registry.get_page_status("/") == "released"
# Modify file directly
data = json.loads(temp_data_file.read_text())
data["pages"][0]["status"] = "dev"
temp_data_file.write_text(json.dumps(data))
# Cache still has old value
assert page_registry.get_page_status("/") == "released"
# After reload, should have new value
page_registry.reload_cache()
assert page_registry.get_page_status("/") == "dev"
class TestConcurrency:
"""Tests for thread safety."""
def test_concurrent_access(self, mock_registry):
"""Test concurrent read/write operations."""
import threading
errors = []
def reader():
try:
for _ in range(100):
page_registry.get_page_status("/")
except Exception as e:
errors.append(e)
def writer():
try:
for i in range(100):
status = "released" if i % 2 == 0 else "dev"
page_registry.set_page_status("/", status)
except Exception as e:
errors.append(e)
threads = [
threading.Thread(target=reader) for _ in range(3)
] + [
threading.Thread(target=writer) for _ in range(2)
]
for t in threads:
t.start()
for t in threads:
t.join()
assert len(errors) == 0, f"Errors occurred: {errors}"
if __name__ == "__main__":
pytest.main([__file__, "-v"])

102
tests/test_permissions.py Normal file
View File

@@ -0,0 +1,102 @@
# -*- coding: utf-8 -*-
"""Unit tests for permissions module."""
import pytest
from flask import Flask
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
from mes_dashboard.core.permissions import is_admin_logged_in, get_current_admin, admin_required
@pytest.fixture
def app():
"""Create a test Flask app."""
app = Flask(__name__)
app.secret_key = "test-secret-key"
app.config["TESTING"] = True
return app
class TestIsAdminLoggedIn:
"""Tests for is_admin_logged_in function."""
def test_admin_logged_in(self, app):
"""Test when admin is logged in."""
with app.test_request_context():
from flask import session
session["admin"] = {"username": "admin", "mail": "admin@test.com"}
assert is_admin_logged_in() is True
def test_admin_not_logged_in(self, app):
"""Test when admin is not logged in."""
with app.test_request_context():
assert is_admin_logged_in() is False
class TestGetCurrentAdmin:
"""Tests for get_current_admin function."""
def test_get_admin_when_logged_in(self, app):
"""Test getting admin info when logged in."""
with app.test_request_context():
from flask import session
admin_data = {"username": "admin", "mail": "admin@test.com"}
session["admin"] = admin_data
result = get_current_admin()
assert result == admin_data
def test_get_admin_when_not_logged_in(self, app):
"""Test getting admin info when not logged in."""
with app.test_request_context():
result = get_current_admin()
assert result is None
class TestAdminRequired:
"""Tests for admin_required decorator."""
def test_admin_required_when_logged_in(self, app):
"""Test decorator allows access when admin is logged in."""
@app.route("/test")
@admin_required
def test_route():
return "success"
with app.test_client() as client:
with client.session_transaction() as sess:
sess["admin"] = {"username": "admin"}
response = client.get("/test")
assert response.status_code == 200
assert response.data == b"success"
def test_admin_required_when_not_logged_in(self, app):
"""Test decorator redirects when admin is not logged in."""
from flask import Blueprint
# Register auth blueprint first with correct endpoint name
auth_bp = Blueprint("auth", __name__, url_prefix="/admin")
@auth_bp.route("/login", endpoint="login")
def login_view():
return "login"
app.register_blueprint(auth_bp)
# Now add the protected route
@app.route("/test")
@admin_required
def test_route():
return "success"
with app.test_client() as client:
response = client.get("/test")
assert response.status_code == 302
assert "/admin/login" in response.location
if __name__ == "__main__":
pytest.main([__file__, "-v"])