Skip to content

fix(core): QRZ password usable without keychain persistence + parseXml regression tests (stacked on #4043)#4044

Merged
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:fix/qrz-session-password-fallback
Jul 5, 2026
Merged

fix(core): QRZ password usable without keychain persistence + parseXml regression tests (stacked on #4043)#4044
ten9876 merged 2 commits into
aethersdr:mainfrom
jensenpat:fix/qrz-session-password-fallback

Conversation

@jensenpat

Copy link
Copy Markdown
Collaborator

Stacked on #4043 — merge that first. This branch contains #4043's commits plus one of mine; after #4043 merges, the diff reduces to my commit alone.

What live testing on a keychain-less build surfaced

Deploying a test build whose worktree had never run scripts/setup/setup-qtkeychain.sh (so find_package(Qt6Keychain QUIET) silently compiled persistence out) exposed two gaps:

1. Typed password unusable without a keychain

In a keychain-less build — or when the keychain read fails (locked keychain, ACL denial on an unsigned test binary) — a password typed in Radio Setup → QRZ was never handed to QrzClient at all: savePassword() only logged a warning. Lookups fell back to country prefix cards seconds after the operator entered valid credentials, which reads as "lookup is broken".

CallsignLookupService now keeps a memory-only session password and applies it whenever the keychain can't supply one. Persistence and usability are separate concerns: without a keychain the password still works for the whole session and simply has to be re-entered after a restart (the QRZ tab's Test Login already worked, since it uses the field values directly). Never written to disk.

2. Zero test coverage let the #4043 bug ship

The <QRZDatabase> root-element bug (login broken end-to-end since #3990) shipped because parseXml had no tests. ParsedResponse/parseXml are now public-for-tests with a comment saying why, and qrz_callsign_test pins the three live response shapes:

  • login success (Session/Key)
  • lookup success (Callsign + Session)
  • session timeout (Error, no Key)

Verified the tests catch the original bug (Principle VIII/XI): against the pre-#4043 parser they fail 6/6; with #4043's guard, all 53 checks pass.

Testing

  • qrz_callsign_test: ALL PASS with fix(core): QRZ login broken end-to-end since #3990 #4043; 6 failures without it (regression coverage proven both ways).
  • Root-caused from pulled test-Mac logs: WRN aether.qrz: QRZ password persistence unavailable: built without QtKeychain → prefix-fallback-only lookups; this PR + a properly staged qtkeychain build fix both symptoms.

💻 Generated with Claude Code (claude-fable-5 7/1/26) with architecture by @jensenpat

@jensenpat jensenpat self-assigned this Jul 5, 2026
@jensenpat jensenpat marked this pull request as ready for review July 5, 2026 16:29
@jensenpat jensenpat requested review from a team as code owners July 5, 2026 16:29

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @jensenpat — this is a clean, well-reasoned fix, and the "prove the test fails against the pre-#4043 parser" step (Principle VIII/XI) is exactly the discipline that would have caught the original regression. I reviewed against current main.

Heads-up: #4043 has already merged

Commit 1e301dc (#4043) is on main now, which is why this PR shows mergeable_state: dirty. A rebase onto current main will drop the overlapping parseXml guard and reduce the diff to your actual contribution — no action needed beyond the rebase you already anticipated in the description.

What I verified (all good)

  • Session-password fallback (CallsignLookupService): the memory-only m_sessionPassword is applied both in the keychain-less #else path and in the HAVE_KEYCHAIN read-failure branch, so it also rescues the locked-keychain / ACL-denied case on keychain-enabled builds — nice touch beyond the stated scope. savePassword() assigns m_sessionPassword before the empty-check, so clearing the field correctly clears the session copy too. Never touches disk, consistent with the existing QrzClient::m_password handling.
  • Regression tests: parseXml is static, so exposing it public-for-tests is the minimal surface change. Field names (call, firstName, city, grid, licenseClass, hasLatLon, latitude/longitude) match CallsignInfo/the parser, <cmath> is already included for std::abs, and the three fixtures pin the real <QRZDatabase>-wrapped shapes including the error/no-key timeout path.
  • CMake: adding QrzClient.cpp + Qt6::Network to qrz_callsign_test is required for the QNetworkReply link; the Windows/macOS keychain runtime-lib copies are all guarded by WIN32/APPLE + EXISTS, so they're no-ops where the staged libs aren't present and don't perturb the packaged (windeployqt/macdeployqt) paths.

No blocking issues from me — the code is correct and in-scope; just rebase now that #4043 has landed. Nice work.


🤖 aethersdr-agent · cost: $4.7873 · model: claude-opus-4-8

jensenpat added 2 commits July 5, 2026 10:48
…l regression tests — Principle VIII.

Two gaps surfaced by live testing on the keychain-less test-Mac build:

1. Session-password fallback. In a build without QtKeychain (or when the
   keychain read fails — locked keychain, ACL denial on an unsigned test
   binary), a password typed in Radio Setup → QRZ was never handed to
   QrzClient at all: savePassword() only warned, so lookups fell back to
   prefix cards even seconds after the operator entered valid
   credentials. CallsignLookupService now keeps a memory-only
   m_sessionPassword and applies it whenever the keychain can't supply
   one — persistence and usability are separate concerns. Never written
   to disk; a restart still requires re-entry when no keychain exists.

2. Parser regression tests. The aethersdr#4043 root-element bug (readElementText
   on <QRZDatabase> swallowed the whole response; login broken end-to-end
   since aethersdr#3990) shipped because parseXml had zero test coverage.
   ParsedResponse/parseXml are now public-for-tests, and
   qrz_callsign_test pins the three live response shapes: login
   (Session/Key), lookup (Callsign + Session), and session-timeout
   (Error, no Key). Verified the tests catch the original bug: against
   the pre-aethersdr#4043 parser they fail 6/6; with the aethersdr#4043 guard, all pass.

Stacked on aethersdr#4043 (requires its parser guard for the new tests to pass).

💻 Generated with Claude Code (claude-fable-5 7/1/26) with architecture by @jensenpat
…inciple XI.

macOS twin of the Windows DLL staging fix in aethersdr#4043: a staged
third_party/qtkeychain resolves via an absolute rpath into the build
checkout, so a bundle copied to another Mac (SFTP test deploys) fails
to load libqt6keychain and the app dies at launch.  Copy the dylib
into Contents/Frameworks post-build and add the standard
@executable_path/../Frameworks rpath at link time (linker-added rpath
keeps the ad-hoc signature valid; macdeployqt release DMGs are
unaffected).  Proven by hiding the staging dir and launching — the
app resolves the bundled copy.

💻 Generated with Claude Code (claude-fable-5 7/1/26) with architecture by @jensenpat
@ten9876 ten9876 force-pushed the fix/qrz-session-password-fallback branch from 0989245 to 2c2e761 Compare July 5, 2026 17:54

@ten9876 ten9876 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving — this closes exactly the two follow-ups I asked for on #4043, and closes them well.

The regression test is the one that mattered. QRZ login broke end-to-end and shipped precisely because no test drove parseXml against a root-wrapped response. Your new cases pin all three real shapes: a full <QRZDatabase version="1.34" xmlns=…>-wrapped login yields the sessionKey, a lookup with Callsign+Session siblings retains it, and an expired body surfaces sessionError == "Session Timeout". That's the difference between "fixed" and "fixed to stay fixed" (Principle VIII) — a future token-walk refactor now trips a test instead of silently muting login. I ran the suite offscreen: ALL PASS, and a full build links clean (761/761).

The keychain-optional password fix is correct. The memory-only m_sessionPassword (documented — and verified — never written to disk) means a typed password works immediately when the keychain is empty, locked, or absent, while a stored keychain value still wins when present. That's exactly the failure I flagged on #4043 (keychain-missing → empty password → every lookup fails), and the precedence is right. No new persistence or security surface — the password already flowed as a QString.

Exposing parseXml/ParsedResponse as public static is the thin, stateless test seam I offered as the option — a clean way to pin the parser without a friend declaration. The macOS dylib-bundling commit layers correctly after the existing Windows staging block and is verifiable via check-macos.

Housekeeping: this was stacked on #4043's individual commits, which no longer matched main's squash-merge — I rebased it onto current origin/main (dropping the two already-landed commits) to clear the conflict and force-pushed with a lease; your two commits are unchanged and still yours. One pre-existing -Wunused-result on QTemporaryFile::open() in the cty.dat test (not touched by this PR) is worth folding into a future warning sweep, not a gate here.

Approving and enabling auto-merge. 73 — reviewed with Claude Code on behalf of KK7GWY.

@ten9876 ten9876 merged commit 3880b2e into aethersdr:main Jul 5, 2026
6 checks passed
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.

2 participants