prop/bugs.md
Graham McIntire 0704253af8
fix(security,test): viewer-aware profile queries + create_contact ordering + valkey test isolation
- /u/:callsign profile no longer leaks private contacts or pending beacons;
  Radio.list_contacts_for_user/2 and Beacons.list_beacons_for_user/2 now
  filter by viewer (owner/admin see everything, others see only public).
- Radio.create_contact/2 places user_id on the struct before
  submission_changeset so validate_user_or_email/1 accepts authenticated
  submissions that omit submitter_email.
- valkey_test.exs runs async: false; its setup mutates global Application
  env and Process registry, which otherwise crashed concurrent ConnCase
  GridCache.clear/0 calls (Mox.UnexpectedCallError on SCAN).
2026-05-12 08:49:08 -05:00

4.9 KiB

Bugs Found 2026-05-12

All three fixed; mix test --seed 949374 now passes (3893 tests, 0 failures).

1. Public profile leaks private contacts and pending beacons [FIXED]

Severity: High
Category: Privacy / authorization bypass
Files:

  • lib/microwaveprop_web/live/user_profile_live.ex:21
  • lib/microwaveprop_web/live/user_profile_live.ex:22
  • lib/microwaveprop_web/live/user_profile_live.ex:92
  • lib/microwaveprop_web/live/user_profile_live.ex:130
  • lib/microwaveprop/beacons.ex:61

/u/:callsign is mounted in the public LiveView session, but it loads Radio.list_contacts_for_user(user) and Beacons.list_beacons_for_user(user) without checking whether the visitor is the profile owner or an admin.

list_contacts_for_user/1 returns every submitted contact for that user, including contacts where private == true. The profile template then renders those rows and links to /contacts/:id. The contact detail page correctly 404s for unauthorized viewers, but the public profile has already exposed the contact timestamp, stations, band, mode, and distance.

list_beacons_for_user/1 explicitly returns approved and pending beacons for the public profile page. The profile template renders pending beacon callsign, frequency, grid, keying, status, and link. This contradicts the beacon visibility model where pending beacons should only be visible to the submitter and admins.

Suggested fix: split the profile query by viewer. Anonymous/non-owner visitors should see only non-private contacts and approved beacons. Owners/admins can see their private contacts and pending beacons. Add LiveView tests for anonymous, owner, and admin views.

2. Radio.create_contact/2 rejects authenticated submissions unless callers also pass an email [FIXED]

Severity: Medium
Category: Broken context contract / validation ordering
Files:

  • lib/microwaveprop/radio.ex:629
  • lib/microwaveprop/radio.ex:632
  • lib/microwaveprop/radio.ex:633
  • lib/microwaveprop/radio/contact.ex:109
  • lib/microwaveprop/radio/contact.ex:173

Radio.create_contact(attrs, user_id) adds user_id after Contact.submission_changeset/2 has already run validate_user_or_email/1. That validation only sees fields cast from attrs, so a valid authenticated create with user_id but no submitter_email returns an invalid changeset with submitter_email: can't be blank.

Confirmed with:

Radio.create_contact(valid_attrs_without_submitter_email, user.id)
#=> {:error, %Ecto.Changeset{errors: [submitter_email: {"can't be blank", []}]}}

Current web/API call sites mask this by inserting the user's email into params before calling the context, but the public function's contract and implementation disagree. Any future trusted caller that relies on the user_id argument alone will fail unexpectedly.

Suggested fix: put user_id into the struct or attrs before calling Contact.submission_changeset/2, or move the ownership validation to after maybe_put_user_id/2. Add a context-level regression test that passes user_id without submitter_email.

3. Full test suite is order-dependent around Valkey-backed GridCache.clear/0 [FIXED]

Severity: Medium
Category: Test isolation / flaky suite
Files:

  • test/support/conn_case.ex:45
  • test/microwaveprop/weather/grid_cache_valkey_test.exs:26
  • test/microwaveprop/weather/grid_cache_valkey_test.exs:35
  • lib/microwaveprop/weather/grid_cache.ex:477

One mix test run failed with 3 setup failures. ConnCase calls GridCache.clear/0 for every web test. If a Valkey cache test has registered Microwaveprop.Valkey.Conn and swapped :valkey_adapter, GridCache.clear/0 takes the Valkey path and calls Valkey.scan_match/1. In the failed run, that reached Microwaveprop.Valkey.MockAdapter.command/3 without a SCAN expectation, crashing unrelated web tests during setup.

Failure shape:

Mox.UnexpectedCallError: no expectation defined for
Microwaveprop.Valkey.MockAdapter.command/3
args: [Microwaveprop.Valkey.Conn, ["SCAN", "0", "MATCH", "prop:wg:*", "COUNT", "500"], ...]

Verification:

mix test --seed 949374
# 3880 tests, 3 failures, 6 skipped

mix precommit
# 3880 tests, 0 failures, 6 skipped

The subsequent passing mix precommit run makes this look order-dependent rather than a deterministic failure.

Suggested fix: make Valkey test setup restore global process/env state before other tests can observe it, or avoid global Process.register/2 for Conn. Also consider making ConnCase force the ETS fallback or installing a default Valkey mock stub for GridCache.clear/0.

Verification Notes

  • mix compile --warnings-as-errors passed.
  • mix credo --strict reported design/refactoring/style issues only; no bug-class findings were added from Credo.
  • mix test --seed 949374 failed as described in bug 3.
  • mix precommit passed afterward, confirming the checked-in code still clears the normal project gate for this run.