import pytest from django.db import IntegrityError from taggit.models import Tag from apps.blog.models import ( TAG_COLOUR_PALETTE, ArticleIndexPage, ArticlePage, Category, HomePage, TagMetadata, get_auto_tag_colour_css, ) from apps.blog.tests.factories import AuthorFactory @pytest.mark.django_db def test_home_page_creation(home_page): assert HomePage.objects.count() == 1 @pytest.mark.django_db def test_article_index_parent_restriction(): assert ArticleIndexPage.parent_page_types == ["blog.HomePage"] @pytest.mark.django_db def test_article_compute_read_time_excludes_code(home_page): index = ArticleIndexPage(title="Articles", slug="articles") home_page.add_child(instance=index) author = AuthorFactory() article = ArticlePage( title="A", slug="a", author=author, summary="s", body=[("rich_text", "
one two three
"), ("code", {"language": "python", "raw_code": "x y z"})], ) index.add_child(instance=article) article.save() assert article.read_time_mins == 1 @pytest.mark.django_db def test_tag_metadata_css_and_uniqueness(): tag = Tag.objects.create(name="llms", slug="llms") meta = TagMetadata.objects.create(tag=tag, colour="cyan") assert meta.get_css_classes()["bg"] == "bg-brand-cyan/10" with pytest.raises(IntegrityError): TagMetadata.objects.create(tag=tag, colour="pink") @pytest.mark.django_db def test_article_default_category_is_assigned(home_page): index = ArticleIndexPage(title="Articles", slug="articles") home_page.add_child(instance=index) author = AuthorFactory() article = ArticlePage( title="Categorised", slug="categorised", author=author, summary="s", body=[("rich_text", "body
")], ) index.add_child(instance=article) article.save() assert article.category.slug == "general" @pytest.mark.django_db def test_article_read_time_is_not_recomputed_when_body_text_is_unchanged(home_page, monkeypatch): index = ArticleIndexPage(title="Articles", slug="articles") home_page.add_child(instance=index) author = AuthorFactory() article = ArticlePage( title="Stable read time", slug="stable-read-time", author=author, summary="s", body=[("rich_text", "body words
")], ) index.add_child(instance=article) article.save() def fail_compute(): raise AssertionError("read time should not be recomputed when body text is unchanged") monkeypatch.setattr(article, "_compute_read_time", fail_compute) article.title = "Retitled" article.save() article.refresh_from_db() assert article.read_time_mins == 1 @pytest.mark.django_db def test_category_ordering(): Category.objects.get_or_create(name="General", slug="general") Category.objects.create(name="Z", slug="z", sort_order=2) Category.objects.create(name="A", slug="a", sort_order=1) names = list(Category.objects.values_list("name", flat=True)) assert names == ["General", "A", "Z"] # ── Auto tag colour tests ──────────────────────────────────────────────────── def test_auto_tag_colour_is_deterministic(): """Same tag name always produces the same colour.""" css1 = get_auto_tag_colour_css("python") css2 = get_auto_tag_colour_css("python") assert css1 == css2 def test_auto_tag_colour_is_case_insensitive(): """Tag colour assignment is case-insensitive.""" assert get_auto_tag_colour_css("Python") == get_auto_tag_colour_css("python") def test_auto_tag_colour_returns_valid_palette_entry(): """Returned CSS dict must be from the palette.""" css = get_auto_tag_colour_css("llms") assert css in TAG_COLOUR_PALETTE def test_auto_tag_colour_distributes_across_palette(): """Different tag names should map to multiple palette entries.""" sample_tags = ["python", "javascript", "rust", "go", "ruby", "java", "typescript", "css", "html", "sql", "llms", "mlops"] colours = {get_auto_tag_colour_css(t)["text"] for t in sample_tags} assert len(colours) >= 3, "Tags should spread across at least 3 palette colours" @pytest.mark.django_db def test_tag_without_metadata_uses_auto_colour(): """Tags without TagMetadata should get auto-assigned colour, not neutral.""" tag = Tag.objects.create(name="fastapi", slug="fastapi") expected = get_auto_tag_colour_css("fastapi") # Verify no metadata exists assert not TagMetadata.objects.filter(tag=tag).exists() # The template tag helper should fall back to auto colour from apps.core.templatetags.core_tags import _resolve_tag_css assert _resolve_tag_css(tag) == expected @pytest.mark.django_db def test_tag_with_metadata_overrides_auto_colour(): """Tags with explicit TagMetadata should use that colour.""" tag = Tag.objects.create(name="django", slug="django") TagMetadata.objects.create(tag=tag, colour="pink") from apps.core.templatetags.core_tags import _resolve_tag_css css = _resolve_tag_css(tag) assert css["text"] == "text-brand-pink" # ── Auto slug tests ────────────────────────────────────────────────────────── @pytest.mark.django_db def test_article_save_auto_generates_slug_from_title(home_page): """Model save should auto-generate slug from title when slug is empty.""" index = ArticleIndexPage(title="Articles", slug="articles") home_page.add_child(instance=index) author = AuthorFactory() article = ArticlePage( title="My Great Article", slug="my-great-article", author=author, summary="summary", body=[("rich_text", "body
")], ) index.add_child(instance=article) article.save() assert article.slug == "my-great-article" @pytest.mark.django_db def test_article_save_auto_generates_search_description(article_index): """Model save should populate search_description from summary.""" author = AuthorFactory() article = ArticlePage( title="SEO Auto", slug="seo-auto", author=author, summary="This is the article summary.", body=[("rich_text", "body
")], ) article_index.add_child(instance=article) article.save() assert article.search_description == "This is the article summary." @pytest.mark.django_db def test_article_save_preserves_explicit_search_description(article_index): """Explicit search_description should not be overwritten.""" author = AuthorFactory() article = ArticlePage( title="SEO Explicit", slug="seo-explicit", author=author, summary="Generated summary.", search_description="Custom SEO description.", body=[("rich_text", "body
")], ) article_index.add_child(instance=article) article.save() assert article.search_description == "Custom SEO description."