|
# retoor <retoor@molodetz.nl>
|
|
|
|
import types
|
|
import devplacepy.seo as seo
|
|
import devplacepy.attachments as attach
|
|
|
|
|
|
def _seo_request():
|
|
return types.SimpleNamespace(
|
|
base_url="https://x.test/",
|
|
url=types.SimpleNamespace(path="/posts/abc"),
|
|
query_params={},
|
|
)
|
|
|
|
|
|
def test_truncate_no_overflow():
|
|
assert len(seo.truncate("x" * 200)) <= 160
|
|
assert seo.truncate("short text") == "short text"
|
|
assert seo.truncate("") == ""
|
|
|
|
|
|
def test_software_application_url_is_per_project():
|
|
schema = seo.software_application_schema(
|
|
{"uid": "p1", "slug": "p1-foo", "title": "P"}, "https://x.test"
|
|
)
|
|
assert schema["url"] == "https://x.test/projects/p1-foo"
|
|
|
|
|
|
def test_schema_types():
|
|
assert seo.organization_schema("https://x.test")["@type"] == "Organization"
|
|
assert (
|
|
seo.news_article_schema(
|
|
{"uid": "n1", "title": "T", "synced_at": "2026-01-01"}, "https://x.test"
|
|
)["@type"]
|
|
== "NewsArticle"
|
|
)
|
|
assert (
|
|
seo.software_source_code_schema(
|
|
{"uid": "g1", "title": "G", "language": "python"}, "https://x.test"
|
|
)["@type"]
|
|
== "SoftwareSourceCode"
|
|
)
|
|
|
|
|
|
def test_news_article_publisher_is_organization():
|
|
schema = seo.news_article_schema(
|
|
{"uid": "n1", "title": "T", "synced_at": "2026-01-01"}, "https://x.test"
|
|
)
|
|
assert schema["publisher"]["@type"] == "Organization"
|
|
|
|
|
|
def test_project_schema_type_mapping():
|
|
assert seo.project_schema_type("game") == "VideoGame"
|
|
assert seo.project_schema_type("game_asset") == "CreativeWork"
|
|
assert seo.project_schema_type("website") == "WebApplication"
|
|
assert seo.project_schema_type("software") == "SoftwareApplication"
|
|
assert seo.project_schema_type("mobile_app") == "SoftwareApplication"
|
|
assert seo.project_schema_type(None) == "SoftwareApplication"
|
|
assert seo.project_schema_type("unknown") == "SoftwareApplication"
|
|
|
|
|
|
def test_software_application_schema_is_type_aware():
|
|
game = seo.software_application_schema(
|
|
{"uid": "p1", "slug": "p1-g", "title": "G", "project_type": "game", "platforms": "PC, Web"},
|
|
"https://x.test",
|
|
)
|
|
assert game["@type"] == "VideoGame"
|
|
assert game["gamePlatform"] == ["PC", "Web"]
|
|
assert "applicationCategory" not in game
|
|
web = seo.software_application_schema(
|
|
{"uid": "p2", "slug": "p2-w", "title": "W", "project_type": "website"},
|
|
"https://x.test",
|
|
)
|
|
assert web["@type"] == "WebApplication"
|
|
assert web["browserRequirements"] == "Requires JavaScript"
|
|
assert web["applicationCategory"] == "DeveloperApplication"
|
|
|
|
|
|
def test_software_application_schema_rating_and_image():
|
|
project = {"uid": "p1", "slug": "p1-s", "title": "S", "project_type": "software"}
|
|
plain = seo.software_application_schema(project, "https://x.test")
|
|
assert "aggregateRating" not in plain
|
|
assert "image" not in plain
|
|
rich = seo.software_application_schema(
|
|
project,
|
|
"https://x.test",
|
|
image_url="https://x.test/img.png",
|
|
star_count=7,
|
|
comment_count=3,
|
|
)
|
|
assert rich["aggregateRating"]["ratingCount"] == "7"
|
|
assert rich["image"] == "https://x.test/img.png"
|
|
assert rich["interactionStatistic"]["userInteractionCount"] == 3
|
|
|
|
|
|
def test_software_application_schema_website_and_screenshots():
|
|
project = {
|
|
"uid": "p1",
|
|
"slug": "p1-s",
|
|
"title": "S",
|
|
"project_type": "software",
|
|
"website_url": "https://myproject.dev",
|
|
}
|
|
schema = seo.software_application_schema(
|
|
project,
|
|
"https://x.test",
|
|
screenshot_urls=["https://x.test/a.png", "https://x.test/b.png"],
|
|
)
|
|
assert schema["sameAs"] == ["https://myproject.dev"]
|
|
assert schema["screenshot"] == ["https://x.test/a.png", "https://x.test/b.png"]
|
|
bare = seo.software_application_schema(
|
|
{"uid": "p2", "slug": "p2-s", "title": "B"}, "https://x.test"
|
|
)
|
|
assert "sameAs" not in bare
|
|
assert "screenshot" not in bare
|
|
|
|
|
|
def test_project_devlog_schema_builds_blog_graph():
|
|
project = {"uid": "p1", "slug": "p1-s", "title": "Nebula"}
|
|
assert seo.project_devlog_schema(project, [], "https://x.test") is None
|
|
posts = [
|
|
{
|
|
"post": {
|
|
"uid": "a1",
|
|
"slug": "a1-first",
|
|
"title": "First update",
|
|
"content": "Progress!",
|
|
"created_at": "2026-01-01T00:00:00+00:00",
|
|
},
|
|
"author": {"username": "dev"},
|
|
"comment_count": 2,
|
|
}
|
|
]
|
|
schema = seo.project_devlog_schema(project, posts, "https://x.test")
|
|
assert schema["@type"] == "Blog"
|
|
assert schema["url"] == "https://x.test/projects/p1-s#devlog"
|
|
entry = schema["blogPost"][0]
|
|
assert entry["@type"] == "BlogPosting"
|
|
assert entry["headline"] == "First update"
|
|
assert entry["url"] == "https://x.test/posts/a1-first"
|
|
assert entry["author"]["name"] == "dev"
|
|
assert entry["commentCount"] == 2
|
|
|
|
|
|
def test_site_url_precedence(monkeypatch):
|
|
import devplacepy.database as database
|
|
|
|
monkeypatch.setattr(seo, "SITE_URL", "https://constant.test")
|
|
|
|
monkeypatch.setattr(
|
|
database, "get_setting", lambda *a, **k: "https://setting.test"
|
|
)
|
|
assert seo.site_url(None) == "https://setting.test"
|
|
|
|
monkeypatch.setattr(database, "get_setting", lambda *a, **k: "")
|
|
assert seo.site_url(None) == "https://constant.test"
|
|
|
|
monkeypatch.setattr(seo, "SITE_URL", "")
|
|
request = types.SimpleNamespace(base_url="https://req.test/")
|
|
assert seo.site_url(request) == "https://req.test"
|
|
|
|
|
|
def test_site_url_falls_back_to_request(monkeypatch):
|
|
monkeypatch.setattr(seo, "SITE_URL", "")
|
|
request = types.SimpleNamespace(base_url="http://fallback.test/")
|
|
assert seo.site_url(request) == "http://fallback.test"
|
|
|
|
|
|
def test_upload_allowlist_excludes_dangerous_types():
|
|
assert ".svg" not in attach.ALLOWED_UPLOAD_TYPES
|
|
assert ".html" not in attach.ALLOWED_UPLOAD_TYPES
|
|
assert ".png" in attach.ALLOWED_UPLOAD_TYPES
|
|
assert ".svg" not in attach.POST_IMAGE_EXTENSIONS
|
|
|
|
|
|
def test_detect_mime_neutralizes_dangerous_types():
|
|
assert attach._detect_mime(b"", "x.svg") == "application/octet-stream"
|
|
assert attach._detect_mime(b"", "x.html") == "application/octet-stream"
|
|
assert attach._detect_mime(b"", "x.png") == "image/png"
|
|
|
|
|
|
def test_base_seo_context_emits_keywords_key():
|
|
ctx = seo.base_seo_context(
|
|
_seo_request(),
|
|
title="A Post",
|
|
description="Body text",
|
|
keywords="python, sqlite",
|
|
)
|
|
assert "meta_keywords" in ctx
|
|
assert ctx["meta_keywords"] == "python, sqlite"
|
|
|
|
|
|
def test_base_seo_context_default_keywords_empty_without_target():
|
|
ctx = seo.base_seo_context(
|
|
_seo_request(), title="A Post", description="Body text"
|
|
)
|
|
assert ctx["meta_keywords"] == ""
|
|
|
|
|
|
def test_base_seo_context_description_strips_markdown(monkeypatch):
|
|
monkeypatch.setattr(seo, "_ready_seo_metadata", lambda target: None)
|
|
ctx = seo.base_seo_context(
|
|
_seo_request(),
|
|
title="A Post",
|
|
description="## Heading\n\nThis is **bold** body with `code`.",
|
|
)
|
|
assert "##" not in ctx["meta_description"]
|
|
assert "**" not in ctx["meta_description"]
|
|
assert "`" not in ctx["meta_description"]
|
|
assert "bold" in ctx["meta_description"]
|
|
|
|
|
|
def test_base_seo_context_plain_default_fills_fields_for_target(monkeypatch):
|
|
monkeypatch.setattr(seo, "_ready_seo_metadata", lambda target: None)
|
|
ctx = seo.base_seo_context(
|
|
_seo_request(),
|
|
title="Async Python Database Tooling",
|
|
description="A **guide** to async python database tooling with sqlite.",
|
|
seo_target=("post", "abc"),
|
|
)
|
|
assert ctx["meta_keywords"]
|
|
assert "python" in ctx["meta_keywords"]
|
|
assert ctx["meta_description"]
|
|
assert "**" not in ctx["meta_description"]
|
|
|
|
|
|
def test_base_seo_context_consumes_ready_metadata(monkeypatch):
|
|
monkeypatch.setattr(
|
|
seo,
|
|
"_ready_seo_metadata",
|
|
lambda target: {
|
|
"seo_title": "Generated Title",
|
|
"seo_description": "Generated clean description",
|
|
"seo_keywords": "generated, metadata",
|
|
},
|
|
)
|
|
ctx = seo.base_seo_context(
|
|
_seo_request(),
|
|
title="Raw **markdown** title",
|
|
description="raw markdown body",
|
|
seo_target=("post", "abc"),
|
|
)
|
|
assert "Generated Title" in ctx["page_title"]
|
|
assert ctx["meta_description"] == "Generated clean description"
|
|
assert ctx["meta_keywords"] == "generated, metadata"
|