📄 test_phase4.py 9,739 bytes Apr 21, 2026 📋 Raw

"""Tests for blog module Phase 4 features."""

import os
import sys
import pytest
import tempfile
import shutil
from pathlib import Path
from datetime import datetime, timezone

Add parent to path

sys.path.insert(0, str(Path(file).parent.parent))

from blog.storage import init_db, POSTS_DIR, get_db, slugify
from blog.service import (
create_post, get_related_posts, search_posts,
list_posts, get_post, update_post, publish_post
)
from blog.models import CreatePostRequest, UpdatePostRequest

@pytest.fixture
def temp_blog_dir():
"""Create a temporary blog directory for testing."""
temp_dir = tempfile.mkdtemp()
original_dir = os.environ.get("BLOG_DATA_DIR")

# Set up temp directory
os.environ["BLOG_DATA_DIR"] = temp_dir
posts_dir = Path(temp_dir) / "posts"
posts_dir.mkdir(parents=True, exist_ok=True)

# Re-initialize DB
import blog.storage as storage_module
storage_module.DB_PATH = Path(temp_dir) / "blog.db"
storage_module.POSTS_DIR = posts_dir
storage_module.DATA_DIR = Path(temp_dir)

init_db()

yield temp_dir

# Cleanup
if original_dir:
    os.environ["BLOG_DATA_DIR"] = original_dir
else:
    del os.environ["BLOG_DATA_DIR"]
shutil.rmtree(temp_dir)

class TestSearch:
"""Tests for the FTS5 search endpoint."""

def test_search_empty_query(self, temp_blog_dir):
    """Search with empty query returns empty results."""
    result = search_posts("")
    assert result.total == 0
    assert len(result.results) == 0

def test_search_finds_by_title(self, temp_blog_dir):
    """Search finds posts by title."""
    # Create a post
    create_post(CreatePostRequest(
        title="Building Multi Agent Systems",
        content_md="This is about building agents.",
        category="engineering",
        tags=["ai", "agents"],
        status="published"
    ))

    # Search for it
    result = search_posts("multi agent")
    assert result.total == 1
    assert result.results[0].title == "Building Multi Agent Systems"

def test_search_finds_by_excerpt(self, temp_blog_dir):
    """Search finds posts by excerpt."""
    # Create a post with explicit excerpt
    create_post(CreatePostRequest(
        title="Some Post",
        excerpt="Deep dive into machine learning techniques",
        content_md="Content here.",
        category="engineering",
        status="published"
    ))

    # Search for excerpt content
    result = search_posts("machine learning")
    assert result.total == 1
    assert "machine" in result.results[0].excerpt.lower()

def test_search_only_published(self, temp_blog_dir):
    """Search only returns published posts."""
    # Create published post
    create_post(CreatePostRequest(
        title="Published Post About Python",
        content_md="Python content.",
        status="published"
    ))

    # Create draft post
    create_post(CreatePostRequest(
        title="Draft Post About Python",
        content_md="More Python content.",
        status="draft"
    ))

    # Search for Python
    result = search_posts("python")
    assert result.total == 1
    assert result.results[0].title == "Published Post About Python"

def test_search_ranking(self, temp_blog_dir):
    """Search results are ranked by relevance."""
    # Create multiple posts
    create_post(CreatePostRequest(
        title="FastAPI Tutorial",
        content_md="Learning FastAPI",
        status="published"
    ))

    create_post(CreatePostRequest(
        title="Django vs FastAPI",
        content_md="Comparing frameworks",
        status="published"
    ))

    result = search_posts("FastAPI")
    assert result.total == 2
    # All should have rank values (they may be 0 for some matches)
    for r in result.results:
        assert r.rank is not None

class TestRelatedPosts:
"""Tests for the related posts algorithm."""

def test_related_same_category(self, temp_blog_dir):
    """Posts in same category are related."""
    # Create posts in same category
    create_post(CreatePostRequest(
        title="Post One",
        content_md="Content one.",
        category="engineering",
        tags=["python"],
        status="published"
    ))

    create_post(CreatePostRequest(
        title="Post Two",
        content_md="Content two.",
        category="engineering",
        tags=["javascript"],
        status="published"
    ))

    result = get_related_posts("post-one", limit=3)
    assert result.total == 1
    assert result.posts[0].slug == "post-two"

