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:
19
backend/tests/test_email_service.py
Normal file
19
backend/tests/test_email_service.py
Normal 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
|
||||
@@ -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'
|
||||
|
||||
24
backend/tests/test_pdf_ai_service.py
Normal file
24
backend/tests/test_pdf_ai_service.py
Normal 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()
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user