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.
223 lines
7.3 KiB
Python
223 lines
7.3 KiB
Python
# retoor <retoor@molodetz.nl>
|
|
|
|
import hashlib
|
|
import time
|
|
from datetime import datetime, timedelta, timezone
|
|
from playwright.sync_api import expect
|
|
from tests.conftest import BASE_URL
|
|
from devplacepy.database import get_table
|
|
from devplacepy.utils import hash_password, generate_uid
|
|
from uuid import uuid4
|
|
from datetime import datetime, timezone
|
|
import requests
|
|
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_login_page_loads(page, app_server):
|
|
page.goto(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
|
assert page.is_visible("h2:has-text('Welcome Back')")
|
|
assert page.is_visible("#email")
|
|
assert page.is_visible("#password")
|
|
assert page.is_visible("button:has-text('Sign in')")
|
|
|
|
|
|
def test_login_nonexistent_email(page, app_server):
|
|
page.goto(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
|
page.fill("#email", "nobody@nowhere.devplace")
|
|
page.fill("#password", "secret123")
|
|
page.click("button:has-text('Sign in')")
|
|
expect(page.locator("text=Invalid email or password")).to_be_visible()
|
|
|
|
|
|
def test_signup_link_from_login(page, app_server):
|
|
page.goto(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
|
page.click("a:has-text('Create new account')")
|
|
page.wait_for_url("**/auth/signup", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_login_next_redirects_to_target(page, app_server, seeded_db):
|
|
page.goto(f"{BASE_URL}/auth/login?next=/gists", wait_until="domcontentloaded")
|
|
page.fill("#email", seeded_db["alice"]["email"])
|
|
page.fill("#password", seeded_db["alice"]["password"])
|
|
page.click("button:has-text('Sign in')")
|
|
page.wait_for_url("**/gists", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_login_next_rejects_external(page, app_server, seeded_db):
|
|
page.goto(
|
|
f"{BASE_URL}/auth/login?next=https://evil.example.com",
|
|
wait_until="domcontentloaded",
|
|
)
|
|
page.fill("#email", seeded_db["alice"]["email"])
|
|
page.fill("#password", seeded_db["alice"]["password"])
|
|
page.click("button:has-text('Sign in')")
|
|
page.wait_for_url("**/feed", wait_until="domcontentloaded")
|
|
|
|
|
|
def test_landing_nav_to_login(page, app_server):
|
|
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
|
page.click("a:has-text('Login')")
|
|
page.wait_for_url(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
|
assert page.is_visible("h2:has-text('Welcome Back')")
|
|
|
|
|
|
def test_landing_log_in_link(page, app_server):
|
|
page.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
|
page.click("text=Log In")
|
|
page.wait_for_url(f"{BASE_URL}/auth/login", wait_until="domcontentloaded")
|
|
|
|
|
|
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
|