Merge branch '2-news-api' into 3-market-api

This commit is contained in:
2025-09-30 15:50:49 +02:00
21 changed files with 925 additions and 32 deletions

View File

@@ -16,8 +16,8 @@ def unified_checks(model: AppModels, input):
for item in content.portfolio:
assert item.asset not in (None, "", "null")
assert isinstance(item.asset, str)
assert item.percentage > 0
assert item.percentage <= 100
assert item.percentage >= 0.0
assert item.percentage <= 100.0
assert isinstance(item.percentage, (int, float))
assert item.motivation not in (None, "", "null")
assert isinstance(item.motivation, str)
@@ -41,6 +41,7 @@ class TestPredictor:
def test_gemini_model_output(self, inputs):
unified_checks(AppModels.GEMINI, inputs)
@pytest.mark.slow
def test_ollama_qwen_model_output(self, inputs):
unified_checks(AppModels.OLLAMA_QWEN, inputs)

View File

@@ -0,0 +1,38 @@
import os
import pytest
from app.news import CryptoPanicWrapper
@pytest.mark.limited
@pytest.mark.news
@pytest.mark.api
@pytest.mark.skipif(not os.getenv("CRYPTOPANIC_API_KEY"), reason="CRYPTOPANIC_API_KEY not set")
class TestCryptoPanicAPI:
def test_crypto_panic_api_initialization(self):
crypto = CryptoPanicWrapper()
assert crypto is not None
def test_crypto_panic_api_get_latest_news(self):
crypto = CryptoPanicWrapper()
articles = crypto.get_latest_news(query="", total=2)
assert isinstance(articles, list)
assert len(articles) == 2
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""
# Useless since both methods use the same endpoint
# def test_crypto_panic_api_get_top_headlines(self):
# crypto = CryptoPanicWrapper()
# articles = crypto.get_top_headlines(total=2)
# assert isinstance(articles, list)
# assert len(articles) == 2
# for article in articles:
# assert article.source is not None or article.source != ""
# assert article.time is not None or article.time != ""
# assert article.title is not None or article.title != ""
# assert article.description is not None or article.description != ""

View File

@@ -0,0 +1,34 @@
import pytest
from app.news import DuckDuckGoWrapper
@pytest.mark.news
@pytest.mark.api
class TestDuckDuckGoNews:
def test_duckduckgo_initialization(self):
news = DuckDuckGoWrapper()
assert news.tool is not None
def test_duckduckgo_get_latest_news(self):
news = DuckDuckGoWrapper()
articles = news.get_latest_news(query="crypto", total=2)
assert isinstance(articles, list)
assert len(articles) == 2
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""
def test_duckduckgo_get_top_headlines(self):
news = DuckDuckGoWrapper()
articles = news.get_top_headlines(total=2)
assert isinstance(articles, list)
assert len(articles) == 2
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""

View File

@@ -0,0 +1,34 @@
import pytest
from app.news import GoogleNewsWrapper
@pytest.mark.news
@pytest.mark.api
class TestGoogleNews:
def test_gnews_api_initialization(self):
gnews_api = GoogleNewsWrapper()
assert gnews_api is not None
def test_gnews_api_get_latest_news(self):
gnews_api = GoogleNewsWrapper()
articles = gnews_api.get_latest_news(query="crypto", total=2)
assert isinstance(articles, list)
assert len(articles) == 2
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""
def test_gnews_api_get_top_headlines(self):
news_api = GoogleNewsWrapper()
articles = news_api.get_top_headlines(total=2)
assert isinstance(articles, list)
assert len(articles) == 2
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""

View File

@@ -0,0 +1,37 @@
import os
import pytest
from app.news import NewsApiWrapper
@pytest.mark.news
@pytest.mark.api
@pytest.mark.skipif(not os.getenv("NEWS_API_KEY"), reason="NEWS_API_KEY not set")
class TestNewsAPI:
def test_news_api_initialization(self):
news_api = NewsApiWrapper()
assert news_api.client is not None
def test_news_api_get_latest_news(self):
news_api = NewsApiWrapper()
articles = news_api.get_latest_news(query="crypto", total=2)
assert isinstance(articles, list)
assert len(articles) > 0 # Ensure we got some articles (apparently it doesn't always return the requested number)
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""
def test_news_api_get_top_headlines(self):
news_api = NewsApiWrapper()
articles = news_api.get_top_headlines(total=2)
assert isinstance(articles, list)
# assert len(articles) > 0 # apparently it doesn't always return SOME articles
for article in articles:
assert article.source is not None or article.source != ""
assert article.time is not None or article.time != ""
assert article.title is not None or article.title != ""
assert article.description is not None or article.description != ""

24
tests/api/test_reddit.py Normal file
View File

@@ -0,0 +1,24 @@
import pytest
from praw import Reddit
from app.social.reddit import MAX_COMMENTS, RedditWrapper
@pytest.mark.social
@pytest.mark.api
class TestRedditWrapper:
def test_initialization(self):
wrapper = RedditWrapper()
assert wrapper.client_id is not None
assert wrapper.client_secret is not None
assert isinstance(wrapper.tool, Reddit)
def test_get_top_crypto_posts(self):
wrapper = RedditWrapper()
posts = wrapper.get_top_crypto_posts(limit=2)
assert isinstance(posts, list)
assert len(posts) == 2
for post in posts:
assert post.title != ""
assert isinstance(post.comments, list)
assert len(post.comments) <= MAX_COMMENTS
for comment in post.comments:
assert comment.description != ""

