9.3 KiB
Bugs Found 2026-05-12
4. ADIF parser incorrectly identifies tags inside field values
Severity: Medium
Category: Logic / Parsing
Status: OPEN
The AdifImport.parse_fields/1 function uses Regex.scan/3 globally on the record string. ADIF field values can contain < and : characters. If a value contains something that looks like an ADIF tag (e.g., a note containing <MODE:2>CW), the parser will incorrectly identify it as a new tag, potentially overwriting existing fields or creating bogus ones.
Reproduction:
adif = "<CALL:6>N0CALL<BAND:3>3cm<NOTES:20>Contains <MODE:2>CW inside<EOR>"
# Notes will be "Contains <MODE:2>CW "
# Mode will be "CW" (overwritten or newly added, even though it was part of notes)
Suggested fix:
Rewrite parse_fields/1 to be a sequential parser. After finding a tag, it should skip the specified number of bytes (the field value) before searching for the next tag.
5. MechanismClassifier uses hardcoded Sporadic-E MUF factor
Severity: Low
Category: Logic / Consistency
Status: OPEN
MechanismClassifier.try_sporadic_e/1 uses a hardcoded factor of 5.0 to estimate the Sporadic-E MUF from foEs (muf_mhz = 5.0 * foes). However, the Microwaveprop.Propagation.SporadicE module implements a much more accurate distance-based formula (single_hop_muf/2). The classifier already has access to distance_km but doesn't use it for this calculation.
Suggested fix:
Update MechanismClassifier.try_sporadic_e/1 to call SporadicE.single_hop_muf/2 using the contact's distance.
6. Radio.ensure_positions!/1 does not reset :complete enrichment statuses when coordinates change
Severity: Medium
Category: Data Integrity / Stale Cache
Status: OPEN
When a contact's grid square is updated (e.g., from a 4-character to an 8-character grid), Radio.ensure_positions!/1 recomputes the coordinates. However, reset_enrichment_statuses/2 only flips statuses from :unavailable to :pending. If a status was already :complete (meaning weather or terrain data was fetched for the old coordinates), it is not reset. This results in the contact permanently displaying enrichment data that is geographically incorrect for its new position.
Suggested fix:
Update Radio.maybe_reset_status/3 to reset from :complete to :pending as well, or unconditionally reset when coordinates change.
7. Radio.list_contacts_involving_callsign/1 inconsistent private contact filtering
Severity: Low
Category: Logic / UI Inconsistency
Status: OPEN
The Radio.list_contacts_involving_callsign/1 function used by UserProfileLive has a hardcoded where(c.private == false) filter. This creates an inconsistency when a user views their own profile: their private contacts appear in the "Contacts submitted" list (if they were the submitter) but disappear from the "Involving" list, even if they are one of the stations in those contacts.
Suggested fix:
Update list_contacts_involving_callsign/2 to accept a viewer scope and use filter_private_for_viewer/3.
8. HrrrNativeClient.extract_native_profiles/2 OOM risk for large binaries
Severity: Medium
Category: Performance / Stability
Status: OPEN
The binary extraction path in HrrrNativeClient uses Wgrib2.extract_grid (which employs the -lola grid extraction). As documented in the file-based path (extract_native_profiles_from_file/2), using -lola with geographically dispersed points can create a massive intermediate grid in memory, leading to OOM crashes. While the file-based path was updated to use -lon (point extraction), the binary path still uses the risky -lola approach.
Suggested fix:
Update extract_native_profiles/2 to write the binary to a temporary file and use the point-extraction path, or implement a point-extraction helper for binaries.
9. HrrrNativeGridWorker.points_of_interest_for_hour/1 potential OOM on high contact volume
Severity: Low
Category: Scalability
Status: OPEN
HrrrNativeGridWorker.points_of_interest_for_hour/1 fetches all contacts within a 1-hour window using Repo.all/1 without any limit or batching. If the system scales to a high volume of contacts (e.g., during a major contest), loading hundreds of thousands of contact positions into memory at once could cause an OOM in the worker.
Suggested fix: Use a stream or batch the query for contact positions.
Previous 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:21lib/microwaveprop_web/live/user_profile_live.ex:22lib/microwaveprop_web/live/user_profile_live.ex:92lib/microwaveprop_web/live/user_profile_live.ex:130lib/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:629lib/microwaveprop/radio.ex:632lib/microwaveprop/radio.ex:633lib/microwaveprop/radio/contact.ex:109lib/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:45test/microwaveprop/weather/grid_cache_valkey_test.exs:26test/microwaveprop/weather/grid_cache_valkey_test.exs:35lib/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-errorspassed.mix credo --strictreported design/refactoring/style issues only; no bug-class findings were added from Credo.mix test --seed 949374failed as described in bug 3.mix precommitpassed afterward, confirming the checked-in code still clears the normal project gate for this run.