Imported from maximuml/tracker-lp-bits (
.agents/skills/testing-tracker-lp-bits/SKILL.md). Install upstream withnpx skills add maximuml/tracker-lp-bits --skill testing-tracker-lp-bits. Copyright stays with the author.
Testing tracker-lp-bits end-to-end
Use this skill when asked to test the tracker-lp-bits app in the local Docker Compose stack.
Test environment prerequisites
- Docker Compose stack must be running:
docker compose up -d. - The
phpcontainer isphp:8.4-fpm-alpineand/var/www/htmlmaps to the repo. - A sysop test user must exist. If not, create one and promote it to
UC_SYSOPwithuploadpos = 'yes'andclear_user_cache(<id>). - The
c_secure_passcookie can be generated for browser/curl use with:App\Support\AuthCookie::buildToken($user->id, $user->auth_key, time() + 86400);
Browser smoke suite (primary verification)
tests/browser/ is the blocking Playwright gate (ADR 0016, CI job
browser-smoke). Prefer it over hand-written probes for page-level
regressions — it already covers real login and signup forms, 10
authenticated pages, forums → viewforum → viewtopic navigation,
escaped-markup leaks, failed resources/pageerror, CSP violation
baselines, axe baselines, and mobile horizontal overflow.
cd tests/browser && npm ci && npx playwright test
Prerequisites on a fresh migrate:fresh --seed database (documented in
tests/browser/README.md): security.iv=no (image captcha),
security.maxip raised above the seeded 2, basic.baseUrl equal to
the browser origin (http://127.0.0.1), and
php artisan db:seed --class=BrowserSmokeSeeder for the torrent/topic
fixtures — the base seeders ship reference data only. globalSetup
performs one real POST /login and replays storageState; /login is
throttled 10 req/min, so do not log in per page.
Common environment gotchas
basic.BASEURLmay be empty insettings, producinghttp:///announce.phpand breaking/announce.php. Set it tolocalhost(or the real host) and clear Redis settings cache.agent_allowed_familyseed for BiglyBT may contain an invalid regex such as^BiglyBT\ /3.... Use a valid regex like/^BiglyBT\/3\.([0-9])\.([0-9])\.([0-9])/to avoidpreg_matchwarnings from/announce.php.php artisan meilisearch:importmay report success but swap to an emptytorrentsindex because ofMeiliSearchRepository::doImportFromDatabase(). Verify the document count with:
If the count is 0, manually add the test torrents to MeiliSearch or fix the import logic.curl -s 'http://localhost:7700/indexes/torrents/stats'
Test flow for setlist upload PRs
- Open
/upload.phpwith a logged-in sysop user. - Fill Torrent name with a name like
Linkin Park - Hamburg, Germany, Volksparkstadion (03.06.2026). - Click Fill setlist — the button should switch to
Loading...and become disabled, then the Description textarea should be populated. - Verify
/setlist_lookup.php?name=<name>returns JSON withsuccess=true,data(artist/venue/date/sets/source) andtext(formatted BBCode). - Upload torrents via
takeupload.php(browser file inputs may not work under automation) using the setlist text from the previous step. - Verify
/torrents.phplistings and search (?search=Linkin+Park,?search=Hamburg,?search=Volksparkstadion). - Verify
/announce.phpreturns valid bencode anddetails.phpshows seeders/peers. - Verify edit/promote/delete flows.
- Run
composer validateandvendor/bin/phpstan analyse. - Check
docker logs nexusphp-phpfor new fatals or deprecation warnings.
Known limitations
- The legacy autocomplete on
/torrents.phpmay not register native keystrokes in headless automation; triggersuggest(0, '<term>')from the console to verify it. - GitHub Actions CI may not start due to account billing/spending limits; rely on local Docker verification when that happens.
Testing /announce.php end-to-end
Use these notes when verifying the BitTorrent announce endpoint in the Docker stack.
Request host / basic.BASEURL must match
AnnounceService::checkTrackerUrl() compares the current request host with the tracker URL built from basic.BASEURL. A mismatch causes a warning message response and aborts peer processing.
CriticalPathTestsetsbasic.BASEURLtoopenresty, so it must be run with:docker compose exec -e CRITICAL_PATH_BASE_URL=http://openresty php vendor/bin/phpunit \ --filter testCriticalPath tests/Feature/CriticalPathTest.php --no-coverage- For manual tests from the
phpcontainer, requesthttp://openresty/announce.phpand ensurebasic.BASEURLisopenresty:App\Support\Settings::saveBatch('basic', ['BASEURL' => 'openresty']); \Illuminate\Support\Facades\Redis::flushAll();
peer_id and info_hash encoding
AnnounceRequest validates info_hash and peer_id with strlen() == 20 on the raw binary string. Use 20-byte values and build the query with PHP_QUERY_RFC3986:
$peerId = '-qB4' . sprintf('%02d', random_int(0, 99)) . random_bytes(14); // 20 bytes
$query = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
Use User-Agent: qBittorrent/4.x.x and a -qB4... peer_id to pass the AgentAllowRepository allow-list check.
Controlling the peer IP
Network::clientIp() reads HTTP_X_FORWARDED_FOR first. Use distinct 10.0.0.x IPs per peer to avoid the same-IP seeder warning (You cannot seed the same torrent in the same location from more than 1 client.):
curl_setopt($ch, CURLOPT_HTTPHEADER, ['X-Forwarded-For: 10.0.0.1']);
Lock / cache behavior
- The short re-announce lock (
isReAnnounce:<md5(passkey+info_hash)>) has a 5s TTL. A second request within that window returns early and does not insert a duplicate peer. - The
isReAnnounceearly-return response is built bybuildInitialRepDict(), which now queries thepeerstable live forcomplete/incompletecounts, so the response reflects the current peer state even when thetorrent_hash_<infoHash>_contentcache has not expired.
Missing required parameters
The openresty Lua filter in .docker/openresty/lua/tracker_filter.lua rejects requests missing required announce parameters with 400 Bad Request and a bencoded failure reason (e.g. Missing parameter: port) before PHP is reached.
Testing /scrape and /scrape.php end-to-end
Both GET /scrape and GET /scrape.php dispatch to ScrapeController::scrape through the FPM wrapper. ScrapeService::parseInfoHashes() reads QUERY_STRING directly, URL-decodes each info_hash value, and matches the resulting raw 20-byte binary against torrents.info_hash.
- Send
info_hashvalues URL-encoded raw binary (not hex). Userawurlencode($infoHashBinary); a custom encoder must zero-pad bytes < 16 (e.g.%0E, not%E). - Repeat
info_hashquery parameters for multi-torrent scrape (info_hash=...&info_hash=...). - Valid responses are
200Content-Type: text/plain; charset=utf-8with afilesdict keyed by raw info_hash. - Invalid passkey returns
200withfailure reasonand sets Redispasskey_invalid:<passkey>with a 24h TTL. - Missing
info_hashreturns200withwarning message,files: [], plusinterval/min interval.
Testing cleanup:run and the cleanup container
docker compose exec php php artisan cleanup:run --forceshould exit0and stream legacycleanup_cli.phpprogress, ending with[CLEANUP_RUN] DONE, cost time: N seconds.- The
cleanupservice in.docker/php/entrypoint.shruns the command in a 60s loop. Verify withdocker compose logs --since 2m cleanup.
Testing the merged php8 regression bundle
Typical settings for a full php8 regression run:
INSERT INTO settings (name, value) VALUES ('use_challenge_response_authentication','no'),('security.iv','no'),('tweak.where','yes'),('meilisearch.enabled','no'),('torrent.download_support_passkey','yes'),('torrent.approval_status_none_visible','yes') ON DUPLICATE KEY UPDATE value=VALUES(value);
Then clear the Redis settings cache (nexus_settings_in_nexus, nexus_settings_in_laravel).
public/torrents/must exist and be writable bywww-data;takeupload.phpwrites the.torrentfile throughgetFullDirectory(main.torrent_dir)which resolves relative to the FPMgetcwd()(public/).storage/framework/views/must be writable soTorrentPolicydenial views can be compiled.- Generate a fresh
.torrentwithRhilip\Bencode\Bencodeandannounce=http://openresty/announce.php, upload via/takeupload.php, captureinfo_hashfrom the DB for announce/scrape probes. - For first authenticated downloads, add
letdown=1(download.php?id=<id>&letdown=1) to bypass theshowdlnoticeredirect todownloadnotice.php.
Testing PR 19a+19b auth (signup/confirm/recover/login/logout)
- Disable login-attempt bans or clear
loginattemptsbefore repeated login probes (DELETE FROM loginattempts). - Use
Rhilip\Bencode\Bencodeto generate a minimal.torrentand upload via/takeupload.phpfor regression. - Signup and confirm resend POSTs need
_tokenand a shared cookie jar between the GET form and POST. - Confirm hash:
md5(str_pad($secret, 20))(becauseStrings::padHashpads to 20 bytes). - Recover reset hash:
md5(str_pad($editsecret, 20) . $email . $passhash . str_pad($editsecret, 20)). - When
main.smtptype='none',Mail::sentLegacywrites the email body to/tmp/nexus-YYYY-MM-DD.login thephpcontainer; read reset/confirm URLs and new passwords from that log. - Watch for
Mail::sentLegacy/SupportContextglobals-drain regression: if auth wrappers do not loadconfig/allconfig.php,$GLOBALS['smtptype']is empty andMail::sentmay callstderr()/Style::cssRowand throw aTypeError. - Use
AuthCookie::verifyToken($rawCookieValue)(withurldecodeif reading from curl jar) to inspect the APP_KEY-encryptedc_secure_passcookie.
Testing PR #188 comment migration (public/comment.php wrapper)
- The legacy wrapper dispatches to Laravel
WebCommentControllerroutes under theauth.nexus:nexus-webmiddleware. GET /comment.php?action=add&pid=<id>&type=torrentshould rendercomments.createusingFrame::composeBegin/composeEnd(BBCode editor,textarea name="body", submitid="qr").POST /comment.php?action=add&type=torrent(withpidandbody) should redirect todetails.php?id=<pid>#<newId>.GET /comment.php?action=add&pid=<id>&type=torrent&sub=quote&cid=<id>should prefill[quote=<username>]<text>[/quote].GET/POST /comment.php?action=edit&cid=<id>&type=<type>should load an edit form and redirect todetails.php?id=<pid>.GET /comment.php?action=delete&cid=<id>&type=<type>should show confirmation; thesure=1link removes the comment.GET /comment.php?action=vieworiginal&cid=<id>&type=<type>(staff/commanage) should displaycomments.ori_text.type=offerredirects tooffers.php?id=<id>&off_details=1#<newId>.- Anti-flood rejects a second comment by a normal user within 10s with HTTP 403 and
Comment Flooding Not Allowed. - Common gotchas found during testing:
CommentRepository::getParent()casts Eloquent models with(array), producing private-property arrays instead of['name' => ..., 'owner' => ...]. Use$model->toArray()orjson_decode(json_encode($model), true).public/comment.phpmust put the query string into the$uripassed toRequest::create();REQUEST_URIin the$serverarray is overwritten by Symfony and the query is lost for POST requests. This causesStoreCommentRequestto failtypevalidation.public/comment.phpmust not stripcidfrom the query whenaction=addbecausesub=quoteneedscidas a query parameter.- The rendered
form actioninresources/views/comments/_form.blade.phpis not HTML-escaped to&; browsers tolerate raw&, but the legacydetails.phpquick-comment form useshtmlspecialchars.
Testing php8 PR #199-#205 legacy-page migrations
This section covers the combined php8 branch migrations (usercp, edit/takeedit, mybonus/myhr, topten/log, index.php, and friends/messages/getrss/sendmessage/userhistory/invite).
Login/logout gotchas
public/login.phpbuildsIlluminate\Http\Request::create($uri, $_SERVER['REQUEST_METHOD'], $_GET, ...)for both GET and POST. This means a form POST to/login.phplosesusername/password/_token, so Laravel returns419(CSRF) because no_tokenreachesVerifyCsrfToken.- The direct Laravel
/loginroute works as expected (POST returns302toindex.phpand setsc_secure_pass). - Workaround for UI automation: navigate to
http://openresty/logout, thenhttp://openresty/login, fill the form, and submit; or use curl against/loginwith a CSRF token extracted from/login.
Download/announce/scrape
download.php?id=<id>redirects todownloadnotice.phpon the first authenticated download. Append&letdown=1to bypass the notice and receiveContent-Type: application/x-bittorrent.announce.phprequires a 20-bytepeer_idand an allowedUser-Agent(e.g.uTorrent/3000with peer_id-UT3000...).scrape.phpreturns a validfilesdict wheninfo_hashis supplied as raw binary URL-encoded.
Forums viewunread state setup
- To make
/forums.php?action=viewunreadlist a topic, ensureusers.last_catchupis0(or lower thantopics.lastpost) andreadpostshas no row for that user/topic. Then click Catch up to updatelast_catchupand verifyviewunreadshows nothing afterwards.
Known failures in the bundle
/userhistory.php?id=1(default action, noaction=query) currently throwsTypeError: App\Support\PageLayout::header(): Argument #1 ($title) must be of type string, null givenbecausestdhead()is called with anulltitle inresources/views/userhistory/_userhistory_legacy.php. The named actions (viewposts,viewcomments) render correctly.public/login.phpPOST login is broken as described above.
Testing PR #27 staff/mod page migrations
storage/framework/views/must be writable by the PHP-FPM worker (www-data, gid 82). Also createpublic/tmp; otherwise Blade view compilation fails withtempnam(): file created in the system's temporary directoryand the first request returns HTTP 500.- A
cheatersrow, acommentsrow on an existing torrent, anoffersrow, and a pending user (status='pending') are useful for exercisingcheaterbox.php,report.php,makepoll.php/polloverview.php, andmodtask.php confirmuser. - The
bans.phpform can be exercised viacurlbecause the native submit button may not register under automation. /modtask.phpedituserrequires the same fields as theuserdetails.phpedit form, includingemail,username,title,avatar,signature,privacy,donor,uploadpos,downloadpos,forumpost, etc.cruprfmanagepermission meansemailandusernamecannot be omitted or they will be blanked./deletemessage.phporiginally comparedmessages.location(asmallint) to the strings'in','out', and'both', which never matched. PR #27 fixed this by using the numericPM_DELETED/savedsemantics frommessages/_messages_legacy.phpand ensuringlang_deletemessageis loaded.
Testing PR #231 unified nexus.php dispatcher (public wrapper consolidation)
PR #231 replaces per-page dispatching in public/*.php with require __DIR__ . '/nexus.php'; and a single Request::create() call inside public/nexus.php.
What to verify
php -l public/nexus.phpand representative wrappers (index.php,torrents.php,details.php,comment.php,takelogin.php,takesignup.php,takeupload.php,confirmemail.php,forums.php,userdetails.php)../vendor/bin/phpstan analyseandphp artisan test --testsuite Unit/Featurepass.$_GET/$_POST/$_FILES/$_SERVER/$_COOKIEare copied into the LaravelRequestcorrectly; special wrappers rewrite$_GET/$nexusRoutebeforenexus.php.- Clean URLs without
.phpfall back tonexus.phpand redirect to the corresponding.phpwrapper, e.g./torrents->302to/torrents.php?and/details/2->302to/details.php?id=2. - Special routing wrappers:
details.phpsets$nexusRoute = '/details/' . $idand unsets$_GET['id'].comment.phpmapsaction=add|edit|delete|vieworiginalto the Laravel/comment/*routes.takelogin.phpandtakesignup.phpset$nexusRouteto/loginand/signup.
- Tracker endpoints:
announce.phpandscrape.phpdefineIN_NEXUSand still return200bencoded responses. - PATH_INFO:
confirmemail.php/<id>/<32-md5>/<email>should reach the legacyconfirmemailpartial and return a legacy<h1>Not Found</h1>fromhttperr()when the hash does not match. - POST wrappers (
takesignup.php,takeupload.php,comment.php,takemessage.php,takestaffmess.php,deletemessage.php) should produce validation/error pages or redirects, not 405/500. - Admin/utility wrappers (
catmanage.php,forummanage.php,moforums.php,fields.php,formats.php,videoformats.php,faqactions.php,faqmanage.php) render forms/tables. - Critical path pages (
index.php,torrents.php,details.php,userdetails.php,forums.php,offers.php,mybonus.php,bitbucket-upload.php,topten.php,log.php,staffpanel.php) render 200.
Common gotchas
takelogin.phpandtakesignup.phpneed the CSRF token extracted from the unauthenticated form page; fetching the token while logged in may return an empty token because/loginand/signupredirect authenticated users toindex.php.takeupload.phprequires a valid generated.torrentand the_token; an empty title should redirect to/error?error=The+title+cannot+be+empty.comment.phpPOSTaction=addneedspid=<torrentId>&type=torrentandbody; success redirects todetails.php?id=<pid>#<newId>.comment.phpaction=delete&cid=<id>&type=torrent&sure=1should redirect to the parent details page.mailtest.phpPOST form fields areaction=sendmailandemail=<address>; withsmtp.smtptype=nonethe expected result isUnable to send mail. (SMTP disabled or mail not sent).- For
announce.phpandscrape.php, build the query withrawurlencode()on the raw 20-byteinfo_hashandpeer_idand use aUser-Agentfrom the allow list (qBittorrent/4.0.0).
Testing PR #219 remaining public/*.php migrations
Pages and expected behavior
/adduser.phpGET renders a user-creation form; POST with alphanumeric username (3-20 chars) creates the user and 302s touserdetails.php?id=<new>./bitbucketlog.phplists uploaded avatar images with[Delete]links./complains.phpis intended to be reachable by both anonymous users (file a complaint) and logged-in admins (view/reply). In the migrated route it is placed inside theauth.nexus:nexus-webgroup, while the partial'scur_user_check()aborts for logged-in users, so the page is unusable without a route/middleware change./confirmemail.php/<id>/<md5>/<email>relies on$_SERVER['PATH_INFO']matching:/(\d{1,10})/([\w]{32})/(.+):. The openrestytry_filesdoes not preservePATH_INFOfor these URLs; the request 404s. Needs an explicit rewrite/location or$_GETfallback./cron.phpreturns plain text; whenuseCronTriggerCleanUpis true andautoclean()does not run, it printsClean-up not triggered./delete.php?id=<torrentid>(POSTid,reasontype) should delete the torrent and printTorrent deleted. If the page is blank, check thatmkglobal("id")was followed byglobal $id;sointval($id)sees the parsed value./downloadnotice.php?torrentid=<id>renders a first-time notice; submitting the form 302s todownload.php?id=<id>&letdown=1./email-gateway.phpis intentionally empty (the partial callsexit(0);)./increment-bulk.phpGET renders a batch bonus/invite/upload form;take-increment-bulk.phpPOST redirects toincrement-bulk.php?sent=1&type=<type>./maxlogin.phplistsloginattempts;?action=ban|unban|delete&id=<row>mutates the table./ok.php?type=<type>renders status messages (signup,sysop,confirmed,adminactivate). A blank page meansmkglobal("type")populated$GLOBALS['type']but the local$typevariable was not declaredglobalafterextract($GLOBALS, EXTR_SKIP)./setlist_lookup.php?name=...returnsContent-Type: application/json; charset=utf-8withsuccess,data, andtext./testip.phpacceptsipby GET/POST and prints whether it is banned based onbans./thanks.phpPOSTid=<torrentid>inserts athanksrow and awards bonus points; returns empty body on success.
Common Blade-wrap regression (mkglobal / extract)
Any partial that starts with extract($GLOBALS, EXTR_SKIP); and then calls mkglobal("foo") will set $GLOBALS['foo'] but not create the local variable $foo (because EXTR_SKIP refuses to overwrite the already-extracted copy of $GLOBALS? actually because mkglobal only updates $GLOBALS and PHP variable scope does not auto-bind globals in function-less include). The fix is to add global $foo; immediately after mkglobal("foo") in the partial, or replace the mkglobal call with direct $_GET/$_POST access. PR #219 hit this in ok, delete, and several other legacy pages.
Confirm hash for confirmemail.php
- Use the user's
editsecretpadded to 20 chars:$secret = str_pad($editsecret, 20);$md5 = md5($secret . $email . $secret);. - The URL format is
/confirmemail.php/<id>/<md5>/<email>(or/confirmemail/<id>/<md5>/<email>once routing is fixed).
Testing takesignup.php / signup.php
- The form at
/signup.phpusescrypto-js.js, but the inline script callssha256(password)whilecrypto-js.jsonly exposesCryptoJS.SHA256. The normal "Sign up!" button therefore fails to populate the hiddenwantpasswordandwantpassword_hashedfields and the server returnsDon't leave any fields blank.. - To test the
/signupPOST route from the browser console, set the hiddenwantpasswordtoCryptoJS.SHA256('password').toString(),wantpassword_hashedto1, andpassagainto the same hash, then submit the form. takesignup.phpsets$nexusRoute = '/signup'andrequire __DIR__ . '/nexus.php', so POST/GET totakesignup.phpshould reach the same Laravel route as/signup.- Direct
curlPOSTs totakelogin.phpandtakesignup.phptend to return 419 because curl's cookie handling does not play well with Laravel's encrypted session/XSRF cookies; prefer a real browser session for these endpoints.
Testing PR #232 view-context and signup/login hashing
PR #232 injects a filtered $context into every Blade view (View::composer('*')) and replaces the remaining extract($GLOBALS, EXTR_SKIP) / mkglobal() calls in delete/fastdelete/ok partials with SupportContext::getRequestInput().
What to verify
grepshows 215extract($context, EXTR_SKIP)inresources/viewsand 0extract($GLOBALS/mkglobal(in views.php -lon changed files;phpstan analyse;php artisan cache:clear config:clear view:clear route:clear;redis-cli FLUSHDB.- Signup (
/signup.php): the auth layout loads jQuery, layer, andjs/crypto-js.js;Form::passwordHashJshashes withCryptoJS.SHA256(password).toString(); hiddenwantpassword/wantpassword_hashedare appended; submit reachesok.php?type=confirm. - Login (
/login.php): direct password form posts totakelogin.phpand lands onindex.php; only uses challenge-response whensecurity.use_challenge_response_authentication=yes. - Critical path:
index,torrents.php,details.php?id=2,download.php?id=2&letdown=1(bencode),forums.php,offers.php,userdetails.php?id=1,logout.php. - Legacy
public/nexus.phppages:/ajax.php,/latestcomments.php,/shoutbox_sse.php,/getattachment.php,/attachment.php,/image.php. delete.php(POSTreasontypefromedit.phpdelete form) andfastdelete.php?id=<id>&sure=1remove the torrent and printTorrent deleted!.ok.php?type=signup|confirmed|confirmrenders the legacy status messages.- POST wrappers:
takemessage.php(success message + newmessagesrow),takeupdate.php(mark reportdealtwith),takeinvite.php(redirects toinvite.php?id=<uid>&sent=1; actual insert requires working mail). - Confirm no
extract(ormkglobalwarnings indocker logs nexusphp-phpduring the run.
Common gotchas
/signup.phponly works ifauth.blade.phpincludes jQuery and layer; without them theSign up!button does nothing because the inline handler usesjQuery/layer./image.php?action=regimagemay return 404 whensecurity.iv=noor the configured captcha driver does not implementoutputImage(); this is environment/config, not necessarily a PR regression.takeinvite.phpredirects withsent=1even whenMail::sentcannot send becausesmtp.smtptype=none; the insert is gated onsent_mail()returningtrue.
Testing PR #52/53 drain-blade-globals
PR #52/53 removes include/bittorrent.php and include/cleanup_cli.php, loads include/core.php from artisan, refactors CleanupRun to call CleanupService::runFull() directly, drains raw $_GET/$_POST/$_SERVER/$GLOBALS usage from ~115 legacy Blade/PHP partials, and routes public/announce.php and public/scrape.php through public/nexus.php.
What to verify
docker compose exec php php artisan cleanup:run --forceexits 0 and printsFull cleanup is done+[CLEANUP_RUN] DONE, cost time: <n> seconds.include/bittorrent.phpandinclude/cleanup_cli.phpdo not exist;artisanrequiresinclude/core.php.php -lon changed files;grep -Rinresources/viewsfor$_GET|$_POST|$_REQUEST|$_SERVER|$_FILES|$_COOKIEreturns nothing;extract($context, EXTR_SKIP)is present./announce.phpand/scrape.phpreturn valid bencode (use a valid-qB4...peer_id and 20-byteinfo_hash).- Signup from
/signup.phpcreates a user and lands onok.php?type=confirm; the new user can log in and visituserdetails.php/usercp.php. - Critical path (
index,torrents,details,download,forums,offers,userdetails,login/logout) renders without 500. - SupportContext-heavy pages:
usersearch,usercp,messages,modtask,forum,offers,getrss,torrentrss,invite,bitbucket-upload,settings,freeleech,magic,medal,task,bonus-log,uploaders. - POST wrappers:
takemessage,sendmessage,takeinvite,modrules,moforums,deletemessage,delete/fastdelete.
Common gotchas
- After the superglobal draining, search
resources/views/**forSupportContext::get*(bare_word)(e.g.getQuery(action)orgetPost(returnto)) and fix missing string quotes./complains.phpand/forums.phpPOST actions (setlocked,hltopic,setsticky) will throwUndefined constant "..."if left unquoted. - Use
http://localhost(nothttp://openresty) for browser/curl tests whenbasic.BASEURLis set tolocalhost; otherwise redirects andc_secure_passcookies will not match. - A
c_secure_passcookie generated onlocalhostcannot be reused onopenresty. Generate tokens withApp\Support\AuthCookie::buildToken()inside thephpcontainer for curl scripts. takeinvite.phpneeds working mail to persist an invite; useinvite.php?id=<uid>for UI verification and expectsent=1redirect.download.php?id=<id>&letdown=1should returnapplication/x-bittorrentand a bencode payload.comment.phpshould be POSTed as a normal form (curl -d ...), not withcurl -X POST -L, so the 302 redirect is followed with GET.
Testing PR #283-#286 combined helper migration (devin/phase7-5-6-helpers)
Branch devin/phase7-5-6-helpers contains origin/php8 + PR #283 (Phase 5.2 typed SiteConfig), #284 (Phase 7.1), #285 (Phase 7.2-7.4), and #286 (Phase 7.5-7.6 helper migration). A full re-test of this branch exercises all four PRs.
What to verify
- Lint/static gates:
composer validate --strict,php -longit diff --name-only origin/php8...HEAD -- '*.php',php artisan view:cache, PHPStan default/level5/level5.app/level6 clean. - Unit/feature suites:
phpunit --testsuite Unitandtests/Feature/CriticalPathTest.phpwith-d memory_limit=1G; note PHPUnit deprecation notices are not test failures. php artisan meilisearch:importimports torrents;curl -s 'http://localhost:7700/indexes/torrents/stats'returnsnumberOfDocuments >= 1./edit.php?id=<torrent>must load withoutTypeErrorwhenpos_state_until/pick_untilarenull;Form::datetimepickerInput()now accepts?string./takeedit.phpacceptspos_state=normal+ empty deadline andpos_state=sticky+ futurepos_state_until, redirecting todetails.php?id=<id>&edited=1.- Re-open
/edit.phpand confirm the selected promotion and deadline are persisted. - UI helper smoke:
/upload.php,/torrents.php(search + category/promotion filters),/details.php,/usercp.php,/settings.php(Authority/Torrent Settings),/userdetails.php,/messages.php,/index.php,/downloadnotice.php,/download.php,/forums.php,/offers.php,/topten.php,/log.php,/latestcomments.php,/faq.php,/rules.php,/contactstaff.php,/staffpanel.php,/mybonus.php. - Tracker endpoints:
/announce.phprejects invalid passkey/info_hash/peer_id with bencoded failure reasons; valid request returnsinterval/peers(awarning messagefor frequent requests is expected);/scrape.phpreturns bencodefilesdict (the key is raw 20-byteinfo_hash, so Pythonbencodemay need raw-byte key handling). c_secure_passcookie for curl scripts can be generated from inside thephpcontainer withApp\Support\AuthCookie::buildToken(); setAPP_KEYexplicitly because the standalone script does not boot the Laravel container:APP_KEY='base64:...' docker compose exec -T -e APP_KEY="$APP_KEY" php php /var/www/html/build_cookie.php- Do not double-encode
info_hash/peer_idin announce URLs; build the query string manually withurllib.parse.quote(raw_bytes)rather than passing raw bytes throughrequestsparams.
Common gotchas
CriticalPathTestleavesbasic.BASEURLset toopenresty; restore it tolocalhostand flush caches before host-side browser/curl tests:
Then runUPDATE settings SET value='localhost' WHERE name='basic.BASEURL';php artisan config:clear view:clear route:clear cache:clear.- The
phpcontainer has nobash; usesh -cfor inline environment variables. - Browser file inputs are not reliably drivable; submit
/takeupload.phpand/takeedit.phpvia an authenticated curl/Python session and use the browser to verify the resulting pages. .gitis not mounted inside thephpcontainer, sophp -lon changed files must be run from the host (git diff origin/php8...HEAD -- '*.php' | xargs -P4 -n1 docker compose exec -T php php -l).downloadnotice.phpmay only appear for a normal user's first authenticated download; after thatdownload.php?id=<id>&letdown=1returns the.torrentdirectly.
Testing PR #299 / Phase 11 converted view consolidation
Scope
PR #299 converts the remaining resources/views/**/_*_legacy.php partials to *.blade.php and updates UtilityController (ajax) and ShoutboxController (SSE). All take*, staff, utility, and converted public pages should render without Cannot redeclare worker fatals or unescaped-HTML regressions.
What to verify
- Static gates:
composer validate --strict,php -lon changed PHP/Blade files,php artisan view:cache, PHPStan default/level5/level5.app/level6 clean. - Unit/feature suites:
phpunit --testsuite UnitandCriticalPathTestwith-d memory_limit=1G; restorebasic.BASEURLtolocalhostand clear caches afterwards. php artisan meilisearch:statsshows the expected document count; new uploads are searchable./upload.phprenders with file/name/desc/category/taxonomy/Pick fields;/takeupload.phpaccepts a generated.torrentand 302s todetails.php?id=<id>&uploaded=1(new) ordetails.php?id=<id>&existed=1(duplicate)./edit.php?id=<torrent>loads with promotion,pos_state, andpos_state_untilfields;/takeedit.phppersists changes and redirects todetails.php?id=<id>&edited=1./torrent.php?id=<id>alias renders the same content as/details.php?id=<id>./torrents.php?search=<name>and/torrents.php?cat401=1&spstate=5return expected results./announce.phpand/scrape.phpreturn valid bencode; 19-byteinfo_hash/peer_idreturn bencoded failure reasons./shoutbox_sse.phpstreamstext/event-streamevent: pingmessages./ajax.phpactions return JSON without PHP worker fatals;clearShoutBoxshould load theUsermodel fromSupportContextand pass it toPermission::canto avoid relying on Laravel'sAuth::user()in legacy AJAX paths./messages.php(inbox) should render:messagemenu()andinsertJumpTo()must be defined before they are called inresources/views/messages/_messages.blade.php./delete.php?id=<torrent>,/takeinvite.php,/checkuser.phpno longer call a missingbark()helper; use\App\Support\LegacyResponse::abort($title, $msg)instead./takeconfirm.phploadslang/en/lang_takeconfirm.phpwhich references$SITENAME/$REPORTMAIL; ensureLegacyRequestMiddlewaresets these as local variables before requiring language files./takereseed.php?id=<torrent>falls back toidwhenreseedidis absent and guards againstnulltorrent./ajax.php?action=saveUserMedalhandles string-encodedparamsand validates each entry before indexing.
Test data / helpers
- Generate a fresh
.torrentwithbencodepyandannounce=http://localhost/announce.phpfor a clean upload test. - Use
xdotool+scrotto drive the visible Chrome window and capture named screenshots without the huge HTML dumps from thecomputerscreenshot tool:WID=$(xdotool search --onlyvisible --name 'NexusPHP') xdotool windowactivate $WID xdotool key ctrl+l xdotool type --delay 10 'http://localhost/<page>.php' xdotool key Return sleep 3 scrot -u /home/ubuntu/screenshots/ss_<page>.png
PR #299 re-run notes
- The six targeted regressions from the first E2E run (
/messages.php,/delete.php?id=<torrent>,/takeconfirm.php,/takereseed.php?id=<torrent>,/ajax.php?action=saveUserMedal,/ajax.php?action=clearShoutBox) were fixed by commite7daa1fd. /thanks.phpis referenced bypublic/js/common.js(ajax.post('thanks.php', ...)); addRoute::match(['get', 'post'], '/thanks', [TorrentActionController::class, 'thanks'])->name('thanks.legacy')toroutes/legacy/auth.phpto make the convertedresources/views/thankspartial reachable./page.phpis a dynamic loader and requires aviewquery parameter; the convertedresources/views/page/_page.blade.phpnow returns a 400 response whenviewis missing instead of throwing a 500RuntimeException./image.phponly works with?action=regimage&imagehash=<valid>; without params it returns 404 (expected captcha behavior when the image captcha is disabled).
Phase 12 combined re-run notes
- After the MeiliSearch
visible/banned/anonymousyes/no cast fix and thetorrent/_edithidden-input fix, keyword search (/torrents.php?search=<name>), category/promotion filters, and torrent editing all work for PRs #299-#302. /modtask.phpis a POST-action processor, not a browseable staff page. A GET request withoutactionfalls through topuke()and displays "Permission denied. For security reason, we logged this action" even when thePermission::can(MANAGE_USER_BASIC_INFO)check succeeds. To verify it, POSTaction=edituseroraction=confirmuserwith all required fields fromuserdetails.php./attachments.phpis not a registered route in the combined branch; the route appears to be handled throughattachments/or not exposed as a public.phppage, so a 404 there is expected.php -lon changed files must run from the host side because.gitis not mounted into thephpcontainer. Usegit diff --name-only origin/php8...HEAD -- '*.php' | sed 's|^|/var/www/html/|' | xargs -P4 -n1 docker compose exec -T php php -l.CriticalPathTestsetsbasic.BASEURL='openresty'and must be followed by a restore tolocalhostplus Redis/Laravel cache clears before host-side browser/curl tests.- After clearing caches, run
docker compose exec -T php php artisan view:cacheanddocker compose exec -T php php artisan route:cacheso legacy pages do not recompile on every request.
Testing PR #308 / Phase 14 final-polish (devin/phase14-final-polish)
PR #308 removes app/Support/Legacy/functions.php and inlines helpers into typed \App\Support\* static methods.
What to verify
composer validate --strict,php -lon changed files,php artisan view:cache,php artisan route:cache, PHPStan default + level6.phpunit --testsuite Unit(bump memory to-d memory_limit=1G) andtests/Feature/CriticalPathTest.phpwithCRITICAL_PATH_BASE_URL=http://openresty./torrentrss.php?passkey=<passkey>returns valid RSS when a torrent has anulldescr(the fix coerces$row['descr']to(string)in the view)./staff.phpand/users.phpfor a normal user return a graceful legacyPermission denied!page viaapp/Exceptions/Handler.php./staff.phpand/users.phpforsysopstill render the staff/user list.- Legacy public pages (
forums,messages,usercp,mybonus,donate,shoutbox,shoutbox_history,opensearch) and staff pages (settings,staffpanel,staffmess,staffbox,topten,catmanage,forummanage) return200. - API smoke for
normaluser,sysop, and a freshly factory-created user if relevant.
Common gotchas
- The
InsufficientPermissionExceptionthrown inside Blade partials is wrapped in nestedIlluminate\View\ViewExceptionobjects; the exception handler must recursively unwrapViewException::getPrevious()and catchHttpResponseExceptionfromLegacyResponse::permissionDenied(). - After
CriticalPathTest, restorebasic.BASEURL='localhost'and clear the Redis keysnexus_settings_in_nexusandnexus_settings_in_laravelplus Laravel caches before host-side tests. - The Unit suite may need
php -d memory_limit=1Gto avoid exhausting the default 128 M limit inRouteServiceProvider. php -lon changed files must run from the host because.gitis not mounted in thephpcontainer:git diff --diff-filter=ACMR --name-only origin/php8...HEAD -- '*.php' '*.blade.php' | sed 's|^|/var/www/html/|' | xargs -P4 -n1 docker compose exec -T php php -l
Testing Phase 17 full DB-in-views bridge (PR #320)
Phase 17 migrates the remaining public/admin/listing Blade/PHP views into resources/legacy/*.php partials rendered through App\Repositories\LegacyViewRepository and routes in routes/legacy/{public,auth}.php. The four key runtime fixes are:
App\Auth\Permission::user()resolves thenexus-webguard first, falls back to the default guard, and returns?Userso legacy cookie-auth routes do not throwReturn value must be of type App\Models\User, null returned..docker/openresty/sites/app.conf.templateremoves$uri/fromlocation / try_files, so the/torrentsroute is not shadowed by thepublic/torrents/directory.lang/en/lang_userhistory.phpadds the missinghead_user_historykey.TorrentActionController::downloadnoticehandles POST, writes theshowdlnoticepreference, and redirects to/download?id=<id>&letdown=1;Path::resolveresolves relative paths againstROOT_PATHso the torrent file is served withContent-Type: application/x-bittorrent.
Environment / fixtures
- The
phpcontainer cannot rungit diffin a worktree, sophp -lon changed files must be driven from the host:git diff --diff-filter=ACMR --name-only origin/php8..HEAD -- '*.php' '*.blade.php' \ | sed 's|^|/var/www/html/|' \ | xargs -P4 -n1 docker compose -p tracker-lp-bits exec -T php php -l - Many Phase 17 golden paths reference
torrents.id=3andusers.id=10322(which does not exist). Create a test torrent fixture:
Then in MySQL:cp /home/ubuntu/repos/phase17i-worktree2/torrents/39.torrent \ /home/ubuntu/repos/phase17i-worktree2/torrents/3.torrent
For regression tests, insert aINSERT INTO torrents (id, info_hash, name, filename, save_as, owner, category, source, medium, codec, standard, processing, audiocodec, size, added, type, numfiles, sp_state, visible, banned, approval_status, anonymous, url, pos_state, cache_stamp, hr, price, pieces_hash) VALUES (3, NULL, 'Phase17 Fixture 3', 'phase17_fixture3.torrent', 'Phase17 Fixture 3', 1, 401, 0, 0, 0, 0, 0, 0, 1234, NOW(), 'single', 1, 1, 'yes', 'no', 1, 'no', NULL, 'normal', 0, 0, 0, '') ON DUPLICATE KEY UPDATE name=VALUES(name), filename=VALUES(filename), save_as=VALUES(save_as), owner=VALUES(owner), category=VALUES(category); INSERT INTO torrent_extras (torrent_id, descr) VALUES (3, '') ON DUPLICATE KEY UPDATE descr=VALUES(descr);torrent_extrasrow withdescr=''for the fixture torrent so the edit form renders a pre-filled description. The view now casts missing/nulldescrandtechnical_infoto strings, so a missing row no longer causes aTypeError. - Generate a fresh sysop cookie for curl/Puppeteer:
APP_KEY='base64:WUbN2wa2kl3E1VDW4iKaH3RBHw3hKY7BK0hWEkBZmGg=' docker compose -p tracker-lp-bits exec -T -e APP_KEY="$APP_KEY" php php -r \ 'require "/var/www/html/vendor/autoload.php"; require "/var/www/html/bootstrap/app.php"; echo \App\Support\AuthCookie::buildToken(1, null, time()+3600);' \ > /home/ubuntu/phase17-cookie.txt basic.BASEURLshould match the test host. For host-side tests againsthttp://localhost:
Then clear Laravel/Redis caches and rebuild view/route caches.UPDATE settings SET value='localhost' WHERE name='basic.BASEURL';
Golden-path smoke list
- Phase 17a:
/aboutnexus.php,/faq.php,/rules.php,/donate.php. - 17b:
/topten.php?type=1&subtype=0&lim=10. - 17c:
/stats.php,/allagents.php,/mysql_stats.php,/viewfilelist.php?id=3,/viewsnatches.php?id=3,/searchsuggest.php?q=test,/autocomplete_torrents.php?q=test,/nowarn.php. - 17d:
/viewpeerlist.php?id=3,/getusertorrentlistajax.php?userid=1&type=uploaded|seeding|leeching|completed|incomplete(useuserid=1because10322does not exist). - 17e:
/search.php. - 17f:
/details.php?id=3,/edit.php?id=3,/upload.php,/torrent_info.php?id=3. - 17i:
/staffpanel.php,/reports.php,/bans.php,/cheaterbox.php,/iphistory.php,/catmanage.php?action=view&type=searchbox|category|source|secondicon,/forummanage.php,/settings.php,/modtask.php. - 17j/k/l:
/downloadnotice.php?torrentid=1&type=firsttime(GET form, thenPOSTwithid=1&type=firsttime&hidenotice=1),/download?id=1&letdown=1and/download?id=3&letdown=1(should returnapplication/x-bittorrent),/usercp.php,/userdetails.php?id=1,/myhr.php,/warned.php,/user-ban-log.php. - Regression:
/login.php,/signup.php,/index.php,/torrents.php,/torrents,/staffpanel.php. - Previously failing routes:
/userhistory.php?id=1,/viewpeerlist.php?id=1.
Pass criteria
- All GET/POST routes return HTTP
200(or302for explicit redirects) with noWhoops,Fatal error,Parse error,Server Error,Stack trace,Internal Server Error, orReturn value must be of typetext. /downloadand the redirect fromPOST /downloadnoticereturnContent-Type: application/x-bittorrentwith a non-empty bencode body./searchsuggest.php?q=testreturns a JSON array (["test",[],[]]);/autocomplete_torrents.php?q=testreturns a JSON object ({"torrents":[]})./viewfilelist.php?id=3and/getusertorrentlistajax.php?userid=1&type=uploadedreturn HTML containing a<table>ortext_no_record.- After browsing,
php artisan view:cache,php artisan route:cache, andopenresty -tall pass.
Common gotchas
/edit.php?id=<torrent>needs a matchingtorrentsrow. Atorrent_extrasrow with a non-nulldescris needed only to pre-fill the description; the view now casts missing/nulldescrandtechnical_infoto strings, soForm::bbcodeEditor()no longer throws aTypeError.LegacyRequestMiddlewarerewrites.phpURLs (/details.php?id=3→/details/3,/viewfilelist.php?id=3→/viewfilelist?id=3) before routing, so no per-pagepublic/*.phpwrappers are needed for these paths.basic.BASEURLmay be reset toopenrestyafterCriticalPathTestorcleanup:run. Re-set it tolocalhostand clear caches for host-side tests; only the announce/comment URL inside the generated.torrentis affected, not the download itself.
Testing PR #322-#337 (Phase 19 legacy layout migration)
Scope
All active legacy PHP partials under app/Services/Legacy/partials/ are now rendered through resources/views/layouts/legacy.blade.php with per-page resources/views/<name>/index.blade.php wrappers. Affected routes include /index.php, /forums.php, /usercp.php, /messages.php, /sendmessage.php, /log.php, /news.php, /makepoll.php, /polloverview.php, /catmanage.php, /complains.php, /offers.php, /latestcomments.php, /shoutbox_history.php, /friends.php, /bitbucketlog.php, /downloadnotice.php, /donated.php, /clearcache.php, /invite.php, /mybonus.php//my_bonus.php, /usersearch.php, and the Filament /nexusphp admin panel.
Environment setup
- Ensure the
nexusphp_php/nexusphp-openrestycontainers mount/home/ubuntu/repos/tracker-lp-bitsat/var/www/html. - Set
basic.BASEURLtoopenrestyand clear Laravel/Redis caches. - Generate a
c_secure_passcookie forid=1(sysop) using the knownAPP_KEY:docker compose -p tracker-lp-bits exec -T -e APP_KEY='base64:WUbN2wa2kl3E1VDW4iKaH3RBHw3hKY7BK0hWEkBZmGg=' php php -r \ 'require "/var/www/html/vendor/autoload.php"; require "/var/www/html/bootstrap/app.php"; echo \App\Support\AuthCookie::buildToken(1, null, time()+3600);' \ > /home/ubuntu/phase19-cookie.txt - Add a minimal forum fixture (
forums.id=179,topics.id=185,posts.id=139) soviewforum/viewtopic/newtopiccan be exercised.
Automation notes
- Use Playwright over CDP (
http://localhost:29229) or launch headful with the Devin Chrome binary at/opt/.devin/playwright_browsers/chromium-1097/chrome-linux/chrome. - The legacy layout wrapper means every rendered page should have exactly one
<html>,<head>, and<body>block. Checkpage.content()forInternal Server Error,Fatal error,Whoops, orPage Expired. - Theme switching is exercised via
/usercp.php?action=trackerby selectingstylesheet=4(Classic) orstylesheet=6(Dark Passion) and submitting the form; the redirect target is/usercp.php?action=tracker&type=saved.
Known route/shim issues
donated.phpwas originally missingresources/views/donated/_donated.blade.php; the fix is a one-line view that calls\App\Repositories\LegacyViewRepository::render('donated', get_defined_vars()).makepoll.phphad<table><form>HTML nesting; the browser ejected the<form>before its<input>elements, preventing submission. The fix is to wrap the<table>inside the<form>./my_bonus.phpneeds more than a Laravel route alias.LegacyRequestMiddleware::EXTRA_LANG_FILESmaps the detectedSCRIPT_NAMEto language files. Because/my_bonus.phpresolves to scriptmy_bonusand there is nolang_my_bonus.php,$lang_mybonusstrings are empty. Add'my_bonus' => ['mybonus.php']toEXTRA_LANG_FILESso the page renders identically to/mybonus.php.- The Filament admin panel at
/nexusphpmust be excluded from legacy URL rewriting. IfLegacyRequestMiddlewarerewrites/nexusphp(or/nexusphp/user/users,/livewire/update,/api/*,/horizon) to a legacySCRIPT_NAME, Filament returns a 404. Add aLARAVEL_ONLY_PREFIXESlist (api,livewire,filament,nexusphp,horizon) and apassthroughRequest()path that setsSCRIPT_NAME=/index.phpand dropsPATH_INFO.
Quick pass/fail gate
- All listed routes return HTTP
200(or302for form save redirects) and no fatal text. php artisan view:cache,php artisan route:cache, andopenresty -tsucceed after browsing.- Screenshot key pages in both desktop (1280x900) and mobile (375x667) viewports; legacy fixed-width themes may overflow horizontally on mobile but must not hide primary content.
Testing PR #359-#362 (Phase 21 performance / profiling / queue)
Scope
Phase 21 combines:
devin/phase21-perf(#359, cache/eager-load)devin/phase21-5-3-octane(#360, MySQL read/write split + statelessSupportContext)devin/phase21-4-jobs(#361, Horizon queue jobs,cleanupcontainer removed)devin/phase21-6-profiling(#362,X-Queries-Countheader)
Quick verification checklist
docker compose -p tracker-lp-bits up -d --remove-orphansmust remove thenexusphp-cleanupcontainer.docker exec -i nexusphp-queue pgrep -f horizonmust showphp artisan horizonand worker processes.- Static gates:
composer validate --strict,php -l(inside thephpcontainer on/var/www/html/appand/var/www/html/routes),phpstandefault/level5.app/level6,phpunit --testsuite Unit,php artisan view:cache,php artisan route:cache,openresty -t. X-Queries-Countheader must be present on legacy pages and contain a non-negative integer. Verify with:curl -s -D - -o /dev/null -b "c_secure_pass=$(cat cookie.txt)" http://openresty/index.php curl -s -D - -o /dev/null -b "c_secure_pass=$(cat cookie.txt)" http://openresty/forums.php- Read/write split sticky behavior: create a temporary
public/rw_test.phpthat boots Laravel, runs ausersUPDATE, and comparesDB::connection('mysql')->getReadPdo()withgetPdo()in the same request. The two PDO objects must be identical after a write:$conn = DB::connection('mysql'); $conn->table('users')->where('id', $sysopId)->update(['last_access' => now()->toDateTimeString()]); echo json_encode(['read_is_write' => $conn->getReadPdo() === $conn->getPdo()]); - Stateless
SupportContext: hit/index.phpwith a sysop cookie and then with a second-user cookie in sequence; each response must contain the matching username (<b>devintest</b>vs<b>crit63522073</b>) and no cross-request leakage. - DB mutations still work: forum new topic (
POST /forums.php action=post&type=new&id=179), offer add/vote/delete (/offers.php), PM send/delete (/takemessage.php,/deletemessage.php),usercp.phppersonal/forum/tracker save forms,catmanage.php?action=del, and shoutbox post/delete. - API
POST /api/v1/usercp/settingsandPOST /api/v1/usercp/forumwith a Sanctum Bearer token must return{"ret":0,...}. - Queue jobs:
AttendanceJob,CleanupJob,HrCheckJob,SeedBonusJob,UpdateTorrentSeedersEtc,UpdateUserSeedingLeechingTimeshould dispatch without throwing. A helper script usingdispatch_sync()inside thephpcontainer is fine for E2E verification. Checkfailed_jobsis empty afterwards. php artisan cleanup:run --forcemust complete and printFull cleanup is done.- Benchmark with
ab(orcurlloop) for/index.phpand/forums.phpusing the sysop cookie; captureRequests per second,Failed requests, andX-Queries-Countvalues.
Common gotchas
php -lon the host will fail becausephpis not installed on the VM; run it insidenexusphp-phpagainst/var/www/html/appand/var/www/html/routes.- Temporary helper scripts that need to be hit from the browser (
rw_test.php) must be placed underpublic/, not the repo root, because the web root is/var/www/html/public. php artisan cleanup:run --forcemay report 0 cost time if all cleanup classes ran recently; the important signal isFull cleanup is doneand no exceptions.abcannot pass cookie values containing=using-C; use-H "Cookie: c_secure_pass=<token>"instead.composer validatemust be run from/var/www/htmlinside thephpcontainer.
Testing Phase 20c/d/e API parity
Use these notes when verifying the new Sanctum API endpoints for usercp, messages, topics, and nested posts.
Creating an API test user
If the sysop account's password is unknown, generate a fresh factory user with class = User::CLASS_SYSOP and a Sanctum token with all abilities:
docker exec -i nexusphp-php php artisan tinker --execute="
use App\\Models\\User;
\$u = User::factory()->admin()->create();
\$t = \$u->createToken('phase20-smoke', ['*'])->plainTextToken;
echo json_encode(['id' => \$u->id, 'username' => \$u->username, 'token' => \$t]);
"
The default factory password is 123456, which validates against WebAuthService::validatePassword() because the factory passhash is created with the legacy MD5 format and auth_key is empty.
API endpoint request shapes
Send mutation bodies as JSON (Content-Type: application/json) to avoid form-data parse issues on PATCH requests.
GET /api/v1/forums— returns{"ret":0,"data":{"data":[...]}}. CheckX-Queries-Count.POST /api/v1/usercp/forum— setstopicsperpageandpostsperpage.POST /api/v1/usercp/tracker— setstorrentsperpage,pmnum,sbnum,sbrefresh, etc. It does not updatetopicsperpage/postsperpage.POST /api/v1/usercp/security— requirescurrent_passwordand optionallyprivacy,resetpasskey,new_password.POST /api/v1/messages— create withreceiver,subject,msg.GET /api/v1/messages— mailbox list (defaultmailbox=0corresponds to the inbox).GET /api/v1/messages/{id}— show and auto mark-as-read.PATCH /api/v1/messages/{id}— updateunread(yes/no) orlocation.GET /api/v1/messages-unread— list unread messages.DELETE /api/v1/messages/{id}— delete for the authenticated user.POST /api/v1/topics— create topic inforumidwithsubject/body; responsefirstPostandlastPostshould be equal.GET /api/v1/topics/{topic}/posts— list posts.POST /api/v1/topics/{topic}/posts— reply.PATCH /api/v1/topics/{topic}/posts/{post}— edit body.DELETE /api/v1/topics/{topic}/posts/{post}— delete reply.DELETE /api/v1/topics/{topic}— delete topic.
Legacy flow equivalents
/usercp.php?action=personal|forum|tracker|securitystill render and post to/usercp.phpwithaction=<tab>&type=save./sendmessage.php?receiver=<id>renders;POST /takemessage.phpsends PM.GET /deletemessage.php?id=<id>&type=indeletes inbox PM./forums.php?action=newtopic&forumid=179renders;POST /forums.php action=post&id=179&type=newcreates a topic.POST /forums.php action=post&id=<topicid>&type=replyreplies.POST /forums.php action=post&id=<postid>&type=editedits.GET /forums.php?action=deletepost&postid=<id>&sure=1deletes a reply.GET /forums.php?action=deletetopic&topicid=<id>&sure=1deletes a topic.
Devin Secrets Needed
- None beyond the existing test DB credentials and
APP_KEY(already mounted in the Docker stack).