feat: replace DiceBear avatar proxy with local Multiavatar SVG generation and remove avatar_style from signup
This commit is contained in:
+16
-17
@@ -47,16 +47,14 @@ def app_server(test_db_path):
|
||||
env = os.environ.copy()
|
||||
env["DEVPLACE_DATABASE_URL"] = f"sqlite:///{test_db_path}"
|
||||
env["SECRET_KEY"] = "test-secret-key"
|
||||
err_path = Path("/tmp/devplace_test_server_err.log")
|
||||
with open(err_path, "w") as err_f:
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
|
||||
"--host", "127.0.0.1", "--port", "10501"],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=err_f,
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
[sys.executable, "-m", "uvicorn", "devplacepy.main:app",
|
||||
"--host", "127.0.0.1", "--port", "10501"],
|
||||
cwd=str(PROJECT_ROOT),
|
||||
env=env,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
deadline = time.time() + 20
|
||||
while time.time() < deadline:
|
||||
try:
|
||||
@@ -69,8 +67,13 @@ def app_server(test_db_path):
|
||||
proc.terminate(); proc.wait()
|
||||
raise RuntimeError("Server did not start")
|
||||
yield proc
|
||||
proc.terminate()
|
||||
proc.wait(timeout=10)
|
||||
try:
|
||||
proc.terminate()
|
||||
proc.wait(timeout=10)
|
||||
except Exception:
|
||||
proc.kill()
|
||||
proc.wait(timeout=5)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def playwright_instance():
|
||||
@@ -120,11 +123,7 @@ def login_user(page, user):
|
||||
page.fill("#email", user["email"])
|
||||
page.fill("#password", user["password"])
|
||||
page.click("button:has-text('Sign in')")
|
||||
try:
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
except Exception as e:
|
||||
print(f"\n[LOGIN FAIL] URL: {page.url}, title: {page.title()}, err: {e}")
|
||||
raise
|
||||
page.wait_for_url("**/feed", timeout=10000, wait_until="domcontentloaded")
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
from devplacepy.avatar import avatar_url, generate_avatar_svg
|
||||
|
||||
|
||||
def test_avatar_url_format():
|
||||
url = avatar_url("multiavatar", "testuser", 128)
|
||||
assert url == "/avatar/multiavatar/testuser?size=128"
|
||||
|
||||
|
||||
def test_avatar_url_different_size():
|
||||
url = avatar_url("multiavatar", "testuser", 64)
|
||||
assert "size=64" in url
|
||||
|
||||
|
||||
def test_avatar_url_contains_seed():
|
||||
url = avatar_url("multiavatar", "customuser", 128)
|
||||
assert "customuser" in url
|
||||
|
||||
|
||||
def test_generate_avatar_svg_returns_string():
|
||||
svg = generate_avatar_svg("testuser")
|
||||
assert isinstance(svg, str)
|
||||
assert len(svg) > 100
|
||||
|
||||
|
||||
def test_generate_avatar_svg_is_svg():
|
||||
svg = generate_avatar_svg("testuser")
|
||||
assert svg.strip().startswith("<svg")
|
||||
assert "xmlns" in svg
|
||||
|
||||
|
||||
def test_generate_avatar_svg_deterministic():
|
||||
svg1 = generate_avatar_svg("testuser")
|
||||
svg2 = generate_avatar_svg("testuser")
|
||||
assert svg1 == svg2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_different_for_different_seeds():
|
||||
svg1 = generate_avatar_svg("alice")
|
||||
svg2 = generate_avatar_svg("bob")
|
||||
assert svg1 != svg2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_fallback_on_exception():
|
||||
svg = generate_avatar_svg("a")
|
||||
assert isinstance(svg, str)
|
||||
assert len(svg) > 50
|
||||
|
||||
|
||||
def test_generate_avatar_svg_includes_rect_for_fallback():
|
||||
svg = generate_avatar_svg("a")
|
||||
assert "rect" in svg or "path" in svg
|
||||
|
||||
|
||||
def test_avatar_url_with_different_seeds():
|
||||
url1 = avatar_url("multiavatar", "alice", 128)
|
||||
url2 = avatar_url("multiavatar", "bob", 128)
|
||||
assert url1 != url2
|
||||
|
||||
|
||||
def test_generate_avatar_svg_empty_seed_uses_fallback():
|
||||
svg = generate_avatar_svg("")
|
||||
assert isinstance(svg, str)
|
||||
assert "rect" in svg or "svg" in svg
|
||||
+16
-16
@@ -3,7 +3,7 @@ from tests.conftest import BASE_URL
|
||||
|
||||
|
||||
def create_post(page, content, topic="random", title=None):
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.check(f"input[value='{topic}']")
|
||||
@@ -15,7 +15,7 @@ def create_post(page, content, topic="random", title=None):
|
||||
|
||||
|
||||
def signup(page, username, email, password):
|
||||
page.goto(f"{BASE_URL}/auth/signup")
|
||||
page.goto(f"{BASE_URL}/auth/signup", wait_until="domcontentloaded")
|
||||
page.fill("#username", username)
|
||||
page.fill("#email", email)
|
||||
page.fill("#password", password)
|
||||
@@ -35,7 +35,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 1 — LANDING PAGE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/")
|
||||
pa.goto(f"{BASE_URL}/", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Devplace.net")
|
||||
assert pa.is_visible("text=The Developer Social Network")
|
||||
assert pa.is_visible("text=Join DevPlace Free")
|
||||
@@ -58,7 +58,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 3 — ALICE EXPLORES FEED
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/feed")
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
for tab_text in ("Topics", "All", "Trending", "Recent", "Following",
|
||||
"Devlog", "Showcase", "Question", "Rant", "Fun"):
|
||||
assert pa.is_visible(f"text={tab_text}")
|
||||
@@ -95,7 +95,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 5 — ALICE CREATES TWO PROJECTS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/projects")
|
||||
pa.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
pa.locator("#create-project-btn").click()
|
||||
pa.fill("#title", "N8Nme")
|
||||
pa.fill("#description", "Visual workflow builder with 300+ integrations.")
|
||||
@@ -124,7 +124,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 6 — ALICE EDITS HER PROFILE
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
assert pa.is_visible("text=Member")
|
||||
assert pa.is_visible("text=Posts")
|
||||
@@ -146,11 +146,11 @@ def test_full_user_journey(browser, app_server):
|
||||
assert pa.is_visible("text=https://github.com/alice_demo")
|
||||
|
||||
# Switch profile tabs
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=N8Nme")
|
||||
assert pa.is_visible("text=DWN")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=posts")
|
||||
pa.goto(f"{BASE_URL}/profile/alice_demo?tab=posts", wait_until="domcontentloaded")
|
||||
pa.wait_for_timeout(300)
|
||||
assert pa.is_visible("text=API Progress Update")
|
||||
assert pa.is_visible("text=Pixel Editor WASM")
|
||||
@@ -159,17 +159,17 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 7 — BOB EXPLORES ALICE'S STUFF
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo")
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=alice_demo")
|
||||
assert pb.is_visible("text=Full-stack developer")
|
||||
assert pb.is_visible("text=Berlin, Germany")
|
||||
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo?tab=projects")
|
||||
pb.goto(f"{BASE_URL}/profile/alice_demo?tab=projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
assert pb.is_visible("text=DWN")
|
||||
|
||||
# Browse projects, search
|
||||
pb.goto(f"{BASE_URL}/projects")
|
||||
pb.goto(f"{BASE_URL}/projects", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("text=Recently Released")
|
||||
assert pb.is_visible("text=N8Nme")
|
||||
pb.fill("input[placeholder='Search projects...']", "N8Nme")
|
||||
@@ -226,7 +226,7 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 10 — ALICE CHECKS NOTIFICATIONS
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.bring_to_front()
|
||||
pa.goto(f"{BASE_URL}/notifications")
|
||||
pa.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("h2:has-text('Notifications')")
|
||||
mark_all = pa.locator("button:has-text('Mark all read')")
|
||||
if mark_all.is_visible():
|
||||
@@ -237,13 +237,13 @@ def test_full_user_journey(browser, app_server):
|
||||
# ACT 11 — BOB CHECKS NOTIFICATIONS TOO
|
||||
# ═══════════════════════════════════════════════
|
||||
pb.bring_to_front()
|
||||
pb.goto(f"{BASE_URL}/notifications")
|
||||
pb.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
assert pb.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 12 — LOGOUT, LOGIN AGAIN
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/auth/logout")
|
||||
pa.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pa.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# Log back in
|
||||
@@ -257,13 +257,13 @@ def test_full_user_journey(browser, app_server):
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
# Bob logs out
|
||||
pb.goto(f"{BASE_URL}/auth/logout")
|
||||
pb.goto(f"{BASE_URL}/auth/logout", wait_until="domcontentloaded")
|
||||
pb.wait_for_url("**/", wait_until="domcontentloaded")
|
||||
|
||||
# ═══════════════════════════════════════════════
|
||||
# ACT 13 — FINAL VERIFICATION
|
||||
# ═══════════════════════════════════════════════
|
||||
pa.goto(f"{BASE_URL}/feed")
|
||||
pa.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
assert pa.is_visible("text=Topics")
|
||||
assert pa.is_visible("text=alice_demo")
|
||||
|
||||
|
||||
+31
-4
@@ -3,13 +3,13 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def test_messages_page_loads(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible(".messages-layout")
|
||||
|
||||
|
||||
def test_messages_search_input(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
search = page.locator("#message-search")
|
||||
assert search.is_visible()
|
||||
assert search.get_attribute("placeholder") == "Search conversations..."
|
||||
@@ -17,7 +17,7 @@ def test_messages_search_input(alice):
|
||||
|
||||
def test_messages_empty_state(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
empty = page.locator("text=No conversations yet")
|
||||
if empty.is_visible():
|
||||
pass
|
||||
@@ -27,8 +27,35 @@ def test_messages_empty_state(alice):
|
||||
|
||||
def test_messages_search_for_bob(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
page.fill("#message-search", "bob_test")
|
||||
page.locator("#message-search").press("Enter")
|
||||
page.wait_for_timeout(500)
|
||||
assert page.is_visible(".messages-layout")
|
||||
|
||||
|
||||
def test_messages_header_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible("text=Home")
|
||||
assert page.is_visible("text=Projects")
|
||||
|
||||
|
||||
def test_messages_bell_icon(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
bell = page.locator(".topnav-icon").first
|
||||
assert bell.is_visible()
|
||||
|
||||
|
||||
def test_messages_topnav_user(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
assert page.is_visible(f"text={user['username']}")
|
||||
|
||||
|
||||
def test_messages_avatar_on_page(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/messages")
|
||||
avatar = page.locator("img.avatar-img").first
|
||||
assert avatar.is_visible()
|
||||
|
||||
@@ -3,13 +3,13 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def test_notifications_page_loads(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_mark_all_read(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
mark_all = page.locator("button:has-text('Mark all read')")
|
||||
if mark_all.is_visible():
|
||||
mark_all.click()
|
||||
@@ -18,13 +18,43 @@ def test_mark_all_read(alice):
|
||||
|
||||
def test_notifications_navigation(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/notifications", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_notifications_bell_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
page.goto(f"{BASE_URL}/feed")
|
||||
bell = page.locator(".topnav-icon").first
|
||||
assert bell.is_visible()
|
||||
|
||||
|
||||
def test_notifications_empty_state(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
empty = page.locator("text=No notifications yet")
|
||||
if empty.is_visible():
|
||||
pass
|
||||
else:
|
||||
assert page.is_visible("h2:has-text('Notifications')")
|
||||
|
||||
|
||||
def test_notifications_header_has_mark_all(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
mark_all = page.locator("button:has-text('Mark all read')")
|
||||
assert mark_all.is_visible()
|
||||
|
||||
|
||||
def test_notifications_topnav_user(alice):
|
||||
page, user = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
assert page.is_visible(f"text={user['username']}")
|
||||
|
||||
|
||||
def test_notifications_avatar_visible(alice):
|
||||
page, _ = alice
|
||||
page.goto(f"{BASE_URL}/notifications")
|
||||
avatar = page.locator("img.avatar-img").first
|
||||
assert avatar.is_visible()
|
||||
|
||||
@@ -3,14 +3,6 @@ from tests.conftest import BASE_URL
|
||||
|
||||
def create_post(page, topic="random", content="Test post content", title=None):
|
||||
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
|
||||
fab_count = page.locator(".feed-fab").count()
|
||||
if fab_count == 0:
|
||||
title = page.title()
|
||||
has_fab = page.evaluate("document.body.innerHTML.includes('feed-fab')")
|
||||
body_start = page.evaluate("document.body.innerHTML.substring(0, 500)")
|
||||
print(f"\n[CREATE_POST] No FAB! URL={page.url} Title={title} has_fab={has_fab}")
|
||||
print(f"[CREATE_POST] Body start: {body_start[:300]}")
|
||||
page.wait_for_timeout(2000)
|
||||
page.locator(".feed-fab").first.wait_for(state="visible", timeout=10000)
|
||||
page.locator(".feed-fab").first.click()
|
||||
page.check(f"input[value='{topic}']")
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
from devplacepy.utils import hash_password, verify_password, generate_uid, slugify, time_ago
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def test_hash_password_returns_string():
|
||||
result = hash_password("test123")
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 20
|
||||
|
||||
|
||||
def test_verify_password_correct():
|
||||
hashed = hash_password("correct-password")
|
||||
assert verify_password("correct-password", hashed) is True
|
||||
|
||||
|
||||
def test_verify_password_wrong():
|
||||
hashed = hash_password("correct-password")
|
||||
assert verify_password("wrong-password", hashed) is False
|
||||
|
||||
|
||||
def test_generate_uid_is_unique():
|
||||
uid1 = generate_uid()
|
||||
uid2 = generate_uid()
|
||||
assert uid1 != uid2
|
||||
|
||||
|
||||
def test_generate_uid_format():
|
||||
uid = generate_uid()
|
||||
assert isinstance(uid, str)
|
||||
assert len(uid) == 36 # UUID v4 format
|
||||
|
||||
|
||||
def test_slugify_basic():
|
||||
assert slugify("Hello World") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_special_chars():
|
||||
assert slugify("Hello! World???") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_multiple_dashes():
|
||||
assert slugify("hello---world") == "hello-world"
|
||||
|
||||
|
||||
def test_slugify_leading_trailing():
|
||||
assert slugify("--hello--") == "hello"
|
||||
|
||||
|
||||
def test_time_ago_just_now():
|
||||
now = datetime.utcnow().isoformat()
|
||||
result = time_ago(now)
|
||||
assert result == "just now"
|
||||
|
||||
|
||||
def test_time_ago_minutes():
|
||||
dt = (datetime.utcnow() - timedelta(minutes=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "m ago" in result
|
||||
|
||||
|
||||
def test_time_ago_hours():
|
||||
dt = (datetime.utcnow() - timedelta(hours=3)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "h ago" in result
|
||||
|
||||
|
||||
def test_time_ago_days():
|
||||
dt = (datetime.utcnow() - timedelta(days=5)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "d ago" in result
|
||||
|
||||
|
||||
def test_time_ago_months():
|
||||
dt = (datetime.utcnow() - timedelta(days=60)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "mo ago" in result
|
||||
|
||||
|
||||
def test_time_ago_years():
|
||||
dt = (datetime.utcnow() - timedelta(days=400)).isoformat()
|
||||
result = time_ago(dt)
|
||||
assert "y ago" in result
|
||||
Reference in New Issue
Block a user