- Add fetcher/tests/ with 21 tests covering all bug fixes:
- test_utils: get_with_protocol URL normalization (5 tests)
- test_config: scheduled_tasks.json PK uniqueness, Celery queue
settings (7 tests)
- test_publish: URL routing and view for /task/publish_<id>/ (4 tests)
- test_template_field: url_detail.html renders images_url field (2 tests)
- test_search_postprocess: URL host filtering and param stripping (3 tests)
- Replace empty fetcher/tests.py with tests/ package
- Add .gitea/workflows/test.yml: runs on PRs and pushes to main,
installs dependencies and runs `manage.py test fetcher.tests`
This commit is contained in:
1 parent
3a33697ca3
commit
d18eda2fc4
8 files changed
+298
-3
No files matched your search
@@ -0,0 +1,57 @@
|
|||||||
|
name: Tests
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: matitos
|
||||||
|
POSTGRES_USER: supermatitos
|
||||||
|
POSTGRES_PASSWORD: supermatitos
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
env:
|
||||||
|
DJANGO_SECRET_KEY: ci-test-secret
|
||||||
|
DJANGO_ALLOWED_HOSTS: '*'
|
||||||
|
DJANGO_ALLOWED_ORIGINS: 'http://localhost'
|
||||||
|
DB_NAME: matitos
|
||||||
|
DB_USER: supermatitos
|
||||||
|
DB_PASSWORD: supermatitos
|
||||||
|
DB_HOST: localhost
|
||||||
|
DB_PORT: '5432'
|
||||||
|
REDIS_CACHE_HOST: localhost
|
||||||
|
REDIS_CACHE_PORT: '6379'
|
||||||
|
REDIS_CELERY_HOST: localhost
|
||||||
|
REDIS_CELERY_PORT: '6379'
|
||||||
|
PATH_LOGS_DIRECTORY: /tmp/logs
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: app_urls/requirements.txt
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -r app_urls/requirements.txt
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
working-directory: app_urls
|
||||||
|
run: python manage.py test fetcher.tests -v 2
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
||||||
Whitespace-only changes.
@@ -0,0 +1,68 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
SCHEDULED_TASKS_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||||
|
"scheduled_tasks.json",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTasksFixtureTests(SimpleTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
|
with open(SCHEDULED_TASKS_PATH) as f:
|
||||||
|
cls.data = json.load(f)
|
||||||
|
cls.periodic_tasks = [
|
||||||
|
e
|
||||||
|
for e in cls.data
|
||||||
|
if e.get("model") == "django_celery_beat.periodictask"
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_json_is_valid_and_loads(self):
|
||||||
|
self.assertIsInstance(self.data, list)
|
||||||
|
self.assertTrue(len(self.data) > 0)
|
||||||
|
|
||||||
|
def test_periodic_task_primary_keys_are_unique(self):
|
||||||
|
pks = [e["pk"] for e in self.periodic_tasks]
|
||||||
|
self.assertEqual(len(pks), len(set(pks)))
|
||||||
|
|
||||||
|
def test_notify_status_has_unique_pk(self):
|
||||||
|
notify = [
|
||||||
|
e
|
||||||
|
for e in self.periodic_tasks
|
||||||
|
if e["fields"].get("task") == "fetcher.tasks.notify_status"
|
||||||
|
]
|
||||||
|
self.assertEqual(len(notify), 1)
|
||||||
|
other_pks = {
|
||||||
|
e["pk"]
|
||||||
|
for e in self.periodic_tasks
|
||||||
|
if e["fields"].get("task") != "fetcher.tasks.notify_status"
|
||||||
|
}
|
||||||
|
self.assertNotIn(notify[0]["pk"], other_pks)
|
||||||
|
|
||||||
|
def test_interval_schedule_primary_keys_are_unique(self):
|
||||||
|
schedules = [e for e in self.data if e["model"] == "django_celery_beat.intervalschedule"]
|
||||||
|
pks = [e["pk"] for e in schedules]
|
||||||
|
self.assertEqual(len(pks), len(set(pks)))
|
||||||
|
|
||||||
|
def test_expected_critical_tasks_present(self):
|
||||||
|
tasks = {e["fields"].get("task") for e in self.periodic_tasks}
|
||||||
|
self.assertIn("fetcher.tasks.process_raw_urls", tasks)
|
||||||
|
self.assertIn("fetcher.tasks.fetch_feeds", tasks)
|
||||||
|
self.assertIn("fetcher.tasks.notify_status", tasks)
|
||||||
|
|
||||||
|
|
||||||
|
class CeleryQueueSettingsTests(SimpleTestCase):
|
||||||
|
def test_contains_worker_queues(self):
|
||||||
|
queues = [q.name for q in settings.CELERY_TASK_QUEUES]
|
||||||
|
self.assertIn("default", queues)
|
||||||
|
self.assertIn("light", queues)
|
||||||
|
self.assertIn("heavy", queues)
|
||||||
|
|
||||||
|
def test_does_not_contain_deprecated_low_queue(self):
|
||||||
|
queues = [q.name for q in settings.CELERY_TASK_QUEUES]
|
||||||
|
self.assertNotIn("low", queues)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from django.test import RequestFactory, SimpleTestCase
|
||||||
|
from django.urls import resolve, reverse
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
from .. import views
|
||||||
|
|
||||||
|
|
||||||
|
class PublishRouteTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.factory = RequestFactory()
|
||||||
|
|
||||||
|
def test_publish_url_resolves_to_publish_view(self):
|
||||||
|
match = resolve("/task/publish_42/")
|
||||||
|
self.assertEqual(match.func, views.publish)
|
||||||
|
self.assertEqual(match.kwargs, {"id": 42})
|
||||||
|
|
||||||
|
def test_publish_url_reverse(self):
|
||||||
|
url = reverse("publish", args=[7])
|
||||||
|
self.assertEqual(url, "/task/publish_7/")
|
||||||
|
|
||||||
|
@patch("fetcher.views.Publisher")
|
||||||
|
def test_publish_invokes_publisher(self, mock_publisher_cls):
|
||||||
|
mock_instance = mock_publisher_cls.return_value
|
||||||
|
mock_instance.publish.return_value = None
|
||||||
|
|
||||||
|
request = self.factory.get("/task/publish_9/")
|
||||||
|
response = views.publish(request, 9)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
mock_publisher_cls.assert_called_once_with()
|
||||||
|
mock_instance.publish.assert_called_once_with(9)
|
||||||
|
self.assertIn(b"9", response.content)
|
||||||
|
|
||||||
|
@patch("fetcher.views.Publisher")
|
||||||
|
def test_publish_reports_error_gracefully(self, mock_publisher_cls):
|
||||||
|
mock_instance = mock_publisher_cls.return_value
|
||||||
|
mock_instance.publish.side_effect = RuntimeError("boom")
|
||||||
|
|
||||||
|
request = self.factory.get("/task/publish_3/")
|
||||||
|
response = views.publish(request, 3)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertIn(b"Error publishing", response.content)
|
||||||
|
mock_instance.publish.assert_called_once_with(3)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
|
from ..models import Search
|
||||||
|
from ..src.fetch_search_instances import FetcherAbstract
|
||||||
|
|
||||||
|
|
||||||
|
class _ConcreteFetcher(FetcherAbstract):
|
||||||
|
def _fetch_raw_urls(self, keyword_search):
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def _get_name(self):
|
||||||
|
return "test-fetcher"
|
||||||
|
|
||||||
|
|
||||||
|
class PostProcessUrlsTests(SimpleTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.fetcher = _ConcreteFetcher()
|
||||||
|
|
||||||
|
def test_url_host_filters_to_host(self):
|
||||||
|
obj_search = Search(search="cnbc.com", type=Search.TYPE_ENUM.URL_HOST)
|
||||||
|
raw = [
|
||||||
|
"https://www.cnbc.com/article/1",
|
||||||
|
"https://www.foxnews.com/article/2",
|
||||||
|
"cnbc.com/article/3",
|
||||||
|
]
|
||||||
|
result = self.fetcher._post_process_urls(raw, obj_search)
|
||||||
|
self.assertEqual(
|
||||||
|
result,
|
||||||
|
["https://www.cnbc.com/article/1", "cnbc.com/article/3"],
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_keyword_search_keeps_all_urls(self):
|
||||||
|
obj_search = Search(search="child abuse", type=Search.TYPE_ENUM.KEYWORD_SEARCH)
|
||||||
|
raw = [
|
||||||
|
"https://www.cnbc.com/article/1",
|
||||||
|
"https://www.foxnews.com/article/2",
|
||||||
|
]
|
||||||
|
result = self.fetcher._post_process_urls(raw, obj_search)
|
||||||
|
self.assertEqual(result, raw)
|
||||||
|
|
||||||
|
def test_query_parameters_are_removed(self):
|
||||||
|
obj_search = Search(search="cnbc.com", type=Search.TYPE_ENUM.URL_HOST)
|
||||||
|
raw = ["https://www.cnbc.com/article/1?param=1234&h=yes"]
|
||||||
|
result = self.fetcher._post_process_urls(raw, obj_search)
|
||||||
|
self.assertEqual(result, ["https://www.cnbc.com/article/1"])
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from django.test import SimpleTestCase
|
||||||
|
from django.template.loader import get_template
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
|
||||||
|
def _build_url_item():
|
||||||
|
return SimpleNamespace(
|
||||||
|
id=1,
|
||||||
|
url="https://example.com/article",
|
||||||
|
ts_fetch=datetime(2025, 7, 1, 12, 0, 0),
|
||||||
|
status="valid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_url_content(images_url=None):
|
||||||
|
return SimpleNamespace(
|
||||||
|
date_published=datetime(2025, 7, 1, 11, 0, 0),
|
||||||
|
title="Example title",
|
||||||
|
description="Example description",
|
||||||
|
content="Example body",
|
||||||
|
valid_content=True,
|
||||||
|
language="en",
|
||||||
|
tags=["a", "b"],
|
||||||
|
authors=["Author"],
|
||||||
|
keywords=["kw"],
|
||||||
|
image_main_url="https://example.com/img.jpg",
|
||||||
|
images_url=images_url,
|
||||||
|
videos_url=["https://example.com/v.mp4"],
|
||||||
|
url_host="https://example.com",
|
||||||
|
site_name="Example",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class UrlDetailTemplateTests(SimpleTestCase):
|
||||||
|
def _render(self, url_content):
|
||||||
|
template = get_template("url_detail.html")
|
||||||
|
context = {
|
||||||
|
"url_item": _build_url_item(),
|
||||||
|
"url_content": url_content,
|
||||||
|
"sources": ["feeds"],
|
||||||
|
"searches": ["child abuse"],
|
||||||
|
"models": ["llama3.2:3b"],
|
||||||
|
"prompt": "Summarize",
|
||||||
|
"url_canonical": None,
|
||||||
|
}
|
||||||
|
return template.render(context)
|
||||||
|
|
||||||
|
def test_image_urls_field_renders(self):
|
||||||
|
html = self._render(_build_url_content(images_url=["https://example.com/1.jpg"]))
|
||||||
|
self.assertIn("https://example.com/1.jpg", html)
|
||||||
|
|
||||||
|
def test_legacy_image_urls_attribute_is_not_referenced(self):
|
||||||
|
# Model exposes `images_url`, not `image_urls`. Rendering the template
|
||||||
|
# must not fail nor silently rely on a non-existent field.
|
||||||
|
with open(
|
||||||
|
"fetcher/templates/url_detail.html"
|
||||||
|
) as f:
|
||||||
|
source = f.read()
|
||||||
|
self.assertNotIn("url_content.image_urls", source)
|
||||||
|
self.assertIn("url_content.images_url", source)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from django.test import SimpleTestCase
|
||||||
|
from ..src.utils import get_with_protocol
|
||||||
|
|
||||||
|
|
||||||
|
class GetWithProtocolTests(SimpleTestCase):
|
||||||
|
def test_missing_protocol_prepends_https(self):
|
||||||
|
self.assertEqual(get_with_protocol("example.com"), "https://example.com")
|
||||||
|
|
||||||
|
def test_missing_protocol_with_path(self):
|
||||||
|
self.assertEqual(
|
||||||
|
get_with_protocol("example.com/article"),
|
||||||
|
"https://example.com/article",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_upgrades_http_to_https(self):
|
||||||
|
self.assertEqual(get_with_protocol("http://example.com"), "https://example.com")
|
||||||
|
|
||||||
|
def test_preserves_https(self):
|
||||||
|
self.assertEqual(get_with_protocol("https://example.com"), "https://example.com")
|
||||||
|
|
||||||
|
def test_empty_string_becomes_https(self):
|
||||||
|
self.assertEqual(get_with_protocol(""), "https://")
|
||||||
Reference in new issue
Block a user