"""图片解析边界测试,不访问外部服务或数据库。""" 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': '销售方' }