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.
This commit is contained in:
NimBold
2026-08-28 05:55:57 +03:30
parent 9d1e8d994a
commit 300e225a3a
2 changed files with 53 additions and 2 deletions
+48
View File
@@ -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: [{
+5 -2
View File
@@ -1255,8 +1255,11 @@ export const useDownloadStore = create<DownloadState>((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<DownloadState>((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