Refactor configuration handling and improve error management across services; normalize placeholder values for SMTP and Stripe configurations; enhance local storage fallback logic in StorageService; add tests for new behaviors and edge cases.

This commit is contained in:
Your Name
2026-03-26 14:15:10 +02:00
parent 688d411537
commit bc8a5dc290
19 changed files with 423 additions and 95 deletions

View File

@@ -0,0 +1,19 @@
"""Tests for SMTP configuration normalization."""
from app.services.email_service import send_email
def test_placeholder_smtp_host_is_treated_as_unconfigured(app, monkeypatch):
"""A copied sample SMTP host should not trigger a network call."""
with app.app_context():
app.config.update({
"SMTP_HOST": "smtp.your-provider.com",
"SMTP_PORT": 587,
"SMTP_USER": "noreply@dociva.io",
"SMTP_PASSWORD": "replace-with-smtp-password",
})
def fail_if_called(*args, **kwargs):
raise AssertionError("SMTP should not be contacted for placeholder config")
monkeypatch.setattr("smtplib.SMTP", fail_if_called)
assert send_email("user@example.com", "Subject", "<p>Body</p>") is False

View File

@@ -1,6 +1,10 @@
"""Tests for shared OpenRouter configuration resolution across AI services."""
from app.services.openrouter_config_service import get_openrouter_settings
from app.services.openrouter_config_service import (
LEGACY_SAMPLE_OPENROUTER_API_KEY,
extract_openrouter_text,
get_openrouter_settings,
)
from app.services.pdf_ai_service import _call_openrouter
from app.services.site_assistant_service import _request_ai_reply
@@ -85,7 +89,7 @@ class TestOpenRouterConfigService:
monkeypatch.setattr(
'app.services.openrouter_config_service._load_dotenv_settings',
lambda: {
'OPENROUTER_API_KEY': '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',
'OPENROUTER_API_KEY': LEGACY_SAMPLE_OPENROUTER_API_KEY,
},
)
@@ -95,6 +99,27 @@ class TestOpenRouterConfigService:
assert settings.api_key == ''
def test_extract_openrouter_text_supports_string_and_list_content(self):
assert extract_openrouter_text({
'choices': [{'message': {'content': ' plain text reply '}}],
}) == 'plain text reply'
assert extract_openrouter_text({
'choices': [{
'message': {
'content': [
{'type': 'text', 'text': 'First part'},
{'type': 'text', 'content': 'Second part'},
None,
],
},
}],
}) == 'First part\nSecond part'
assert extract_openrouter_text({
'choices': [{'message': {'content': None}}],
}) == ''
class TestAiServicesUseSharedConfig:
def test_pdf_ai_uses_flask_config(self, app, monkeypatch):
@@ -166,4 +191,4 @@ class TestAiServicesUseSharedConfig:
assert captured['headers']['Authorization'] == 'Bearer assistant-key'
assert captured['json']['model'] == 'assistant-model'
assert captured['json']['messages'][-1] == {'role': 'user', 'content': 'How do I merge files?'}
assert captured['usage']['model'] == 'assistant-model'
assert captured['usage']['model'] == 'assistant-model'

View File

@@ -0,0 +1,24 @@
"""Service-level tests for PDF AI helpers."""
import pytest
from app.services.pdf_ai_service import PdfAiError, _extract_text_from_pdf
def test_extract_text_from_pdf_rejects_password_protected_documents(monkeypatch):
"""Password-protected PDFs should surface a specific actionable error."""
class FakeReader:
def __init__(self, input_path):
self.is_encrypted = True
self.pages = []
def decrypt(self, password):
return 0
monkeypatch.setattr("PyPDF2.PdfReader", FakeReader)
with pytest.raises(PdfAiError) as exc:
_extract_text_from_pdf("/tmp/protected.pdf")
assert exc.value.error_code == "PDF_ENCRYPTED"
assert "unlock" in exc.value.user_message.lower()

View File

