import pytest import requests from tests.conftest import BASE_URL def test_robots_txt_exists(app_server): r = requests.get(f"{BASE_URL}/robots.txt") assert r.status_code == 200 assert "User-agent:" in r.text assert "Disallow: /auth/" in r.text assert "Sitemap:" in r.text def test_sitemap_xml_exists(app_server): r = requests.get(f"{BASE_URL}/sitemap.xml") assert r.status_code == 200 assert "" in r.text def test_landing_page_has_unique_title(page, app_server): page.goto(f"{BASE_URL}/", wait_until="domcontentloaded") title = page.title() assert title and "DevPlace" in title def test_feed_page_has_unique_title(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") page.fill("#username", "seo_title_test") page.fill("#email", "seo_title@test.dev") page.fill("#password", "secret123") page.fill("#confirm_password", "secret123") page.click("button:has-text('Create account')") page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded") title = page.title() assert title and "Feed" in title def test_login_page_is_noindex(page, app_server): page.goto(f"{BASE_URL}/auth/login", wait_until="domcontentloaded") meta = page.locator('meta[name="robots"]') content = meta.get_attribute("content") assert content and "noindex" in content def test_signup_page_is_noindex(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") meta = page.locator('meta[name="robots"]') content = meta.get_attribute("content") assert content and "noindex" in content def test_feed_page_has_canonical(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") page.fill("#username", "seo_canon_test") page.fill("#email", "seo_canon@test.dev") page.fill("#password", "secret123") page.fill("#confirm_password", "secret123") page.click("button:has-text('Create account')") page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded") link = page.locator('link[rel="canonical"]') href = link.get_attribute("href") assert href and href.startswith("http") def test_feed_page_has_og_tags(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") page.fill("#username", "seo_og_test") page.fill("#email", "seo_og@test.dev") page.fill("#password", "secret123") page.fill("#confirm_password", "secret123") page.click("button:has-text('Create account')") page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded") og = page.locator('meta[property="og:title"]') content = og.get_attribute("content") assert content and len(content) > 0 def test_feed_page_has_twitter_card(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") page.fill("#username", "seo_twit_test") page.fill("#email", "seo_twit@test.dev") page.fill("#password", "secret123") page.fill("#confirm_password", "secret123") page.click("button:has-text('Create account')") page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded") card = page.locator('meta[name="twitter:card"]') content = card.get_attribute("content") assert content == "summary_large_image" def test_post_page_has_structured_data(page, app_server): page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded") page.fill("#username", "seo_schema_test") page.fill("#email", "seo_schema@test.dev") page.fill("#password", "secret123") page.fill("#confirm_password", "secret123") page.click("button:has-text('Create account')") page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded") page.locator(".feed-fab").click() page.fill("#post-content", "This is a post for SEO testing with structured data schema validation") page.fill("#post-title", "SEO Post For Schema") page.locator("#create-post-modal button.btn-primary:has-text('Post')").click() page.wait_for_url(f"{BASE_URL}/posts/*", timeout=10000, wait_until="domcontentloaded") scripts = page.locator('script[type="application/ld+json"]') count = scripts.count() assert count >= 1 text = scripts.first.text_content() assert "DiscussionForumPosting" in text or "WebSite" in text def test_profile_page_has_structured_data(alice): page, user = alice page.goto(f"{BASE_URL}/profile/{user['username']}", wait_until="domcontentloaded") scripts = page.locator('script[type="application/ld+json"]') count = scripts.count() assert count >= 1 text = scripts.first.text_content() assert "ProfilePage" in text or "WebSite" in text def test_messages_page_is_noindex(alice): page, user = alice page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded") page.wait_for_url("**/messages", wait_until="domcontentloaded") meta = page.locator('meta[name="robots"]') content = meta.get_attribute("content") assert content and "noindex" in content def test_notifications_page_is_noindex(alice): page, user = alice page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded") page.wait_for_url("**/notifications", wait_until="domcontentloaded") meta = page.locator('meta[name="robots"]') content = meta.get_attribute("content") assert content and "noindex" in content def test_x_robots_tag_header(app_server): r = requests.get(f"{BASE_URL}/feed", allow_redirects=True) assert "X-Robots-Tag" in r.headers def test_x_content_type_options_header(app_server): r = requests.get(f"{BASE_URL}/feed", allow_redirects=True) assert r.headers.get("X-Content-Type-Options") == "nosniff" def _seed_news(): from datetime import datetime, timezone from devplacepy.database import get_table from devplacepy.utils import generate_uid, make_combined_slug uid = generate_uid() slug = make_combined_slug("SEO Test News Article", uid) get_table("news").insert({ "uid": uid, "slug": slug, "title": "SEO Test News Article", "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 test_robots_disallows_admin_and_uploads(app_server): r = requests.get(f"{BASE_URL}/robots.txt") assert "Disallow: /admin/" in r.text assert "Disallow: /uploads/" in r.text def test_sitemap_headers_and_static_entries(app_server): r = requests.get(f"{BASE_URL}/sitemap.xml") assert r.headers["content-type"].startswith("application/xml") assert "cache-control" in {k.lower() for k in r.headers} assert f"{BASE_URL}/gists" in r.text def test_sitemap_includes_published_news(app_server): slug, _ = _seed_news() r = requests.get(f"{BASE_URL}/sitemap.xml") assert f"/news/{slug}" in r.text def test_feed_canonical_preserves_pagination(page, app_server): page.goto(f"{BASE_URL}/feed?page=2", wait_until="domcontentloaded") href = page.locator('link[rel="canonical"]').get_attribute("href") assert href.endswith("?page=2"), href def test_feed_canonical_drops_non_pagination_params(page, app_server): page.goto(f"{BASE_URL}/feed?sort=new", wait_until="domcontentloaded") href = page.locator('link[rel="canonical"]').get_attribute("href") assert href.endswith("/feed"), href def test_default_og_image_is_raster(page, app_server): page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded") og = page.locator('meta[property="og:image"]').get_attribute("content") assert og.endswith(".png"), og def test_news_detail_has_newsarticle_schema(page, app_server): slug, _ = _seed_news() page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded") scripts = page.locator('script[type="application/ld+json"]') text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count())) assert "NewsArticle" in text assert page.locator('meta[property="og:type"]').get_attribute("content") == "article" 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(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_post_uid_redirects_to_canonical_slug(app_server): slug, uid = _seed_post() r = requests.get(f"{BASE_URL}/posts/{uid}", allow_redirects=False) assert r.status_code == 301 assert r.headers["location"].endswith(f"/posts/{slug}"), r.headers.get("location") def test_post_slug_served_without_redirect(app_server): slug, uid = _seed_post() r = requests.get(f"{BASE_URL}/posts/{slug}", allow_redirects=False) assert r.status_code == 200 def test_gist_uid_redirects_to_canonical_slug(app_server): slug, uid = _seed_gist() r = requests.get(f"{BASE_URL}/gists/{uid}", allow_redirects=False) assert r.status_code == 301 assert r.headers["location"].endswith(f"/gists/{slug}"), r.headers.get("location") def test_project_uid_redirects_to_canonical_slug(app_server): slug, uid = _seed_project() r = requests.get(f"{BASE_URL}/projects/{uid}", allow_redirects=False) assert r.status_code == 301 assert r.headers["location"].endswith(f"/projects/{slug}"), r.headers.get("location") def test_news_uid_redirects_to_canonical_slug(app_server): slug, uid = _seed_news() 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") def test_missing_profile_returns_404(app_server): r = requests.get(f"{BASE_URL}/profile/no-such-user-xyz", allow_redirects=False) assert r.status_code == 404 def test_post_og_image_uses_content_image(page, app_server): slug, _ = _seed_post(image="seo-content.png") page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded") og = page.locator('meta[property="og:image"]').get_attribute("content") assert og.endswith("/static/uploads/seo-content.png"), og def test_post_page_has_breadcrumb_schema(page, app_server): slug, _ = _seed_post() page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded") scripts = page.locator('script[type="application/ld+json"]') text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count())) assert "BreadcrumbList" in text def test_post_schema_has_date_modified(page, app_server): slug, _ = _seed_post() page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded") scripts = page.locator('script[type="application/ld+json"]') text = " ".join(scripts.nth(i).text_content() for i in range(scripts.count())) assert "dateModified" in text def test_news_detail_image_has_alt_text(page, app_server): slug, uid = _seed_news() _seed_news_image(uid) page.goto(f"{BASE_URL}/news/{slug}", wait_until="domcontentloaded") alt = page.locator(".news-detail-image img").get_attribute("alt") assert alt and alt.strip() def test_feed_pagination_emits_rel_next(page, app_server): topic = _seed_feed_posts(26) page.goto(f"{BASE_URL}/feed?topic={topic}", wait_until="domcontentloaded") nxt = page.locator('link[rel="next"]') assert nxt.count() == 1 href = nxt.get_attribute("href") assert "before=" in href and f"topic={topic}" in href, href def test_post_detail_has_no_rel_next(page, app_server): slug, _ = _seed_post() page.goto(f"{BASE_URL}/posts/{slug}", wait_until="domcontentloaded") assert page.locator('link[rel="next"]').count() == 0