- /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).
118 lines
4.9 KiB
Markdown
118 lines
4.9 KiB
Markdown
# 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```text
|
|
Mox.UnexpectedCallError: no expectation defined for
|
|
Microwaveprop.Valkey.MockAdapter.command/3
|
|
args: [Microwaveprop.Valkey.Conn, ["SCAN", "0", "MATCH", "prop:wg:*", "COUNT", "500"], ...]
|
|
```
|
|
|
|
Verification:
|
|
|
|
```text
|
|
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.
|