2026-06-13 16:32:33 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
2026-06-05 20:34:03 +02:00
|
|
|
|
2026-06-13 16:32:33 +02:00
|
|
|
from datetime import datetime, timezone
|
2026-06-05 20:34:03 +02:00
|
|
|
from devplacepy.database import (
|
|
|
|
|
get_table,
|
|
|
|
|
get_users_by_uids,
|
|
|
|
|
get_vote_counts,
|
|
|
|
|
get_user_votes,
|
|
|
|
|
get_comment_counts_by_post_uids,
|
|
|
|
|
resolve_by_slug,
|
|
|
|
|
get_user_stars,
|
|
|
|
|
get_user_rank,
|
|
|
|
|
build_pagination,
|
|
|
|
|
)
|
|
|
|
|
from devplacepy.utils import generate_uid
|
2026-06-13 16:32:33 +02:00
|
|
|
def _now_db_helpers():
|
2026-06-05 20:34:03 +02:00
|
|
|
return datetime.now(timezone.utc).isoformat()
|
2026-06-13 16:32:33 +02:00
|
|
|
def _user_db_helpers():
|
2026-06-05 20:34:03 +02:00
|
|
|
uid = generate_uid()
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"username": f"dbh_{uid[:8]}",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
2026-06-09 18:48:08 +02:00
|
|
|
"email": f"{uid[:8]}@t.dev",
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 20:34:03 +02:00
|
|
|
return uid
|
|
|
|
|
def _post(owner, **extra):
|
|
|
|
|
uid = generate_uid()
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("posts").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"slug": f"{uid[:8]}-post",
|
|
|
|
|
"title": None,
|
|
|
|
|
"content": "db helper post body",
|
|
|
|
|
"topic": "random",
|
|
|
|
|
"project_uid": None,
|
|
|
|
|
"image": None,
|
|
|
|
|
"stars": 0,
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
**extra,
|
|
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 20:34:03 +02:00
|
|
|
return uid
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
def _comment(owner, target_uid):
|
|
|
|
|
uid = generate_uid()
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("comments").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": owner,
|
|
|
|
|
"target_uid": target_uid,
|
|
|
|
|
"target_type": "post",
|
|
|
|
|
"content": "db helper comment",
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
return uid
|
|
|
|
|
def _vote(target_type, target_uid, value):
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("votes").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"user_uid": generate_uid(),
|
|
|
|
|
"target_uid": target_uid,
|
|
|
|
|
"target_type": target_type,
|
|
|
|
|
"value": value,
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
2026-06-13 16:32:33 +02:00
|
|
|
from uuid import uuid4
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
from devplacepy.database import get_table
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
|
|
|
|
|
|
2026-06-05 20:34:03 +02:00
|
|
|
def test_get_users_by_uids_dedupes(local_db):
|
|
|
|
|
a, b = generate_uid(), generate_uid()
|
|
|
|
|
users = get_table("users")
|
2026-06-13 16:32:33 +02:00
|
|
|
users.insert({"uid": a, "username": f"u_{a[:6]}", "created_at": _now_db_helpers()})
|
|
|
|
|
users.insert({"uid": b, "username": f"u_{b[:6]}", "created_at": _now_db_helpers()})
|
2026-06-05 20:34:03 +02:00
|
|
|
result = get_users_by_uids([a, b, a])
|
|
|
|
|
assert set(result.keys()) == {a, b}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_vote_counts_groups_up_and_down(local_db):
|
|
|
|
|
target = generate_uid()
|
|
|
|
|
votes = get_table("votes")
|
|
|
|
|
for value in (1, 1, -1):
|
2026-06-09 18:48:08 +02:00
|
|
|
votes.insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"user_uid": generate_uid(),
|
|
|
|
|
"target_uid": target,
|
|
|
|
|
"target_type": "post",
|
|
|
|
|
"value": value,
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 20:34:03 +02:00
|
|
|
ups, downs = get_vote_counts([target])
|
|
|
|
|
assert ups[target] == 2
|
|
|
|
|
assert downs[target] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_user_votes_returns_user_value(local_db):
|
|
|
|
|
target = generate_uid()
|
|
|
|
|
user = generate_uid()
|
2026-06-09 18:48:08 +02:00
|
|
|
get_table("votes").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"user_uid": user,
|
|
|
|
|
"target_uid": target,
|
|
|
|
|
"target_type": "post",
|
|
|
|
|
"value": 1,
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 20:34:03 +02:00
|
|
|
assert get_user_votes(user, [target]) == {target: 1}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_comment_counts_by_post_uids(local_db):
|
|
|
|
|
post = generate_uid()
|
|
|
|
|
comments = get_table("comments")
|
|
|
|
|
for _ in range(2):
|
2026-06-09 18:48:08 +02:00
|
|
|
comments.insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"user_uid": generate_uid(),
|
|
|
|
|
"target_type": "post",
|
|
|
|
|
"target_uid": post,
|
|
|
|
|
"content": "hi",
|
2026-06-13 16:32:33 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
2026-06-14 09:48:10 +02:00
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
2026-06-09 18:48:08 +02:00
|
|
|
}
|
|
|
|
|
)
|
2026-06-05 20:34:03 +02:00
|
|
|
assert get_comment_counts_by_post_uids([post]) == {post: 2}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_resolve_by_slug_finds_by_slug_or_uid(local_db):
|
2026-06-13 16:32:33 +02:00
|
|
|
owner = _user_db_helpers()
|
2026-06-05 20:34:03 +02:00
|
|
|
uid = _post(owner)
|
|
|
|
|
posts = get_table("posts")
|
|
|
|
|
slug = posts.find_one(uid=uid)["slug"]
|
|
|
|
|
assert resolve_by_slug(posts, slug)["uid"] == uid
|
|
|
|
|
assert resolve_by_slug(posts, uid)["uid"] == uid
|
|
|
|
|
assert resolve_by_slug(posts, "missing-slug-xyz") is None
|
|
|
|
|
|
|
|
|
|
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
def test_get_user_stars_counts_all_votable_contributions(local_db):
|
2026-06-13 16:32:33 +02:00
|
|
|
user = _user_db_helpers()
|
feat: add api key auth, devii agent, openai gateway, and admin service management
This commit introduces a comprehensive set of new features including API key authentication with CLI management commands (get, reset, backfill), a Devii agentic assistant with WebSocket terminal and session bootstrap, an OpenAI-compatible LLM gateway service, and an admin service management panel. It also adds Playwright browser automation for bot support, configures internal gateway URLs, refactors content editing/deletion to support JSON API responses, and updates documentation across AGENTS.md, README.md, and the developer docs site.
2026-06-08 17:38:33 +02:00
|
|
|
post_uid = _post(user)
|
|
|
|
|
for _ in range(7):
|
|
|
|
|
_vote("post", post_uid, 1)
|
|
|
|
|
comment_uid = _comment(user, post_uid)
|
|
|
|
|
_vote("comment", comment_uid, 1)
|
|
|
|
|
_vote("comment", comment_uid, 1)
|
|
|
|
|
_vote("comment", comment_uid, -1)
|
|
|
|
|
assert get_user_stars(user) == 8
|
2026-06-05 20:34:03 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_user_rank_unknown_user_is_none(local_db):
|
|
|
|
|
assert get_user_rank(generate_uid()) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_build_pagination_metadata():
|
|
|
|
|
first = build_pagination(1, 100, 25)
|
|
|
|
|
assert first["total_pages"] == 4
|
|
|
|
|
assert first["has_prev"] is False
|
|
|
|
|
assert first["has_next"] is True
|
|
|
|
|
middle = build_pagination(2, 100, 25)
|
|
|
|
|
assert middle["has_prev"] is True
|
|
|
|
|
assert middle["has_next"] is True
|
|
|
|
|
clamped = build_pagination(99, 10, 25)
|
|
|
|
|
assert clamped["page"] == 1
|
|
|
|
|
assert clamped["total_pages"] == 1
|
2026-06-13 16:32:33 +02:00
|
|
|
|
|
|
|
|
|
2026-06-19 01:46:53 +02:00
|
|
|
def test_news_usage_accumulates_and_computes_averages(local_db):
|
|
|
|
|
from devplacepy.database import add_news_usage, get_news_usage
|
|
|
|
|
|
|
|
|
|
get_table("news_usage").delete()
|
|
|
|
|
empty = get_news_usage()
|
|
|
|
|
assert empty["calls"] == 0
|
|
|
|
|
assert empty["avg_tokens"] == 0.0
|
|
|
|
|
|
|
|
|
|
totals = {
|
|
|
|
|
"calls": 2,
|
|
|
|
|
"prompt_tokens": 2000,
|
|
|
|
|
"completion_tokens": 400,
|
|
|
|
|
"total_tokens": 2400,
|
|
|
|
|
"cost_usd": 0.0005,
|
|
|
|
|
"upstream_latency_ms": 1600.0,
|
|
|
|
|
"total_latency_ms": 1800.0,
|
|
|
|
|
}
|
|
|
|
|
add_news_usage(totals)
|
|
|
|
|
add_news_usage(totals)
|
|
|
|
|
|
|
|
|
|
usage = get_news_usage()
|
|
|
|
|
assert usage["calls"] == 4
|
|
|
|
|
assert usage["total_tokens"] == 4800
|
|
|
|
|
assert abs(usage["cost_usd"] - 0.001) < 1e-9
|
|
|
|
|
assert usage["avg_tokens"] == 1200.0
|
|
|
|
|
assert usage["avg_cost_usd"] == 0.00025
|
|
|
|
|
assert usage["avg_tokens_per_second"] == 250.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_news_usage_ignores_zero_call_batches(local_db):
|
|
|
|
|
from devplacepy.database import add_news_usage, get_news_usage
|
|
|
|
|
|
|
|
|
|
get_table("news_usage").delete()
|
|
|
|
|
add_news_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
|
|
|
|
|
assert get_news_usage()["calls"] == 0
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 13:59:40 +02:00
|
|
|
def test_issue_usage_accumulates_and_computes_averages(local_db):
|
|
|
|
|
from devplacepy.database import add_issue_usage, get_issue_usage
|
|
|
|
|
|
|
|
|
|
get_table("issue_usage").delete()
|
|
|
|
|
empty = get_issue_usage()
|
|
|
|
|
assert empty["calls"] == 0
|
|
|
|
|
assert empty["avg_tokens"] == 0.0
|
|
|
|
|
|
|
|
|
|
totals = {
|
|
|
|
|
"calls": 2,
|
|
|
|
|
"prompt_tokens": 2000,
|
|
|
|
|
"completion_tokens": 400,
|
|
|
|
|
"total_tokens": 2400,
|
|
|
|
|
"cost_usd": 0.0005,
|
|
|
|
|
"upstream_latency_ms": 1600.0,
|
|
|
|
|
"total_latency_ms": 1800.0,
|
|
|
|
|
}
|
|
|
|
|
add_issue_usage(totals)
|
|
|
|
|
add_issue_usage(totals)
|
|
|
|
|
|
|
|
|
|
usage = get_issue_usage()
|
|
|
|
|
assert usage["calls"] == 4
|
|
|
|
|
assert usage["total_tokens"] == 4800
|
|
|
|
|
assert abs(usage["cost_usd"] - 0.001) < 1e-9
|
|
|
|
|
assert usage["avg_tokens"] == 1200.0
|
|
|
|
|
assert usage["avg_cost_usd"] == 0.00025
|
|
|
|
|
assert usage["avg_tokens_per_second"] == 250.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_issue_usage_ignores_zero_call_batches(local_db):
|
|
|
|
|
from devplacepy.database import add_issue_usage, get_issue_usage
|
|
|
|
|
|
|
|
|
|
get_table("issue_usage").delete()
|
|
|
|
|
add_issue_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
|
|
|
|
|
assert get_issue_usage()["calls"] == 0
|
|
|
|
|
|
|
|
|
|
|
2026-06-19 22:15:22 +02:00
|
|
|
def test_seo_usage_accumulates_and_computes_averages(local_db):
|
|
|
|
|
from devplacepy.database import add_seo_usage, get_seo_usage
|
|
|
|
|
|
|
|
|
|
get_table("seo_usage").delete()
|
|
|
|
|
empty = get_seo_usage()
|
|
|
|
|
assert empty["calls"] == 0
|
|
|
|
|
assert empty["avg_tokens"] == 0.0
|
|
|
|
|
assert empty["avg_cost_usd"] == 0.0
|
|
|
|
|
|
|
|
|
|
totals = {
|
|
|
|
|
"calls": 2,
|
|
|
|
|
"prompt_tokens": 2000,
|
|
|
|
|
"completion_tokens": 400,
|
|
|
|
|
"total_tokens": 2400,
|
|
|
|
|
"cost_usd": 0.0005,
|
|
|
|
|
"upstream_latency_ms": 1600.0,
|
|
|
|
|
"total_latency_ms": 1800.0,
|
|
|
|
|
}
|
|
|
|
|
add_seo_usage(totals)
|
|
|
|
|
add_seo_usage(totals)
|
|
|
|
|
|
|
|
|
|
usage = get_seo_usage()
|
|
|
|
|
assert usage["calls"] == 4
|
|
|
|
|
assert usage["total_tokens"] == 4800
|
|
|
|
|
assert abs(usage["cost_usd"] - 0.001) < 1e-9
|
|
|
|
|
assert usage["avg_tokens"] == 1200.0
|
|
|
|
|
assert usage["avg_cost_usd"] == 0.00025
|
|
|
|
|
assert usage["avg_tokens_per_second"] == 250.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_add_seo_usage_ignores_zero_call_batches(local_db):
|
|
|
|
|
from devplacepy.database import add_seo_usage, get_seo_usage
|
|
|
|
|
|
|
|
|
|
get_table("seo_usage").delete()
|
|
|
|
|
add_seo_usage({"calls": 0, "total_tokens": 999, "cost_usd": 1.0})
|
|
|
|
|
assert get_seo_usage()["calls"] == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upsert_seo_metadata_inserts_then_updates_single_row(local_db):
|
|
|
|
|
from devplacepy.database import (
|
|
|
|
|
get_seo_metadata,
|
|
|
|
|
has_fresh_seo_metadata,
|
|
|
|
|
upsert_seo_metadata,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
target_uid = generate_uid()
|
|
|
|
|
table = get_table("seo_metadata")
|
|
|
|
|
|
|
|
|
|
upsert_seo_metadata(
|
|
|
|
|
"post", target_uid, "Plain Title", "Plain description", "alpha, beta", "pending", "plain"
|
|
|
|
|
)
|
|
|
|
|
assert has_fresh_seo_metadata("post", target_uid) is False
|
|
|
|
|
assert get_seo_metadata("post", target_uid) is None
|
|
|
|
|
|
|
|
|
|
upsert_seo_metadata(
|
|
|
|
|
"post", target_uid, "AI Title", "AI description", "python, sqlite", "ready", "ai"
|
|
|
|
|
)
|
|
|
|
|
ready = get_seo_metadata("post", target_uid)
|
|
|
|
|
assert ready is not None
|
|
|
|
|
assert ready["seo_title"] == "AI Title"
|
|
|
|
|
assert ready["seo_keywords"] == "python, sqlite"
|
|
|
|
|
assert ready["generated_at"]
|
|
|
|
|
assert ready["deleted_at"] is None
|
|
|
|
|
assert has_fresh_seo_metadata("post", target_uid) is True
|
|
|
|
|
|
|
|
|
|
rows = list(table.find(target_type="post", target_uid=target_uid))
|
|
|
|
|
assert len(rows) == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_seo_metadata_batch_returns_only_ready_live_rows(local_db):
|
|
|
|
|
from devplacepy.database import (
|
|
|
|
|
get_seo_metadata_batch,
|
|
|
|
|
upsert_seo_metadata,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
ready_uid = generate_uid()
|
|
|
|
|
pending_uid = generate_uid()
|
|
|
|
|
upsert_seo_metadata("gist", ready_uid, "T", "D", "k1, k2", "ready", "ai")
|
|
|
|
|
upsert_seo_metadata("gist", pending_uid, "T", "D", "k1, k2", "pending", "plain")
|
|
|
|
|
|
|
|
|
|
batch = get_seo_metadata_batch("gist", [ready_uid, pending_uid])
|
|
|
|
|
assert ready_uid in batch
|
|
|
|
|
assert pending_uid not in batch
|
|
|
|
|
assert batch[ready_uid]["status"] == "ready"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_seo_metadata_stale_drops_ready_status(local_db):
|
|
|
|
|
from devplacepy.database import (
|
|
|
|
|
get_seo_metadata,
|
|
|
|
|
mark_seo_metadata_stale,
|
|
|
|
|
upsert_seo_metadata,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
target_uid = generate_uid()
|
|
|
|
|
upsert_seo_metadata("project", target_uid, "T", "D", "k", "ready", "ai")
|
|
|
|
|
assert get_seo_metadata("project", target_uid) is not None
|
|
|
|
|
|
|
|
|
|
mark_seo_metadata_stale("project", target_uid)
|
|
|
|
|
assert get_seo_metadata("project", target_uid) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_seo_metadata_is_soft_deletable_and_indexed(local_db):
|
|
|
|
|
from devplacepy.database import SOFT_DELETE_TABLES
|
|
|
|
|
|
|
|
|
|
assert "seo_metadata" in SOFT_DELETE_TABLES
|
|
|
|
|
columns = get_table("seo_metadata").columns
|
|
|
|
|
assert "deleted_at" in columns
|
|
|
|
|
assert "deleted_by" in columns
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 18:50:30 +02:00
|
|
|
def test_interleave_by_author_spreads_consecutive_runs():
|
|
|
|
|
from devplacepy.database import interleave_by_author
|
2026-06-13 16:32:33 +02:00
|
|
|
|
|
|
|
|
rows = [
|
|
|
|
|
{"uid": 1, "user_uid": "a"},
|
|
|
|
|
{"uid": 2, "user_uid": "a"},
|
|
|
|
|
{"uid": 3, "user_uid": "a"},
|
|
|
|
|
{"uid": 4, "user_uid": "b"},
|
2026-06-14 18:50:30 +02:00
|
|
|
{"uid": 5, "user_uid": "b"},
|
2026-06-13 16:32:33 +02:00
|
|
|
{"uid": 6, "user_uid": "c"},
|
|
|
|
|
]
|
2026-06-14 18:50:30 +02:00
|
|
|
spread = interleave_by_author(rows)
|
|
|
|
|
authors = [r["user_uid"] for r in spread]
|
|
|
|
|
assert len(spread) == len(rows)
|
|
|
|
|
assert all(authors[i] != authors[i + 1] for i in range(len(authors) - 1))
|
2026-06-13 16:32:33 +02:00
|
|
|
|
|
|
|
|
|
2026-06-14 18:50:30 +02:00
|
|
|
def test_interleave_by_author_preserves_per_author_order():
|
|
|
|
|
from devplacepy.database import interleave_by_author
|
2026-06-13 16:32:33 +02:00
|
|
|
|
2026-06-14 18:50:30 +02:00
|
|
|
rows = [
|
|
|
|
|
{"uid": 1, "user_uid": "a"},
|
|
|
|
|
{"uid": 2, "user_uid": "a"},
|
|
|
|
|
{"uid": 3, "user_uid": "a"},
|
|
|
|
|
{"uid": 4, "user_uid": "b"},
|
|
|
|
|
]
|
|
|
|
|
spread = interleave_by_author(rows)
|
|
|
|
|
a_order = [r["uid"] for r in spread if r["user_uid"] == "a"]
|
|
|
|
|
assert a_order == [1, 2, 3]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_interleave_by_author_single_author_keeps_order():
|
|
|
|
|
from devplacepy.database import interleave_by_author
|
|
|
|
|
|
|
|
|
|
rows = [{"uid": i, "user_uid": "a"} for i in range(4)]
|
|
|
|
|
spread = interleave_by_author(rows)
|
|
|
|
|
assert [r["uid"] for r in spread] == [0, 1, 2, 3]
|
2026-06-19 10:06:09 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _relation(actor_uid, target_uid, kind):
|
|
|
|
|
get_table("user_relations").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": generate_uid(),
|
|
|
|
|
"user_uid": actor_uid,
|
|
|
|
|
"target_uid": target_uid,
|
|
|
|
|
"kind": kind,
|
|
|
|
|
"created_at": _now_db_helpers(),
|
|
|
|
|
"deleted_at": None,
|
|
|
|
|
"deleted_by": None,
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_user_relations_partitions_both_kinds(local_db):
|
|
|
|
|
from devplacepy.database import get_user_relations, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
actor = _user_db_helpers()
|
|
|
|
|
blocked = _user_db_helpers()
|
|
|
|
|
muted = _user_db_helpers()
|
|
|
|
|
_relation(actor, blocked, "block")
|
|
|
|
|
_relation(actor, muted, "mute")
|
|
|
|
|
invalidate_user_relations(actor)
|
|
|
|
|
|
|
|
|
|
relations = get_user_relations(actor)
|
|
|
|
|
assert relations["block"] == frozenset({blocked})
|
|
|
|
|
assert relations["mute"] == frozenset({muted})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_user_relations_anonymous_is_empty(local_db):
|
|
|
|
|
from devplacepy.database import get_user_relations
|
|
|
|
|
|
|
|
|
|
relations = get_user_relations(None)
|
|
|
|
|
assert relations["block"] == frozenset()
|
|
|
|
|
assert relations["mute"] == frozenset()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_silenced_uids_is_union(local_db):
|
|
|
|
|
from devplacepy.database import get_silenced_uids, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
actor = _user_db_helpers()
|
|
|
|
|
blocked = _user_db_helpers()
|
|
|
|
|
muted = _user_db_helpers()
|
|
|
|
|
_relation(actor, blocked, "block")
|
|
|
|
|
_relation(actor, muted, "mute")
|
|
|
|
|
invalidate_user_relations(actor)
|
|
|
|
|
|
|
|
|
|
assert get_silenced_uids(actor) == frozenset({blocked, muted})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_soft_deleted_relation_is_ignored(local_db):
|
|
|
|
|
from devplacepy.database import get_blocked_uids, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
actor = _user_db_helpers()
|
|
|
|
|
target = _user_db_helpers()
|
|
|
|
|
_relation(actor, target, "block")
|
|
|
|
|
invalidate_user_relations(actor)
|
|
|
|
|
assert get_blocked_uids(actor) == frozenset({target})
|
|
|
|
|
|
|
|
|
|
row = get_table("user_relations").find_one(user_uid=actor, target_uid=target)
|
|
|
|
|
get_table("user_relations").update(
|
|
|
|
|
{"id": row["id"], "deleted_at": _now_db_helpers()}, ["id"]
|
|
|
|
|
)
|
|
|
|
|
invalidate_user_relations(actor)
|
|
|
|
|
assert get_blocked_uids(actor) == frozenset()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_paginate_excludes_blocked_authors(local_db):
|
|
|
|
|
from devplacepy.database import paginate, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
viewer = _user_db_helpers()
|
|
|
|
|
blocked = _user_db_helpers()
|
|
|
|
|
visible = _user_db_helpers()
|
|
|
|
|
blocked_post = _post(blocked)
|
|
|
|
|
visible_post = _post(visible)
|
|
|
|
|
_relation(viewer, blocked, "block")
|
|
|
|
|
invalidate_user_relations(viewer)
|
|
|
|
|
|
|
|
|
|
rows, _ = paginate(get_table("posts"), viewer_uid=viewer)
|
|
|
|
|
uids = {r["uid"] for r in rows}
|
|
|
|
|
assert visible_post in uids
|
|
|
|
|
assert blocked_post not in uids
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_paginate_without_viewer_keeps_all(local_db):
|
|
|
|
|
from devplacepy.database import paginate, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
viewer = _user_db_helpers()
|
|
|
|
|
blocked = _user_db_helpers()
|
|
|
|
|
blocked_post = _post(blocked)
|
|
|
|
|
_relation(viewer, blocked, "block")
|
|
|
|
|
invalidate_user_relations(viewer)
|
|
|
|
|
|
|
|
|
|
rows, _ = paginate(get_table("posts"), viewer_uid=None)
|
|
|
|
|
assert blocked_post in {r["uid"] for r in rows}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_load_comments_drops_blocked_author(local_db):
|
|
|
|
|
from devplacepy.database import load_comments, invalidate_user_relations
|
|
|
|
|
|
|
|
|
|
viewer = _user_db_helpers()
|
|
|
|
|
host = _user_db_helpers()
|
|
|
|
|
blocked = _user_db_helpers()
|
|
|
|
|
post_uid = _post(host)
|
|
|
|
|
_comment(blocked, post_uid)
|
|
|
|
|
_relation(viewer, blocked, "block")
|
|
|
|
|
invalidate_user_relations(viewer)
|
|
|
|
|
|
|
|
|
|
viewer_row = get_table("users").find_one(uid=viewer)
|
|
|
|
|
comments = load_comments("post", post_uid, viewer_row)
|
|
|
|
|
assert comments == []
|
|
|
|
|
|
|
|
|
|
others = load_comments("post", post_uid, None)
|
|
|
|
|
assert len(others) == 1
|
feat: add author-username search to feed/gists/projects listings and partial-index migration
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
2026-06-20 00:24:51 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_get_uids_by_username_match_returns_matching_uids(local_db):
|
|
|
|
|
from devplacepy.database import get_uids_by_username_match
|
|
|
|
|
|
|
|
|
|
target = generate_uid()
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": target,
|
|
|
|
|
"username": "searchablehero",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
feat: add author-username search to feed/gists/projects listings and partial-index migration
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
2026-06-20 00:24:51 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
assert target in get_uids_by_username_match("searchablehero")
|
|
|
|
|
assert target in get_uids_by_username_match("hablehe")
|
|
|
|
|
assert get_uids_by_username_match("") == []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_text_search_clause_matches_author_username(local_db):
|
|
|
|
|
from devplacepy.database import text_search_clause
|
|
|
|
|
|
|
|
|
|
author = generate_uid()
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": author,
|
|
|
|
|
"username": "authorquery",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
feat: add author-username search to feed/gists/projects listings and partial-index migration
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
2026-06-20 00:24:51 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
post_uid = _post(author, title="unrelated title", content="unrelated body")
|
|
|
|
|
posts = get_table("posts")
|
|
|
|
|
|
|
|
|
|
clause = text_search_clause(
|
|
|
|
|
posts, "authorquery", ("title", "content"), author_field="user_uid"
|
|
|
|
|
)
|
|
|
|
|
assert clause is not None
|
|
|
|
|
rows = list(posts.find(clause, deleted_at=None))
|
|
|
|
|
assert post_uid in {r["uid"] for r in rows}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_text_search_clause_without_author_field_ignores_username(local_db):
|
|
|
|
|
from devplacepy.database import text_search_clause
|
|
|
|
|
|
|
|
|
|
author = generate_uid()
|
|
|
|
|
get_table("users").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": author,
|
|
|
|
|
"username": "lonelyauthor",
|
Add the trust and safety subsystem and the App Store compliance work
Implements the moderation and consent obligations a social platform carries,
so the web version and any client that speaks to it enforce the same rules.
Moderation core (services/moderation/, database/moderation.py): a reportable
target registry, the content filter and its choke points, the report queue with
atomic resolution, enforcement actions, consent tracking, maturity gating, and
account deletion with a grace window.
Surfaces: POST /reports plus the member report list, /admin/moderation and the
per-report admin view, /workspaces, terms acceptance at /auth/terms, consent and
account deletion under /profile, the report button and dialog partials, the
maturity gate, and the moderation stylesheet and ReportDialog client.
Every user-generated surface stays reportable by construction: new content tables
are registered in REPORTABLE_TARGETS or listed in UNREPORTABLE_TABLES with a
reason, and the registry test fails the suite on anything left unclassified.
Docs: community guidelines, content moderation, intellectual property, privacy,
terms, contact, and the admin-only moderation operations page, plus the
moderation API group and the Devii moderation actions.
Compliance record: applecomp.md is the requirement register, applechanges.md the
gap analysis against this codebase, and appleimpl.md the implementation design
they resolve to.
Tests cover the report flow, admin moderation, consent, account deletion, terms
acceptance, workspaces, and the registry invariant across the unit, api, and e2e
tiers.
2026-08-09 00:18:20 +02:00
|
|
|
"terms_version": "1",
|
feat: add author-username search to feed/gists/projects listings and partial-index migration
Extend `database.text_search_clause` with an `author_field` parameter that resolves username matches to user UIDs, enabling author-username search across the three public listings (`/feed`, `/gists`, `/projects`). Update the corresponding API docs and README route descriptions to reflect the new search scope. Add six partial indexes (`idx_comments_target_live`, `idx_votes_target_live`, `idx_reactions_target_live`, `idx_gists_live_created`, `idx_projects_live_created`, `idx_attachments_user_created_live`) to optimize filtered queries on non-deleted rows. Introduce a hot-settings cache (`_hot_settings`) with a 2-second TTL for `maintenance_mode`, `rate_limit_per_minute`, and `rate_limit_window_seconds`, plus a periodic rate-limit store sweep (`_sweep_rate_limit_store`) to evict stale IP entries every 60 seconds. Add `GZipMiddleware` to the FastAPI app for response compression.
2026-06-20 00:24:51 +02:00
|
|
|
"created_at": _now_db_helpers(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
_post(author, title="nothing here", content="nothing here either")
|
|
|
|
|
posts = get_table("posts")
|
|
|
|
|
|
|
|
|
|
clause = text_search_clause(posts, "lonelyauthor", ("title", "content"))
|
|
|
|
|
rows = list(posts.find(clause, deleted_at=None)) if clause is not None else []
|
|
|
|
|
assert all(r["user_uid"] != author for r in rows)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_text_search_clause_still_matches_text_fields(local_db):
|
|
|
|
|
from devplacepy.database import text_search_clause
|
|
|
|
|
|
|
|
|
|
author = _user_db_helpers()
|
|
|
|
|
post_uid = _post(author, title="distinctivekeyword", content="body text")
|
|
|
|
|
posts = get_table("posts")
|
|
|
|
|
|
|
|
|
|
clause = text_search_clause(
|
|
|
|
|
posts, "distinctivekeyword", ("title", "content"), author_field="user_uid"
|
|
|
|
|
)
|
|
|
|
|
assert clause is not None
|
|
|
|
|
rows = list(posts.find(clause, deleted_at=None))
|
|
|
|
|
assert post_uid in {r["uid"] for r in rows}
|
feat: add per-user avatar seed regeneration with irreversible random avatar replacement
Implement a new `avatar_seed` column on the users table that overrides the username-based seed for Multiavatar generation. Introduce a null-safe `avatar_seed(user)` choke point in `avatar.py` that resolves `user.get("avatar_seed") or user.get("username")`, registered as a Jinja global so every render site (`_avatar_link.html`, `avatar_url(...)` calls, SEO `og_image`, issues ad-hoc dicts, devRant payload/PNG) propagates a regenerated seed. Add `POST /profile/{username}/regenerate-avatar` endpoint (owner-or-admin only) that writes a fresh `generate_uid()` to `avatar_seed`, invalidates the target's user cache, and audits `profile.avatar.regenerate`. The previous seed is overwritten and never stored, making regeneration irreversible. Document the feature in `AGENTS.md` and `README.md`, add the API endpoint to `docs_api.py`, and include the `regenerate_avatar` Devii tool in `CONFIRM_REQUIRED`.
2026-06-28 00:31:34 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
def _notification(user_uid, target_url, read=False):
|
|
|
|
|
uid = generate_uid()
|
|
|
|
|
get_table("notifications").insert(
|
|
|
|
|
{
|
|
|
|
|
"uid": uid,
|
|
|
|
|
"user_uid": user_uid,
|
|
|
|
|
"type": "comment",
|
|
|
|
|
"message": "someone replied",
|
|
|
|
|
"related_uid": generate_uid(),
|
|
|
|
|
"target_url": target_url,
|
|
|
|
|
"read": read,
|
|
|
|
|
"created_at": _now_db_helpers(),
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
return uid
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _is_read(uid):
|
|
|
|
|
return bool(get_table("notifications").find_one(uid=uid)["read"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_marks_exact_url(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
user = _user_db_helpers()
|
|
|
|
|
uid = _notification(user, "/posts/abc-post")
|
|
|
|
|
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
|
|
|
|
|
assert _is_read(uid) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_matches_anchor_prefix(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
user = _user_db_helpers()
|
|
|
|
|
uid = _notification(user, "/posts/abc-post#comment-5")
|
|
|
|
|
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 1
|
|
|
|
|
assert _is_read(uid) is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_leaves_other_targets(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
user = _user_db_helpers()
|
|
|
|
|
keep = _notification(user, "/posts/other-post")
|
|
|
|
|
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0
|
|
|
|
|
assert _is_read(keep) is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_only_unread_counted(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
user = _user_db_helpers()
|
|
|
|
|
_notification(user, "/posts/abc-post", read=True)
|
|
|
|
|
assert mark_notifications_read_by_target(user, "/posts/abc-post") == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_empty_args(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
user = _user_db_helpers()
|
|
|
|
|
assert mark_notifications_read_by_target("", "/posts/abc") == 0
|
|
|
|
|
assert mark_notifications_read_by_target(user, "") == 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_mark_notifications_scoped_per_user(local_db):
|
|
|
|
|
from devplacepy.database import mark_notifications_read_by_target
|
|
|
|
|
|
|
|
|
|
owner = _user_db_helpers()
|
|
|
|
|
other = _user_db_helpers()
|
|
|
|
|
other_uid = _notification(other, "/posts/abc-post")
|
|
|
|
|
assert mark_notifications_read_by_target(owner, "/posts/abc-post") == 0
|
|
|
|
|
assert _is_read(other_uid) is False
|