This commit is contained in:
beabigegg
2025-09-02 13:11:48 +08:00
parent a60d965317
commit b11a8272c4
76 changed files with 15321 additions and 200 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# 測試模組初始化

182
tests/conftest.py Normal file
View File

@@ -0,0 +1,182 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
pytest 配置和 fixtures
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import pytest
import tempfile
import os
from pathlib import Path
from app import create_app, db
from app.models.user import User
from app.models.job import TranslationJob
@pytest.fixture(scope='session')
def app():
"""建立測試應用程式"""
# 建立臨時資料庫
db_fd, db_path = tempfile.mkstemp()
# 測試配置
test_config = {
'TESTING': True,
'SQLALCHEMY_DATABASE_URI': f'sqlite:///{db_path}',
'WTF_CSRF_ENABLED': False,
'SECRET_KEY': 'test-secret-key',
'UPLOAD_FOLDER': tempfile.mkdtemp(),
'MAX_CONTENT_LENGTH': 26214400,
'SMTP_SERVER': 'localhost',
'SMTP_PORT': 25,
'SMTP_SENDER_EMAIL': 'test@example.com',
'LDAP_SERVER': 'localhost',
'LDAP_PORT': 389,
'LDAP_BIND_USER_DN': 'test',
'LDAP_BIND_USER_PASSWORD': 'test',
'LDAP_SEARCH_BASE': 'dc=test',
'REDIS_URL': 'redis://localhost:6379/15' # 使用測試資料庫
}
app = create_app('testing')
# 覆蓋測試配置
for key, value in test_config.items():
app.config[key] = value
with app.app_context():
db.create_all()
yield app
db.drop_all()
os.close(db_fd)
os.unlink(db_path)
@pytest.fixture
def client(app):
"""建立測試客戶端"""
return app.test_client()
@pytest.fixture
def runner(app):
"""建立 CLI 測試執行器"""
return app.test_cli_runner()
@pytest.fixture
def auth_user(app):
"""建立測試使用者"""
with app.app_context():
user = User(
username='testuser',
display_name='Test User',
email='test@panjit.com.tw',
department='IT',
is_admin=False
)
db.session.add(user)
db.session.commit()
return user
@pytest.fixture
def admin_user(app):
"""建立管理員使用者"""
with app.app_context():
admin = User(
username='admin',
display_name='Admin User',
email='admin@panjit.com.tw',
department='IT',
is_admin=True
)
db.session.add(admin)
db.session.commit()
return admin
@pytest.fixture
def sample_job(app, auth_user):
"""建立測試翻譯任務"""
with app.app_context():
job = TranslationJob(
user_id=auth_user.id,
original_filename='test.docx',
file_extension='.docx',
file_size=1024,
file_path='/tmp/test.docx',
source_language='auto',
target_languages=['en', 'vi'],
status='PENDING'
)
db.session.add(job)
db.session.commit()
return job
@pytest.fixture
def authenticated_client(client, auth_user):
"""已認證的測試客戶端"""
with client.session_transaction() as sess:
sess['user_id'] = auth_user.id
sess['username'] = auth_user.username
sess['is_admin'] = auth_user.is_admin
return client
@pytest.fixture
def admin_client(client, admin_user):
"""管理員測試客戶端"""
with client.session_transaction() as sess:
sess['user_id'] = admin_user.id
sess['username'] = admin_user.username
sess['is_admin'] = admin_user.is_admin
return client
@pytest.fixture
def sample_file():
"""建立測試檔案"""
import io
# 建立假的 DOCX 檔案內容
file_content = b"Mock DOCX file content for testing"
return io.BytesIO(file_content)
@pytest.fixture
def mock_dify_response():
"""模擬 Dify API 回應"""
return {
'answer': 'This is a translated text.',
'metadata': {
'usage': {
'prompt_tokens': 10,
'completion_tokens': 5,
'total_tokens': 15,
'prompt_unit_price': 0.0001,
'prompt_price_unit': 'USD'
}
}
}
@pytest.fixture
def mock_ldap_response():
"""模擬 LDAP 認證回應"""
return {
'username': 'testuser',
'display_name': 'Test User',
'email': 'test@panjit.com.tw',
'department': 'IT',
'user_principal_name': 'testuser@panjit.com.tw'
}

