107 lines
4.4 KiB
Python
107 lines
4.4 KiB
Python
"""图片解析边界测试,不访问外部服务或数据库。"""
|
|
import importlib.util
|
|
import io
|
|
import json
|
|
from decimal import Decimal
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from PIL import Image
|
|
|
|
|
|
def test_recognition_module_exists():
|
|
assert importlib.util.find_spec('app.services.pl_quota_recognition') is not None
|
|
|
|
|
|
def png():
|
|
out = io.BytesIO()
|
|
Image.new('RGB', (64, 64), 'white').save(out, 'PNG')
|
|
return out.getvalue()
|
|
|
|
|
|
def configured(monkeypatch, payload, status=200):
|
|
from app.services import pl_quota_recognition as svc
|
|
monkeypatch.setattr(svc, 'settings', SimpleNamespace(
|
|
DEEPSEEK_API_KEY='test-only', DEEPSEEK_BASE_URL='https://api.deepseek.com',
|
|
DEEPSEEK_VISION_MODEL='deepseek-flash',
|
|
))
|
|
calls = []
|
|
def post(url, **kwargs):
|
|
calls.append((url, kwargs))
|
|
return SimpleNamespace(status_code=status, json=lambda: {
|
|
'choices': [{'finish_reason': 'stop', 'message': {'content': json.dumps(payload)}}]
|
|
})
|
|
monkeypatch.setattr(svc.requests, 'post', post)
|
|
return svc, calls
|
|
|
|
|
|
def test_candidate_fields_only_no_activation_or_link_ids(monkeypatch):
|
|
svc, calls = configured(monkeypatch, {
|
|
'certNo': 'G-1', 'totalQty': '12.123456', 'quantityUnit': '吨',
|
|
'status': 'active', 'usedQty': 0, 'tenantNo': 'other', 'contractId': 'foreign',
|
|
})
|
|
result = svc.recognize_quota_cert('purchase', png())
|
|
assert result['candidates'] == {'certNo': 'G-1', 'totalQty': Decimal('12.123456'), 'quantityUnit': '吨'}
|
|
assert result['requiresConfirmation'] is True
|
|
assert len(calls) == 1
|
|
assert calls[0][1]['allow_redirects'] is False
|
|
request = calls[0][1]['json']
|
|
assert request['response_format'] == {'type': 'json_object'}
|
|
assert request['messages'][1]['content'][1]['image_url']['url'].startswith('data:image/png;base64,')
|
|
|
|
|
|
def test_invalid_field_dropped_without_guessing_other_values(monkeypatch):
|
|
svc, _ = configured(monkeypatch, {'totalQty': 'NaN', 'validTo': 'not-a-date', 'buyerName': '购方'})
|
|
result = svc.recognize_quota_cert('purchase', png())
|
|
assert result['candidates'] == {'buyerName': '购方'}
|
|
assert set(result['invalidFields']) == {'totalQty', 'validTo'}
|
|
|
|
|
|
def test_transport_references_have_no_database_identity(monkeypatch):
|
|
svc, _ = configured(monkeypatch, {'vehicleNumbersJson': ['皖A001', '皖A002'], 'purchaseRefs': [
|
|
{'referenceType': 'purchase_permit', 'referenceNo': 'G-1', 'purchaseCertId': 'injected', 'id': 'injected'}
|
|
]})
|
|
result = svc.recognize_quota_cert('transport', png())
|
|
assert result['candidates']['purchaseRefs'] == [{'referenceType': 'purchase_permit', 'referenceNo': 'G-1'}]
|
|
assert len(result['candidates']['vehicleNumbersJson']) == 2
|
|
|
|
|
|
def test_invalid_image_rejected_before_network(monkeypatch):
|
|
svc, calls = configured(monkeypatch, {})
|
|
with pytest.raises(HTTPException):
|
|
svc.recognize_quota_cert('purchase', b'not an image')
|
|
assert calls == []
|
|
|
|
|
|
def test_partial_reference_recognition_keeps_readable_numbers(monkeypatch):
|
|
svc, _ = configured(monkeypatch, {'purchaseRefs': [
|
|
{'referenceType': 'purchase_permit', 'referenceNo': 'G-1'},
|
|
{'referenceType': 'unknown', 'referenceNo': None},
|
|
{'referenceType': None, 'referenceNo': 'Readable-2'},
|
|
]})
|
|
result = svc.recognize_quota_cert('transport', png())
|
|
assert result['candidates']['purchaseRefs'] == [
|
|
{'referenceType': 'purchase_permit', 'referenceNo': 'G-1'},
|
|
{'referenceType': 'unknown', 'referenceNo': 'Readable-2'},
|
|
]
|
|
assert 'purchaseRefs[1]' in result['invalidFields']
|
|
|
|
|
|
@pytest.mark.parametrize('status', [401, 429, 500])
|
|
def test_provider_error_is_sanitized_and_allows_manual_entry(monkeypatch, status):
|
|
svc, _ = configured(monkeypatch, {}, status)
|
|
with pytest.raises(HTTPException) as error:
|
|
svc.recognize_quota_cert('purchase', png())
|
|
assert '手工' in error.value.detail
|
|
assert 'test-only' not in error.value.detail
|
|
|
|
|
|
def test_merge_candidates_preserves_user_edits_and_zero():
|
|
from app.services.pl_quota_recognition import merge_unedited_candidates
|
|
assert merge_unedited_candidates({'totalQty': 0, 'buyerName': ''},
|
|
{'totalQty': 100, 'buyerName': '识别名', 'sellerName': '销售方'},
|
|
{'buyerName'}) == {
|
|
'totalQty': 0, 'buyerName': '', 'sellerName': '销售方'
|
|
}
|