mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
06916226df118d6f37fefc68f43886b01170a1eb
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
61c33d527c |
fix(setup): atomic CA-bundle install with PEM-frame verification
Hardens TrustCACertFromBytes against the failure mode behind issue #262 (corrupted /etc/pki/tls/certs/ca-bundle.crt on a SoundTouch 20) and against silent transport-time corruption of our own writes. Three-part change. 1. Atomic write path. The previous flow piped bytes straight into the live bundle via `cat > <path>`; a dropped SSH session or partial write left the device with a half-written trust store and no way to roll back. The new path: - uploads to <bundlePath>.aftertouch.tmp (sibling on the same filesystem, same rw remount), - reads the tmp back over SSH, - validates the readback at the PEM-frame layer + the AfterTouch sentinel bracketing, - atomically `mv`s the tmp into place, - on any verification failure: `rm -f` the tmp; the live bundle is never touched, so there is no rollback semantics to reason about. The .original backup written on first install stays as defense-in-depth (manual recovery for corruption from outside this code path), but it is no longer the primary safety net. 2. New validators in pkg/service/setup/ca_validation.go. - validateCABundleBytes: BEGIN/END marker counts match, every decoded block is a CERTIFICATE with a non-empty body, decoded block count equals BEGIN-marker count (catches a block with unparseable base64 body), trailing non-PEM/non-comment content rejected. - validateAfterTouchLabelBracketing: CALabel appears exactly twice and brackets exactly one CERTIFICATE block. - stripAfterTouchEntries: collapses any number of stale AfterTouch entries from the existing bundle. Older releases reported to have appended without stripping, so long-lived devices can carry several copies; we strip them all and log the cleanup count rather than failing validation. Unpaired sentinels (truncated prior install) surface as a structured anomaly the caller logs and warns about. The validators stay at the PEM-frame layer on purpose — an earlier iteration called x509.ParseCertificate per block and rejected the real ST20 bundle on block 29 (Go 1.23+ disallows negative serial numbers, but Mozilla CCADB still ships ancient CA roots that have them). Shipping that version would have made every legitimate speaker install fail. The corruption mode #262 surfaces at the PEM-framing layer; x509-level checks aren't what we needed. 3. testdata/ca_bundle_st20_pristine.crt is the pristine /etc/pki/tls/certs/ca-bundle.crt captured off a real SoundTouch 20 (firmware 27.0.6.46330.5043500, snapshot 2022-08-04). Mozilla CCADB public dataset, 165 certs, ~251 KB. TestValidateRealSpeakerBundle locks in the cert count and asserts the strip pass is a no-op against a bundle that has never been touched by AfterTouch. Test infrastructure. mockSSH (both the setup-package and the handlers-package copies) now mirrors UploadContent into a private map so a subsequent `cat <path>` on the same path returns what was written there. Lets the tmp-readback step in TrustCACertFromBytes work against tests that only scripted the live-bundle path, without per-test wiring. Two new behavioural tests in setup_test.go: TestTrustCACert_StripsMultipleStaleEntriesSilently (pins the multi-entry cleanup contract) and TestTrustCACert_PostUploadVerificationFailureCleansUpTmp (pins the rollback-free recovery: live bundle untouched, tmp removed). Refs #262. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
0e10bfcb14 |
test(setup): wire issue #235 — Spotify Connect /now_playing reports IsPresetable=false
Two-part iteration. First, the fakespeaker grows a `/now_playing` route with a default STANDBY fixture — issue #235 is the first one in this series that needs to override /now_playing, and adding the route on its own would be infrastructure noise; bundled here it has an immediate consumer. The regression test then locks in the device-side signal at the heart of #235: when a SoundTouch is targeted by Spotify Connect (Spotify app sends audio to the speaker), the speaker's /now_playing reports - source = SPOTIFY - sourceAccount = SpotifyConnectUserName (the marker) - ContentItem.location = /playback/container/<base64 spotify:...> — a perfectly resolvable URI - **ContentItem.isPresetable = false** The contradiction (resolvable location + isPresetable=false) is the reason the CLI's storeCurrentPreset at cmd/soundtouch-cli/cmd_preset.go:41 refuses to act and emits "current content cannot be preset" — exactly the reporter's symptom. The test base64-decodes the location to surface the contradiction explicitly: it should yield a `spotify:` URI. When AfterTouch grows a fallback path (CLI --force, or service-side resolution to the device's own Spotify integration via the SoundTouch Spotify source provider), the assertion here stays sound — it tests what the device emits, not what the CLI decides — but a sibling test should assert the new fallback path produces a successful preset. Fixture pattern matches the rest of the issue series: testdata/issue235/ next to the test, fakespeaker driven via FixtureOverrides, doc-comment naming what would have to change for the assertion to flip. Refs #235. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
dd535cdb52 |
test(setup): pin factory-reset behaviour from issue #234
Wires the device-side state the reporter described in https://github.com/gesellix/Bose-SoundTouch/issues/234 into the fakespeaker via FixtureOverrides, and exercises GetLiveDeviceInfo + syncSources against it. The factory-reset state has two observable signals: - `/info` returns an empty `<margeAccountUUID/>` because Marge.xml is missing from the persistence partition. AfterTouch's "is the device paired?" check at setup.go:632 keys on AccountID, so this is the canonical "needs re-pairing" signal. - `/sources` lists only AUX, BLUETOOTH, AIRPLAY, the SpotifyConnectUserName placeholder, NOTIFICATION, and QPLAY — TUNEIN, LOCAL_INTERNET_RADIO, and any post-pairing Spotify accounts are gone until the speaker is nudged with a `<sourcesUpdated/>` notification or re-pairs. Today AfterTouch has no auto-recovery for either signal — it just passes the state through. The test locks in that contract by asserting: - GetLiveDeviceInfo reports an empty MargeAccountUUID, - persisted Sources.xml contains AUX/BLUETOOTH/AIRPLAY sourceKeys, - persisted Sources.xml does NOT contain TUNEIN/LOCAL_INTERNET_RADIO. When auto-recovery lands (e.g. an automatic POST of the sourcesUpdated notification during sync, or marge-side source replenishment), the absence assertions will flip — at which point update them to assert the survivors are *present*, and adjust the doc-comment so the contract stays in sync with the code. Pattern mirrors pkg/service/setup/issue218_regression_test.go: a testdata fixture next to the test, fakespeaker driven via Config.FixtureOverrides, doc-comment naming what would have to change for the assertion to flip. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
2fabdece64 |
test(fakespeaker): wire issue-specific payloads via Config.FixtureOverrides
Introduces a per-route fixture-override hook on fakespeaker.Config so open issues with concrete device-side payloads can become repeatable regression tests, then demonstrates the pattern by wiring issue #218. Foundation. Config grows a single optional field: FixtureOverrides map[string][]byte Routes named in the map (e.g. "/presets", "/sources", "/info") return the supplied bytes; routes not in the map fall through to the embedded testdata defaults the screenshot pipeline relies on. Stateful handlers (/getGroup, /addGroup, /updateGroup, /removeGroup) are unaffected because they're code-driven, not fixture-driven. The override slice is snapshotted at construction so later mutations of the caller's slice don't change the served body. Zero-value Config keeps the existing behaviour, so cmd/dummy-speaker + scripts/screenshots are untouched. Iteration zero — issue #218. pkg/service/setup/issue218_regression_test.go starts a fakespeaker serving the reporter's LOCAL_INTERNET_RADIO preset XML verbatim (URL: content.api.bose.io/core02/svc-bmx-adapter-orion/prod/orion/station?…), runs Manager.syncPresets against it, then asserts the persisted Presets.xml retains the Bose cloud URL prefix. This locks in the "location preserved through sync" contract; when AfterTouch starts rewriting the URL to its own base (the eventual fix for #218), the assertion flips and the fixture stays unchanged — the test is the carrier for the decision. Pattern reference for future issue regression tests: this exemplar mirrors pkg/service/marge/recents_sourceproviderid_regression_test.go's style (issue link, trigger chain in the doc-comment, locked-in assertion) but is the first one to drive the device side via fakespeaker rather than an inline httptest.NewServer. Subsequent issues with device-side payloads (#234 factory-reset state, #235 Spotify-as-preset, …) can reuse the FixtureOverrides hook without further infrastructure. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |