2026-06-22 18:41:53 +02:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
import time
|
|
|
|
|
|
|
|
|
|
import requests
|
|
|
|
|
|
|
|
|
|
from devplacepy.database import get_table
|
|
|
|
|
from devplacepy.services.game import economy, store
|
|
|
|
|
from tests.conftest import BASE_URL
|
|
|
|
|
|
|
|
|
|
JSON = {"Accept": "application/json"}
|
|
|
|
|
_counter = [0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _signup():
|
|
|
|
|
_counter[0] += 1
|
|
|
|
|
name = f"gapi{int(time.time() * 1000)}{_counter[0]}"
|
|
|
|
|
session = requests.Session()
|
|
|
|
|
session.post(
|
|
|
|
|
f"{BASE_URL}/auth/signup",
|
|
|
|
|
data={
|
|
|
|
|
"username": name,
|
|
|
|
|
"email": f"{name}@t.dev",
|
|
|
|
|
"password": "secret123",
|
|
|
|
|
"confirm_password": "secret123",
|
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
|
|
|
"birth_date": "1990-01-01",
|
|
|
|
|
"accept_terms": "1",
|
2026-06-22 18:41:53 +02:00
|
|
|
},
|
|
|
|
|
allow_redirects=True,
|
|
|
|
|
)
|
|
|
|
|
return session, name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _reset_farm(username, coins=100000):
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
if farm:
|
|
|
|
|
get_table("game_plots").delete(farm_uid=farm["uid"])
|
|
|
|
|
get_table("game_quests").delete(farm_uid=farm["uid"])
|
|
|
|
|
get_table("game_farms").delete(uid=farm["uid"])
|
2026-07-21 03:36:29 +02:00
|
|
|
get_table("game_market_ticks").delete()
|
2026-06-22 18:41:53 +02:00
|
|
|
farm = store.ensure_farm(user["uid"])
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], "coins": coins}, ["uid"])
|
|
|
|
|
return user
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_state_requires_auth(app_server, seeded_db):
|
|
|
|
|
response = requests.get(f"{BASE_URL}/game/state", headers=JSON)
|
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_state_returns_farm_json(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
response = session.get(f"{BASE_URL}/game/state", headers=JSON)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
farm = response.json()["farm"]
|
|
|
|
|
for key in (
|
|
|
|
|
"coins",
|
|
|
|
|
"level",
|
|
|
|
|
"ci_tier",
|
|
|
|
|
"plots",
|
|
|
|
|
"crops",
|
|
|
|
|
"perks",
|
|
|
|
|
"quests",
|
|
|
|
|
"prestige",
|
|
|
|
|
"streak",
|
|
|
|
|
"daily_available",
|
|
|
|
|
):
|
|
|
|
|
assert key in farm
|
|
|
|
|
assert len(farm["crops"]) == len(economy.CROPS)
|
|
|
|
|
assert len(farm["perks"]) == len(economy.PERKS)
|
|
|
|
|
assert len(farm["quests"]) == economy.DAILY_QUEST_COUNT
|
|
|
|
|
|
|
|
|
|
|
feat: add telegram notification channel with outbox service and per-user preferences
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
2026-06-22 22:52:02 +02:00
|
|
|
def _set_farm(username, **fields):
|
|
|
|
|
user = get_table("users").find_one(username=username)
|
|
|
|
|
farm = store.get_farm(user["uid"])
|
|
|
|
|
get_table("game_farms").update({"uid": farm["uid"], **fields}, ["uid"])
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def test_leaderboard_is_public_json(app_server, seeded_db):
|
|
|
|
|
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
assert "entries" in response.json()
|
|
|
|
|
|
|
|
|
|
|
feat: add telegram notification channel with outbox service and per-user preferences
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
2026-06-22 22:52:02 +02:00
|
|
|
def test_leaderboard_entries_carry_score_and_prestige(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
_set_farm(name, prestige=50, total_harvests=20, ci_tier=3)
|
Full test suite is now the mandatory final validation; fix everything it surfaced
- Policy: every change ends with make test (all tiers, all tests) green; docs and agent guardrails updated accordingly
- schema.py: ensure the full filtered/indexed column set of instances via get_table (ingress_slug, ports_json, container_gateway, slug, status, ...) so a partial first insert can never break the ingress proxy
- docs_api: award body param location body -> json; gateway endpoints documented public -> user to match enforced auth
- tests: missing get_table import (trash restore), audit read as admin (award), deterministic online-roster and leaderboard-cache handling, container visibility updated to the primary-admin-only rule, scoped AI usage heading selector past the hidden Tools nav links
2026-07-22 23:54:42 +02:00
|
|
|
deadline = time.time() + 20
|
|
|
|
|
while True:
|
|
|
|
|
response = requests.get(f"{BASE_URL}/game/leaderboard", headers=JSON)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
entries = response.json()["entries"]
|
|
|
|
|
mine = next(
|
|
|
|
|
(entry for entry in entries if entry["username"] == name), None
|
|
|
|
|
)
|
|
|
|
|
if mine is not None or time.time() >= deadline:
|
|
|
|
|
break
|
|
|
|
|
time.sleep(0.5)
|
feat: add telegram notification channel with outbox service and per-user preferences
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
2026-06-22 22:52:02 +02:00
|
|
|
assert entries
|
|
|
|
|
scores = [entry["score"] for entry in entries]
|
|
|
|
|
assert scores == sorted(scores, reverse=True)
|
|
|
|
|
for entry in entries:
|
|
|
|
|
assert "score" in entry and "prestige" in entry
|
Full test suite is now the mandatory final validation; fix everything it surfaced
- Policy: every change ends with make test (all tiers, all tests) green; docs and agent guardrails updated accordingly
- schema.py: ensure the full filtered/indexed column set of instances via get_table (ingress_slug, ports_json, container_gateway, slug, status, ...) so a partial first insert can never break the ingress proxy
- docs_api: award body param location body -> json; gateway endpoints documented public -> user to match enforced auth
- tests: missing get_table import (trash restore), audit read as admin (award), deterministic online-roster and leaderboard-cache handling, container visibility updated to the primary-admin-only rule, scoped AI usage heading selector past the hidden Tools nav links
2026-07-22 23:54:42 +02:00
|
|
|
assert mine is not None
|
feat: add telegram notification channel with outbox service and per-user preferences
Extend the notification system with a third channel (telegram) alongside existing in_app and push channels. Add `telegram_enabled` column to `notification_preferences` table, update `NOTIFICATION_CHANNELS` and `_NOTIFICATION_CHANNEL_COLUMNS` mappings, and set telegram default to off (`_NOTIFICATION_CHANNEL_DEFAULTS`). Create `telegram_outbox` table with columns for uid, user_uid, chat_id, text, status, attempts, created_at, and sent_at, plus an index on status/id for efficient polling. Register `TelegramOutboxService` in the service manager lifecycle. Update API documentation strings to describe the new channel and its pairing requirement. Extend admin notification defaults view to include telegram column. Pass `notif_telegram_paired` flag to profile template based on `telegram_store.is_paired()` check. Update `NotificationPrefForm` and `NotificationDefaultForm` model literals to accept "telegram" as a valid channel value.
2026-06-22 22:52:02 +02:00
|
|
|
assert mine["prestige"] == 50
|
|
|
|
|
assert mine["score"] == economy.farm_score(store.get_farm(
|
|
|
|
|
get_table("users").find_one(username=name)["uid"]
|
|
|
|
|
))
|
|
|
|
|
assert mine["score"] >= 50 * economy.SCORE_PRESTIGE
|
|
|
|
|
|
|
|
|
|
|
2026-06-22 18:41:53 +02:00
|
|
|
def test_plant_returns_updated_farm(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
response = session.post(
|
|
|
|
|
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
farm = response.json()["farm"]
|
|
|
|
|
assert farm["coins"] == 100000 - economy.crop_for("shell").cost
|
|
|
|
|
assert any(plot["state"] == "growing" for plot in farm["plots"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_plant_requires_auth(app_server, seeded_db):
|
|
|
|
|
response = requests.post(
|
|
|
|
|
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_plant_insufficient_coins_returns_400(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name, coins=0)
|
|
|
|
|
response = session.post(
|
|
|
|
|
f"{BASE_URL}/game/plant", data={"slot": 0, "crop": "shell"}, headers=JSON
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
assert "error" in response.json()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_harvest_empty_returns_400(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
response = session.post(
|
|
|
|
|
f"{BASE_URL}/game/harvest", data={"slot": 0}, headers=JSON
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_buy_plot_and_upgrade_ci(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
buy = session.post(f"{BASE_URL}/game/buy-plot", headers=JSON)
|
|
|
|
|
assert buy.status_code == 200
|
|
|
|
|
assert buy.json()["farm"]["plot_count"] == economy.STARTING_PLOTS + 1
|
|
|
|
|
upgrade = session.post(f"{BASE_URL}/game/upgrade", headers=JSON)
|
|
|
|
|
assert upgrade.status_code == 200
|
|
|
|
|
assert upgrade.json()["farm"]["ci_tier"] == 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_daily_claim_then_double_claim_400(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
first = session.post(f"{BASE_URL}/game/daily", headers=JSON)
|
|
|
|
|
assert first.status_code == 200
|
|
|
|
|
assert first.json()["farm"]["streak"] == 1
|
|
|
|
|
second = session.post(f"{BASE_URL}/game/daily", headers=JSON)
|
|
|
|
|
assert second.status_code == 400
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_upgrade_perk_returns_updated_farm(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
response = session.post(
|
|
|
|
|
f"{BASE_URL}/game/perk", data={"perk": "growth"}, headers=JSON
|
|
|
|
|
)
|
|
|
|
|
assert response.status_code == 200
|
|
|
|
|
growth = next(p for p in response.json()["farm"]["perks"] if p["key"] == "growth")
|
|
|
|
|
assert growth["level"] == 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_prestige_below_level_returns_400(app_server, seeded_db):
|
|
|
|
|
session, name = _signup()
|
|
|
|
|
_reset_farm(name)
|
|
|
|
|
response = session.post(f"{BASE_URL}/game/prestige", headers=JSON)
|
|
|
|
|
assert response.status_code == 400
|