|
# retoor <retoor@molodetz.nl>
|
|
|
|
import requests
|
|
from tests.conftest import BASE_URL
|
|
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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"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_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 _seed_comment_on_post(commenter_uid):
|
|
from datetime import datetime, timezone
|
|
from uuid import uuid4
|
|
from devplacepy.database import get_table
|
|
|
|
post_slug, post_uid = _seed_post_seo()
|
|
comment_uid = str(uuid4())
|
|
get_table("comments").insert(
|
|
{
|
|
"deleted_at": None,
|
|
"deleted_by": None,
|
|
"uid": comment_uid,
|
|
"target_type": "post",
|
|
"target_uid": post_uid,
|
|
"post_uid": post_uid,
|
|
"user_uid": commenter_uid,
|
|
"content": "Activity tab link comment body",
|
|
"parent_uid": None,
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
return post_slug, post_uid, comment_uid
|
|
|
|
|
|
def test_activity_comment_exposes_parent_post_url(app_server):
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
commenter = _seed_owner()
|
|
username = get_table("users").find_one(uid=commenter)["username"]
|
|
post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
|
|
refresh_snapshot()
|
|
|
|
r = requests.get(
|
|
f"{BASE_URL}/profile/{username}?tab=activity",
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
assert r.status_code == 200
|
|
activities = r.json()["activities"]
|
|
comment_acts = [a for a in activities if a.get("type") == "comment"]
|
|
assert comment_acts, "expected a comment activity"
|
|
act = comment_acts[0]
|
|
assert act["url"] == f"/posts/{post_slug}#comment-{comment_uid}"
|
|
assert act["target_type"] == "post"
|
|
|
|
|
|
def test_activity_comment_url_matches_notification_target(app_server):
|
|
from devplacepy.database import get_table, refresh_snapshot, resolve_object_url
|
|
|
|
commenter = _seed_owner()
|
|
username = get_table("users").find_one(uid=commenter)["username"]
|
|
_post_slug, post_uid, comment_uid = _seed_comment_on_post(commenter)
|
|
refresh_snapshot()
|
|
|
|
expected = f"{resolve_object_url('post', post_uid)}#comment-{comment_uid}"
|
|
|
|
r = requests.get(
|
|
f"{BASE_URL}/profile/{username}?tab=activity",
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
act = [a for a in r.json()["activities"] if a.get("type") == "comment"][0]
|
|
assert act["url"] == expected
|
|
|
|
|
|
def test_activity_post_exposes_url(app_server):
|
|
from datetime import datetime, timezone
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
owner = _seed_owner()
|
|
username = get_table("users").find_one(uid=owner)["username"]
|
|
post_slug, post_uid = _seed_post_seo()
|
|
get_table("posts").update({"uid": post_uid, "user_uid": owner}, ["uid"])
|
|
refresh_snapshot()
|
|
|
|
r = requests.get(
|
|
f"{BASE_URL}/profile/{username}?tab=activity",
|
|
headers={"Accept": "application/json"},
|
|
)
|
|
assert r.status_code == 200
|
|
post_acts = [a for a in r.json()["activities"] if a.get("type") == "post"]
|
|
assert post_acts, "expected a post activity"
|
|
assert post_acts[0]["url"] == f"/posts/{post_slug}"
|
|
|
|
|
|
def test_activity_comment_card_renders_overlay_link(app_server):
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
commenter = _seed_owner()
|
|
username = get_table("users").find_one(uid=commenter)["username"]
|
|
post_slug, _post_uid, comment_uid = _seed_comment_on_post(commenter)
|
|
refresh_snapshot()
|
|
|
|
r = requests.get(f"{BASE_URL}/profile/{username}?tab=activity")
|
|
assert r.status_code == 200
|
|
assert "activity-card card-link-host" in r.text
|
|
assert f'class="card-link" href="/posts/{post_slug}#comment-{comment_uid}"' in r.text
|
|
|
|
|
|
def test_profile_json_exposes_online_presence(app_server):
|
|
import time
|
|
|
|
name = f"pres{int(time.time() * 1000)}"
|
|
session = requests.Session()
|
|
session.post(
|
|
f"{BASE_URL}/auth/signup",
|
|
data={
|
|
"username": name,
|
|
"email": f"{name}@t.dev",
|
|
"password": "secret123",
|
|
"confirm_password": "secret123",
|
|
},
|
|
allow_redirects=True,
|
|
)
|
|
r = session.get(
|
|
f"{BASE_URL}/profile/{name}", headers={"Accept": "application/json"}
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["profile_online"] is True
|
|
assert body["profile_user"]["last_seen"]
|
|
|
|
|
|
def test_profile_json_offline_when_last_seen_stale(app_server):
|
|
from datetime import datetime, timezone, timedelta
|
|
from devplacepy.config import PRESENCE_TIMEOUT_SECONDS
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
owner = _seed_owner()
|
|
username = get_table("users").find_one(uid=owner)["username"]
|
|
stale = (
|
|
datetime.now(timezone.utc)
|
|
- timedelta(seconds=PRESENCE_TIMEOUT_SECONDS + 120)
|
|
).isoformat()
|
|
get_table("users").update({"uid": owner, "last_seen": stale}, ["uid"])
|
|
refresh_snapshot()
|
|
|
|
r = requests.get(
|
|
f"{BASE_URL}/profile/{username}", headers={"Accept": "application/json"}
|
|
)
|
|
assert r.status_code == 200
|
|
body = r.json()
|
|
assert body["profile_online"] is False
|
|
assert body["profile_user"]["last_seen"] == stale
|
|
|
|
|
|
def test_profile_hero_avatar_has_presence_dot(app_server):
|
|
import time
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
name = f"phero{int(time.time() * 1000)}"
|
|
s = requests.Session()
|
|
s.post(
|
|
f"{BASE_URL}/auth/signup",
|
|
data={
|
|
"username": name,
|
|
"email": f"{name}@t.dev",
|
|
"password": "secret123",
|
|
"confirm_password": "secret123",
|
|
},
|
|
allow_redirects=True,
|
|
)
|
|
refresh_snapshot()
|
|
uid = get_table("users").find_one(username=name)["uid"]
|
|
html = s.get(f"{BASE_URL}/profile/{name}").text
|
|
assert "profile-avatar-wrap" in html
|
|
assert "presence-dot" in html
|
|
assert f'data-presence-uid="{uid}"' in html
|
|
|
|
|
|
def test_followers_list_avatar_has_presence_dot(app_server):
|
|
import time
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
|
|
stamp = int(time.time() * 1000)
|
|
target = f"folt{stamp}"
|
|
follower = f"folf{stamp}"
|
|
st = requests.Session()
|
|
sf = requests.Session()
|
|
for sess, name in ((st, target), (sf, follower)):
|
|
sess.post(
|
|
f"{BASE_URL}/auth/signup",
|
|
data={
|
|
"username": name,
|
|
"email": f"{name}@t.dev",
|
|
"password": "secret123",
|
|
"confirm_password": "secret123",
|
|
},
|
|
allow_redirects=True,
|
|
)
|
|
sf.post(f"{BASE_URL}/follow/{target}", allow_redirects=True)
|
|
refresh_snapshot()
|
|
follower_uid = get_table("users").find_one(username=follower)["uid"]
|
|
html = st.get(f"{BASE_URL}/profile/{target}?tab=followers").text
|
|
assert "follow-user" in html
|
|
assert f'data-presence-uid="{follower_uid}"' in html
|
|
assert "data-presence-last-seen" in html
|
|
|
|
|
|
def test_viewing_profile_marks_notification_read(app_server):
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from devplacepy.database import get_table, refresh_snapshot
|
|
from devplacepy.utils import generate_uid
|
|
|
|
name = f"nmark{int(time.time() * 1000)}"
|
|
session = requests.Session()
|
|
session.post(
|
|
f"{BASE_URL}/auth/signup",
|
|
data={
|
|
"username": name,
|
|
"email": f"{name}@t.dev",
|
|
"password": "secret123",
|
|
"confirm_password": "secret123",
|
|
},
|
|
allow_redirects=True,
|
|
)
|
|
refresh_snapshot()
|
|
user = get_table("users").find_one(username=name)
|
|
notif_uid = generate_uid()
|
|
get_table("notifications").insert(
|
|
{
|
|
"uid": notif_uid,
|
|
"user_uid": user["uid"],
|
|
"type": "follow",
|
|
"message": "someone followed you",
|
|
"related_uid": generate_uid(),
|
|
"target_url": f"/profile/{name}",
|
|
"read": False,
|
|
"created_at": datetime.now(timezone.utc).isoformat(),
|
|
}
|
|
)
|
|
refresh_snapshot()
|
|
|
|
r = session.get(f"{BASE_URL}/profile/{name}", allow_redirects=True)
|
|
assert r.status_code == 200
|
|
|
|
refresh_snapshot()
|
|
assert bool(get_table("notifications").find_one(uid=notif_uid)["read"]) is True
|