@@ -1,5 +1,6 @@
"""Tests for storage service — local mode (S3 not configured in tests)."""
import os
from unittest.mock import Mock
from app.services.storage_service import StorageService
@@ -53,4 +54,47 @@ class TestStorageServiceLocal:
with open(os.path.join(output_dir, 'test.pdf'), 'w') as f:
f.write('test')
assert svc.file_exists(f'outputs/{task_id}/test.pdf') is True
assert svc.file_exists(f'outputs/{task_id}/test.pdf') is True
def test_placeholder_s3_credentials_disable_s3(self, app):
"""Copied sample AWS credentials should not activate S3 mode."""
with app.app_context():
app.config.update({
'AWS_ACCESS_KEY_ID': 'your-access-key',
'AWS_SECRET_ACCESS_KEY': 'your-secret-key',
'AWS_S3_BUCKET': 'dociva-temp-files',
})
svc = StorageService()
assert svc.use_s3 is False
def test_upload_falls_back_to_local_when_s3_upload_fails(self, app, monkeypatch):
"""A broken S3 upload should still preserve a working local download."""
with app.app_context():
app.config.update({
'AWS_ACCESS_KEY_ID': 'real-looking-key',
'AWS_SECRET_ACCESS_KEY': 'real-looking-secret',
'AWS_S3_BUCKET': 'dociva-temp-files',
'STORAGE_ALLOW_LOCAL_FALLBACK': True,
})
svc = StorageService()
task_id = 's3-fallback-test'
input_path = '/tmp/test_storage_fallback.pdf'
with open(input_path, 'wb') as f:
f.write(b'%PDF-1.4 fallback')
class DummyClientError(Exception):
pass
failing_client = Mock()
failing_client.upload_file.side_effect = DummyClientError('boom')
monkeypatch.setattr('botocore.exceptions.ClientError', DummyClientError)
monkeypatch.setattr(StorageService, 'client', property(lambda self: failing_client))
key = svc.upload_file(input_path, task_id)
url = svc.generate_presigned_url(key, original_filename='fallback.pdf')
assert key == f'outputs/{task_id}/test_storage_fallback.pdf'
assert svc.file_exists(key) is True
assert '/api/download/s3-fallback-test/test_storage_fallback.pdf' in url
os.unlink(input_path)

View File

@@ -32,10 +32,34 @@ class TestStripeRoutes:
})
assert response.status_code == 503
def test_checkout_placeholder_config_returns_503(self, client, app):
"""Copied sample Stripe values should be treated as not configured."""
self._login(client, email="stripe-placeholder@test.com")
app.config.update({
"STRIPE_SECRET_KEY": "sk_test_XXXXXXXXXXXXXXXXXXXXXXXX",
"STRIPE_PRICE_ID_PRO_MONTHLY": "price_XXXXXXXXXXXXXXXX",
"STRIPE_PRICE_ID_PRO_YEARLY": "price_XXXXXXXXXXXXXXXX",
})
response = client.post("/api/stripe/create-checkout-session", json={
"billing": "monthly",
})
assert response.status_code == 503
def test_portal_requires_auth(self, client):
response = client.post("/api/stripe/create-portal-session")
assert response.status_code == 401
def test_portal_placeholder_config_returns_503(self, client, app):
"""Portal access should not attempt Stripe calls when config is only sample data."""
self._login(client, email="stripe-portal@test.com")
app.config.update({
"STRIPE_SECRET_KEY": "sk_test_XXXXXXXXXXXXXXXXXXXXXXXX",
"STRIPE_PRICE_ID_PRO_MONTHLY": "price_XXXXXXXXXXXXXXXX",
"STRIPE_PRICE_ID_PRO_YEARLY": "price_XXXXXXXXXXXXXXXX",
})
response = client.post("/api/stripe/create-portal-session")
assert response.status_code == 503
def test_webhook_missing_signature(self, client):
"""Webhook without config returns ignored status."""
response = client.post(