Compare commits

...
5 Commits
Author SHA1 Message Date
Tobias Gesellchen e0a84d5904 Split register and unregister device tests (#133) 2026-03-27 22:03:21 +01:00
dependabot[bot]andlnx01 5d080cf35f ci(deps): bump codecov/codecov-action from 5 to 6 in the security-actions group (#132)
Bumps the security-actions group with 1 update:
[codecov/codecov-action](https://github.com/codecov/codecov-action).

Updates `codecov/codecov-action` from 5 to 6
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/releases">codecov/codecov-action's
releases</a>.</em></p>
<blockquote>
<h2>v6.0.0</h2>
<h2>⚠️ This version introduces support for node24 which make cause
breaking changes for systems that do not currently support node24.
⚠️</h2>
<h2>What's Changed</h2>
<ul>
<li>Revert &quot;Revert &quot;build(deps): bump actions/github-script
from 7.0.1 to 8.0.0&quot;&quot; by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1929">codecov/codecov-action#1929</a></li>
<li>Th/6.0.0 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1928">codecov/codecov-action#1928</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.4...v6.0.0">https://github.com/codecov/codecov-action/compare/v5.5.4...v6.0.0</a></p>
<h2>v5.5.4</h2>
<p>This is a mirror of <code>v5.5.2</code>. <code>v6</code> will be
released which requires <code>node24</code></p>
<h2>What's Changed</h2>
<ul>
<li>Revert &quot;build(deps): bump actions/github-script from 7.0.1 to
8.0.0&quot; by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1926">codecov/codecov-action#1926</a></li>
<li>chore(release): 5.5.4 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1927">codecov/codecov-action#1927</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.3...v5.5.4">https://github.com/codecov/codecov-action/compare/v5.5.3...v5.5.4</a></p>
<h2>v5.5.3</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump actions/github-script from 7.0.1 to 8.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1874">codecov/codecov-action#1874</a></li>
<li>chore(release): bump to 5.5.3 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1922">codecov/codecov-action#1922</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.2...v5.5.3">https://github.com/codecov/codecov-action/compare/v5.5.2...v5.5.3</a></p>
<h2>v5.5.2</h2>
<h2>What's Changed</h2>
<ul>
<li>check gpg only when skip-validation = false by <a
href="https://github.com/maxweng-sentry"><code>@​maxweng-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1894">codecov/codecov-action#1894</a></li>
<li>chore: <code>disable_search</code> alignment by <a
href="https://github.com/freemanzMrojo"><code>@​freemanzMrojo</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1881">codecov/codecov-action#1881</a></li>
<li>chore(release): 5.5.2 by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1902">codecov/codecov-action#1902</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/maxweng-sentry"><code>@​maxweng-sentry</code></a>
made their first contribution in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1894">codecov/codecov-action#1894</a></li>
<li><a
href="https://github.com/freemanzMrojo"><code>@​freemanzMrojo</code></a>
made their first contribution in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1881">codecov/codecov-action#1881</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.1...v5.5.2">https://github.com/codecov/codecov-action/compare/v5.5.1...v5.5.2</a></p>
<h2>v5.5.1</h2>
<h2>What's Changed</h2>
<ul>
<li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1833">codecov/codecov-action#1833</a></li>
<li>build(deps): bump github/codeql-action from 3.28.18 to 3.29.9 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1861">codecov/codecov-action#1861</a></li>
<li>Document a <code>codecov-cli</code> version reference example by <a
href="https://github.com/webknjaz"><code>@​webknjaz</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1774">codecov/codecov-action#1774</a></li>
<li>docs: fix typo in README by <a
href="https://github.com/datalater"><code>@​datalater</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1866">codecov/codecov-action#1866</a></li>
<li>fix: update to use local app/ dir by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1872">codecov/codecov-action#1872</a></li>
<li>build(deps): bump github/codeql-action from 3.29.9 to 3.29.11 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1867">codecov/codecov-action#1867</a></li>
<li>build(deps): bump actions/checkout from 4.2.2 to 5.0.0 by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a>[bot]
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1868">codecov/codecov-action#1868</a></li>
<li>fix: overwrite pr number on fork by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1871">codecov/codecov-action#1871</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md">codecov/codecov-action's
changelog</a>.</em></p>
<blockquote>
<h2>v5.5.2</h2>
<h3>What's Changed</h3>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2">https://github.com/codecov/codecov-action/compare/v5.5.1..v5.5.2</a></p>
<h2>v5.5.1</h2>
<h3>What's Changed</h3>
<ul>
<li>fix: overwrite pr number on fork by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1871">codecov/codecov-action#1871</a></li>
<li>build(deps): bump actions/checkout from 4.2.2 to 5.0.0 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1868">codecov/codecov-action#1868</a></li>
<li>build(deps): bump github/codeql-action from 3.29.9 to 3.29.11 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1867">codecov/codecov-action#1867</a></li>
<li>fix: update to use local app/ dir by <a
href="https://github.com/thomasrockhu-codecov"><code>@​thomasrockhu-codecov</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1872">codecov/codecov-action#1872</a></li>
<li>docs: fix typo in README by <a
href="https://github.com/datalater"><code>@​datalater</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1866">codecov/codecov-action#1866</a></li>
<li>Document a <code>codecov-cli</code> version reference example by <a
href="https://github.com/webknjaz"><code>@​webknjaz</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1774">codecov/codecov-action#1774</a></li>
<li>build(deps): bump github/codeql-action from 3.28.18 to 3.29.9 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1861">codecov/codecov-action#1861</a></li>
<li>build(deps): bump ossf/scorecard-action from 2.4.1 to 2.4.2 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1833">codecov/codecov-action#1833</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1">https://github.com/codecov/codecov-action/compare/v5.5.0..v5.5.1</a></p>
<h2>v5.5.0</h2>
<h3>What's Changed</h3>
<ul>
<li>feat: upgrade wrapper to 0.2.4 by <a
href="https://github.com/jviall"><code>@​jviall</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1864">codecov/codecov-action#1864</a></li>
<li>Pin actions/github-script by Git SHA by <a
href="https://github.com/martincostello"><code>@​martincostello</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1859">codecov/codecov-action#1859</a></li>
<li>fix: check reqs exist by <a
href="https://github.com/joseph-sentry"><code>@​joseph-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1835">codecov/codecov-action#1835</a></li>
<li>fix: Typo in README by <a
href="https://github.com/spalmurray"><code>@​spalmurray</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1838">codecov/codecov-action#1838</a></li>
<li>docs: Refine OIDC docs by <a
href="https://github.com/spalmurray"><code>@​spalmurray</code></a> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1837">codecov/codecov-action#1837</a></li>
<li>build(deps): bump github/codeql-action from 3.28.17 to 3.28.18 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1829">codecov/codecov-action#1829</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0">https://github.com/codecov/codecov-action/compare/v5.4.3..v5.5.0</a></p>
<h2>v5.4.3</h2>
<h3>What's Changed</h3>
<ul>
<li>build(deps): bump github/codeql-action from 3.28.13 to 3.28.17 by
<code>@​app/dependabot</code> in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1822">codecov/codecov-action#1822</a></li>
<li>fix: OIDC on forks by <a
href="https://github.com/joseph-sentry"><code>@​joseph-sentry</code></a>
in <a
href="https://redirect.github.com/codecov/codecov-action/pull/1823">codecov/codecov-action#1823</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3">https://github.com/codecov/codecov-action/compare/v5.4.2..v5.4.3</a></p>
<h2>v5.4.2</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/codecov/codecov-action/commit/57e3a136b779b570ffcdbf80b3bdc90e7fab3de2"><code>57e3a13</code></a>
Th/6.0.0 (<a
href="https://redirect.github.com/codecov/codecov-action/issues/1928">#1928</a>)</li>
<li><a
href="https://github.com/codecov/codecov-action/commit/f67d33dda8a42b51c42a8318a1f66468119e898b"><code>f67d33d</code></a>
Revert &quot;Revert &quot;build(deps): bump actions/github-script from
7.0.1 to 8.0.0&quot;&quot;...</li>
<li>See full diff in <a
href="https://github.com/codecov/codecov-action/compare/v5...v6">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=codecov/codecov-action&package-manager=github_actions&previous-version=5&new-version=6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:47:00 +01:00
dependabot[bot]andlnx01 bcd383bdff ci(deps): bump actions/deploy-pages from 4 to 5 in the actions-core group (#131)
Bumps the actions-core group with 1 update:
[actions/deploy-pages](https://github.com/actions/deploy-pages).

Updates `actions/deploy-pages` from 4 to 5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/actions/deploy-pages/releases">actions/deploy-pages's
releases</a>.</em></p>
<blockquote>
<h2>v5.0.0</h2>
<h1>Changelog</h1>
<ul>
<li>Update Node.js version to 24.x <a
href="https://github.com/salmanmkc"><code>@​salmanmkc</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/404">#404</a>)</li>
<li>Add workflow file for publishing releases to immutable action
package <a
href="https://github.com/Jcambass"><code>@​Jcambass</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/374">#374</a>)</li>
<li>Bump braces from 3.0.2 to 3.0.3 in the npm_and_yarn group across 1
directory <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/360">#360</a>)</li>
<li>Make the rebuild dist workflow work nicer with Dependabot <a
href="https://github.com/yoannchaudet"><code>@​yoannchaudet</code></a>
(<a
href="https://redirect.github.com/actions/deploy-pages/issues/361">#361</a>)</li>
<li>Bump the non-breaking-changes group across 1 directory with 3
updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/358">#358</a>)</li>
<li>Delete repeated sentence <a
href="https://github.com/garethsb"><code>@​garethsb</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/359">#359</a>)</li>
<li>Update README.md <a
href="https://github.com/tsusdere"><code>@​tsusdere</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/348">#348</a>)</li>
<li>Bump the non-breaking-changes group with 4 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/341">#341</a>)</li>
<li>Remove error message for file permissions <a
href="https://github.com/TooManyBees"><code>@​TooManyBees</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/340">#340</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.5...v4.0.6">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.5</h2>
<h1>Changelog</h1>
<ul>
<li>On API error, the error message will surface the API request ID <a
href="https://github.com/TooManyBees"><code>@​TooManyBees</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/324">#324</a>)</li>
<li>Bump the non-breaking-changes group with 2 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/318">#318</a>)</li>
<li>Bump the non-breaking-changes group with 1 update <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/316">#316</a>)</li>
<li>Bump the non-breaking-changes group with 3 updates <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/314">#314</a>)</li>
<li>Bump release-drafter/release-drafter from 5.25.0 to 6.0.0 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/311">#311</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.4...v4.0.5">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.4</h2>
<h1>Changelog</h1>
<ul>
<li>Update api-client.js <a
href="https://github.com/lmammino"><code>@​lmammino</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/295">#295</a>)</li>
<li>fix typo: compatibilty -&gt; compatibility <a
href="https://github.com/SimonSiefke"><code>@​SimonSiefke</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/298">#298</a>)</li>
<li>Bump <code>@​actions/artifact</code> from 2.0.1 to 2.1.1 <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> (<a
href="https://redirect.github.com/actions/deploy-pages/issues/310">#310</a>)</li>
<li>Update Dependabot config to group non-breaking changes <a
href="https://github.com/JamesMGreene"><code>@​JamesMGreene</code></a>
(<a
href="https://redirect.github.com/actions/deploy-pages/issues/307">#307</a>)</li>
</ul>
<hr />
<p>See details of <a
href="https://github.com/actions/deploy-pages/compare/v4.0.3...v4.0.4">all
code changes</a> since previous release.</p>
<p>⚠️ For use with products other than GitHub.com, such as GitHub
Enterprise Server, please consult the <a
href="https://github.com/actions/deploy-pages/#compatibility">compatibility
table</a>.</p>
<h2>v4.0.3</h2>
<h1>Changelog</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/actions/deploy-pages/commit/cd2ce8fcbc39b97be8ca5fce6e763baed58fa128"><code>cd2ce8f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/404">#404</a>
from salmanmkc/node24</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/bbe2a950ee52d4f5cbe74e6d9d6a8803676e91d5"><code>bbe2a95</code></a>
Update Node.js version to 24.x</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/854d7aa1b99e4509c4d1b53d69b7ba4eaf39215a"><code>854d7aa</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/374">#374</a>
from actions/Jcambass-patch-1</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/306bb814f29679fd12f0e4b0014bc1f3a7e7f4bc"><code>306bb81</code></a>
Add workflow file for publishing releases to immutable action
package</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/b74272834adc04f971da4b0b055c49fa8d7f90c9"><code>b742728</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/360">#360</a>
from actions/dependabot/npm_and_yarn/npm_and_yarn-513...</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/72732942c639e67ea3f70165fd2e012dd6d95027"><code>7273294</code></a>
Bump braces in the npm_and_yarn group across 1 directory</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/963791f01c40ef3eff219c255dbfb97a6f2c9f87"><code>963791f</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/361">#361</a>
from actions/dependabot-friendly</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/51bb29d9d7bfe15d731c4957ce1887b5ae8c6727"><code>51bb29d</code></a>
Make the rebuild dist workflow safer for Dependabot</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/89f3d10406f57ee86e6517a982b3fb0438bd6dc5"><code>89f3d10</code></a>
Merge pull request <a
href="https://redirect.github.com/actions/deploy-pages/issues/358">#358</a>
from actions/dependabot/npm_and_yarn/non-breaking-cha...</li>
<li><a
href="https://github.com/actions/deploy-pages/commit/bce735589bbbfa569f1d2ac003277b590d743e4c"><code>bce7355</code></a>
Merge branch 'main' into
dependabot/npm_and_yarn/non-breaking-changes-99c12deb21</li>
<li>Additional commits viewable in <a
href="https://github.com/actions/deploy-pages/compare/v4...v5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/deploy-pages&package-manager=github_actions&previous-version=4&new-version=5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:46:51 +01:00
dependabot[bot]andlnx01 7a09a2ddc0 deps(deps): bump golang.org/x/image from 0.37.0 to 0.38.0 in the golang group (#130)
Bumps the golang group with 1 update:
[golang.org/x/image](https://github.com/golang/image).

Updates `golang.org/x/image` from 0.37.0 to 0.38.0
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/golang/image/commit/23ae9ed61c1d3343fb95015810f62dcbf444976e"><code>23ae9ed</code></a>
tiff: cap buffer growth to prevent OOM from malicious IFD offset</li>
<li><a
href="https://github.com/golang/image/commit/e589e60f29d0bbbf6400e250e024f93cbc4961ee"><code>e589e60</code></a>
webp: allow VP8L + VP8X(with alpha)</li>
<li>See full diff in <a
href="https://github.com/golang/image/compare/v0.37.0...v0.38.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/image&package-manager=go_modules&previous-version=0.37.0&new-version=0.38.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-27 08:46:41 +01:00
Tobias Gesellchen b04b0bcc32 Add account registration/login (#129) 2026-03-27 08:37:50 +01:00
28 changed files with 648 additions and 18 deletions
+7 -1
View File
@@ -43,8 +43,14 @@ jobs:
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Build service
run: make build-service
- name: Run HTTP client integration tests
run: make test-http-client
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
file: ./coverage.out
flags: unittests
+1 -1
View File
@@ -34,4 +34,4 @@ jobs:
path: '_site'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
uses: actions/deploy-pages@v5
+31 -1
View File
@@ -103,7 +103,37 @@ test-coverage:
$(GOCMD) tool cover -html=coverage.out -o coverage.html
@echo "Coverage report generated: coverage.html"
check: fmt vet test
check: fmt vet test test-http-client
test-http-client:
@echo "Running HTTP client integration tests..."
@docker network create soundtouch-test-net || true
@docker build -t soundtouch-service-test .
@docker run -d --name soundtouch-service --network soundtouch-test-net \
-e PORT=8000 \
soundtouch-service-test
@echo "Waiting for service to start..."
@sleep 5
@docker run --rm --network soundtouch-test-net \
-v $(PWD)/tests/integration/http-client:/workdir \
jetbrains/intellij-http-client:2026.1 \
--env-file /workdir/http-client.env.json \
--env ci \
/workdir/create_account.http \
/workdir/register_device.http \
/workdir/power_on.http \
/workdir/get_provider_settings.http \
/workdir/get_full_account.http \
/workdir/get_group.http \
/workdir/unregister_device.http \
--report; \
EXIT_CODE=$$?; \
docker logs soundtouch-service; \
docker stop soundtouch-service; \
docker rm soundtouch-service; \
docker rmi soundtouch-service-test; \
docker network rm soundtouch-test-net; \
exit $$EXIT_CODE
fmt:
@echo "Formatting code..."
+67
View File
@@ -652,6 +652,73 @@ func listMusicServiceAccounts(c *cli.Context) error {
return nil
}
// pairDevice triggers the Stockholm registration flow via WebSocket
func pairDevice(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
accountID := c.String("id")
token := c.String("token")
PrintDeviceHeader("Pairing device with Marge account", clientConfig.Host, clientConfig.Port)
fmt.Printf(" Account ID: %s\n", accountID)
// We need a WebSocket client for this
ws := client.NewWebSocketClient(nil)
err = ws.Connect()
if err != nil {
return fmt.Errorf("failed to connect to device WebSocket: %w", err)
}
defer func() { _ = ws.Disconnect() }()
err = ws.PairWithAccount(accountID, token)
if err != nil {
return fmt.Errorf("failed to send pairing request: %w", err)
}
PrintSuccess("Pairing request sent successfully")
fmt.Println("💡 The device will now register itself with the cloud service.")
return nil
}
// unpairDevice triggers the Stockholm unregistration flow via WebSocket
func unpairDevice(c *cli.Context) error {
clientConfig := GetClientConfig(c)
client, err := CreateSoundTouchClient(clientConfig)
if err != nil {
return err
}
PrintDeviceHeader("Unpairing device from Marge account", clientConfig.Host, clientConfig.Port)
// We need a WebSocket client for this
ws := client.NewWebSocketClient(nil)
err = ws.Connect()
if err != nil {
return fmt.Errorf("failed to connect to device WebSocket: %w", err)
}
defer func() { _ = ws.Disconnect() }()
err = ws.UnPairFromAccount()
if err != nil {
return fmt.Errorf("failed to send unpairing request: %w", err)
}
PrintSuccess("Unpairing request sent successfully")
return nil
}
// getServiceDisplayName returns a user-friendly display name for a service
func getServiceDisplayName(source string) string {
switch source {
+2 -2
View File
@@ -196,7 +196,7 @@ var httpClient = &http.Client{
}
func fetchTuneInMetadata(url string) (*Metadata, error) {
if !strings.Contains(url, "tunein.com/radio/") {
if !strings.Contains(url, "tunein.com/radio/") && !strings.Contains(url, "127.0.0.1") && !strings.Contains(url, "localhost") {
return nil, fmt.Errorf("url is not a TuneIn radio URL")
}
@@ -256,7 +256,7 @@ func fetchTuneInMetadata(url string) (*Metadata, error) {
}
func fetchSpotifyMetadata(url string) (*Metadata, error) {
if !strings.Contains(url, "open.spotify.com/") {
if !strings.Contains(url, "open.spotify.com/") && !strings.Contains(url, "127.0.0.1") && !strings.Contains(url, "localhost") {
return nil, fmt.Errorf("url is not a Spotify URL")
}
+4 -4
View File
@@ -7,7 +7,7 @@ import (
)
func TestFetchTuneInMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
html := `
<!doctype html>
<html>
@@ -30,7 +30,7 @@ func TestFetchTuneInMetadata(t *testing.T) {
defer func() { httpClient = oldClient }()
metadata, err := fetchTuneInMetadata("https://tunein.com/radio/WDR-2-Rheinland-1004-s213886/")
metadata, err := fetchTuneInMetadata(ts.URL + "/radio/WDR-2-Rheinland-1004-s213886/")
if err != nil {
t.Fatalf("fetchTuneInMetadata() error = %v", err)
}
@@ -162,7 +162,7 @@ func TestResolveLocationSpotify(t *testing.T) {
}
func TestFetchSpotifyMetadata(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
html := `
<!doctype html>
<html>
@@ -185,7 +185,7 @@ func TestFetchSpotifyMetadata(t *testing.T) {
defer func() { httpClient = oldClient }()
metadata, err := fetchSpotifyMetadata("https://open.spotify.com/album/7F50uh7oGitmAEScRKV6pD")
metadata, err := fetchSpotifyMetadata(ts.URL + "/album/7F50uh7oGitmAEScRKV6pD")
if err != nil {
t.Fatalf("fetchSpotifyMetadata() error = %v", err)
}
+24
View File
@@ -2038,6 +2038,30 @@ func main() {
},
},
},
{
Name: "pair",
Usage: "Pair the device with a Marge cloud account (Stockholm registration)",
Action: pairDevice,
Before: RequireHost,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "id",
Usage: "Marge account ID (e.g., 1234567)",
Required: true,
},
&cli.StringFlag{
Name: "token",
Usage: "User authorization token",
Required: true,
},
},
},
{
Name: "unpair",
Usage: "Unpair the device from its Marge cloud account",
Action: unpairDevice,
Before: RequireHost,
},
},
},
// Token commands
+2
View File
@@ -677,6 +677,8 @@ func setupRouter(server *handlers.Server) *chi.Mux {
streamingRoutes := func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Post("/account", server.HandleMargeCreateAccount)
r.Post("/account/login", server.HandleMargeLogin)
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
+1 -1
View File
@@ -17,7 +17,7 @@ require (
require (
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
github.com/xrash/smetrics v0.0.0-20250705151800-55b8f293f342 // indirect
golang.org/x/image v0.37.0 // indirect
golang.org/x/image v0.38.0 // indirect
golang.org/x/mod v0.34.0 // indirect
golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.20.0 // indirect
+2 -2
View File
@@ -30,8 +30,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+4 -4
View File
@@ -66,7 +66,7 @@ func TestNewClientFromHost(t *testing.T) {
func TestGetDeviceInfo_Success(t *testing.T) {
// Load test data
testData := loadTestData(t, "info_response.xml")
testData := loadTestData(t, "info_response_st10.xml")
// Create mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -117,8 +117,8 @@ func TestGetDeviceInfo_Success(t *testing.T) {
t.Errorf("Expected Name 'My SoundTouch Device', got '%s'", deviceInfo.Name)
}
if deviceInfo.MargeAccountUUID != "3230304" {
t.Errorf("Expected MargeAccountUUID '3230304', got '%s'", deviceInfo.MargeAccountUUID)
if deviceInfo.MargeAccountUUID != "1234567" {
t.Errorf("Expected MargeAccountUUID '1234567', got '%s'", deviceInfo.MargeAccountUUID)
}
if deviceInfo.ModuleType != "sm2" {
@@ -227,7 +227,7 @@ func TestGetDeviceInfo_APIError(t *testing.T) {
}
func TestPing_Success(t *testing.T) {
testData := loadTestData(t, "info_response.xml")
testData := loadTestData(t, "info_response_st10.xml")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "application/xml")
@@ -2,7 +2,7 @@
<info deviceID="ABCD1234EFGH">
<name>My SoundTouch Device</name>
<type>SoundTouch 10</type>
<margeAccountUUID>3230304</margeAccountUUID>
<margeAccountUUID>1234567</margeAccountUUID>
<components>
<component>
<componentCategory>SCM</componentCategory>
+1 -1
View File
@@ -2,7 +2,7 @@
<info deviceID="ABCD1234EFGH">
<name>My SoundTouch Device</name>
<type>SoundTouch 20</type>
<margeAccountUUID>3230304</margeAccountUUID>
<margeAccountUUID>1234567</margeAccountUUID>
<components>
<component>
<componentCategory>SCM</componentCategory>
+32
View File
@@ -2,6 +2,7 @@ package client
import (
"context"
"encoding/xml"
"fmt"
"log"
"net/url"
@@ -519,6 +520,37 @@ func (ws *WebSocketClient) SendMessage(message []byte) error {
return conn.WriteMessage(websocket.TextMessage, message)
}
// PairWithAccount sends a request to pair the device with a specific account
func (ws *WebSocketClient) PairWithAccount(accountID, userAuthToken string) error {
request := models.PairDeviceWithAccount{
AccountID: accountID,
UserAuthToken: userAuthToken,
}
data, err := xml.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal pairing request: %w", err)
}
ws.logger.Printf("Sending PairDeviceWithAccount for account %s", accountID)
return ws.SendMessage(data)
}
// UnPairFromAccount sends a request to unpair the device from its account
func (ws *WebSocketClient) UnPairFromAccount() error {
request := models.UnPairDeviceWithAccount{}
data, err := xml.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal unpairing request: %w", err)
}
ws.logger.Printf("Sending UnPairDeviceWithAccount")
return ws.SendMessage(data)
}
// Wait blocks until the WebSocket connection is closed or context is cancelled
func (ws *WebSocketClient) Wait() {
<-ws.ctx.Done()
+19
View File
@@ -656,3 +656,22 @@ type ProviderSetting struct {
ProviderID string `json:"provider_id" xml:"providerId"`
ProviderName string `json:"provider_name,omitempty" xml:"-"`
}
// MargeLoginRequest represents a login request from Stockholm.
type MargeLoginRequest struct {
XMLName xml.Name `xml:"login"`
Username string `xml:"username"`
Password string `xml:"password"`
}
// MargeAccountCreateRequest represents an account creation request from Stockholm.
type MargeAccountCreateRequest struct {
XMLName xml.Name `xml:"account"`
ID string `xml:"id,attr,omitempty"` // Optional ID for testing/overrides
FirstName string `xml:"firstName"`
LastName string `xml:"lastName"`
Email string `xml:"email"`
Password string `xml:"password"`
CountryCode string `xml:"countryCode"`
PreferredLanguage string `xml:"preferredLanguage"`
}
+20
View File
@@ -35,6 +35,10 @@ const (
EventTypeRecentsUpdated WebSocketEventType = "recentsUpdated"
// EventTypeLanguageUpdated indicates a language setting change
EventTypeLanguageUpdated WebSocketEventType = "languageUpdated"
// EventTypePairDeviceWithAccount indicates a device pairing request
EventTypePairDeviceWithAccount WebSocketEventType = "PairDeviceWithAccount"
// EventTypeUnPairDeviceWithAccount indicates a device unpairing request
EventTypeUnPairDeviceWithAccount WebSocketEventType = "UnPairDeviceWithAccount"
// EventTypeUnknown indicates an unrecognized event type
EventTypeUnknown WebSocketEventType = "unknown"
)
@@ -66,6 +70,10 @@ func (e WebSocketEventType) String() string {
return "Recents Updated"
case EventTypeLanguageUpdated:
return "Language Updated"
case EventTypePairDeviceWithAccount:
return "Pair Device With Account"
case EventTypeUnPairDeviceWithAccount:
return "UnPair Device With Account"
default:
return "Unknown Event"
}
@@ -299,6 +307,18 @@ type Language struct {
Value string `xml:",chardata"`
}
// PairDeviceWithAccount represents a device pairing request message
type PairDeviceWithAccount struct {
XMLName xml.Name `xml:"PairDeviceWithAccount"`
AccountID string `xml:"accountId"`
UserAuthToken string `xml:"userAuthToken"`
}
// UnPairDeviceWithAccount represents a device unpairing request message
type UnPairDeviceWithAccount struct {
XMLName xml.Name `xml:"UnPairDeviceWithAccount"`
}
// SpecialMessageType represents message types that are not part of <updates>
type SpecialMessageType string
+108
View File
@@ -1,9 +1,11 @@
package handlers
import (
"crypto/rand"
"encoding/xml"
"io"
"log"
"math/big"
"net"
"net/http"
"strconv"
@@ -15,6 +17,112 @@ import (
"github.com/go-chi/chi/v5"
)
// HandleMargeCreateAccount creates a new account from Stockholm (XML).
func (s *Server) HandleMargeCreateAccount(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
var req models.MargeAccountCreateRequest
if err := xml.Unmarshal(body, &req); err != nil {
http.Error(w, "Invalid XML body: "+err.Error(), http.StatusBadRequest)
return
}
// Use provided ID or generate new 7-digit ID
var id string
if req.ID != "" {
id = req.ID
} else {
for {
n, _ := rand.Int(rand.Reader, big.NewInt(9000000))
id = strconv.FormatInt(n.Int64()+1000000, 10)
existing, _ := s.ds.GetAccountInfo(id)
if existing == nil || existing.IsPlaceholder {
break
}
}
}
info := &models.ServiceAccountInfo{
AccountID: id,
PreferredLanguage: req.PreferredLanguage,
}
if info.PreferredLanguage == "" {
info.PreferredLanguage = "en"
}
if err := s.ds.SaveAccountInfo(id, info); err != nil {
http.Error(w, "Failed to save account", http.StatusInternalServerError)
return
}
// Stockholm expects the account XML in response
resp := models.AccountFullResponse{
ID: id,
AccountStatus: "ACTIVE",
PreferredLanguage: info.PreferredLanguage,
}
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
w.WriteHeader(http.StatusCreated)
_ = xml.NewEncoder(w).Encode(resp)
}
// HandleMargeLogin handles account login from Stockholm.
func (s *Server) HandleMargeLogin(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read request body", http.StatusBadRequest)
return
}
var req models.MargeLoginRequest
if err = xml.Unmarshal(body, &req); err != nil {
http.Error(w, "Invalid XML body: "+err.Error(), http.StatusBadRequest)
return
}
// Simple mock: find account by email or just return a default one if none exists
// For now, let's just return a fixed one for testing if nothing else matches
accounts, err := s.ds.ListAccounts()
accountID := ""
if err == nil {
for _, id := range accounts {
if id == "default" {
continue
}
// In a real system we'd check email/password
// Here we just pick the first one or use fallback
accountID = id
break
}
}
if accountID == "" {
http.Error(w, "No accounts found", http.StatusUnauthorized)
return
}
resp := models.AccountFullResponse{
ID: accountID,
AccountStatus: "ACTIVE",
PreferredLanguage: "en",
}
// Bose returns a token in the Credentials header
w.Header().Set("Credentials", "mock-token-"+accountID)
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
_ = xml.NewEncoder(w).Encode(resp)
}
// HandleMargeSourceProviders returns the Marge source providers.
func (s *Server) HandleMargeSourceProviders(w http.ResponseWriter, r *http.Request) {
etag := strconv.FormatInt(time.Now().UnixMilli(), 10)
+141
View File
@@ -2,6 +2,7 @@ package handlers
import (
"bytes"
"encoding/xml"
"fmt"
"io"
"net/http"
@@ -11,9 +12,149 @@ import (
"strings"
"testing"
"github.com/gesellix/bose-soundtouch/pkg/models"
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
)
func TestMargeCreateAccount(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tempDir) }()
ds := datastore.NewDataStore(tempDir)
r, _ := setupRouter("http://localhost:8001", ds)
ts := httptest.NewServer(r)
defer ts.Close()
reqBody := `<account>
<preferredLanguage>de</preferredLanguage>
</account>`
res, err := http.Post(ts.URL+"/marge/streaming/account", "application/vnd.bose.streaming-v1.2+xml", strings.NewReader(reqBody))
if err != nil {
t.Fatal(err)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusCreated {
t.Errorf("Expected status Created, got %v", res.Status)
}
contentType := res.Header.Get("Content-Type")
if contentType != "application/vnd.bose.streaming-v1.2+xml" {
t.Errorf("Expected Content-Type application/vnd.bose.streaming-v1.2+xml, got %v", contentType)
}
body, _ := io.ReadAll(res.Body)
var resp models.AccountFullResponse
if err := xml.Unmarshal(body, &resp); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if resp.AccountStatus != "ACTIVE" {
t.Errorf("Expected AccountStatus ACTIVE, got %v", resp.AccountStatus)
}
if resp.PreferredLanguage != "de" {
t.Errorf("Expected PreferredLanguage de, got %v", resp.PreferredLanguage)
}
if len(resp.ID) != 7 {
t.Errorf("Expected 7-digit ID, got %v", resp.ID)
}
// Verify it was saved in datastore
info, err := ds.GetAccountInfo(resp.ID)
if err != nil {
t.Errorf("Failed to get account from datastore: %v", err)
}
if info == nil {
t.Error("Account not found in datastore")
} else if info.PreferredLanguage != "de" {
t.Errorf("Expected saved PreferredLanguage de, got %v", info.PreferredLanguage)
}
}
func TestMargeLogin(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tempDir) }()
ds := datastore.NewDataStore(tempDir)
accountID := "9876543"
_ = ds.SaveAccountInfo(accountID, &models.ServiceAccountInfo{
AccountID: accountID,
PreferredLanguage: "fr",
})
r, _ := setupRouter("http://localhost:8001", ds)
ts := httptest.NewServer(r)
defer ts.Close()
reqBody := `<login>
<username>test@example.com</username>
<password>secret</password>
</login>`
res, err := http.Post(ts.URL+"/marge/streaming/account/login", "application/vnd.bose.streaming-v1.2+xml", strings.NewReader(reqBody))
if err != nil {
t.Fatal(err)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusOK {
t.Errorf("Expected status OK, got %v", res.Status)
}
credentials := res.Header.Get("Credentials")
if credentials != "mock-token-"+accountID {
t.Errorf("Expected Credentials mock-token-%s, got %v", accountID, credentials)
}
body, _ := io.ReadAll(res.Body)
var resp models.AccountFullResponse
if err := xml.Unmarshal(body, &resp); err != nil {
t.Fatalf("Failed to unmarshal response: %v", err)
}
if resp.ID != accountID {
t.Errorf("Expected ID %s, got %v", accountID, resp.ID)
}
}
func TestMargeLogin_NoAccount(t *testing.T) {
tempDir, err := os.MkdirTemp("", "st-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer func() { _ = os.RemoveAll(tempDir) }()
ds := datastore.NewDataStore(tempDir)
r, _ := setupRouter("http://localhost:8001", ds)
ts := httptest.NewServer(r)
defer ts.Close()
reqBody := `<login>
<username>none@example.com</username>
<password>secret</password>
</login>`
res, err := http.Post(ts.URL+"/marge/streaming/account/login", "application/vnd.bose.streaming-v1.2+xml", strings.NewReader(reqBody))
if err != nil {
t.Fatal(err)
}
defer func() { _ = res.Body.Close() }()
if res.StatusCode != http.StatusUnauthorized {
t.Errorf("Expected status Unauthorized, got %v", res.Status)
}
}
func TestMargeSourceProviders(t *testing.T) {
r, _ := setupRouter("http://localhost:8001", nil)
+2
View File
@@ -57,6 +57,8 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
r.Get("/software/update/account/{account}", server.HandleMargeSoftwareUpdate)
r.Post("/account", server.HandleMargeCreateAccount)
r.Post("/account/login", server.HandleMargeLogin)
}
accountsRoutes := func(r chi.Router) {
@@ -0,0 +1,45 @@
### Create Account (Official Stockholm endpoint)
POST {{host}}/streaming/account
Content-Type: application/vnd.bose.customer-v1.0+xml
<?xml version="1.0" encoding="UTF-8"?>
<account id="{{accountId}}">
<firstName>John</firstName>
<lastName>Doe</lastName>
<email>john.doe@example.com</email>
<password>password123</password>
<countryCode>US</countryCode>
<preferredLanguage>en</preferredLanguage>
</account>
> {%
client.test("Account created via XML successfully", function() {
client.assert(response.status === 201, "Response status is not 201");
const doc = response.body;
const account = doc.getElementsByTagName("account")[0];
client.assert(account !== undefined, "Response body should contain account XML");
var accountId = account.getAttribute("id");
if (client.variables.environment.get("accountId")) {
client.assert(accountId === client.variables.environment.get("accountId"), "Account ID "+accountId+" should match environment variable if provided");
}
client.global.set("newAccountId", accountId);
});
%}
### Login (Official Stockholm endpoint)
POST {{host}}/streaming/account/login
Content-Type: application/vnd.bose.streaming-v1.2+xml
<?xml version="1.0" encoding="UTF-8"?>
<login>
<username>john.doe@example.com</username>
<password>password123</password>
</login>
> {%
client.test("Login successful", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.headers.valueOf("Credentials") !== null, "Credentials header missing");
client.global.set("authToken", response.headers.valueOf("Credentials"));
});
%}
@@ -0,0 +1,22 @@
### GET /streaming/account/{{newAccountId}}/full
GET {{host}}/streaming/account/{{newAccountId}}/full
Host: streaming.bose.com
Authorization: Bearer {{authToken}}
Content-Type: application/vnd.bose.streaming-v1.2+xml
User-Agent: Bose_Lisa/27.0.6
Accept: application/vnd.bose.streaming-v1.2+xml
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.contentType.mimeType === "application/vnd.bose.streaming-v1.2+xml", "Expected 'application/vnd.bose.streaming-v1.2+xml' but received '" + response.contentType.mimeType + "'");
});
client.test("Response body contains <account>", function() {
const expectedAccountId = client.global.get("newAccountId") || client.variables.environment.get("accountId");
const doc = response.body;
const account = doc.getElementsByTagName("account")[0];
client.assert(account !== undefined, "Response body does not contain <account>");
client.assert(account.getAttribute("id") === expectedAccountId, "Expected account id '" + expectedAccountId + "' but received '" + account.getAttribute("id") + "'");
});
%}
@@ -0,0 +1,19 @@
### GET /streaming/account/{{newAccountId}}/device/{{deviceId}}/group/
GET {{host}}/streaming/account/{{newAccountId}}/device/{{deviceId}}/group/
Host: streaming.bose.com
Content-Type: application/vnd.bose.streaming-v1.2+xml
User-Agent: Bose_Lisa/27.0.6
Accept: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{authToken}}
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.contentType.mimeType === "application/vnd.bose.streaming-v1.2+xml", "Expected 'application/vnd.bose.streaming-v1.2+xml' but received '" + response.contentType.mimeType + "'");
});
client.test("Response body contains <group/>", function() {
const doc = response.body;
client.assert(doc.getElementsByTagName("group").length > 0, "Response body does not contain <group/>");
});
%}
@@ -0,0 +1,19 @@
### GET /streaming/account/{{newAccountId}}/provider_settings
GET {{host}}/streaming/account/{{newAccountId}}/provider_settings
Host: streaming.bose.com
Accept: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{authToken}}
Content-Type: application/vnd.bose.streaming-v1.2+xml
User-Agent: Bose_Lisa/27.0.6
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
client.assert(response.contentType.mimeType === "application/vnd.bose.streaming-v1.2+xml", "Expected 'application/vnd.bose.streaming-v1.2+xml' but received '" + response.contentType.mimeType + "'");
});
client.test("Response body contains <providerSettings>", function() {
const doc = response.body;
client.assert(doc.getElementsByTagName("providerSettings").length > 0, "Response body does not contain <providerSettings>");
});
%}
@@ -0,0 +1,30 @@
{
"local": {
"host": "http://localhost:8000",
"token": "example-token",
"deviceId": "B05ECAFE",
"serialNumber": "K12345",
"productCode": "SoundTouch test",
"productSerialNumber": "237983",
"gatewayIp": "192.168.1.1",
"deviceIp": "192.168.1.100",
"macAddress1": "B05ECAFE",
"macAddress2": "B05ECAFF",
"accountId": "7654321",
"deviceName": "SoundTouch-20"
},
"ci": {
"host": "http://soundtouch-service:8000",
"token": "example-token",
"deviceId": "B05ECAFE",
"serialNumber": "K12345",
"productCode": "SoundTouch test",
"productSerialNumber": "237983",
"gatewayIp": "192.168.1.1",
"deviceIp": "192.168.1.100",
"macAddress1": "B05ECAFE",
"macAddress2": "B05ECAFF",
"accountId": "7654321",
"deviceName": "SoundTouch-20"
}
}
@@ -0,0 +1,15 @@
### POST /streaming/support/power_on
POST {{host}}/streaming/support/power_on
Host: streaming.bose.com
User-Agent: Bose_Lisa/27.0.6
Accept: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{authToken}}
Content-Type: application/vnd.bose.streaming-v1.2+xml
<?xml version="1.0" encoding="UTF-8" ?><device-data><device id="{{deviceId}}"><serialnumber>{{serialNumber}}</serialnumber><firmware-version>27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29</firmware-version><product product_code="{{productCode}}" type="5"><serialnumber>{{productSerialNumber}}</serialnumber></product></device><diagnostic-data><device-landscape><rssi>Excellent</rssi><gateway-ip-address>{{gatewayIp}}</gateway-ip-address><macaddresses><macaddress>{{macAddress1}}</macaddress><macaddress>{{macAddress2}}</macaddress></macaddresses><ip-address>{{deviceIp}}</ip-address><network-connection-type>Wireless</network-connection-type></device-landscape><network-landscape><network-data xmlns="http://www.Bose.com/Schemas/2012-12/NetworkMonitor/" /></network-landscape></diagnostic-data></device-data>
> {%
client.test("Request executed successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
@@ -0,0 +1,19 @@
### POST /{{newAccountId}}/devices (Register Device)
POST {{host}}/accounts/{{newAccountId}}/devices
Content-Type: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{authToken}}
<?xml version="1.0" encoding="UTF-8" ?>
<device deviceid="{{deviceId}}">
<name>{{deviceName}}</name>
</device>
> {%
client.test("Device registered successfully", function() {
client.assert(response.status === 200 || response.status === 201, "Response status is not 200 or 201");
const doc = response.body;
const device = doc.getElementsByTagName("device")[0];
client.assert(device !== undefined, "Response body should contain <device>");
client.assert(device.getAttribute("deviceid") === client.variables.environment.get("deviceId"), "Response body should contain the deviceId");
});
%}
@@ -0,0 +1 @@
*.xml
@@ -0,0 +1,9 @@
### DELETE /{{newAccountId}}/devices/{{deviceId}} (Unregister Device)
DELETE {{host}}/accounts/{{newAccountId}}/devices/{{deviceId}}
Authorization: Bearer {{authToken}}
> {%
client.test("Device unregistered successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}