2ND
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 測試模組初始化
|
182
tests/conftest.py
Normal file
182
tests/conftest.py
Normal 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
206
tests/test_auth_api.py
Normal 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
266
tests/test_files_api.py
Normal 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
237
tests/test_jobs_api.py
Normal 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
308
tests/test_models.py
Normal 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
|
Reference in New Issue
Block a user