Implement phased modernization infrastructure for transitioning from multi-page legacy routing to SPA portal-shell architecture, plus post-delivery hardening fixes for policy loading, fallback consistency, and governance drift detection. Key changes: - Add route contract enrichment with scope/visibility/compatibility policies - Canonical 302 redirects from legacy direct-entry to /portal-shell/ routes - Asset readiness enforcement and runtime fallback retirement for in-scope routes - Shared feature-flag helpers (env > config > default) replacing duplicated _to_bool - Defensive copy for lru_cached policy payloads preventing mutation corruption - Unified retired-fallback response helper across app and blueprint routes - Frontend/backend route-contract cross-validation in governance gates - Shell CSS token fallback values for routes rendered outside shell scope - Local-safe .env.example defaults with production recommendation comments - Legacy contract fallback warning logging and single-hop redirect optimization Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
323 lines
11 KiB
Python
323 lines
11 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Integration tests for Job Query API routes.
|
|
|
|
Tests the API endpoints with mocked service dependencies.
|
|
"""
|
|
|
|
import pytest
|
|
import json
|
|
from unittest.mock import patch, MagicMock
|
|
|
|
from mes_dashboard import create_app
|
|
|
|
|
|
@pytest.fixture
|
|
def app():
|
|
"""Create test Flask application."""
|
|
app = create_app()
|
|
app.config['TESTING'] = True
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create test client."""
|
|
return app.test_client()
|
|
|
|
|
|
class TestJobQueryPage:
|
|
"""Tests for /job-query page route."""
|
|
|
|
def test_page_returns_html(self, client):
|
|
"""Should redirect direct entry to canonical shell page."""
|
|
response = client.get('/job-query', follow_redirects=False)
|
|
assert response.status_code == 302
|
|
assert response.location.endswith('/portal-shell/job-query')
|
|
|
|
|
|
class TestGetResources:
|
|
"""Tests for /api/job-query/resources endpoint."""
|
|
|
|
@patch('mes_dashboard.services.resource_cache.get_all_resources')
|
|
def test_get_resources_success(self, mock_get_resources, client):
|
|
"""Should return resources list."""
|
|
mock_get_resources.return_value = [
|
|
{
|
|
'RESOURCEID': 'RES001',
|
|
'RESOURCENAME': 'Machine-01',
|
|
'WORKCENTERNAME': 'WC-A',
|
|
'RESOURCEFAMILYNAME': 'FAM-01'
|
|
},
|
|
{
|
|
'RESOURCEID': 'RES002',
|
|
'RESOURCENAME': 'Machine-02',
|
|
'WORKCENTERNAME': 'WC-B',
|
|
'RESOURCEFAMILYNAME': 'FAM-02'
|
|
}
|
|
]
|
|
|
|
response = client.get('/api/job-query/resources')
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'data' in data
|
|
assert 'total' in data
|
|
assert data['total'] == 2
|
|
assert data['data'][0]['RESOURCEID'] in ['RES001', 'RES002']
|
|
|
|
@patch('mes_dashboard.services.resource_cache.get_all_resources')
|
|
def test_get_resources_empty(self, mock_get_resources, client):
|
|
"""Should return error when no resources available."""
|
|
mock_get_resources.return_value = []
|
|
|
|
response = client.get('/api/job-query/resources')
|
|
assert response.status_code == 500
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
@patch('mes_dashboard.services.resource_cache.get_all_resources')
|
|
def test_get_resources_exception(self, mock_get_resources, client):
|
|
"""Should handle exception gracefully."""
|
|
mock_get_resources.side_effect = Exception('ORA-01017 invalid username/password')
|
|
|
|
response = client.get('/api/job-query/resources')
|
|
assert response.status_code == 500
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert data['error'] == '服務暫時無法使用'
|
|
assert 'ORA-01017' not in data['error']
|
|
|
|
|
|
class TestQueryJobs:
|
|
"""Tests for /api/job-query/jobs endpoint."""
|
|
|
|
def test_missing_resource_ids(self, client):
|
|
"""Should return error without resource_ids."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert '設備' in data['error']
|
|
|
|
def test_empty_resource_ids(self, client):
|
|
"""Should return error for empty resource_ids."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': [],
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_missing_start_date(self, client):
|
|
"""Should return error without start_date."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert '日期' in data['error']
|
|
|
|
def test_missing_end_date(self, client):
|
|
"""Should return error without end_date."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-01-01'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_invalid_date_range(self, client):
|
|
"""Should return error for invalid date range."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-12-31',
|
|
'end_date': '2024-01-01'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert '結束日期' in data['error'] or '早於' in data['error']
|
|
|
|
def test_date_range_exceeds_limit(self, client):
|
|
"""Should reject date range > 365 days."""
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2023-01-01',
|
|
'end_date': '2024-12-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
assert '365' in data['error']
|
|
|
|
@patch('mes_dashboard.routes.job_query_routes.get_jobs_by_resources')
|
|
def test_query_jobs_success(self, mock_query, client):
|
|
"""Should return jobs list on success."""
|
|
mock_query.return_value = {
|
|
'data': [
|
|
{'JOBID': 'JOB001', 'RESOURCENAME': 'Machine-01', 'JOBSTATUS': 'Complete'}
|
|
],
|
|
'total': 1,
|
|
'resource_count': 1
|
|
}
|
|
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'data' in data
|
|
assert data['total'] == 1
|
|
assert data['data'][0]['JOBID'] == 'JOB001'
|
|
|
|
@patch('mes_dashboard.routes.job_query_routes.get_jobs_by_resources')
|
|
def test_query_jobs_service_error(self, mock_query, client):
|
|
"""Should return error from service."""
|
|
mock_query.return_value = {'error': '查詢失敗: Database error'}
|
|
|
|
response = client.post(
|
|
'/api/job-query/jobs',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
|
|
class TestQueryJobTxnHistory:
|
|
"""Tests for /api/job-query/txn/<job_id> endpoint."""
|
|
|
|
@patch('mes_dashboard.routes.job_query_routes.get_job_txn_history')
|
|
def test_get_txn_history_success(self, mock_query, client):
|
|
"""Should return transaction history."""
|
|
mock_query.return_value = {
|
|
'data': [
|
|
{
|
|
'JOBTXNHISTORYID': 'TXN001',
|
|
'JOBID': 'JOB001',
|
|
'TXNDATE': '2024-01-15 10:30:00',
|
|
'FROMJOBSTATUS': 'Open',
|
|
'JOBSTATUS': 'In Progress'
|
|
}
|
|
],
|
|
'total': 1,
|
|
'job_id': 'JOB001'
|
|
}
|
|
|
|
response = client.get('/api/job-query/txn/JOB001')
|
|
assert response.status_code == 200
|
|
data = json.loads(response.data)
|
|
assert 'data' in data
|
|
assert data['total'] == 1
|
|
assert data['job_id'] == 'JOB001'
|
|
|
|
@patch('mes_dashboard.routes.job_query_routes.get_job_txn_history')
|
|
def test_get_txn_history_service_error(self, mock_query, client):
|
|
"""Should return error from service."""
|
|
mock_query.return_value = {'error': '查詢失敗: Job not found'}
|
|
|
|
response = client.get('/api/job-query/txn/INVALID_JOB')
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
|
|
class TestExportJobs:
|
|
"""Tests for /api/job-query/export endpoint."""
|
|
|
|
def test_missing_resource_ids(self, client):
|
|
"""Should return error without resource_ids."""
|
|
response = client.post(
|
|
'/api/job-query/export',
|
|
json={
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_missing_dates(self, client):
|
|
"""Should return error without dates."""
|
|
response = client.post(
|
|
'/api/job-query/export',
|
|
json={
|
|
'resource_ids': ['RES001']
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
def test_invalid_date_range(self, client):
|
|
"""Should return error for invalid date range."""
|
|
response = client.post(
|
|
'/api/job-query/export',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-12-31',
|
|
'end_date': '2024-01-01'
|
|
}
|
|
)
|
|
assert response.status_code == 400
|
|
data = json.loads(response.data)
|
|
assert 'error' in data
|
|
|
|
@patch('mes_dashboard.routes.job_query_routes.export_jobs_with_history')
|
|
def test_export_success(self, mock_export, client):
|
|
"""Should return CSV streaming response."""
|
|
# Mock generator that yields CSV content
|
|
def mock_generator(*args):
|
|
yield '\ufeff設備名稱,工單ID\n'
|
|
yield 'Machine-01,JOB001\n'
|
|
|
|
mock_export.return_value = mock_generator()
|
|
|
|
response = client.post(
|
|
'/api/job-query/export',
|
|
json={
|
|
'resource_ids': ['RES001'],
|
|
'start_date': '2024-01-01',
|
|
'end_date': '2024-01-31'
|
|
}
|
|
)
|
|
assert response.status_code == 200
|
|
assert 'text/csv' in response.content_type
|
|
assert 'attachment' in response.headers.get('Content-Disposition', '')
|
|
assert 'job_history_export.csv' in response.headers.get('Content-Disposition', '')
|