Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions phpbb-test-board/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ Saves PNG screenshots of an extension's or style's pages with [Playwright](https

`phpbbmodders/seenotes` needs `--seed` with seed-forum's standard fixtures; it then runs seed-forum's `bin/seed-seenotes-fixtures.php` from the same folder, which gives the members reasons, notes and warnings. Reasons show only to staff, so every shot is taken as the admin: a post's mini-profile with the reason, a profile with the reason and the Moderator notes link, a post by a member without a reason with the notes button, the MCP post details notice, MCP → Moderator notes, ACP → Manage users → Moderator notes, and the ACP reasons and settings pages (in full).

`phpbbmodders/prunenotifications` needs `--seed` with seed-forum's standard fixtures. It makes three members dormant, with last visits two to five years ago and 1,840 unread notifications between them, gives the admin 12, then saves the ACP page with the counts and settings (in full), the Prune now confirmation, and the message after every batch has run and the table has been optimized.

`phpbbmodders/topicclaim` needs `--seed` with seed-forum's standard fixtures, for its `forum_moderator` user. That user claims the busiest topic with a note, and the admin claims another topic in the same forum. Claims show only to moderators, so every shot is taken as the admin: the forum's topic list, the claim notice on the topic page, the claim page (with take over, transfer and release), MCP → Claimed topics and the ACP settings.

`phpbbmodders/printallposts` sets the board to two posts per page, so the busiest topic spans two pages, then saves the **Topic tools** menu open on it and the **Print all** page showing every post, both as a guest. Use `--seed` with seed-forum's standard fixtures.
Expand Down
121 changes: 121 additions & 0 deletions phpbb-test-board/feature_checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -1332,7 +1332,128 @@ def delete_with_name(name: str) -> str:
ok(re.search(r'<dd><a %s>' % to_form, profile), "the profile's Moderator notes link opens the Give a reason form")


def check_prunenotifications(b: Board) -> None:
"""Prune a dormant member's notifications from the ACP in batches and by cron, and optimize the table."""
def config(name: str) -> str:
rows = b.sql("SELECT config_value FROM phpbb_config WHERE config_name = ?", (name,))
return rows[0][0] if rows else ""

def notifications(user_id: int) -> int:
return int(b.sql("SELECT COUNT(notification_id) FROM phpbb_notifications WHERE user_id = ?", (user_id,))[0][0])

def add_notifications(user_id: int, count: int) -> None:
b.php("for ($i = 0; $i < %d; $i++) { $db->sql_query('INSERT INTO ' . NOTIFICATIONS_TABLE . ' '"
" . $db->sql_build_array('INSERT', ['notification_type_id' => 1, 'item_id' => $i + 1,"
" 'item_parent_id' => 0, 'user_id' => %d, 'notification_read' => 0, 'notification_time' => 1000,"
" 'notification_data' => ''])); }" % (count, user_id), check=True)

def logged(operation: str) -> int:
return int(b.sql("SELECT COUNT(log_id) FROM phpbb_log WHERE log_operation = ?", (operation,))[0][0])

def follow_refresh(body: str) -> tuple:
"""Follow the ACP's meta refreshes through the prune batches; return (last page, requests)."""
requests_made = 1
while requests_made <= 20:
m = re.search(r'http-equiv="refresh" content="\d+;\s*url=([^"]+)"', body)
if not m:
break
url = m.group(1).replace("&amp;", "&")
body = admin.get(url if url.startswith("http") else f"{b.base}/adm/" + url.lstrip("./"), timeout=60).text
requests_made += 1
return body, requests_made

admin = requests.Session()
sid = b.login(admin, acp=True)
mid = b.sql("SELECT module_id FROM phpbb_modules WHERE module_basename LIKE '%prunenotifications%' "
"AND module_mode = 'settings'")
ok(mid, "ACP module is installed")
if not mid:
return
page_url = f"{b.base}/adm/index.php?i={mid[0][0]}&mode=settings&sid={sid}"
page = admin.get(page_url, timeout=20)
ok(page.ok and 'id="prunenotifications_settings"' in page.text, "ACP page loads")

# A member who last visited long ago, and the admin as an active member
candidates = b.sql("SELECT user_id, user_lastvisit, user_regdate FROM phpbb_users "
"WHERE user_type IN (0, 1) AND user_id <> 2 ORDER BY user_id")
ok(candidates, "the board has a member to make dormant")
if not candidates:
return
dormant, old_lastvisit, old_regdate = int(candidates[0][0]), candidates[0][1], candidates[0][2]
b.sql("UPDATE phpbb_users SET user_lastvisit = 1000, user_regdate = 1000 WHERE user_id = ?", (dormant,))
try:
add_notifications(dormant, 250)
add_notifications(2, 5)
admin_before = notifications(2)

# Settings: two years, batches of 100, optimize after pruning
action, fields = form_fields(page.text, "prunenotifications_settings")
body = post_back(admin, f"{b.base}/adm", page_url, action, {
**fields, "prunenotifications_days": "730", "prunenotifications_batch": "100",
"prunenotifications_cron_enable": "0", "prunenotifications_auto_optimize": "1", "submit": "Submit"})
ok("settings saved" in body.lower(), "ACP saves the settings")
saved = {k: config(f"prunenotifications_{k}") for k in ("days", "batch", "cron_enable", "auto_optimize")}
ok(saved == {"days": "730", "batch": "100", "cron_enable": "0", "auto_optimize": "1"}, f"settings stored: {saved}")

# Out-of-range values are refused
page = admin.get(page_url, timeout=20)
action, fields = form_fields(page.text, "prunenotifications_settings")
body = post_back(admin, f"{b.base}/adm", page_url, action, {
**fields, "prunenotifications_days": "1", "prunenotifications_batch": "100",
"prunenotifications_cron_enable": "0", "prunenotifications_auto_optimize": "1", "submit": "Submit"})
ok("must be from" in body and config("prunenotifications_days") == "730", "an out-of-range period is refused")

page = admin.get(page_url, timeout=20)
ok(re.search(r"<strong>2\d\d</strong>", page.text), "the page counts the dormant member's notifications")

# Prune now: confirm, then three batches of up to 100
action, fields = form_fields(page.text, "prunenotifications_actions")
body = post_back(admin, f"{b.base}/adm", page_url, action, {**fields, "prune": "Prune now"})
ok('id="confirm"' in body, "Prune now asks for confirmation")
action, fields = form_fields(body, "confirm")
body = post_back(admin, f"{b.base}/adm", page_url, action, {**fields, "confirm": "Yes"})
body, requests_made = follow_refresh(body)
ok("form was invalid" not in body.lower(), f"no 'invalid form' error ({requests_made} requests)")
ok(requests_made >= 3, f"the prune ran in batches ({requests_made} requests)")
ok(notifications(dormant) == 0, f"the dormant member's notifications are gone ({notifications(dormant)} left)")
ok(notifications(2) == admin_before, "the active member's notifications are kept")
ok(logged("LOG_PRUNENOTIFICATIONS_PRUNED") >= 1, "the prune is in the admin log")
ok(logged("LOG_PRUNENOTIFICATIONS_OPTIMIZED") >= 1 and int(config("prunenotifications_last_optimize")) > 0,
"the table was optimized after pruning" + ("" if "optimized" in body else f": {page_text(body)[-200:]}"))

# Optimize now, from its own button
page = admin.get(page_url, timeout=20)
action, fields = form_fields(page.text, "prunenotifications_actions")
body = post_back(admin, f"{b.base}/adm", page_url, action, {**fields, "optimize": "Optimize now"})
action, fields = form_fields(body, "confirm")
body = post_back(admin, f"{b.base}/adm", page_url, action, {**fields, "confirm": "Yes"})
ok("table was optimized" in body, "Optimize now works on this database"
+ ("" if "table was optimized" in body else f": {page_text(body)[-200:]}"))

# Cron: a full batch, then the rest with one log entry
add_notifications(dormant, 150)
b.sql("UPDATE phpbb_config SET config_value = '1' WHERE config_name = 'prunenotifications_cron_enable'")
task = "cron.task.phpbbmodders.prunenotifications.prune"
for run in range(2):
b.sql("UPDATE phpbb_config SET config_value = '0' WHERE config_name = 'prunenotifications_next_run'")
b.purge_cache()
out = b.cli("cron:run", task)
ok("rror" not in out, f"cron run {run + 1} finished" + ("" if "rror" not in out else f": {out[-200:]}"))
ok(notifications(dormant) == 0, f"cron deleted the rest ({notifications(dormant)} left)")
ok(logged("LOG_PRUNENOTIFICATIONS_CRON") == 1, "cron logs once, after the backlog is cleared")
ok(int(config("prunenotifications_next_run")) > int(b.php("echo time();")) + 3600,
"cron waits a day once nothing is left")
finally:
b.sql("DELETE FROM phpbb_notifications WHERE notification_time = 1000 AND user_id IN (?, 2)", (dormant,))
b.sql("UPDATE phpbb_users SET user_lastvisit = ?, user_regdate = ? WHERE user_id = ?",
(old_lastvisit, old_regdate, dormant))
b.sql("UPDATE phpbb_config SET config_value = '0' WHERE config_name IN "
"('prunenotifications_cron_enable', 'prunenotifications_auto_optimize')")
b.purge_cache()


CHECKS = {
"phpbbmodders/prunenotifications": check_prunenotifications,
"phpbbmodders/seenotes": check_seenotes,
"phpbbmodders/autodbbackup": check_autodbbackup,
"phpbbmodders/elonw": check_elonw,
Expand Down
51 changes: 51 additions & 0 deletions phpbb-test-board/screenshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -1312,7 +1312,58 @@ def shots_seenotes(board: Board, shoot: Shooter, args: argparse.Namespace) -> No
full_page=True)


def shots_prunenotifications(board: Board, shoot: Shooter, args: argparse.Namespace) -> None:
"""phpbbmodders/prunenotifications: the ACP page, the Prune now confirmation and the finished prune.

Needs seed-forum's fixtures: run with --seed and seed-forum's
bin/seed-standard-fixtures.php. Three members are made dormant (last
visits two to five years ago) and get 1,840 unread notifications between
them; the admin keeps 12. prunenotifications-acp shows the counts and the
settings, prunenotifications-acp-confirm the confirmation, and
prunenotifications-acp-done the message after all batches ran and the
table was optimized.
"""
if not args.seed:
raise RuntimeError("run with --seed and seed-forum's bin/seed-standard-fixtures.php")
members = [r[0] for r in board.sql("SELECT user_id FROM phpbb_users WHERE user_type IN (0, 1) "
"AND user_id <> 2 ORDER BY user_id")][:3]
if len(members) < 3:
raise RuntimeError("the seeded board needs at least three members besides the admin")
year = 365 * 86400
now = int(time.time())
for user_id, (years, count) in zip(members, ((2.2, 1200), (3.5, 480), (5, 160))):
board.sql("UPDATE phpbb_users SET user_lastvisit = ?, user_regdate = ? WHERE user_id = ?",
(now - int(years * year), now - int((years + 1) * year), user_id))
board.php("for ($i = 0; $i < %d; $i++) { $db->sql_query('INSERT INTO ' . NOTIFICATIONS_TABLE . ' '"
" . $db->sql_build_array('INSERT', ['notification_type_id' => 1, 'item_id' => $i + 1,"
" 'item_parent_id' => 0, 'user_id' => %d, 'notification_read' => 0,"
" 'notification_time' => %d, 'notification_data' => ''])); }"
% (count, user_id, now - int(years * year)), check=True)
board.php("for ($i = 0; $i < 12; $i++) { $db->sql_query('INSERT INTO ' . NOTIFICATIONS_TABLE . ' '"
" . $db->sql_build_array('INSERT', ['notification_type_id' => 1, 'item_id' => $i + 1,"
" 'item_parent_id' => 0, 'user_id' => 2, 'notification_read' => 0, 'notification_time' => %d,"
" 'notification_data' => ''])); }" % now, check=True)
set_config(board, {"prunenotifications_days": "730", "prunenotifications_batch": "500",
"prunenotifications_cron_enable": "1", "prunenotifications_auto_optimize": "1"})
acp = "adm/index.php?i=-phpbbmodders-prunenotifications-acp-main_module&mode=settings&sid={sid}"
shoot.shot("prunenotifications-acp", acp, admin=True, full_page=True)

def open_confirm(p) -> None:
p.wait_for_timeout(1200) # phpBB's minimum time before a form may be submitted
p.click("#prunenotifications_prune")
p.locator("form#confirm").wait_for()
shoot.shot("prunenotifications-acp-confirm", acp, admin=True, full_page=True, prepare=open_confirm)

def run_prune(p) -> None:
open_confirm(p)
p.click("form#confirm input[name=confirm]")
# The batches follow each other by meta refresh; wait for the final message
p.get_by_text("The notifications table was optimized").wait_for(timeout=120000)
shoot.shot("prunenotifications-acp-done", acp, admin=True, full_page=True, prepare=run_prune)


SHOTS = {
"phpbbmodders/prunenotifications": shots_prunenotifications,
"phpbbmodders/seenotes": shots_seenotes,
"phpbbmodders/adduser": shots_adduser,
"phpbbmodders/autodbbackup": shots_autodbbackup,
Expand Down
Loading