Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from telegram.error import TelegramError
from telegram.ext import CallbackContext, ContextTypes

from src.downloader import get_media, get_media_size
from src.i18n import get_string
from src.core.downloader import get_media, get_media_size
from src.core.i18n import get_string


def get_main_menu(user: User) -> InlineKeyboardMarkup:
Expand Down
Empty file added src/core/__init__.py
Empty file.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 4 additions & 4 deletions src/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@
from telegram.ext import ContextTypes

import src.messages as message
from src.beauty import handle_beauty
from src.buttons import get_main_menu
from src.i18n import get_string, set_user_language
from src.core.beauty import handle_beauty
from src.core.i18n import get_string, set_user_language


async def start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
user = update.effective_user
if update.message and user:
set_user_language(user.id, "en")
await update.message.reply_text(
f"{get_string(user,'hello')} {user.first_name} {get_string(user,'compare')}"
f"{get_string(user, 'hello')} {user.first_name} {get_string(user, 'compare')}"
)


Expand Down Expand Up @@ -53,7 +53,7 @@ async def service(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:

services = message.getServiceString(user)
await update.message.reply_text(
f"{get_string(user,'hello')} {user.first_name}.\n\n{services}",
f"{get_string(user, 'hello')} {user.first_name}.\n\n{services}",
parse_mode="HTML",
)

Expand Down
2 changes: 1 addition & 1 deletion src/messages.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from telegram import User

from src.i18n import get_string
from src.core.i18n import get_string


def getAboutString(user: User) -> str:
Expand Down
Empty file added tests/core/__init__.py
Empty file.
231 changes: 231 additions & 0 deletions tests/core/test_beauty.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import asyncio
import os
from datetime import date, datetime, timedelta
from unittest.mock import AsyncMock, MagicMock

import requests

from src.core import beauty


def test_is_beauty_image_fresh_returns_false_when_file_does_not_exist(tmp_path):
path = tmp_path / "missing.png"

assert beauty.is_beauty_image_fresh(path, today=date(2026, 3, 28)) is False


def test_is_beauty_image_fresh_returns_true_for_file_from_same_day(tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image")

today = date.today()
_set_file_mtime(path, datetime.combine(today, datetime.min.time()).timestamp())

assert beauty.is_beauty_image_fresh(path, today=today) is True


def test_is_beauty_image_fresh_returns_false_for_file_from_previous_day(tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image")

today = date.today()
yesterday = today - timedelta(days=1)
_set_file_mtime(path, datetime.combine(yesterday, datetime.min.time()).timestamp())

assert beauty.is_beauty_image_fresh(path, today=today) is False


def test_get_beauty_image_path_returns_cached_file_without_downloading(
tmp_path, monkeypatch
):
path = tmp_path / "beauty.png"
path.write_bytes(b"cached")
monkeypatch.setattr(
beauty, "is_beauty_image_fresh", lambda candidate: candidate == path
)

downloader = MagicMock()
to_thread = AsyncMock()
monkeypatch.setattr(beauty.asyncio, "to_thread", to_thread)

result = asyncio.run(beauty.get_beauty_image_path(str(path), downloader))

assert result == path
downloader.assert_not_called()
to_thread.assert_not_awaited()


def test_get_beauty_image_path_downloads_stale_file_in_thread(tmp_path, monkeypatch):
path = tmp_path / "beauty.png"
monkeypatch.setattr(beauty, "is_beauty_image_fresh", lambda candidate: False)

downloader = MagicMock(return_value=str(path))
to_thread = AsyncMock(return_value=str(path))
monkeypatch.setattr(beauty.asyncio, "to_thread", to_thread)

result = asyncio.run(beauty.get_beauty_image_path(str(path), downloader))

assert result == path
to_thread.assert_awaited_once_with(downloader, beauty.DEFAULT_QUERY, str(path))


def test_handle_beauty_replies_with_photo(monkeypatch, tmp_path):
path = tmp_path / "beauty.png"
path.write_bytes(b"image-content")

message = MagicMock()
message.reply_photo = AsyncMock()
update = MagicMock(message=message)

get_path = AsyncMock(return_value=path)
monkeypatch.setattr(beauty, "get_beauty_image_path", get_path)

asyncio.run(beauty.handle_beauty(update))

message.reply_photo.assert_awaited_once()
sent_photo = message.reply_photo.await_args.kwargs["photo"]
assert sent_photo.name == str(path)


def test_handle_beauty_returns_when_download_fails(monkeypatch):
message = MagicMock()
message.reply_photo = AsyncMock()
update = MagicMock(message=message)

monkeypatch.setattr(beauty, "get_beauty_image_path", AsyncMock(return_value=None))

asyncio.run(beauty.handle_beauty(update))

message.reply_photo.assert_not_awaited()


def test_handle_beauty_returns_when_update_has_no_message(monkeypatch):
update = MagicMock(message=None)

get_path = AsyncMock()
monkeypatch.setattr(beauty, "get_beauty_image_path", get_path)

asyncio.run(beauty.handle_beauty(update))

get_path.assert_not_awaited()


def test_get_beauty_image_path_returns_none_when_downloader_fails(
monkeypatch, tmp_path
):
path = tmp_path / "beauty.png"
monkeypatch.setattr(beauty, "is_beauty_image_fresh", lambda candidate: False)

downloader = MagicMock(return_value=None)
to_thread = AsyncMock(return_value=None)
monkeypatch.setattr(beauty.asyncio, "to_thread", to_thread)

result = asyncio.run(beauty.get_beauty_image_path(str(path), downloader))

assert result is None
to_thread.assert_awaited_once_with(downloader, beauty.DEFAULT_QUERY, str(path))


def test_download_beauty_image_returns_none_without_api_key(monkeypatch):
monkeypatch.setattr(beauty, "load_dotenv", MagicMock())
monkeypatch.setattr(beauty.os, "getenv", lambda key: None)

result = beauty.download_beauty_image()

assert result is None


def test_download_beauty_image_returns_none_when_no_results(monkeypatch):
monkeypatch.setattr(beauty, "load_dotenv", MagicMock())
monkeypatch.setattr(beauty.os, "getenv", lambda key: "token")
monkeypatch.setattr(beauty, "cerca_immagini_pixabay", lambda query, api_key: [])

result = beauty.download_beauty_image()

assert result is None


def test_download_beauty_image_returns_output_path_on_success(monkeypatch, tmp_path):
output_path = tmp_path / "beauty.png"

monkeypatch.setattr(beauty, "load_dotenv", MagicMock())
monkeypatch.setattr(beauty.os, "getenv", lambda key: "token")
monkeypatch.setattr(
beauty,
"cerca_immagini_pixabay",
lambda query, api_key: ["https://img/1.png", "https://img/2.png"],
)
monkeypatch.setattr(beauty.random, "choice", lambda items: items[1])
monkeypatch.setattr(beauty, "scarica_risorsa", lambda url, path: True)

result = beauty.download_beauty_image(output_path=str(output_path))

assert result == str(output_path)


def test_download_beauty_image_returns_none_when_resource_download_fails(monkeypatch):
monkeypatch.setattr(beauty, "load_dotenv", MagicMock())
monkeypatch.setattr(beauty.os, "getenv", lambda key: "token")
monkeypatch.setattr(
beauty, "cerca_immagini_pixabay", lambda query, api_key: ["https://img/1.png"]
)
monkeypatch.setattr(beauty.random, "choice", lambda items: items[0])
monkeypatch.setattr(beauty, "scarica_risorsa", lambda url, path: False)

result = beauty.download_beauty_image()

assert result is None


def test_cerca_immagini_pixabay_returns_urls_on_success(monkeypatch):
response = MagicMock()
response.status_code = 200
response.json.return_value = {"hits": [{"webformatURL": "https://img/1.png"}]}
monkeypatch.setattr(beauty.requests, "get", lambda *args, **kwargs: response)

result = beauty.cerca_immagini_pixabay("flowers", "token")

assert result == ["https://img/1.png"]


def test_cerca_immagini_pixabay_returns_empty_list_on_error(monkeypatch):
response = MagicMock()
response.status_code = 500
monkeypatch.setattr(beauty.requests, "get", lambda *args, **kwargs: response)

result = beauty.cerca_immagini_pixabay("flowers", "token")

assert result == []


def test_scarica_risorsa_writes_file_contents(monkeypatch, tmp_path):
output_path = tmp_path / "downloads" / "beauty.png"

response = MagicMock()
response.__enter__.return_value = response
response.iter_content.return_value = [b"chunk-1", b"", b"chunk-2"]
response.raise_for_status.return_value = None
monkeypatch.setattr(beauty.requests, "get", lambda *args, **kwargs: response)

result = beauty.scarica_risorsa("https://img/1.png", str(output_path))

assert result is True
assert output_path.read_bytes() == b"chunk-1chunk-2"


def test_scarica_risorsa_returns_false_on_request_exception(monkeypatch, tmp_path):
output_path = tmp_path / "downloads" / "beauty.png"

monkeypatch.setattr(
beauty.requests,
"get",
MagicMock(side_effect=requests.RequestException("network error")),
)

result = beauty.scarica_risorsa("https://img/1.png", str(output_path))

assert result is False


def _set_file_mtime(path, timestamp: float) -> None:
os.utime(path, (timestamp, timestamp))
16 changes: 8 additions & 8 deletions tests/test_downloader.py → tests/core/test_downloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import yt_dlp

from src.downloader import DOWNLOAD_DIR, build_ydl_opts, get_media, get_media_size
from src.core.downloader import DOWNLOAD_DIR, build_ydl_opts, get_media, get_media_size


# N.B Utilizzo il prefisso di un uid 05adfd95 ... perchè ogni download avrà come prefisso un identificativo univoco.
Expand Down Expand Up @@ -60,7 +60,7 @@ def test_outtmpl_contains_template_titolo():
)


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_getMedia_get_filepath(mock_ydl_class):
mock_ydl = MagicMock()
mock_ydl.extract_info.return_value = {"title": "video", "ext": "mp4"}
Expand All @@ -71,7 +71,7 @@ def test_getMedia_get_filepath(mock_ydl_class):
assert result == "/downloads/video.mp4"


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_getMedia_extract_info(mock_ydl_class):
mock_ydl = MagicMock()
mock_ydl.extract_info.return_value = {"title": "video", "ext": "mp4"}
Expand All @@ -84,7 +84,7 @@ def test_getMedia_extract_info(mock_ydl_class):
)


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_getMedia_get_error_on_exception(mock_ydl_class):
mock_ydl_class.return_value.__enter__.return_value.extract_info.side_effect = (
yt_dlp.utils.DownloadError("Network error")
Expand All @@ -94,7 +94,7 @@ def test_getMedia_get_error_on_exception(mock_ydl_class):
assert result == "error"


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_get_media_size_returns_zero_on_error(mock_ydl_class):
mock_ydl_class.return_value.__enter__.return_value.extract_info.side_effect = (
yt_dlp.utils.DownloadError("errore")
Expand All @@ -104,7 +104,7 @@ def test_get_media_size_returns_zero_on_error(mock_ydl_class):
assert result == 0


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_get_media_size_returns_correct_size(mock_ydl_class):

fileSize = 1024 * 1024 * 10
Expand All @@ -118,7 +118,7 @@ def test_get_media_size_returns_correct_size(mock_ydl_class):
assert result == fileSize


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_get_media_size_returns_zero_when_filesize_not_int(mock_ydl_class):
mock_ydl_class.return_value.__enter__.return_value.extract_info.return_value = {
"filesize": None,
Expand All @@ -129,7 +129,7 @@ def test_get_media_size_returns_zero_when_filesize_not_int(mock_ydl_class):
assert result == 0


@patch("src.downloader.yt_dlp.YoutubeDL")
@patch("src.core.downloader.yt_dlp.YoutubeDL")
def test_get_media_size_returns_zero_when_info_is_none(mock_ydl_class):
mock_ydl_class.return_value.__enter__.return_value.extract_info.return_value = None

Expand Down
Loading