Skip to content

test: durable e2e SABnzbd compliance suite; fix priority-lookup bug - #84

Merged
thedancingdeveloper merged 15 commits into
mainfrom
fix/77-e2e-sab-compliance
Aug 11, 2026
Merged

test: durable e2e SABnzbd compliance suite; fix priority-lookup bug#84
thedancingdeveloper merged 15 commits into
mainfrom
fix/77-e2e-sab-compliance

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Collaborator

Summary

Fixes #77. Adds apps/rustnzb/tests/sab_compliance_e2e.rs: a durable, HTTP-level test suite that spins up the real server (axum::serve + reqwest, same pattern as api_contracts.rs) and exercises the exact SABnzbd API surface that drifted from the real protocol across #65 and #71-#76 without any test catching it — version, addfile, addurl (GET), get_cats, get_scripts, mode=queue&name=priority|rename|delete, and change_cat. Each test cites the upstream sabnzbd/sabnzbd@5.1.x source it was verified against, matching the pattern used in the individual #71-#76 fixes.

This is a stacked PR — its base includes #70 and #78-#83 (all of which it exercises end-to-end) — so its diff will shrink to just the new test file plus the fix below once those merge first.

Bug caught by the new suite

Writing the mode=queue&name=priority end-to-end test immediately failed against real nzo_ids. Root cause: queue_manager::set_job_priority requires an exact job-id match, but SABnzbd clients only ever know the truncated SABnzbd_nzo_<12 chars> form — both the pre-existing top-level handle_priority and the new mode=queue&name=priority route (#72) stripped the SABnzbd_nzo_ prefix and passed the truncated id straight to set_job_priority, so priority changes silently failed against any real nzo_id. This is a pre-existing bug (present in handle_priority before any of #71-#76), not something introduced by this branch — the handler-level tests in sabnzbd_compat.rs didn't catch it because they used full job IDs directly rather than going through the real truncated-id round trip.

Fixed by resolving the full job id via prefix lookup first, the same way pause/resume/rename/change_cat already do.

Test plan

  • cargo test -p nzb-web — full suite passes (30 tests in sabnzbd_compat).
  • cargo test -p rustnzb --test sab_compliance_e2e — all 9 new e2e tests pass.
  • cargo test -p rustnzb — full workspace test suite passes, no regressions.
  • cargo clippy -p nzb-web --all-targets / cargo clippy -p rustnzb --all-targets — clean.

🤖 Generated with Claude Code

NZB360 (and real SABnzbd) add search results by issuing a plain GET
request with mode=addurl, since there's no file body to upload — only
the multipart POST path applied the cat/priority/password overrides
for that mode, so GET requests silently fell through to the "Unknown
mode" branch and any requested category was dropped.

Extract the URL-fetch-and-enqueue logic into a shared handle_addurl()
used by both the GET and POST entry points.
sab_priority_to_priority mapped 0/1/2/-100/3 to Low/Normal/High/Force,
shifted by one from SABnzbd's real numeric codes (constants.py:
Low=-1, Normal=0, High=1, Force=2, Default=-100, Repair=3) and
contradicting sab_priority_matches, which already used the correct
table for queue filtering. Any client setting priority via
addfile/addurl/priority-change got the wrong priority applied.
Real SABnzbd has no top-level mode=priority or mode=rename -- those
are sub-commands of mode=queue, dispatched via the name parameter
(_api_queue_table: delete, rename, priority, purge, pause, resume,
change_complete_action, ...). handle_queue only recognized
delete/pause/resume, so a compliant client's real priority-change or
rename request silently fell through to a plain queue listing.

Adds routing for name=priority, name=rename, name=purge, and a
change_complete_action no-op, alongside the existing (non-standard
but harmless) top-level mode=priority/mode=rename aliases.
Real SABnzbd's get_cats calls list_cats(default=False), which leaves
the default category's config-internal name "*" untouched -- the
"*" -> "Default" substitution only happens for the config UI
(default=True). RustNZB returned the display string "Default"
instead, which a client that specifically recognizes "*" as the
default-category sentinel wouldn't find.

Adds sab_resolve_category() to translate "*" back to RustNZB's
internal "Default" category name wherever a client-supplied cat
value is applied (addfile, addurl, change_cat), so both directions
of the boundary translation stay consistent.
get_scripts is a real top-level SABnzbd API mode
(sabnzbd/api.py::_api_table["get_scripts"]) that fell through
dispatch_mode's default arm as "Unknown mode". RustNZB doesn't
support post-processing scripts, so ["None"] -- the same value real
SABnzbd reports with no scripts configured -- is the correct
permanent response.

Clients that fetch categories and scripts together to populate an
add-download dialog may abort populating the whole dialog (category
picker included) if either call errors, so this is a plausible
second contributor to #65 alongside #73.
Real SABnzbd's _api_queue_delete and _api_history_delete both accept
a comma-separated list of nzo_ids in `value`, and _api_history_delete
additionally accepts a `del_files` flag that removes the completed
output directory from disk. RustNZB's handlers only matched a single
ID (or the literal "all") and never freed disk space regardless of
del_files.

Also fixes a related gap: the POST handler's catch-all mode dispatch
hardcoded `value`/`value2` to None instead of forwarding them from
the query string, which would have silently broken these (and #72's
priority/rename) sub-commands over POST.
Real SABnzbd's _api_change_cat parses `value` as a comma-separated
list of nzo_ids via clean_comma_separated_list, applying the category
change to all of them. handle_change_cat treated `value` as a single
job ID, so a multi-ID request (e.g. bulk re-categorize) matched no
job and silently failed for all of them.
…mpliance

# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
…nce suite (#77)

set_job_priority requires an exact job-id match, but clients only ever
send the truncated SABnzbd_nzo_<12 chars> form -- both the top-level
handle_priority and the new mode=queue&name=priority route stripped
the prefix and passed the truncated id straight through, so priority
changes always silently failed against a real nzo_id. Resolve the
full job id by prefix first, the same way pause/resume/rename/
change_cat already do. This was a pre-existing bug caught by the new
end-to-end suite below, not introduced by the queue-routing fix.

Adds apps/rustnzb/tests/sab_compliance_e2e.rs: a durable HTTP-level
test suite (spins up the real router via axum::serve + reqwest,
unlike the handler-function-level tests in sabnzbd_compat.rs) that
exercises version, addfile, addurl (GET), get_cats, get_scripts,
mode=queue&name=priority/rename/delete, and change_cat -- the exact
surface area that drifted from the real SABnzbd protocol across
issues #65 and #71-#76 without any test catching it.
# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
…ance

# Conflicts:
#	crates/nzb-web/src/sabnzbd_compat.rs
@thedancingdeveloper
thedancingdeveloper enabled auto-merge (squash) August 11, 2026 10:03
@thedancingdeveloper
thedancingdeveloper merged commit 420c9fe into main Aug 11, 2026
4 of 5 checks passed
thedancingdeveloper added a commit that referenced this pull request Aug 11, 2026
Main's CI rust job failed at cargo fmt --all --check after #84
merged -- none of the #71-#77 fix branches were formatted before
merging. No behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add durable e2e test coverage that monitors SABnzbd API compliance

1 participant