Skip to content

test(sip): M6 review hardening — fail-closed 403 assert, cross-endpoint credential fence, registrar refresh test; drop dead unscoped builders - #156

Merged
ryanmurf merged 1 commit into
masterfrom
fix/m6-test-hardening
Jul 17, 2026
Merged

test(sip): M6 review hardening — fail-closed 403 assert, cross-endpoint credential fence, registrar refresh test; drop dead unscoped builders#156
ryanmurf merged 1 commit into
masterfrom
fix/m6-test-hardening

Conversation

@ryanmurf

Copy link
Copy Markdown
Owner

Fast-track test-hardening + dead-code cleanup from the M6 independent reviews (non-blocking items). No production behavior change; the only non-test diff is deletion of uncalled code.

1. AUTH MINOR-1 — fail-closed assert tightened to 403-only

e2e_per_endpoint_auth.rs dangling-auth scenario asserted codes.contains(&403) || codes.contains(&401). A 401 challenge invites a credential retry — that is not fail-closed. Now asserts a hard 403 only, matching the sibling phantom-endpoint assertion. The shipped code already emits a genuine 403 (event_handler.rs "auth section is unresolvable" arm), so this is test-intent tightening.

2. AUTH MINOR-2 — cross-endpoint credential rejection fence + registrar refresh test

e2e_cross_endpoint_auth.rs (new test binary — avoids the process-global pjsip-config race documented in the sibling file): TWO authed endpoints (alpha @ 127.0.0.6, beta @ 127.0.0.7, distinct TEST-only credentials) on one transport.

  • (a) control: alpha's credential from alpha's IP → accepted (proves the digest is well-formed, so (b) cannot pass because of a broken client digest);
  • (b) alpha's valid credential from beta's IP → rejected: handler returns None, wire shows a fresh 401 and never a 200.

Holds by construction today via per-endpoint auth selection (M6 CP4) but was unguarded against future refactors.

RED captured — with selection temporarily reverted to the pre-CP4 all-credentials union (match matched_endpoint_name.as_deref()match None::<&str> in event_handler.rs), the cross-endpoint INVITE authenticates and the test fails exactly at the intended assertion (control (a) still passing):

[E2E] (a) control: alpha cred from alpha IP (127.0.0.6) accepted
thread 'valid_credential_from_wrong_endpoint_ip_is_rejected' panicked at
crates/asterisk-integration-tests/tests/e2e_cross_endpoint_auth.rs:287:5:
assertion `left == right` failed: a valid credential for endpoint alpha presented
from endpoint beta's source IP must be REJECTED (per-endpoint selection, not a
credential union)
  left: Some("xep-cross")
 right: None
test result: FAILED. 0 passed; 1 failed

registrar.rs::test_refresh_before_expiry_keeps_contact_live: seeds a binding 10s from expiry, refreshes via the real handle_register path, asserts the binding is replaced in place with a fresh registered_at (live, TTL restored, exactly one binding, still routable via best_contact).

RED captured — with register() temporarily changed to always push instead of replace-in-place:

thread 'registrar::tests::test_refresh_before_expiry_keeps_contact_live' panicked at
crates/asterisk-sip/src/registrar.rs:629:9:
assertion `left == right` failed: a refresh must replace the binding in place, never duplicate it
  left: 2
 right: 1

Both RED patches were reverted; the shipped diff is green.

3. ROUTING MINOR-2 — dead code DELETED (not scoped)

session_ext.rs free fn build_reinvite and build_update_connected_line built Via/Contact/From from the raw local_addr with no advertised_signaling_hostport scoping. Grep/callgraph over the workspace: zero callers — the only references were their own definitions and the param-only types ReinviteReason / ConnectedLineInfo (also deleted); the crate re-export surface (lib.rs) only exports SessionSupplement/SupplementRegistry from this module. The live re-INVITE path is SipSession::build_reinvite (session/mod.rs, called from event_handler.rs), which IS external-signaling-scoped and covered. Deleting removes the hazard of someone wiring up an unscoped builder later; module doc updated to point at the scoped path. Unused imports pruned (clippy -D warnings clean).

Verification

  • cargo test --workspace --exclude pjsip-shim4467 passed, 1 ignored (68 suites)
  • cargo clippy --workspace --exclude pjsip-shim -- -D warnings → clean
  • Toolchain: pinned 1.97.0; no cargo fmt --all run

…ndpoint credential fence, registrar refresh test; drop dead unscoped builders

Non-blocking items from the M6 independent reviews:

- AUTH MINOR-1: the dangling-auth fail-closed scenario in
  e2e_per_endpoint_auth accepted 403 OR 401; a 401 challenge inviting a
  retry is not fail-closed. Tightened to a hard 403 only, matching the
  sibling phantom-endpoint assertion (the shipped code already emits a
  genuine 403 — test-intent tightening only).

- AUTH MINOR-2: new e2e_cross_endpoint_auth proves a VALID credential
  for endpoint X presented from endpoint Y's source IP is REJECTED
  (per-endpoint selection, not a credential union), with a positive
  control proving the digest itself is valid. Goes RED if selection is
  reverted to the pre-CP4 all-credentials union. Also a named registrar
  test that a refresh-before-expiry REGISTER replaces the binding in
  place with a fresh registered_at (live, TTL restored, no duplicate).

- ROUTING MINOR-2: deleted session_ext.rs free fn build_reinvite and
  build_update_connected_line (+ param-only types ReinviteReason /
  ConnectedLineInfo). Both built Via/Contact/From from the raw
  local_addr without external-signaling scoping and have zero workspace
  callers — the live re-INVITE path is SipSession::build_reinvite,
  which is scoped and covered.
@ryanmurf
ryanmurf merged commit b959812 into master Jul 17, 2026
3 checks passed
@ryanmurf
ryanmurf deleted the fix/m6-test-hardening branch July 17, 2026 12:34
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.

1 participant