Compare commits

..

2 Commits

Author SHA1 Message Date
gronod 1ba9d15954 merge branch 'develop' into 'main' - Release v1.7.16
Create Release / release (push) Successful in 21s
Build and Push Docker Image / build (push) Successful in 2m11s
CI / Security audit (push) Successful in 1m57s
CI / Tests & coverage (push) Successful in 2m29s
CI / Swagger Validation & Coverage (push) Successful in 2m6s
2026-05-24 22:13:28 +01:00
gronod 83c9d4d164 fix: blocklist-search queue ID type mismatch and bump version to 1.7.16
Build and Push Docker Image / build (push) Successful in 2m14s
Docs Check / Markdown lint (push) Successful in 2m29s
CI / Security audit (push) Successful in 2m56s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 3m4s
CI / Swagger Validation & Coverage (push) Successful in 3m52s
Docs Check / Mermaid diagram parse check (push) Successful in 4m8s
CI / Tests & coverage (push) Successful in 4m38s
- Cast arrQueueId to String in both sides of the download lookup comparison
  in /api/dashboard/blocklist-search to resolve false-negative match failure
  caused by DOM dataset string vs Radarr/Sonarr API number type mismatch
- Add regression integration test for string-vs-number arrQueueId matching
- Bump version to 1.7.16, update CHANGELOG.md, openapi.yaml, and JSDoc examples

Resolves #48
2026-05-24 22:12:34 +01:00
8 changed files with 51 additions and 8 deletions
+8
View File
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.7.16] - 2026-05-24
### Fixed
- **Blocklist-Search Queue ID Type Mismatch** — Resolved a bug where the "Blocklist and search" action consistently returned `403 Download not found or permission denied` for all users. The server-side download lookup in `server/routes/dashboard.js` used strict equality (`===`) to compare `arrQueueId` values, but the value sent from the SPA client (read from a DOM `data-*` attribute) is always a `string`, while the value populated from the Radarr/Sonarr queue API is a `number`. Both sides are now cast to `String` before comparison, resolving the false-negative match failure. Resolves Gitea Issue [#48](https://git.i3omb.com/Gandalf/sofarr/issues/48).
---
## [1.7.15] - 2026-05-24
### Fixed
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sofarr",
"version": "1.7.15",
"version": "1.7.16",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sofarr",
"version": "1.7.15",
"version": "1.7.16",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sofarr",
"version": "1.7.15",
"version": "1.7.16",
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
"main": "server/index.js",
"scripts": {
+1 -1
View File
@@ -132,7 +132,7 @@ function createApp({ skipRateLimits = false } = {}) {
* version:
* type: string
* description: sofarr version
* example: "1.7.14"
* example: "1.7.16"
* x-code-samples:
* - lang: curl
* label: cURL
+1 -1
View File
@@ -249,7 +249,7 @@ app.use(express.json({ limit: '64kb' })); // prevent oversized JSON payloads
* version:
* type: string
* description: sofarr version
* example: "1.7.14"
* example: "1.7.16"
*/
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime(), version });
+1 -1
View File
@@ -22,7 +22,7 @@ info:
## SSE Streaming
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
version: 1.7.15
version: 1.7.16
contact:
name: sofarr
license:
+5 -2
View File
@@ -686,9 +686,12 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
return res.status(400).json({ error: 'arrType must be sonarr or radarr' });
}
// Look up the download to verify permission
// Look up the download to verify permission.
// Note: arrQueueId from req.body is always a string (DOM dataset), while
// d.arrQueueId from the Radarr/Sonarr API is a number. Cast both to String
// to avoid a type mismatch causing a false-negative lookup.
const allDownloads = await downloadClientRegistry.getAllDownloads();
const download = allDownloads.find(d => d.arrQueueId === arrQueueId && d.arrType === arrType);
const download = allDownloads.find(d => d.arrQueueId != null && String(d.arrQueueId) === String(arrQueueId) && d.arrType === arrType);
if (!download) {
console.error('[Blocklist] Download not found:', { arrQueueId, arrType });
+32
View File
@@ -972,6 +972,38 @@ describe('POST /api/dashboard/blocklist-search', () => {
expect(res.body.ok).toBe(true);
mockGetAllDownloads.mockRestore();
});
it('matches download correctly when arrQueueId is sent as a string but stored as a number (type mismatch regression)', async () => {
// Regression test for GitHub #48: arrQueueId from the SPA DOM dataset is always
// a string, but the value stored in allDownloads from the Radarr/Sonarr API is a number.
// Without String() casting the === comparison fails and returns 403.
const app = createApp({ skipRateLimits: true });
const { cookies, csrf, csrfCookie } = await getAuthHeaders(app);
const downloadClientRegistry = require('../../server/utils/downloadClients');
const mockGetAllDownloads = vi.spyOn(downloadClientRegistry, 'getAllDownloads').mockResolvedValue([
// arrQueueId stored as a number (as Radarr API returns it)
{ arrQueueId: 9050001, arrType: 'radarr', importIssues: [], qbittorrent: null }
]);
nock(RADARR_BASE)
.delete('/api/v3/queue/9050001')
.query({ removeFromClient: 'true', blocklist: 'true' })
.reply(200, {});
nock(RADARR_BASE)
.post('/api/v3/command')
.reply(200, {});
const res = await request(app)
.post('/api/dashboard/blocklist-search')
.set('Cookie', [...cookies, csrfCookie].join('; '))
.set('X-CSRF-Token', csrf)
// arrQueueId sent as a STRING from the client (as the SPA DOM dataset does)
.send({ arrQueueId: '9050001', arrType: 'radarr', arrInstanceUrl: RADARR_BASE, arrInstanceKey: 'rk', arrContentId: 77, arrContentType: 'movie' });
expect(res.status).toBe(200);
expect(res.body.ok).toBe(true);
mockGetAllDownloads.mockRestore();
});
});
// ---------------------------------------------------------------------------