View File

@@ -2,16 +2,10 @@
Configurazione pytest per i test del progetto upo-appAI.
"""
import sys
import pytest
from pathlib import Path
# Aggiungi il path src al PYTHONPATH per tutti i test
src_path = Path(__file__).parent.parent / "src"
sys.path.insert(0, str(src_path))
from dotenv import load_dotenv
# Carica le variabili d'ambiente per tutti i test
from dotenv import load_dotenv
load_dotenv()
@@ -26,6 +20,10 @@ def pytest_configure(config:pytest.Config):
("gemini", "marks tests that use Gemini model"),
("ollama_gpt", "marks tests that use Ollama GPT model"),
("ollama_qwen", "marks tests that use Ollama Qwen model"),
("news", "marks tests that use news"),
("social", "marks tests that use social media"),
("limited", "marks tests that have limited execution due to API constraints"),
("wrapper", "marks tests for wrapper handler"),
]
for marker in markers:
line = f"{marker[0]}: {marker[1]}"
@@ -35,7 +33,6 @@ def pytest_collection_modifyitems(config, items):
"""Modifica automaticamente gli item di test aggiungendogli marker basati sul nome"""
markers_to_add = {
"api": pytest.mark.api,
"coinbase": pytest.mark.api,
"cryptocompare": pytest.mark.api,
"overview": pytest.mark.slow,
@@ -50,3 +47,13 @@ def pytest_collection_modifyitems(config, items):
for key, marker in markers_to_add.items():
if key in name:
item.add_marker(marker)
# Rimuovo i test "limited" e "slow" se non richiesti esplicitamente
mark_to_remove = ['limited', 'slow']
for mark in mark_to_remove:
markexpr = getattr(config.option, "markexpr", None)
if markexpr and mark in markexpr.lower():
continue
new_mark = (f"({markexpr}) and " if markexpr else "") + f"not {mark}"
setattr(config.option, "markexpr", new_mark)

View File

@@ -0,0 +1,90 @@
import pytest
from app.utils.wrapper_handler import WrapperHandler
class MockWrapper:
def do_something(self) -> str:
return "Success"
class MockWrapper2(MockWrapper):
def do_something(self) -> str:
return "Success 2"
class FailingWrapper(MockWrapper):
def do_something(self):
raise Exception("Intentional Failure")
@pytest.mark.wrapper
class TestWrapperHandler:
def test_all_wrappers_fail(self):
wrappers = [FailingWrapper, FailingWrapper]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
with pytest.raises(Exception) as exc_info:
handler.try_call(lambda w: w.do_something())
assert "All wrappers failed after retries" in str(exc_info.value)
def test_success_on_first_try(self):
wrappers = [MockWrapper, FailingWrapper]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
result = handler.try_call(lambda w: w.do_something())
assert result == "Success"
assert handler.index == 0 # Should still be on the first wrapper
assert handler.retry_count == 0
def test_eventual_success(self):
wrappers = [FailingWrapper, MockWrapper]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=2, retry_delay=0)
result = handler.try_call(lambda w: w.do_something())
assert result == "Success"
assert handler.index == 1 # Should have switched to the second wrapper
assert handler.retry_count == 0
def test_partial_failures(self):
wrappers = [FailingWrapper, MockWrapper, FailingWrapper]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
result = handler.try_call(lambda w: w.do_something())
assert result == "Success"
assert handler.index == 1 # Should have switched to the second wrapper
assert handler.retry_count == 0
# Next call should still succeed on the second wrapper
result = handler.try_call(lambda w: w.do_something())
assert result == "Success"
assert handler.index == 1 # Should still be on the second wrapper
assert handler.retry_count == 0
handler.index = 2 # Manually switch to the third wrapper
result = handler.try_call(lambda w: w.do_something())
assert result == "Success"
assert handler.index == 1 # Should return to the second wrapper after failure
assert handler.retry_count == 0
def test_try_call_all_success(self):
wrappers = [MockWrapper, MockWrapper2]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
results = handler.try_call_all(lambda w: w.do_something())
assert results == {MockWrapper: "Success", MockWrapper2: "Success 2"}
def test_try_call_all_partial_failures(self):
# Only the second wrapper should succeed
wrappers = [FailingWrapper, MockWrapper, FailingWrapper]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
results = handler.try_call_all(lambda w: w.do_something())
assert results == {MockWrapper: "Success"}
# Only the second and fourth wrappers should succeed
wrappers = [FailingWrapper, MockWrapper, FailingWrapper, MockWrapper2]
handler: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers(wrappers, try_per_wrapper=1, retry_delay=0)
results = handler.try_call_all(lambda w: w.do_something())
assert results == {MockWrapper: "Success", MockWrapper2: "Success 2"}
def test_try_call_all_all_fail(self):
# Test when all wrappers fail
handler_all_fail: WrapperHandler[MockWrapper] = WrapperHandler.build_wrappers([FailingWrapper, FailingWrapper], try_per_wrapper=1, retry_delay=0)
with pytest.raises(Exception) as exc_info:
handler_all_fail.try_call_all(lambda w: w.do_something())
assert "All wrappers failed" in str(exc_info.value)