From 83c9d4d1646356d56eeb614004f455314115b663 Mon Sep 17 00:00:00 2001 From: Gronod Date: Sun, 24 May 2026 22:12:34 +0100 Subject: [PATCH] fix: blocklist-search queue ID type mismatch and bump version to 1.7.16 - 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 --- CHANGELOG.md | 8 ++++++++ package-lock.json | 4 ++-- package.json | 2 +- server/app.js | 2 +- server/index.js | 2 +- server/openapi.yaml | 2 +- server/routes/dashboard.js | 7 +++++-- tests/integration/dashboard.test.js | 32 +++++++++++++++++++++++++++++ 8 files changed, 51 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5370463..9fa4784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 6fc79c4..09d8e87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 02a4735..1d6ae3b 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/server/app.js b/server/app.js index e0fe80b..2c04424 100644 --- a/server/app.js +++ b/server/app.js @@ -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 diff --git a/server/index.js b/server/index.js index 6de7ba3..1020fde 100644 --- a/server/index.js +++ b/server/index.js @@ -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 }); diff --git a/server/openapi.yaml b/server/openapi.yaml index 5499028..2983106 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -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: diff --git a/server/routes/dashboard.js b/server/routes/dashboard.js index 25bb7ff..c07b07d 100644 --- a/server/routes/dashboard.js +++ b/server/routes/dashboard.js @@ -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 }); diff --git a/tests/integration/dashboard.test.js b/tests/integration/dashboard.test.js index 10a38fe..f76ae9b 100644 --- a/tests/integration/dashboard.test.js +++ b/tests/integration/dashboard.test.js @@ -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(); + }); }); // ---------------------------------------------------------------------------