206
tests/test_auth_api.py Normal file
View File

@@ -0,0 +1,206 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
認證 API 測試
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import pytest
from unittest.mock import patch, MagicMock
from app.models.user import User
class TestAuthAPI:
"""認證 API 測試類別"""
def test_login_success(self, client, mock_ldap_response):
"""測試成功登入"""
with patch('app.utils.ldap_auth.LDAPAuthService.authenticate_user') as mock_auth:
mock_auth.return_value = mock_ldap_response
response = client.post('/api/v1/auth/login', json={
'username': 'testuser@panjit.com.tw',
'password': 'password123'
})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'user' in data['data']
assert data['data']['user']['username'] == 'testuser'
def test_login_invalid_credentials(self, client):
"""測試無效憑證登入"""
with patch('app.utils.ldap_auth.LDAPAuthService.authenticate_user') as mock_auth:
mock_auth.side_effect = Exception("認證失敗")
response = client.post('/api/v1/auth/login', json={
'username': 'testuser@panjit.com.tw',
'password': 'wrong_password'
})
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_CREDENTIALS'
def test_login_missing_fields(self, client):
"""測試缺少必要欄位"""
response = client.post('/api/v1/auth/login', json={
'username': 'testuser@panjit.com.tw'
# 缺少 password
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert 'MISSING_FIELDS' in data['error']
def test_login_empty_credentials(self, client):
"""測試空的認證資訊"""
response = client.post('/api/v1/auth/login', json={
'username': '',
'password': ''
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_INPUT'
def test_logout_success(self, authenticated_client):
"""測試成功登出"""
response = authenticated_client.post('/api/v1/auth/logout')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['message'] == '登出成功'
def test_logout_without_login(self, client):
"""測試未登入時登出"""
response = client.post('/api/v1/auth/logout')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_get_current_user_success(self, authenticated_client, auth_user):
"""測試取得當前使用者資訊"""
response = authenticated_client.get('/api/v1/auth/me')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'user' in data['data']
assert data['data']['user']['id'] == auth_user.id
def test_get_current_user_without_login(self, client):
"""測試未登入時取得使用者資訊"""
response = client.get('/api/v1/auth/me')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_check_auth_valid(self, authenticated_client, auth_user):
"""測試檢查有效認證狀態"""
response = authenticated_client.get('/api/v1/auth/check')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['authenticated'] is True
def test_check_auth_invalid(self, client):
"""測試檢查無效認證狀態"""
response = client.get('/api/v1/auth/check')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['authenticated'] is False
def test_refresh_session_success(self, authenticated_client, auth_user):
"""測試刷新 Session"""
response = authenticated_client.post('/api/v1/auth/refresh')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['data']['session_refreshed'] is True
def test_refresh_session_without_login(self, client):
"""測試未登入時刷新 Session"""
response = client.post('/api/v1/auth/refresh')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_search_users_success(self, authenticated_client):
"""測試搜尋使用者"""
with patch('app.utils.ldap_auth.LDAPAuthService.search_users') as mock_search:
mock_search.return_value = [
{
'username': 'user1',
'display_name': 'User One',
'email': 'user1@panjit.com.tw',
'department': 'IT'
},
{
'username': 'user2',
'display_name': 'User Two',
'email': 'user2@panjit.com.tw',
'department': 'HR'
}
]
response = authenticated_client.get('/api/v1/auth/search-users?q=user')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert len(data['data']['users']) == 2
def test_search_users_short_term(self, authenticated_client):
"""測試搜尋關鍵字太短"""
response = authenticated_client.get('/api/v1/auth/search-users?q=u')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_SEARCH_TERM'
def test_search_users_without_login(self, client):
"""測試未登入時搜尋使用者"""
response = client.get('/api/v1/auth/search-users?q=user')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_admin_access_with_admin(self, admin_client, admin_user):
"""測試管理員存取管理功能"""
response = admin_client.get('/api/v1/admin/stats')
# 這個測試會因為沒有實際資料而可能失敗,但應該通過認證檢查
# 狀態碼應該是 200 或其他非認證錯誤
assert response.status_code != 401
assert response.status_code != 403
def test_admin_access_without_permission(self, authenticated_client):
"""測試一般使用者存取管理功能"""
response = authenticated_client.get('/api/v1/admin/stats')
assert response.status_code == 403
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'PERMISSION_DENIED'

266
tests/test_files_api.py Normal file
View File

@@ -0,0 +1,266 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
檔案管理 API 測試
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import pytest
import io
import json
from unittest.mock import patch, MagicMock
from app.models.job import TranslationJob
class TestFilesAPI:
"""檔案管理 API 測試類別"""
def test_upload_file_success(self, authenticated_client, auth_user):
"""測試成功上傳檔案"""
# 建立測試檔案
file_data = b'Mock DOCX file content'
file_obj = (io.BytesIO(file_data), 'test.docx')
with patch('app.utils.helpers.save_uploaded_file') as mock_save:
mock_save.return_value = {
'success': True,
'filename': 'original_test_12345678.docx',
'file_path': '/tmp/test_job_uuid/original_test_12345678.docx',
'file_size': len(file_data)
}
response = authenticated_client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': json.dumps(['en', 'vi'])
})
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'job_uuid' in data['data']
assert data['data']['original_filename'] == 'test.docx'
def test_upload_file_no_file(self, authenticated_client):
"""測試未選擇檔案"""
response = authenticated_client.post('/api/v1/files/upload', data={
'source_language': 'auto',
'target_languages': json.dumps(['en'])
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'NO_FILE'
def test_upload_file_invalid_type(self, authenticated_client):
"""測試上傳無效檔案類型"""
file_data = b'Mock text file content'
file_obj = (io.BytesIO(file_data), 'test.txt')
response = authenticated_client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': json.dumps(['en'])
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_FILE_TYPE'
def test_upload_file_too_large(self, authenticated_client, app):
"""測試上傳過大檔案"""
# 建立超過限制的檔案26MB+
large_file_data = b'x' * (26 * 1024 * 1024 + 1)
file_obj = (io.BytesIO(large_file_data), 'large.docx')
response = authenticated_client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': json.dumps(['en'])
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'FILE_TOO_LARGE'
def test_upload_file_invalid_target_languages(self, authenticated_client):
"""測試無效的目標語言"""
file_data = b'Mock DOCX file content'
file_obj = (io.BytesIO(file_data), 'test.docx')
response = authenticated_client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': 'invalid_json'
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_TARGET_LANGUAGES'
def test_upload_file_empty_target_languages(self, authenticated_client):
"""測試空的目標語言"""
file_data = b'Mock DOCX file content'
file_obj = (io.BytesIO(file_data), 'test.docx')
response = authenticated_client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': json.dumps([])
})
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'NO_TARGET_LANGUAGES'
def test_upload_file_without_auth(self, client):
"""測試未認證上傳檔案"""
file_data = b'Mock DOCX file content'
file_obj = (io.BytesIO(file_data), 'test.docx')
response = client.post('/api/v1/files/upload', data={
'file': file_obj,
'source_language': 'auto',
'target_languages': json.dumps(['en'])
})
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_download_translated_file_success(self, authenticated_client, sample_job, auth_user):
"""測試成功下載翻譯檔案"""
# 設定任務為已完成
sample_job.update_status('COMPLETED')
# 添加翻譯檔案記錄
sample_job.add_translated_file(
language_code='en',
filename='test_en_translated.docx',
file_path='/tmp/test_en_translated.docx',
file_size=1024
)
with patch('pathlib.Path.exists') as mock_exists, \
patch('flask.send_file') as mock_send_file:
mock_exists.return_value = True
mock_send_file.return_value = 'file_content'
response = authenticated_client.get(f'/api/v1/files/{sample_job.job_uuid}/download/en')
# send_file 被呼叫表示成功
mock_send_file.assert_called_once()
def test_download_file_not_found(self, authenticated_client, sample_job):
"""測試下載不存在的檔案"""
response = authenticated_client.get(f'/api/v1/files/nonexistent-uuid/download/en')
assert response.status_code == 404
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'JOB_NOT_FOUND'
def test_download_file_permission_denied(self, authenticated_client, sample_job, app):
"""測試下載他人檔案"""
# 建立另一個使用者的任務
from app.models.user import User
from app import db
with app.app_context():
other_user = User(
username='otheruser',
display_name='Other User',
email='other@panjit.com.tw',
department='IT',
is_admin=False
)
db.session.add(other_user)
db.session.commit()
other_job = TranslationJob(
user_id=other_user.id,
original_filename='other.docx',
file_extension='.docx',
file_size=1024,
file_path='/tmp/other.docx',
source_language='auto',
target_languages=['en'],
status='COMPLETED'
)
db.session.add(other_job)
db.session.commit()
response = authenticated_client.get(f'/api/v1/files/{other_job.job_uuid}/download/en')
assert response.status_code == 403
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'PERMISSION_DENIED'
def test_download_file_not_completed(self, authenticated_client, sample_job):
"""測試下載未完成任務的檔案"""
response = authenticated_client.get(f'/api/v1/files/{sample_job.job_uuid}/download/en')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'JOB_NOT_COMPLETED'
def test_download_original_file_success(self, authenticated_client, sample_job):
"""測試下載原始檔案"""
# 添加原始檔案記錄
sample_job.add_original_file(
filename='original_test.docx',
file_path='/tmp/original_test.docx',
file_size=1024
)
with patch('pathlib.Path.exists') as mock_exists, \
patch('flask.send_file') as mock_send_file:
mock_exists.return_value = True
mock_send_file.return_value = 'file_content'
response = authenticated_client.get(f'/api/v1/files/{sample_job.job_uuid}/download/original')
mock_send_file.assert_called_once()
def test_get_supported_formats(self, client):
"""測試取得支援的檔案格式"""
response = client.get('/api/v1/files/supported-formats')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'supported_formats' in data['data']
assert 'max_file_size' in data['data']
# 檢查是否包含基本格式
formats = data['data']['supported_formats']
assert '.docx' in formats
assert '.pdf' in formats
def test_get_supported_languages(self, client):
"""測試取得支援的語言"""
response = client.get('/api/v1/files/supported-languages')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'supported_languages' in data['data']
# 檢查是否包含基本語言
languages = data['data']['supported_languages']
assert 'en' in languages
assert 'zh-TW' in languages
assert 'auto' in languages

237
tests/test_jobs_api.py Normal file
View File

@@ -0,0 +1,237 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
任務管理 API 測試
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import pytest
from app.models.job import TranslationJob
class TestJobsAPI:
"""任務管理 API 測試類別"""
def test_get_user_jobs_success(self, authenticated_client, sample_job):
"""測試取得使用者任務列表"""
response = authenticated_client.get('/api/v1/jobs')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'jobs' in data['data']
assert 'pagination' in data['data']
assert len(data['data']['jobs']) > 0
def test_get_user_jobs_with_status_filter(self, authenticated_client, sample_job):
"""測試按狀態篩選任務"""
response = authenticated_client.get('/api/v1/jobs?status=PENDING')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
# 所有返回的任務都應該是 PENDING 狀態
for job in data['data']['jobs']:
assert job['status'] == 'PENDING'
def test_get_user_jobs_with_pagination(self, authenticated_client, sample_job):
"""測試分頁"""
response = authenticated_client.get('/api/v1/jobs?page=1&per_page=5')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['data']['pagination']['page'] == 1
assert data['data']['pagination']['per_page'] == 5
def test_get_user_jobs_without_auth(self, client):
"""測試未認證取得任務列表"""
response = client.get('/api/v1/jobs')
assert response.status_code == 401
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'AUTHENTICATION_REQUIRED'
def test_get_job_detail_success(self, authenticated_client, sample_job):
"""測試取得任務詳細資訊"""
response = authenticated_client.get(f'/api/v1/jobs/{sample_job.job_uuid}')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'job' in data['data']
assert data['data']['job']['job_uuid'] == sample_job.job_uuid
def test_get_job_detail_not_found(self, authenticated_client):
"""測試取得不存在的任務"""
fake_uuid = '00000000-0000-0000-0000-000000000000'
response = authenticated_client.get(f'/api/v1/jobs/{fake_uuid}')
assert response.status_code == 404
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'JOB_NOT_FOUND'
def test_get_job_detail_invalid_uuid(self, authenticated_client):
"""測試無效的UUID格式"""
invalid_uuid = 'invalid-uuid'
response = authenticated_client.get(f'/api/v1/jobs/{invalid_uuid}')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_UUID'
def test_get_job_detail_permission_denied(self, authenticated_client, app):
"""測試存取他人任務"""
from app.models.user import User
from app import db
with app.app_context():
# 建立另一個使用者和任務
other_user = User(
username='otheruser',
display_name='Other User',
email='other@panjit.com.tw',
department='IT',
is_admin=False
)
db.session.add(other_user)
db.session.commit()
other_job = TranslationJob(
user_id=other_user.id,
original_filename='other.docx',
file_extension='.docx',
file_size=1024,
file_path='/tmp/other.docx',
source_language='auto',
target_languages=['en'],
status='PENDING'
)
db.session.add(other_job)
db.session.commit()
response = authenticated_client.get(f'/api/v1/jobs/{other_job.job_uuid}')
assert response.status_code == 403
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'PERMISSION_DENIED'
def test_retry_job_success(self, authenticated_client, sample_job):
"""測試重試失敗任務"""
# 設定任務為失敗狀態
sample_job.update_status('FAILED', error_message='Test error')
response = authenticated_client.post(f'/api/v1/jobs/{sample_job.job_uuid}/retry')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['data']['status'] == 'PENDING'
assert data['data']['retry_count'] == 1
def test_retry_job_cannot_retry(self, authenticated_client, sample_job):
"""測試無法重試的任務"""
# 設定任務為完成狀態
sample_job.update_status('COMPLETED')
response = authenticated_client.post(f'/api/v1/jobs/{sample_job.job_uuid}/retry')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'CANNOT_RETRY'
def test_retry_job_max_retries(self, authenticated_client, sample_job):
"""測試達到最大重試次數"""
# 設定任務為失敗且重試次數已達上限
sample_job.update_status('FAILED', error_message='Test error')
sample_job.retry_count = 3
from app import db
db.session.commit()
response = authenticated_client.post(f'/api/v1/jobs/{sample_job.job_uuid}/retry')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'CANNOT_RETRY'
def test_get_user_statistics(self, authenticated_client, sample_job):
"""測試取得使用者統計資料"""
response = authenticated_client.get('/api/v1/jobs/statistics')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'job_statistics' in data['data']
assert 'api_statistics' in data['data']
def test_get_user_statistics_with_date_range(self, authenticated_client):
"""測試指定日期範圍的統計"""
response = authenticated_client.get('/api/v1/jobs/statistics?start_date=2024-01-01T00:00:00Z&end_date=2024-12-31T23:59:59Z')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
def test_get_user_statistics_invalid_date(self, authenticated_client):
"""測試無效的日期格式"""
response = authenticated_client.get('/api/v1/jobs/statistics?start_date=invalid-date')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'INVALID_START_DATE'
def test_get_queue_status(self, client, sample_job):
"""測試取得佇列狀態(不需認證)"""
response = client.get('/api/v1/jobs/queue/status')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert 'queue_status' in data['data']
assert 'processing_jobs' in data['data']
def test_cancel_job_success(self, authenticated_client, sample_job):
"""測試取消等待中的任務"""
# 確保任務是 PENDING 狀態
assert sample_job.status == 'PENDING'
response = authenticated_client.post(f'/api/v1/jobs/{sample_job.job_uuid}/cancel')
assert response.status_code == 200
data = response.get_json()
assert data['success'] is True
assert data['data']['status'] == 'FAILED'
def test_cancel_job_cannot_cancel(self, authenticated_client, sample_job):
"""測試取消非等待狀態的任務"""
# 設定任務為處理中
sample_job.update_status('PROCESSING')
response = authenticated_client.post(f'/api/v1/jobs/{sample_job.job_uuid}/cancel')
assert response.status_code == 400
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'CANNOT_CANCEL'
def test_cancel_job_not_found(self, authenticated_client):
"""測試取消不存在的任務"""
fake_uuid = '00000000-0000-0000-0000-000000000000'
response = authenticated_client.post(f'/api/v1/jobs/{fake_uuid}/cancel')
assert response.status_code == 404
data = response.get_json()
assert data['success'] is False
assert data['error'] == 'JOB_NOT_FOUND'

308
tests/test_models.py Normal file
View File

@@ -0,0 +1,308 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
資料模型測試
Author: PANJIT IT Team
Created: 2024-01-28
Modified: 2024-01-28
"""
import pytest
from datetime import datetime, timedelta
from app.models.user import User
from app.models.job import TranslationJob, JobFile
from app.models.cache import TranslationCache
from app.models.stats import APIUsageStats
from app.models.log import SystemLog
from app import db
class TestUserModel:
"""使用者模型測試"""
def test_create_user(self, app):
"""測試建立使用者"""
with app.app_context():
user = User(
username='testuser',
display_name='Test User',
email='test@example.com',
department='IT',
is_admin=False
)
db.session.add(user)
db.session.commit()
assert user.id is not None
assert user.username == 'testuser'
assert user.is_admin is False
def test_user_to_dict(self, app, auth_user):
"""測試使用者轉字典"""
with app.app_context():
user_dict = auth_user.to_dict()
assert 'id' in user_dict
assert 'username' in user_dict
assert 'display_name' in user_dict
assert 'email' in user_dict
assert user_dict['username'] == auth_user.username
def test_user_get_or_create_existing(self, app, auth_user):
"""測試取得已存在的使用者"""
with app.app_context():
user = User.get_or_create(
username=auth_user.username,
display_name='Updated Name',
email=auth_user.email
)
assert user.id == auth_user.id
assert user.display_name == 'Updated Name' # 應該更新
def test_user_get_or_create_new(self, app):
"""測試建立新使用者"""
with app.app_context():
user = User.get_or_create(
username='newuser',
display_name='New User',
email='new@example.com'
)
assert user.id is not None
assert user.username == 'newuser'
def test_update_last_login(self, app, auth_user):
"""測試更新最後登入時間"""
with app.app_context():
old_login_time = auth_user.last_login
auth_user.update_last_login()
assert auth_user.last_login is not None
if old_login_time:
assert auth_user.last_login > old_login_time
class TestTranslationJobModel:
"""翻譯任務模型測試"""
def test_create_translation_job(self, app, auth_user):
"""測試建立翻譯任務"""
with app.app_context():
job = TranslationJob(
user_id=auth_user.id,
original_filename='test.docx',
file_extension='.docx',
file_size=1024,
file_path='/tmp/test.docx',
source_language='auto',
target_languages=['en', 'vi'],
status='PENDING'
)
db.session.add(job)
db.session.commit()
assert job.id is not None
assert job.job_uuid is not None
assert len(job.job_uuid) == 36 # UUID 格式
def test_job_to_dict(self, app, sample_job):
"""測試任務轉字典"""
with app.app_context():
job_dict = sample_job.to_dict()
assert 'id' in job_dict
assert 'job_uuid' in job_dict
assert 'original_filename' in job_dict
assert 'target_languages' in job_dict
assert job_dict['job_uuid'] == sample_job.job_uuid
def test_update_status(self, app, sample_job):
"""測試更新任務狀態"""
with app.app_context():
old_updated_at = sample_job.updated_at
sample_job.update_status('PROCESSING', progress=50.0)
assert sample_job.status == 'PROCESSING'
assert sample_job.progress == 50.0
assert sample_job.processing_started_at is not None
assert sample_job.updated_at > old_updated_at
def test_add_original_file(self, app, sample_job):
"""測試新增原始檔案記錄"""
with app.app_context():
file_record = sample_job.add_original_file(
filename='test.docx',
file_path='/tmp/test.docx',
file_size=1024
)
assert file_record.id is not None
assert file_record.file_type == 'ORIGINAL'
assert file_record.filename == 'test.docx'
def test_add_translated_file(self, app, sample_job):
"""測試新增翻譯檔案記錄"""
with app.app_context():
file_record = sample_job.add_translated_file(
language_code='en',
filename='test_en.docx',
file_path='/tmp/test_en.docx',
file_size=1200
)
assert file_record.id is not None
assert file_record.file_type == 'TRANSLATED'
assert file_record.language_code == 'en'
def test_can_retry(self, app, sample_job):
"""測試是否可以重試"""
with app.app_context():
# PENDING 狀態不能重試
assert not sample_job.can_retry()
# FAILED 狀態且重試次數 < 3 可以重試
sample_job.update_status('FAILED')
sample_job.retry_count = 2
assert sample_job.can_retry()
# 重試次數達到上限不能重試
sample_job.retry_count = 3
assert not sample_job.can_retry()
class TestTranslationCacheModel:
"""翻譯快取模型測試"""
def test_save_and_get_translation(self, app):
"""測試儲存和取得翻譯快取"""
with app.app_context():
source_text = "Hello, world!"
translated_text = "你好,世界!"
# 儲存翻譯
result = TranslationCache.save_translation(
source_text=source_text,
source_language='en',
target_language='zh-TW',
translated_text=translated_text
)
assert result is True
# 取得翻譯
cached_translation = TranslationCache.get_translation(
source_text=source_text,
source_language='en',
target_language='zh-TW'
)
assert cached_translation == translated_text
def test_get_nonexistent_translation(self, app):
"""測試取得不存在的翻譯"""
with app.app_context():
cached_translation = TranslationCache.get_translation(
source_text="Nonexistent text",
source_language='en',
target_language='zh-TW'
)
assert cached_translation is None
def test_generate_hash(self):
"""測試生成文字雜湊"""
text = "Hello, world!"
hash1 = TranslationCache.generate_hash(text)
hash2 = TranslationCache.generate_hash(text)
assert hash1 == hash2
assert len(hash1) == 64 # SHA256 雜湊長度
class TestAPIUsageStatsModel:
"""API 使用統計模型測試"""
def test_record_api_call(self, app, auth_user, sample_job):
"""測試記錄 API 呼叫"""
with app.app_context():
metadata = {
'usage': {
'prompt_tokens': 10,
'completion_tokens': 5,
'total_tokens': 15,
'prompt_unit_price': 0.0001,
'prompt_price_unit': 'USD'
}
}
stats = APIUsageStats.record_api_call(
user_id=auth_user.id,
job_id=sample_job.id,
api_endpoint='/chat-messages',
metadata=metadata,
response_time_ms=1000
)
assert stats.id is not None
assert stats.prompt_tokens == 10
assert stats.total_tokens == 15
assert stats.cost == 10 * 0.0001 # prompt_tokens * prompt_unit_price
def test_get_user_statistics(self, app, auth_user):
"""測試取得使用者統計"""
with app.app_context():
stats = APIUsageStats.get_user_statistics(auth_user.id)
assert 'total_calls' in stats
assert 'successful_calls' in stats
assert 'total_cost' in stats
class TestSystemLogModel:
"""系統日誌模型測試"""
def test_create_log_entry(self, app, auth_user):
"""測試建立日誌項目"""
with app.app_context():
log = SystemLog.log(
level='INFO',
module='test_module',
message='Test message',
user_id=auth_user.id
)
assert log.id is not None
assert log.level == 'INFO'
assert log.module == 'test_module'
assert log.message == 'Test message'
def test_log_convenience_methods(self, app):
"""測試日誌便利方法"""
with app.app_context():
# 測試不同等級的日誌方法
info_log = SystemLog.info('test', 'Info message')
warning_log = SystemLog.warning('test', 'Warning message')
error_log = SystemLog.error('test', 'Error message')
assert info_log.level == 'INFO'
assert warning_log.level == 'WARNING'
assert error_log.level == 'ERROR'
def test_get_logs_with_filters(self, app):
"""測試帶篩選條件的日誌查詢"""
with app.app_context():
# 建立測試日誌
SystemLog.info('module1', 'Test message 1')
SystemLog.error('module2', 'Test message 2')
# 按等級篩選
info_logs = SystemLog.get_logs(level='INFO', limit=10)
assert len([log for log in info_logs if log.level == 'INFO']) > 0
# 按模組篩選
module1_logs = SystemLog.get_logs(module='module1', limit=10)
assert len([log for log in module1_logs if 'module1' in log.module]) > 0