def test_related_shared_tags(self, temp_blog_dir):
    """Posts with shared tags are related."""
    # Create posts with shared tags
    create_post(CreatePostRequest(
        title="Post Alpha",
        content_md="Content alpha.",
        category="engineering",
        tags=["python", "fastapi"],
        status="published"
    ))

    create_post(CreatePostRequest(
        title="Post Beta",
        content_md="Content beta.",
        category="tutorial",
        tags=["python", "django"],  # Shared "python" tag
        status="published"
    ))

    result = get_related_posts("post-alpha", limit=3)
    assert result.total == 1
    assert result.posts[0].slug == "post-beta"

def test_related_ranking(self, temp_blog_dir):
    """Posts with more shared tags rank higher."""
    # Source post
    create_post(CreatePostRequest(
        title="Source Post",
        content_md="Source content.",
        category="engineering",
        tags=["python", "fastapi", "async"],
        status="published"
    ))

    # Post with 1 shared tag
    create_post(CreatePostRequest(
        title="One Tag Match",
        content_md="Content.",
        category="tutorial",
        tags=["python", "flask"],
        status="published"
    ))

    # Post with 2 shared tags (should rank higher)
    create_post(CreatePostRequest(
        title="Two Tag Match",
        content_md="Content.",
        category="tutorial",
        tags=["python", "fastapi", "react"],
        status="published"
    ))

    # Post with same category (2 points) + 1 shared tag (1 point) = 3 points
    create_post(CreatePostRequest(
        title="Same Category Match",
        content_md="Content.",
        category="engineering",
        tags=["python", "flask"],
        status="published"
    ))

    result = get_related_posts("source-post", limit=3)
    assert result.total == 3

    # Same category + 1 tag = 3 points should be first
    assert result.posts[0].slug == "same-category-match"

def test_related_excludes_self(self, temp_blog_dir):
    """Related posts excludes the source post."""
    create_post(CreatePostRequest(
        title="Only Post",
        content_md="Content.",
        category="engineering",
        tags=["python"],
        status="published"
    ))

    result = get_related_posts("only-post", limit=3)
    assert result.total == 0

def test_related_only_published(self, temp_blog_dir):
    """Related posts only includes published posts."""
    create_post(CreatePostRequest(
        title="Published Post",
        content_md="Content.",
        category="engineering",
        tags=["python"],
        status="published"
    ))

    create_post(CreatePostRequest(
        title="Draft Post",
        content_md="Content.",
        category="engineering",
        tags=["python"],
        status="draft"
    ))

    result = get_related_posts("published-post", limit=3)
    assert result.total == 0  # Draft should not appear

def test_related_not_found(self, temp_blog_dir):
    """Related posts for non-existent post returns empty."""
    result = get_related_posts("non-existent", limit=3)
    assert result.total == 0
    assert len(result.posts) == 0

def test_related_respects_limit(self, temp_blog_dir):
    """Related posts respects the limit parameter."""
    # Create 5 posts in same category
    for i in range(5):
        create_post(CreatePostRequest(
            title=f"Post {i}",
            content_md=f"Content {i}.",
            category="engineering",
            status="published"
        ))

    result = get_related_posts("post-0", limit=2)
    assert result.total == 2
    assert len(result.posts) == 2

class TestSlugGeneration:
"""Tests for slug generation."""

def test_slugify_simple(self):
    """Simple titles become valid slugs."""
    assert slugify("Hello World") == "hello-world"

def test_slugify_special_chars(self):
    """Special characters are removed."""
    assert slugify("Hello, World! (2024)") == "hello-world-2024"

def test_slugify_multiple_spaces(self):
    """Multiple spaces become single hyphen."""
    assert slugify("Hello    World") == "hello-world"

if name == "main":
pytest.main([file, "-v"])