Files
DashBoard/tests/e2e/test_admin_auth_e2e.py
egg c38b5f646a feat(modernization): promote deferred routes to in-scope and unify page header styles
Promote /tables, /excel-query, /query-tool, /mid-section-defect from
deferred to full shell-governed in-scope routes with canonical redirects,
content contracts, governance artifacts, and updated CI gates.

Unify all page header gradients to #667eea → #764ba2 and h1 font-size
to 24px for visual consistency across all dashboard pages. Remove
Native Route-View dev annotations from job-query, excel-query, and
query-tool headers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 13:20:06 +08:00

371 lines
13 KiB
Python

# -*- 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
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
from mes_dashboard.routes import auth_routes
@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['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()
@pytest.fixture(autouse=True)
def clear_login_rate_limit():
"""Reset in-memory login attempts to avoid cross-test interference."""
auth_routes._login_attempts.clear()
yield
auth_routes._login_attempts.clear()
def _mock_admin_user(mail: str = "ymirliu@panjit.com.tw") -> dict:
return {
"username": "92367",
"displayName": "Test Admin",
"mail": mail,
"department": "Test Department",
}
class TestFullLoginLogoutFlow:
"""E2E tests for complete login/logout flow."""
@patch('mes_dashboard.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_complete_admin_login_workflow(self, mock_auth, _mock_is_admin, client):
"""Test complete admin login workflow."""
mock_auth.return_value = _mock_admin_user()
spa_enabled = bool(client.application.config.get("PORTAL_SPA_ENABLED", False))
# 1. Access portal entry.
response = client.get("/", follow_redirects=False)
if spa_enabled:
assert response.status_code == 302
assert response.location.rstrip("/").endswith("/portal-shell")
else:
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
if not spa_enabled:
content = response.data.decode("utf-8")
# Portal template should show admin identity in non-SPA mode.
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"
if spa_enabled:
nav = client.get("/api/portal/navigation")
assert nav.status_code == 200
assert nav.get_json()["is_admin"] is True
# 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.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_admin_can_access_all_pages(self, mock_auth, _mock_is_admin, client, temp_page_status):
"""Test admin users can access all pages."""
mock_auth.return_value = _mock_admin_user()
# 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.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_admin_can_change_page_status(self, mock_auth, _mock_is_admin, client, temp_page_status):
"""Test admin can change page status via management interface."""
mock_auth.return_value = _mock_admin_user()
# 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.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_release_dev_page_makes_it_public(self, mock_auth, _mock_is_admin, client, temp_page_status):
"""Test releasing a dev page makes it publicly accessible."""
mock_auth.return_value = _mock_admin_user()
# 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("/api/portal/navigation")
assert response.status_code == 200
payload = response.get_json()
routes = {
page["route"]
for drawer in payload.get("drawers", [])
for page in drawer.get("pages", [])
}
assert "/wip-overview" in routes
assert "/tables" not in routes
assert payload.get("is_admin") is False
@patch('mes_dashboard.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_portal_shows_all_tabs_for_admin(self, mock_auth, _mock_is_admin, client, temp_page_status):
"""Test portal shows all tabs for admin users."""
mock_auth.return_value = _mock_admin_user()
# Login as admin
client.post("/admin/login", data={
"username": "92367",
"password": "password123"
})
response = client.get("/api/portal/navigation")
assert response.status_code == 200
payload = response.get_json()
routes = {
page["route"]
for drawer in payload.get("drawers", [])
for page in drawer.get("pages", [])
}
assert "/wip-overview" in routes
assert "/tables" in routes
assert payload.get("is_admin") is True
class TestSessionPersistence:
"""E2E tests for session persistence."""
@patch('mes_dashboard.routes.auth_routes.is_admin', return_value=True)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_session_persists_across_requests(self, mock_auth, _mock_is_admin, client):
"""Test admin session persists across multiple requests."""
mock_auth.return_value = _mock_admin_user()
# 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 in (302, 401)
# 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 in (302, 401)
@patch('mes_dashboard.routes.auth_routes.is_admin', return_value=False)
@patch('mes_dashboard.routes.auth_routes.authenticate')
def test_non_admin_user_cannot_login(self, mock_auth, _mock_is_admin, client):
"""Test non-admin user cannot access admin features."""
mock_auth.return_value = {
"username": "99999",
"displayName": "Regular User",
"mail": "regular@panjit.com.tw",
"department": "Test",
}
# 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"])