chore: reorganize test files into domain-specific subdirectories under tests/
Split the monolithic test directory into three tiers (unit, api, e2e) with a path-mirroring directory structure. Added corresponding Makefile targets (test-unit, test-api, test-e2e) and updated all documentation references (CLAUDE.md, README.md, testing-cicd.html, testing-framework.html, testing-make.html) to reflect the new layout and naming conventions.
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import requests
|
||||
from tests.conftest import BASE_URL
|
||||
def _seed_news_seo():
|
||||
from datetime import datetime, timezone
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import generate_uid, make_combined_slug
|
||||
|
||||
uid = generate_uid()
|
||||
title = f"SEO Test News Article {uid.split('-')[-1]}"
|
||||
slug = make_combined_slug(title, uid)
|
||||
get_table("news").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"slug": slug,
|
||||
"title": title,
|
||||
"description": "A seeded news article for SEO tests.",
|
||||
"content": "Body content for the seeded article.",
|
||||
"url": "https://example.com/article",
|
||||
"source_name": "ExampleSource",
|
||||
"status": "published",
|
||||
"synced_at": datetime.now(timezone.utc).isoformat(),
|
||||
"show_on_landing": 0,
|
||||
"grade": 8,
|
||||
}
|
||||
)
|
||||
return slug, uid
|
||||
def _seed_owner():
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from devplacepy.database import get_table
|
||||
|
||||
uid = str(uuid4())
|
||||
get_table("users").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"username": f"seo_{uid[:8]}",
|
||||
"email": f"{uid[:8]}@seo.test",
|
||||
"password_hash": "x",
|
||||
"role": "Member",
|
||||
"is_active": True,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return uid
|
||||
def _seed_post_seo(image=None):
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
|
||||
owner = _seed_owner()
|
||||
uid = str(uuid4())
|
||||
slug = make_combined_slug("SEO Detail Post", uid)
|
||||
get_table("posts").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"user_uid": owner,
|
||||
"slug": slug,
|
||||
"title": "SEO Detail Post",
|
||||
"content": "Body text for the SEO detail post.",
|
||||
"topic": "general",
|
||||
"project_uid": None,
|
||||
"image": image,
|
||||
"stars": 0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return slug, uid
|
||||
def _seed_gist():
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
|
||||
owner = _seed_owner()
|
||||
uid = str(uuid4())
|
||||
slug = make_combined_slug("SEO Detail Gist", uid)
|
||||
get_table("gists").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"user_uid": owner,
|
||||
"slug": slug,
|
||||
"title": "SEO Detail Gist",
|
||||
"description": "Gist description for SEO tests.",
|
||||
"source_code": "print('seo')",
|
||||
"language": "python",
|
||||
"stars": 0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return slug, uid
|
||||
def _seed_project():
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
|
||||
owner = _seed_owner()
|
||||
uid = str(uuid4())
|
||||
slug = make_combined_slug("SEO Detail Project", uid)
|
||||
get_table("projects").insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"user_uid": owner,
|
||||
"slug": slug,
|
||||
"title": "SEO Detail Project",
|
||||
"description": "Project description for SEO tests.",
|
||||
"project_type": "software",
|
||||
"platforms": "Linux",
|
||||
"status": "Released",
|
||||
"stars": 0,
|
||||
"created_at": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
)
|
||||
return slug, uid
|
||||
def _seed_news_image(news_uid):
|
||||
from devplacepy.database import get_table
|
||||
|
||||
get_table("news_images").insert(
|
||||
{
|
||||
"news_uid": news_uid,
|
||||
"url": "https://example.com/seo-news-image.jpg",
|
||||
}
|
||||
)
|
||||
def _seed_feed_posts(count):
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from uuid import uuid4
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import make_combined_slug
|
||||
|
||||
owner = _seed_owner()
|
||||
topic = f"seopag{owner[:8]}"
|
||||
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
posts = get_table("posts")
|
||||
for i in range(count):
|
||||
uid = str(uuid4())
|
||||
posts.insert(
|
||||
{
|
||||
"uid": uid,
|
||||
"user_uid": owner,
|
||||
"slug": make_combined_slug(f"seo pag {i}", uid),
|
||||
"title": None,
|
||||
"content": f"seo pag post {i}",
|
||||
"topic": topic,
|
||||
"project_uid": None,
|
||||
"image": None,
|
||||
"stars": 0,
|
||||
"created_at": (base - timedelta(seconds=i)).isoformat(),
|
||||
}
|
||||
)
|
||||
return topic
|
||||
|
||||
|
||||
def test_news_uid_redirects_to_canonical_slug(app_server):
|
||||
slug, uid = _seed_news_seo()
|
||||
r = requests.get(f"{BASE_URL}/news/{uid}", allow_redirects=False)
|
||||
assert r.status_code == 301
|
||||
assert r.headers["location"].endswith(f"/news/{slug}"), r.headers.get("location")
|
||||
@@ -0,0 +1,83 @@
|
||||
# retoor <retoor@molodetz.nl>
|
||||
|
||||
import httpx
|
||||
from devplacepy.services import news as news_mod
|
||||
from devplacepy.services import base as base_mod
|
||||
from devplacepy.services.news import (
|
||||
NewsService,
|
||||
_extract_grade,
|
||||
_get_ai_key,
|
||||
_get_article_images,
|
||||
)
|
||||
from devplacepy.database import get_table
|
||||
from devplacepy.utils import generate_uid
|
||||
from tests.conftest import run_async
|
||||
API_URL = "http://news.test/api"
|
||||
AI_URL = "http://ai.test/v1/chat"
|
||||
LINK_HIGH = "http://news.test/high"
|
||||
LINK_LOW = "http://news.test/low"
|
||||
class FakeResp_news_service:
|
||||
def __init__(self, json_data=None, text="", status=200):
|
||||
self._json = json_data
|
||||
self.text = text
|
||||
self.status_code = status
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400:
|
||||
raise httpx.HTTPError(f"status {self.status_code}")
|
||||
|
||||
def json(self):
|
||||
return self._json
|
||||
class FakeClient_news_service:
|
||||
def __init__(self, articles):
|
||||
self.articles = articles
|
||||
|
||||
async def __aenter__(self):
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *exc):
|
||||
return False
|
||||
|
||||
async def get(self, url, timeout=None):
|
||||
if url == API_URL:
|
||||
return FakeResp_news_service(json_data={"articles": self.articles})
|
||||
return FakeResp_news_service(text='<img src="http://img.test/a.png">')
|
||||
|
||||
async def post(self, url, json=None, headers=None, timeout=None):
|
||||
prompt = json["messages"][0]["content"]
|
||||
if "FailArticle" in prompt:
|
||||
return FakeResp_news_service(status=500, text="err")
|
||||
if "EmptyArticle" in prompt:
|
||||
return FakeResp_news_service(json_data={"choices": [{"message": {"content": ""}}]})
|
||||
if "BadArticle" in prompt:
|
||||
return FakeResp_news_service(
|
||||
json_data={"choices": [{"message": {"content": "no number"}}]}
|
||||
)
|
||||
grade = "9" if "HighArticle" in prompt else "3"
|
||||
return FakeResp_news_service(json_data={"choices": [{"message": {"content": grade}}]})
|
||||
class FailingApiClient(FakeClient_news_service):
|
||||
async def get(self, url, timeout=None):
|
||||
if url == API_URL:
|
||||
raise httpx.HTTPError("api down")
|
||||
return FakeResp_news_service(text="")
|
||||
def _settings_stub(threshold="7"):
|
||||
def fake_get_setting(key, default=None):
|
||||
return {
|
||||
"news_api_url": API_URL,
|
||||
"news_ai_url": AI_URL,
|
||||
"news_ai_model": "test-model",
|
||||
"news_grade_threshold": threshold,
|
||||
"news_ai_key": "",
|
||||
}.get(key, default)
|
||||
|
||||
return fake_get_setting
|
||||
|
||||
|
||||
def test_get_article_images_network_error_returns_empty():
|
||||
client = FakeClient_news_service([])
|
||||
|
||||
async def failing_get(url, timeout=None):
|
||||
raise httpx.HTTPError("down")
|
||||
|
||||
client.get = failing_get
|
||||
assert run_async(_get_article_images("http://news.test/page", client)) == []
|
||||
Reference in New Issue
Block a user