refactor(core): the core's owner filters go through userscope, each held by a second account

Tokens, notifications, quota, debug sessions and the reset-link burn filtered
their owner inline. Ten of the fifteen filters had no test that noticed one
going missing; each now has a test where another account's rows must not
count, show or change.
This commit is contained in:
2026-10-19 23:36:01 +02:00
parent 531eb2eef1
commit ad6ccbac05
11 changed files with 164 additions and 15 deletions
+135
View File
@@ -0,0 +1,135 @@
"""The core's owner filters, each held by a second account whose rows must not count, show or change.
Every one of these reads or writes a user-owned table through `userscope.owned_by`; the fence holds
the form (no inline `Model.user_id == …`), these hold that the filter is there at all. Each test gives
a second account the same kind of rows, so a filter that went missing answers with theirs. DB lane.
"""
from datetime import datetime, timedelta, timezone
from sqlalchemy import func, select
from app import debugsession, notifications, quota
from app.models import DebugScreenshot, DebugSession, Notification, QuotaEvent
from app.routes import tokens as tokens_routes
from app.routes.auth import RESET_TTL, _issue_token
def _notify(db, user, n=1, *, read=False):
for i in range(n):
db.add(Notification(user_id=user.id, type="t", title=f"n{i}", read=read))
db.commit()
def _visible(db, user) -> int:
return db.scalar(
select(func.count())
.select_from(Notification)
.where(Notification.user_id == user.id, Notification.dismissed.is_(False))
)
def test_the_notification_list_shows_only_mine(db, make_user, as_user):
me, other = make_user(), make_user()
_notify(db, me)
_notify(db, other, 2)
body = as_user(me).get("/api/me/notifications").json()
assert (body["total"], len(body["items"])) == (1, 1)
def test_the_unread_count_counts_only_mine(db, make_user, as_user):
me, other = make_user(), make_user()
_notify(db, me)
_notify(db, other, 2)
assert as_user(me).get("/api/me/notifications/unread_count").json() == {"count": 1}
def test_reading_all_reads_only_mine(db, make_user, as_user):
me, other = make_user(), make_user()
_notify(db, me)
_notify(db, other, 2)
assert as_user(me).post("/api/me/notifications/read_all").status_code == 200
assert as_user(other).get("/api/me/notifications/unread_count").json() == {"count": 2}
def test_clearing_all_dismisses_only_mine(db, make_user, as_user):
me, other = make_user(), make_user()
_notify(db, me)
_notify(db, other, 2)
assert as_user(me).post("/api/me/notifications/clear").status_code == 200
db.expire_all()
assert (_visible(db, me), _visible(db, other)) == (0, 2)
def test_trimming_read_notifications_trims_only_mine(db, make_user):
me, other = make_user(), make_user()
_notify(db, me, 3, read=True)
_notify(db, other, 3, read=True)
assert notifications.trim_read(db, me.id, cap=1) == 2
db.commit()
assert _visible(db, other) == 3
def test_todays_actions_count_only_mine(db, make_user):
me, other = make_user(), make_user()
quota.log_action(me.id, quota.QuotaAction.VIDEOS_SEARCH)
quota.log_action(other.id, quota.QuotaAction.VIDEOS_SEARCH)
quota.log_action(other.id, quota.QuotaAction.VIDEOS_SEARCH)
assert quota.actions_today(db, me.id, quota.QuotaAction.VIDEOS_SEARCH) == 1
def test_my_usage_sums_only_mine(db, make_user, as_user):
me, other = make_user(), make_user()
db.add_all(
[
QuotaEvent(user_id=me.id, action="videos.list", units=5),
QuotaEvent(user_id=other.id, action="videos.list", units=7),
]
)
db.commit()
body = as_user(me).get("/api/quota/my-usage").json()
assert (body["today"], body["all_time"], body["by_action"]) == (5, 5, {"videos.list": 5})
def test_the_token_ceiling_counts_only_mine(db, make_user, as_user, monkeypatch):
monkeypatch.setattr(tokens_routes, "MAX_TOKENS", 1)
me, other = make_user(), make_user()
first = as_user(other).post("/api/tokens", json={"name": "theirs", "preset": "hub"})
assert first.status_code == 200, first.text
r = as_user(me).post("/api/tokens", json={"name": "mine", "preset": "hub"})
assert r.status_code == 200, r.text
def test_a_reset_burns_only_my_other_links(client, db, make_user):
me, other = make_user(), make_user()
mine = _issue_token(db, me, "reset", RESET_TTL)
theirs = _issue_token(db, other, "reset", RESET_TTL)
db.commit()
r = client.post(
"/auth/password-reset/confirm", json={"token": mine, "password": "a brand new passphrase"}
)
assert r.json() == {"ok": True}
assert client.post("/auth/password-reset/check", json={"token": theirs}).json() == {"ok": True}
def test_screenshot_storage_counts_only_mine(db, make_user):
me, other = make_user(), make_user()
now = datetime.now(timezone.utc)
for i, (user, size) in enumerate([(me, 10), (other, 1000)]):
db.add(
DebugSession(
id=f"s{i}",
user_id=user.id,
level="DEBUG",
started_at=now,
until=now + timedelta(hours=1),
)
)
db.flush()
db.add(
DebugScreenshot(
session_id=f"s{i}", created_at=now, mime="image/png", size=size, data=b"x" * size
)
)
db.commit()
assert debugsession.stored_screenshot_bytes(db, me.id) == 10