fix(core): QRZ login broken end-to-end since #3990#4043
Conversation
Every QRZ XML reply is wrapped in a root <QRZDatabase> element. Since its name matched neither "Session" nor "Callsign", the loop fell through to readElementText(SkipChildElements) on the ROOT element, which reads to its own end tag — silently consuming and discarding the entire <Session>/<Callsign> subtree before the loop ever revisits it. Net effect: sessionKey/sessionError/info came back empty for every login and lookup, regardless of whether the credentials were valid, so Test Login (and real lookups) always reported "no session key". Skip elements outside a recognized block instead of feeding them to readElementText, so the root tag is passed over rather than consumed. Untested — not built locally; PR aethersdr#3990 (aethersdr#3990) shipped this with no parseXml test coverage against real root-wrapped QRZ XML, so this had no regression test to catch it either. Principle IV.
…to the exe Two gaps found while getting QRZ login working end-to-end on a Windows source build: - README's Windows 11 steps never ran scripts/setup/setup-qtkeychain.ps1, so a from-scratch build silently compiled out credential persistence (Qt6Keychain not found -> HAVE_KEYCHAIN undefined). Test Login worked regardless since it bypasses the keychain entirely, masking the gap. - Even with qtkeychain staged and HAVE_KEYCHAIN on, a plain `cmake --build` dev run crashes with "qt6keychain.dll was not found": windeployqt (which the Windows installer CI job manually feeds the DLL through) isn't part of a local build, so nothing puts the DLL next to the exe. Add the setup-qtkeychain.ps1 step to the README and a WIN32 POST_BUILD copy in CMakeLists.txt so local Windows builds get working credential persistence without a manual DLL copy. Principle IV.
|
Need to split Windows qtkeychain CI into a separate issue. Thx |
|
Verified — the fix is correct and the bug is real. I added
The Windows qtkeychain DLL staging + README steps also look right — and topical: the same missing-qtkeychain failure mode just bit our macOS test build ( Recommend merge. 💻 Generated with Claude Code (claude-fable-5 7/1/26), verification by @jensenpat |
There was a problem hiding this comment.
Thanks for the thorough writeup and the real-account verification, @wa2n-code — I traced all three claims through the code and they hold up.
The parseXml() fix is correct
I confirmed the root cause. Every QRZ response is wrapped in <QRZDatabase>, which is the first StartElement the loop sees. Since it matches neither "Session" nor "Callsign", the pre-fix code fed it straight to r.readElementText(QXmlStreamReader::SkipChildElements) (QrzClient.cpp:222) — which, per Qt semantics, reads until the root's </QRZDatabase> and discards every child, including the <Session><Key>…</Key></Session> block, before the loop can enter it. block never leaves Block::None, so sessionKey/info come back empty for every response. The block == Block::None guard is the right minimal fix: the root (and any other pre-block start tag) is stepped over via readNext() instead of consumed, so the loop reaches <Session>/<Callsign> intact. Nested elements inside those blocks are still handled by SkipChildElements.
CMake / README changes are sound and in scope
The WIN32 + EXISTS-guarded POST_BUILD copy targets third_party/qtkeychain/bin/qt6keychain.dll, which is exactly where scripts/setup/setup-qtkeychain.ps1 installs it ($OutDir\bin\qt6keychain.dll, script lines 25/104) — so the path is consistent, and the EXISTS guard makes it a safe no-op when the DLL isn't staged. Adding the setup step to the README's Windows flow is a reasonable doc fix. No AppSettings/RAII concerns; nothing outside the stated scope.
One suggestion (non-blocking): lock this in with a regression test
You correctly note this shipped because nothing exercised parseXml() against a root-wrapped response — and it couldn't, since parseXml is private static (QrzClient.h:76) and qrz_callsign_test.cpp never touches it. Since the method is already a pure static with no side effects, promoting it to public (or adding a friend test seam) and asserting that a realistic <QRZDatabase><Session><Key>…</Key></Session></QRZDatabase> payload yields a non-empty sessionKey would prevent a future token-loop refactor from silently reintroducing this. Worth doing given it was a total end-to-end breakage that passing CI didn't catch.
Nice catch on a genuinely broken-since-#3990 path. LGTM once you decide on the test.
🤖 aethersdr-agent · cost: $7.8837 · model: claude-opus-4-8
There was a problem hiding this comment.
Thanks for the careful diagnosis here, @wa2n-code — this is a well-tracked root-cause fix, and the writeup made it easy to verify. I traced each change against the source; notes below.
The parseXml() fix is correct ✅
The diagnosis holds up. Every QRZ response is wrapped in a <QRZDatabase> root, which is the first StartElement the loop sees. With block == None it fell through to readElementText(SkipChildElements) on the root — which per Qt reads to the matching </QRZDatabase>, consuming the nested <Session>/<Callsign> blocks before the loop could inspect them. So sessionKey came back empty for every response, which matches the reported "no session key" on valid credentials.
Tracing the fixed loop confirms it: root <QRZDatabase> → block == None → continue, reader advances into <Session> → matched → block = Session, then <Key> is read via readElementText with the block set. Minimal and targeted. r.name() returning the local name means the default xmlns namespace on the root doesn't interfere, as before.
CMake + README changes are consistent ✅
- The
POST_BUILDcopy readsthird_party/qtkeychain/bin/qt6keychain.dll, which is exactly wheresetup-qtkeychain.ps1installs it (CMAKE_INSTALL_PREFIX=$OutDir→$OutDir\bin\qt6keychain.dll, confirmed at script line 104). TheWIN32 AND EXISTSguard keeps it a no-op when keychain isn't staged. - The README step is the missing companion to the existing
find_package(Qt6Keychain)path (CMakeLists.txt:127already prependsthird_party/qtkeychaintoCMAKE_PREFIX_PATH), so the instructions now actually populate what the configure step looks for.
One suggestion (non-blocking)
You correctly note that parseXml() never saw a realistic root-wrapped response in tests — which is precisely how this shipped. A small regression test asserting sessionKey is populated from a <QRZDatabase><Session><Key>…</Key></Session></QRZDatabase> fixture (plus a <Callsign> lookup fixture) would lock this in cheaply. Heads-up that parseXml is private static (QrzClient.h:76) and tests/qrz_callsign_test.cpp doesn't currently touch QrzClient, so you'd need a test seam — a friend test class, or promoting the parse to a public static / free function. Worth it given this is the exact gap that let the bug through.
Scope is coherent — the CMake/README changes are a logically separate concern from the XML parse, but both serve the "QRZ login end-to-end" goal and are small, so bundling is reasonable. CI is green across all six checks. Nice work.
🤖 aethersdr-agent · cost: $7.4797 · model: claude-opus-4-8
…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
left a comment
There was a problem hiding this comment.
Good catch and a clean, well-diagnosed fix — thank you for chasing it down against a real QRZ.com account rather than stopping at "Test Login works." I traced parseXml against both real response shapes (login = <Session> only; lookup = <Callsign> then <Session> siblings under the <QRZDatabase> root) and the guard does exactly what you describe: the root wrapper is skipped instead of having readElementText swallow the whole document. The genuinely valuable side effect is that QRZ's real error strings (Username/password incorrect) now reach the user instead of the generic "no session key." The default xmlns is a non-issue since r.name() returns the local name. I also verified the build half end-to-end: setup-qtkeychain.ps1 installs to third_party/qtkeychain via CMAKE_INSTALL_PREFIX and builds --config Release (so the DLL name matches, no debug suffix), find_package prepends that same path, and the README renumbering is self-contained with Linux/macOS parity intact.
Nothing here is blocking — this fixes a real end-to-end breakage. Two things I'd like before merge, then a couple of nits.
Please add before merge
-
A
parseXmlregression test. This is the one that matters: QRZ login was broken end-to-end and shipped precisely because no test droveparseXmlagainst a root-wrapped response — and this PR fixes it without adding that test, so the exact failure can silently return on any future token-walk refactor. You already name the gap in the PR body; closing it is the difference between "demonstrated" and "demonstrated to stay fixed" (Principle VIII).parseXmlis private static, so it needs a friend-test or a thin exposure. AssertsessionKeyis extracted from a full<QRZDatabase version="1.34"><Session><Key>ABC123</Key></Session></QRZDatabase>login body, andsessionErrorfrom an error body (<Session><Error>Username/password incorrect</Error></Session>). -
Reset
blockon the closing tag (inline suggestion). Your newif (block == None) continue;guard is correct but never re-armed, becauseblockis never set back toNonewhen a<Session>/<Callsign>ends. It's latent today (login/lookup roots contain only those two children), but the moment the client issues a DXCC/bio query — or QRZ adds a top-level sibling — that element's children get read under the stale block. One line makes the guard actually load-bearing.
Nits (optional)
- The DLL-copy guard couples "staged in third_party" to "DLL exists." If
Qt6Keychainresolves from a system/vcpkg install,Qt6Keychain_FOUNDis true butEXISTS(third_party/...)is false → no copy → the exact "qt6keychain.dll not found" crash you're fixing. Narrow (the README path stages third_party), but copying from the found target's own location —$<TARGET_FILE:Qt6Keychain::Qt6Keychain>— handles any provider and drops the hardcoded path. Inline note on the CMake block. - Co-locate the copy with the other bundled-DLL staging. Every other Windows DLL (hidapi, fftw3/fftw3f, ONNX, deepfilter) is copied in one clustered section with a
COMMENTprogress line; this one sits ~200 lines away with no COMMENT. Cheap to keep the pattern single-sourced.
Housekeeping: @jensenpat is already assigned (infrastructure tier owns CMakeLists.txt + *.md per CODEOWNERS, so this routes to them, not full maintainer) — leaving this review as the maintainer-side pass, not a claim on the merge. Commit correctly cites Principle IV. 73 — reviewed with Claude Code on behalf of KK7GWY.
ten9876
left a comment
There was a problem hiding this comment.
Approving — this fixes a real end-to-end breakage (QRZ login has been non-functional since #3990), verified against a live QRZ.com account, with the parseXml root-wrapper fix traced against both real response shapes and the Windows keychain build/DLL path confirmed consistent. CI green across build/macOS/Windows.
Merging with two non-blocking follow-ups tracked from my review above, to fold into a quick follow-up PR rather than gate this one:
- a parseXml regression test against a root-
<QRZDatabase>-wrapped body (the guard that would keep this exact bug from silently recurring — Principle VIII); - the one-line
blockreset on EndElement so the new root-guard is re-armed for future non-Session/Callsign top-level siblings.
The $<TARGET_FILE:Qt6Keychain::Qt6Keychain> DLL-copy robustness tweak is optional polish. Thanks for the careful diagnosis. 73 de KK7GWY (reviewed with Claude Code)
…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
…l regression tests (stacked on #4043) (#4044) **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 #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
What was broken
PR #3990 added QRZ callsign lookup, but the login path was broken end-to-end — verified by testing against a real QRZ.com account.
1. Every QRZ XML response failed to parse (
src/core/QrzClient.cpp)QrzClient::parseXml()walksQXmlStreamReadertokens, special-casing<Session>and<Callsign>start tags. Every other start tag falls through tor.readElementText(QXmlStreamReader::SkipChildElements).The problem: every QRZ response is wrapped in a root
<QRZDatabase>element. That root tag is the firstStartElementthe loop sees, and it doesn't match"Session"or"Callsign"— so the loop callsreadElementText(SkipChildElements)on the root itself. Per Qt's docs that reads "until the correspondingEndElement", i.e. it consumes and discards the entire rest of the document (including the nested<Session><Key>…</Key></Session>block) before the loop ever gets a chance to inspect it.Net effect:
sessionKey/sessionError/infocome back empty for every login and lookup response, valid credentials or not, so every login attempt reported"QRZ login rejected (no session key)".Fix: skip start tags encountered before entering a recognized block instead of feeding them to
readElementText, so the root wrapper is passed over rather than consumed.Note:
tests/qrz_callsign_test.cpp(added in #3990) covers callsign regex/spotter logic but never exercisesparseXml()against a realistic root-wrapped response, which is how this shipped.2. Windows source builds silently compiled out credential persistence, and even with it enabled, the app couldn't find the DLL
Found while confirming fix #1 on a real Windows 11 build:
scripts/setup/setup-qtkeychain.ps1, sofind_package(Qt6Keychain)fails silently (REQUIRE_KEYCHAINdefaultsOFF),HAVE_KEYCHAINnever gets defined, and the QRZ password is never saved —CallsignLookupService::savePassword()/loadPasswordFromKeychain()become no-ops. The Setup dialog's "Test Login" button still worked because it bypasses the keychain and tests raw typed credentials directly, which masked the gap: real lookups (View → Callsign Lookup) always gotAuthFailed(empty password) and silently fell back to offlinecty.datprefix data, appearing as "QRZ.com unavailable".Qt6Keychain foundin the CMake configure log, running straight from the build directory still failed with "qt6keychain.dll was not found" —windeployqt(which the Windows installer CI job manually feeds the DLL through, see.github/workflows/windows-installer.yml) isn't part of a plain local build, so nothing put the DLL next to the exe.Fix: add the
setup-qtkeychain.ps1step to the README's Windows instructions, and add aWIN32-guardedPOST_BUILDstep inCMakeLists.txtthat copiesqt6keychain.dllnext to the built exe when present, so a local dev build gets working persistence without a manual copy.Test plan
setup-qtkeychain.ps1🤖 Generated with Claude Code