From 300e225a3ae0446d642d2ce4bd768648ff9c735f Mon Sep 17 00:00:00 2001 From: NimBold Date: Fri, 28 Aug 2026 05:55:57 +0330 Subject: [PATCH] fix(downloads): align credential recovery after properties edits - Fixes #37. - Mark username-only and username-without-password edits for credential recovery immediately. - Keep in-memory and persisted retry behavior aligned without reusing stale request credentials. --- src/store/useDownloadStore.test.ts | 48 ++++++++++++++++++++++++++++++ src/store/useDownloadStore.ts | 7 +++-- 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/src/store/useDownloadStore.test.ts b/src/store/useDownloadStore.test.ts index 2d2d10a..04358d5 100644 --- a/src/store/useDownloadStore.test.ts +++ b/src/store/useDownloadStore.test.ts @@ -211,6 +211,54 @@ describe('useDownloadStore', () => { expect(useDownloadStore.getState().downloads[0].credentialsRequired).toBe(false); }); + it('marks a username-only properties change for credential recovery', async () => { + useDownloadStore.setState({ + downloads: [{ + id: 'username-only-properties', + url: 'https://secure.example.com/file.bin', + fileName: 'file.bin', + status: 'failed', + category: 'Other', + dateAdded: '', + }] as any[], + }); + + await useDownloadStore.getState().applyProperties('username-only-properties', { + username: 'alice', + }); + + expect(useDownloadStore.getState().downloads[0]).toMatchObject({ + username: 'alice', + credentialsRequired: true, + }); + }); + + it('keeps the recovery marker when clearing a password leaves a username', async () => { + useDownloadStore.setState({ + downloads: [{ + id: 'username-without-password', + url: 'https://secure.example.com/file.bin', + fileName: 'file.bin', + status: 'failed', + category: 'Other', + dateAdded: '', + username: 'alice', + password: 'secret', + credentialsRequired: false, + }] as any[], + }); + + await useDownloadStore.getState().applyProperties('username-without-password', { + password: undefined, + }); + + expect(useDownloadStore.getState().downloads[0]).toMatchObject({ + username: 'alice', + password: undefined, + credentialsRequired: true, + }); + }); + it('clears a persisted Torrent removal reservation when a paused item disables cleanup', async () => { useDownloadStore.setState({ downloads: [{ diff --git a/src/store/useDownloadStore.ts b/src/store/useDownloadStore.ts index 562becd..481fe70 100644 --- a/src/store/useDownloadStore.ts +++ b/src/store/useDownloadStore.ts @@ -1255,8 +1255,11 @@ export const useDownloadStore = create((set, get) => { throw error; } }; - const credentialsUpdated = (['password', 'cookies', 'headers'] as const) + const credentialsUpdated = (['username', 'password', 'cookies', 'headers'] as const) .some(field => Object.prototype.hasOwnProperty.call(updates, field)); + const nextUsernameMaterial = hasCredentialMaterial( + Object.prototype.hasOwnProperty.call(updates, 'username') ? updates.username : item.username, + ); const nextCredentialMaterial = (['password', 'cookies', 'headers'] as const) .some(field => field === 'headers' ? hasCredentialBearingHeaders( @@ -1272,7 +1275,7 @@ export const useDownloadStore = create((set, get) => { : { ...updates, fileName: canonicalizeDownloadFileName(updates.fileName) }), ...(credentialsUpdated && nextCredentialMaterial ? { credentialsRequired: false } - : credentialsUpdated && item.credentialsRequired === true + : credentialsUpdated && (nextUsernameMaterial || item.credentialsRequired === true) ? { credentialsRequired: true } : {}), ...(item.isTorrent === true