Files
DashBoard/tests/test_job_query_routes.py

384 lines
13 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."""
@patch('mes_dashboard.routes.job_query_routes.get_jobs_by_resources')
def test_non_json_payload_returns_415(self, mock_query, client):
response = client.post(
'/api/job-query/jobs',
data='plain-text',
content_type='text/plain',
)
assert response.status_code == 415
payload = response.get_json()
assert 'error' in payload
mock_query.assert_not_called()
@patch('mes_dashboard.routes.job_query_routes.get_jobs_by_resources')
def test_malformed_json_returns_400(self, mock_query, client):
response = client.post(
'/api/job-query/jobs',
data='{"resource_ids":',
content_type='application/json',
)
assert response.status_code == 400
payload = response.get_json()
assert 'error' in payload
mock_query.assert_not_called()
@patch('mes_dashboard.routes.job_query_routes.get_jobs_by_resources')
def test_payload_too_large_returns_413(self, mock_query, client):
client.application.config['MAX_JSON_BODY_BYTES'] = 8
response = client.post(
'/api/job-query/jobs',
data='{"resource_ids":["RES001"]}',
content_type='application/json',
)
assert response.status_code == 413
payload = response.get_json()
assert 'error' in payload
mock_query.assert_not_called()
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."""
@patch('mes_dashboard.routes.job_query_routes.export_jobs_with_history')
def test_non_json_payload_returns_415(self, mock_export, client):
response = client.post(
'/api/job-query/export',
data='plain-text',
content_type='text/plain',
)
assert response.status_code == 415
payload = response.get_json()
assert 'error' in payload
mock_export.assert_not_called()
@patch('mes_dashboard.routes.job_query_routes.export_jobs_with_history')
def test_malformed_json_returns_400(self, mock_export, client):
response = client.post(
'/api/job-query/export',
data='{"resource_ids":',
content_type='application/json',
)
assert response.status_code == 400
payload = response.get_json()
assert 'error' in payload
mock_export.assert_not_called()
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', '')