diff --git a/PROJECT_MEMORY.md b/PROJECT_MEMORY.md index 95e72b2..e00ee2d 100644 --- a/PROJECT_MEMORY.md +++ b/PROJECT_MEMORY.md @@ -18,7 +18,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große - Die jüngste Automatik schützt erfolgreiche Uploads mit einem atomar gespeicherten Abschlussnachweis aus vollständigem Pfad, Hoster, Dateigröße und Änderungszeit. - Schlägt dieser Nachweis fehl, bleibt die Queue erhalten und der Fehler wird als lokale Persistenzstörung behandelt, damit kein stiller Doppel-Upload entsteht. - DoodStream-OTP-Prüfungen verwenden dieselbe Cookie-Sitzung weiter, fassen identische oder parallele Checks zusammen und fordern einen neuen Code nur nach einer ausdrücklichen Aktion mit mindestens 60 Sekunden Abstand an. -- DoodStream-Accounts mit API-Key werden auch beim Health-Check über die API geprüft und lösen keinen Web-OTP aus. +- DoodStream respektiert die ausdrückliche Auswahl `authType=login`: Weblogin und Webupload werden nicht durch einen zusätzlich gespeicherten API-Key übersteuert. API-Accounts und ältere Accounts ohne ausdrückliche Login-Auswahl behalten den API-Weg. - Sammelchecks melden jedes Account-Ergebnis einzeln an den Renderer, sodass fertige Karten sofort grün, rot oder als OTP-pflichtig erscheinen, während die übrigen Accounts weiter geprüft werden. - Neue Online-Backups unterstützen `24 Stunden`, `3 Tage`, `7 Tage` (Standard), `31 Tage` und `Unbegrenzt`. Endliche Schlüssel werden lokal aus dem verschlüsselten Schlüsselbund entfernt und serverseitig ab Ablauf nicht mehr wiederhergestellt; der Dienst räumt abgelaufene Datensätze bei Zugriff oder der nächsten Speicherung auf. - Vorhandene Online-Backups und alte Upload-Payloads ohne Ablaufangabe bleiben zur Abwärtskompatibilität unbegrenzt gültig. @@ -27,6 +27,7 @@ Multi-Hoster-Upload ist eine Electron-Desktopanwendung für Windows, die große - Upload-Status-Badges und ihre Textlabels sind nicht markierbar; kopierbare Fehlerdetails, Logs und Eingabefelder behalten ihre Textauswahl. - VOE-Fehler mit der Meldung `Maximum storage space of the account used up.` gelten als temporärer Accountfehler. Die Retry-Schleife bricht auch nach einem bereits erfolgten Account-Wechsel sofort ab und setzt die Fallback-Kette Account für Account fort, bis ein Upload gelingt oder kein weiterer Account verfügbar ist. - Der DoodStream-Weblogin folgt dem aktuellen Browservertrag über `GET /?op=login_ajax`, behandelt `otp_sent` und `redirect` ausdrücklich und übernimmt `sess_id` auch aus den aktuellen Vue-Daten mit URL-sicheren Sonderzeichen. Die Upload-Server-Ermittlung verwendet `/?op=upload_get_srv` und versteht dessen `server.srv_url`-/`server.disk_id`-Antwort. +- Eine bestätigte DoodStream-Dashboard-Sitzung benötigt beim Account-Check kein Upload-Sessionfeld. Uploads übernehmen unabhängige Kopien der bestätigten Cookie-Sitzung aus dem OTP-Koordinator. Ein fehlender Upload-Server ist vom Login getrennt; bei Web-Accounts findet keine automatische API-Key-Ableitung für Uploads statt. - Version `2.1.44` ist als GitHub- und Forgejo-Release veröffentlicht; Backup-API `2.0.4` blieb bei dieser reinen Veröffentlichung der Desktopanwendung unverändert aktiv. - Der eingebaute Updater liest Releases und Binärdateien von Forgejo; GitHub liefert ergänzend die öffentlichen Release Notes. Ein Release ist deshalb erst vollständig, wenn die vier Assets auch im Forgejo-Release vorhanden sind. - Forgejo bewahrt Leerzeichen in Asset-Namen, GitHub normalisiert sie zu Punkten. Das Forgejo-`latest.yml` und der Release-Plan verwenden Namen wie `Multi-Hoster-Upload Setup 2.1.44.exe`; das GitHub-Manifest muss auf den dort tatsächlich veröffentlichten Punktnamen zeigen. @@ -58,7 +59,7 @@ npm audit --omit=dev ## Offene nächste Schritte -- DoodStream: Der echte OTP-Test nach dem ersten Kompatibilitätsfix meldete weiterhin fehlendes `sess_id`. JSON- und HTTP-Weiterleitungen werden nun vor der Sessionprüfung aufgerufen, einschließlich der dort gesetzten Cookies. 35 gezielte Tests und Lint sind erfolgreich; ein erneuter echter OTP-Login steht aus. Die konkrete Ursache der Nutzersitzung ist noch nicht abschließend bestätigt. Fehlende Sessions liefern HTTP-Status, Gastseiten-/Sessionfeld-Erkennung und Cookie-Anzahl ohne Cookie-Werte oder Zugangsdaten. +- DoodStream: Am 12.09.2026 wurde das authentifizierte Dashboard ohne `sess_id` live bestätigt. Der alte Upload-Aufruf lieferte eine andere Seite ohne Upload-Felder. Ein zwischenzeitlich getesteter API-Ausweichweg bestätigte zwar den Account, wurde auf Nutzerwunsch wieder entfernt; dessen Uploadversuch scheiterte serverseitig mit `No servers available for uploads`. Der aktuelle Web-Upload muss noch live auf Serververfügbarkeit und erfolgreichen Dateitransfer geprüft werden. Die lokale Seitendiagnose protokolliert ausschließlich Strukturmerkmale ohne Formularwerte, OTP oder Cookie-Werte. - Keine offenen Schritte für Release `v2.1.44`; Rollback-Ziel ist Anwendungsversion `2.1.43`. - Bei Bedarf einen Arbeitsweg ohne `&` im absoluten Pfad verwenden oder die npm-Aufrufe weiterhin direkt ausführen. @@ -67,10 +68,11 @@ npm audit --omit=dev Stand: 12.09.2026 - Lint: erfolgreich, 0 Warnungen und 0 Fehler. -- Haupttests: 808 erfolgreich, 0 fehlgeschlagen. +- Haupttests: vollständiger Lauf erfolgreich, 0 fehlgeschlagen. - Backup-API-Tests: 17 erfolgreich, 0 fehlgeschlagen. - Der Regressionstest für die VOE-Fallback-Kette bestätigt bei deaktivierter normaler Rotation genau einen Versuch auf jedem vollen Account und anschließend den erfolgreichen Wechsel auf den vierten Account. - Der öffentliche DoodStream-Webablauf wurde am 12.09.2026 direkt gegen die Startseite und deren aktuelle Browser-Skripte geprüft. Regressionstests bilden den neuen GET-Login, `otp_sent`, `redirect`, Vue-Sessiontokens mit `_`/`-` und die aktuelle `upload_get_srv`-Antwort nach. +- Der lokale Web-Account-Check um 14:35:27 bestätigte das authentifizierte Dashboard ohne Upload-Sessionfeld. Regressionen prüfen zusätzlich explizite Web-Auswahl trotz gespeichertem API-Key, Wiederverwendung der OTP-Sitzung, getrennte Cookie-Kopien für parallele Uploads und Web-Serverausfälle. Der Upload-Aufruf verwendet nachweislich weder API-Ableitung noch einen zweiten Login. - Das Support-Bundle vom 07.09.2026 bestätigt als Ursache der gemeldeten Datei: Wechsel vom Primäraccount auf `Fallback #1`, dort vier unnötige Versuche, anschließend `skip-account-pause` und Abbruch mit `override-same-as-current` statt Weiterschaltung. - Der vollständige opt-in UI-Smoke bestätigte zusätzlich, dass Upload-Status-Badges und deren Labels nicht markierbar sind; die 16 bekannten themenfremden Abweichungen blieben unverändert. - Produktionsabhängigkeiten: `npm audit --omit=dev` meldet 0 Schwachstellen. diff --git a/lib/account-auth.js b/lib/account-auth.js index c82c549..42fe97a 100644 --- a/lib/account-auth.js +++ b/lib/account-auth.js @@ -20,6 +20,10 @@ const { createHash } = require('node:crypto'); function selectUploadAuth(hoster, account) { if (!account || typeof account !== 'object') return {}; + if (account.authType === 'login' && account.username && account.password) { + return { username: account.username, password: account.password }; + } + if (hoster === 'doodstream.com' && account.apiKey) { return { apiKey: account.apiKey }; } @@ -72,6 +76,7 @@ function createDoodstreamOtpCoordinator(options = {}) { const key = credentialKey(username, password); const existing = activeState(key); if (existing?.inFlight) return existing.inFlight; + if (existing?.ready && input.requestNewChallenge !== true) return existing.result; if (otp && !existing?.pending) { return { status: 'otp_required', @@ -87,8 +92,11 @@ function createDoodstreamOtpCoordinator(options = {}) { const operation = (async () => { try { await uploader.login(username, password, otp || undefined); - if (states.get(key)?.operationId === operationId) states.delete(key); - return { status: 'ok', message: 'Login ok, Upload-Seite bereit' }; + const result = { status: 'ok', message: 'Login erfolgreich' }; + if (states.get(key)?.operationId === operationId) { + storeState(key, { operationId, uploader, ready: true, expiresAt: now() + challengeTtlMs, result, inFlight: null }); + } + return result; } catch (error) { if (error?.otpRequired === true) { const result = { status: 'otp_required', message: error.message || 'OTP erforderlich' }; @@ -137,7 +145,18 @@ function createDoodstreamOtpCoordinator(options = {}) { return operation; } - return { check }; + async function acquire(input) { + const result = await check(input); + const session = activeState(credentialKey(input.username, input.password)); + if (result.status !== 'ok' || !session?.ready) { + const error = new Error(result.message || 'OTP erforderlich'); + error.otpRequired = result.status === 'otp_required'; + throw error; + } + return session.uploader.cloneSession(); + } + + return { check, acquire }; } module.exports = { createDoodstreamOtpCoordinator, selectUploadAuth }; diff --git a/lib/doodstream-upload.js b/lib/doodstream-upload.js index 2d7b73f..572bed0 100644 --- a/lib/doodstream-upload.js +++ b/lib/doodstream-upload.js @@ -30,8 +30,8 @@ function _doodstreamLogPath() { let _debugVerbose = false; function setDebugVerbose(v) { _debugVerbose = !!v; } -function _debugLog(msg) { - if (!_debugVerbose) return; +function _debugLog(msg, force = false) { + if (!_debugVerbose && !force) return; try { const logPath = _doodstreamLogPath(); maybeRotateLogFile(logPath, _DOODSTREAM_LOG_MAX_BYTES, _DOODSTREAM_LOG_MAX_BACKUPS); @@ -53,6 +53,20 @@ class DoodstreamUploader { .join('; '); } + cloneSession() { + const uploader = new DoodstreamUploader(); + uploader.cookies = new Map(this.cookies); + uploader.sessId = this.sessId; + return uploader; + } + + _isAuthenticatedPage(html) { + return /href=["']\/settings["']/i.test(html) + && /href=["']\/videos["']/i.test(html) + && /href=["'][^"']*(?:op=logout|\/logout)["']/i.test(html) + && !/]*\bname=["'](?:login|password|loginotp)["']/i.test(html); + } + _parseCookiesFromHeaders(headers) { let setCookies; if (typeof headers.getSetCookie === 'function') { @@ -175,8 +189,9 @@ class DoodstreamUploader { throw err; } else if (json && json.status === 'fail') { throw new Error(`Doodstream Login: ${json.message || 'Login fehlgeschlagen'}`); - } else if (body.includes('Dashboard')) { - // Got dashboard HTML directly — login worked + } else if (res.status === 200 && this._isAuthenticatedPage(body)) { + this.sessId = this._findSessId(body); + return; } else { const msg = (json && json.message) || 'Login fehlgeschlagen'; throw new Error(`Doodstream Login: ${msg}`); @@ -190,8 +205,9 @@ class DoodstreamUploader { } const landing = await this._fetch(target.href, { allowedOrigin: BASE_URL }); const landingHtml = await landing.text(); + this._diagnoseSessionPage('landing', landing, landingHtml); const sessId = this._findSessId(landingHtml); - if (landing.status === 200 && sessId) { + if (landing.status === 200 && (sessId || this._isAuthenticatedPage(landingHtml))) { this.sessId = sessId; return; } @@ -203,6 +219,7 @@ class DoodstreamUploader { async _extractSessId() { const res = await this._fetch(BASE_URL + '/?op=upload'); const html = await res.text(); + this._diagnoseSessionPage('upload', res, html); const sessId = this._findSessId(html); if (res.status === 200 && sessId) { this.sessId = sessId; @@ -210,7 +227,8 @@ class DoodstreamUploader { } this.sessId = ''; const guest = /utype\s*:\s*['"]anon['"]/.test(html); - throw new Error(`Doodstream: sess_id nicht gefunden nach Login (HTTP ${res.status}; guest=${guest}; sessionField=${/sess_id/.test(html)}; cookies=${this.cookies.size})`); + _debugLog(`session-missing HTTP=${res.status} guest=${guest} cookies=${this.cookies.size}`, true); + throw new Error('Doodstream: sess_id nicht gefunden nach Login'); } _findSessId(html) { @@ -225,6 +243,32 @@ class DoodstreamUploader { return match ? match[2].trim() : ''; } + _diagnoseSessionPage(stage, response, html) { + const routes = new Set(); + for (const match of html.matchAll(/\bhref=["']([^"']+)["']/gi)) { + try { + const url = new URL(match[1].replace(/&/g, '&'), BASE_URL); + if (url.origin !== BASE_URL) continue; + const op = url.searchParams.get('op') || ''; + if (/^[a-z_]+$/i.test(op)) routes.add(`op=${op}`); + if (/^\/[a-z_/-]*$/i.test(url.pathname)) routes.add(url.pathname); + } catch {} + } + const fields = [...html.matchAll(/<(?:input|textarea)\b[^>]*\bname=["']([a-z_]+)["']/gi)].map(match => match[1]); + const components = [...html.matchAll(/<([a-z]+-[a-z-]+)\b/g)].map(match => match[1]); + const scripts = [...html.matchAll(/]*\bsrc=["']([^"']+)["']/gi)] + .map(match => match[1].split('?')[0].split('/').pop()) + .filter(name => /^[a-z_.-]+\.js$/i.test(name)); + _debugLog(`session-page ${JSON.stringify({ + stage, status: response.status, bytes: html.length, + challenge: /cf-chl-|challenge-platform|Just a moment|Checking your browser/i.test(html), + sessionField: /sess_id/.test(html), + cookies: [...this.cookies.keys()], + routes: [...routes].slice(0, 60), fields: [...new Set(fields)], + components: [...new Set(components)], scripts: [...new Set(scripts)] + })}`, true); + } + /** * Get upload server URL from web interface */ @@ -242,6 +286,12 @@ class DoodstreamUploader { let json; try { json = JSON.parse(text); } catch { json = null; } + if (json && (json.status === 'fail' || json.success === false)) { + const error = new Error(`Doodstream Upload: ${json.message || json.msg || 'No servers available for uploads'}`); + error.hosterTransient = /no servers|unavailable|temporar/i.test(error.message); + throw error; + } + if (json && json.result && /^https?:\/\//i.test(json.result)) { return json.result; } @@ -337,6 +387,7 @@ class DoodstreamUploader { // Get upload server const uploadUrl = await this._getUploadServer(); + if (!this.sessId) await this._extractSessId(); // Remember which CDN node handled this upload so a later parse failure can // report it — failures sometimes correlate with a specific node. this._lastUploadUrl = uploadUrl; @@ -702,10 +753,11 @@ class DoodstreamUploader { async deriveApiKey() { if (this.apiKey) return this.apiKey; let html = ''; - for (const page of ['/?op=my_account', '/settings', '/?op=profile']) { + for (const page of ['/settings', '/?op=my_account', '/?op=profile']) { try { const res = await this._fetch(BASE_URL + page); const text = await res.text(); + this._diagnoseSessionPage('settings', res, text); if (text && /api[\s_-]?key/i.test(text)) { html = text; break; } if (text && !html) html = text; } catch { /* try next page */ } @@ -715,11 +767,11 @@ class DoodstreamUploader { for (const key of candidates.slice(0, 15)) { if (await this._validateApiKey(key)) { this.apiKey = key; - _debugLog(`api-key derive: validated key (len ${key.length})`); + _debugLog('api-key derive: account API verification succeeded', true); return key; } } - _debugLog(`api-key derive: ${candidates.length} candidate(s), none validated. settings html(2500)=${(html || '').slice(0, 2500)}`); + _debugLog(`api-key derive: ${candidates.length} candidate(s), none validated`, true); return null; } } diff --git a/lib/upload-manager.js b/lib/upload-manager.js index ba5fcf3..e2255a8 100644 --- a/lib/upload-manager.js +++ b/lib/upload-manager.js @@ -25,8 +25,9 @@ const DEFAULT_SETTINGS = { }; class UploadManager extends EventEmitter { - constructor(hosterSettings, globalSettings, accountPools) { + constructor(hosterSettings, globalSettings, accountPools, options = {}) { super(); + this.acquireDoodstreamSession = options.acquireDoodstreamSession || null; this.hosterSettings = hosterSettings || {}; this.globalSettings = globalSettings || {}; this.accountPools = accountPools || {}; @@ -1309,22 +1310,11 @@ class UploadManager extends EventEmitter { await voe.login(task.username, task.password); return voe.upload(task.file, progressCb, signal, throttle); } else if (task.hoster === 'doodstream.com' && task.username) { - // Login-path reliability fix: the web-form upload returns the filecode in - // an HTML form that comes back empty for large files (doodstream backend - // registration timeout). Derive the account's API key from the logged-in - // session ONCE per batch and upload via the official API instead — it - // returns result[0].filecode directly and has no empty-form failure mode. - // Falls back to the web-form upload if no valid key can be derived. - const apiKey = await this._resolveDoodstreamApiKey(task); - if (apiKey) { - this._rotLog('doodstream-via-api', { accountId: task.accountId, fileName: path.basename(task.file) }); - return uploadFile('doodstream.com', task.file, apiKey, progressCb, signal, throttle, { - doodBaseline: await this._getBaseline('doodstream.com', apiKey, signal) - }); - } this._rotLog('doodstream-via-web', { accountId: task.accountId, fileName: path.basename(task.file) }); - const dood = new DoodstreamUploader(); - await dood.login(task.username, task.password); + const dood = this.acquireDoodstreamSession + ? await this.acquireDoodstreamSession(task) + : new DoodstreamUploader(); + if (!this.acquireDoodstreamSession) await dood.login(task.username, task.password); return dood.upload(task.file, progressCb, signal, throttle); } else if (task.hoster === 'clouddrop.cc') { const clouddrop = new ClouddropUploader(task.apiKey); diff --git a/main.js b/main.js index c4ad655..046498e 100644 --- a/main.js +++ b/main.js @@ -2343,7 +2343,9 @@ ipcMain.handle('start-upload', async (_event, payload) => { return { started: true, taskCount: 0, skippedJobs }; } - uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {}, buildAccountPools(config)); + uploadManager = new UploadManager(config.hosterSettings || {}, config.globalSettings || {}, buildAccountPools(config), { + acquireDoodstreamSession: (task) => doodstreamHealthCoordinator.acquire({ username: task.username, password: task.password }) + }); globalThis._mhuUploadManagerRef = uploadManager; const _thisManager = uploadManager; await registerAutomationCompletionJobs(_thisManager, jobs); diff --git a/renderer/i18n.js b/renderer/i18n.js index 9a0fc3a..2ee6def 100644 --- a/renderer/i18n.js +++ b/renderer/i18n.js @@ -564,6 +564,7 @@ ['Einstellungen konnten vor dem Update nicht gespeichert werden', 'Settings could not be saved before the update'], ['Das Update wurde nicht gestartet, weil die Einstellungen vor dem Beenden nicht gespeichert werden konnten', 'The update was not started because the settings could not be saved before quitting'], ['Login ok, Upload-Seite bereit', 'Login successful, upload page ready'], + ['Login erfolgreich', 'Login successful'], ['Login oder API Key fehlt', 'Login or API key is missing'], ['Account-Check lieferte kein gültiges JSON', 'Account check did not return valid JSON'], ['Account-Check fehlgeschlagen', 'Account check failed'], diff --git a/tests/account-auth.test.js b/tests/account-auth.test.js index 3c2df71..4ad69a8 100644 --- a/tests/account-auth.test.js +++ b/tests/account-auth.test.js @@ -49,6 +49,22 @@ function otpRequired(message = 'OTP erforderlich') { return error; } +test('explicit web login is not overridden by a stored API key', () => { + assert.deepEqual(selectUploadAuth('doodstream.com', { authType: 'login', username: 'user', password: 'secret', apiKey: 'KEY' }), { username: 'user', password: 'secret' }); +}); + +test('upload reuses the confirmed OTP session without another login', async () => { + let logins = 0; + const uploadSession = {}; + const coordinator = createDoodstreamOtpCoordinator({ + createUploader: () => ({ async login() { logins++; }, cloneSession: () => uploadSession }) + }); + const health = await coordinator.check({ username: 'user', password: 'secret' }); + assert.equal(health.status, 'ok'); + assert.equal(await coordinator.acquire({ username: 'user', password: 'secret' }), uploadSession); + assert.equal(logins, 1); +}); + test('concurrent and repeated Doodstream checks request only one OTP', async () => { let loginCalls = 0; let releaseLogin; @@ -87,7 +103,7 @@ test('Doodstream OTP verification reuses the challenged uploader session', async assert.equal((await coordinator.check({ username: 'user', password: 'secret' })).status, 'otp_required'); assert.deepEqual(await coordinator.check({ username: 'user', password: 'secret', otp: '123456' }), { status: 'ok', - message: 'Login ok, Upload-Seite bereit' + message: 'Login erfolgreich' }); assert.equal(created, 1); assert.deepEqual(calls, ['', '123456']); diff --git a/tests/doodstream-upload.test.js b/tests/doodstream-upload.test.js index 33a4ba9..9f53a14 100644 --- a/tests/doodstream-upload.test.js +++ b/tests/doodstream-upload.test.js @@ -246,7 +246,7 @@ test('login cannot report success when the redirected session is still a guest', ? JSON.stringify({ status: 'redirect', message: '/dashboard' }) : `` )); - await assert.rejects(up.login('user', 'password'), /guest=true; sessionField=true; cookies=0/); + await assert.rejects(up.login('user', 'password'), /sess_id nicht gefunden/); assert.equal(up.sessId, ''); }); @@ -261,6 +261,56 @@ test('login redirect chains cannot forward session cookies to another origin', a assert.equal(calls, 1); }); +test('authenticated dashboard verifies web login without an upload token or API calls', async (t) => { + const up = new DoodstreamUploader(); + t.mock.method(globalThis, 'fetch', async (url) => { + const target = new URL(url); + assert.equal(target.hostname, 'doodstream.com'); + if (target.searchParams.get('op') === 'login_ajax') { + return new Response(JSON.stringify({ status: 'redirect', message: '/dashboard' }), { + headers: { 'set-cookie': 'xfsts=SESSION; Path=/' } + }); + } + return new Response('SettingsVideosLogout'); + }); + await up.login('user', 'secret', '123456'); + assert.equal(up.apiKey, ''); + assert.equal(up.sessId, ''); + assert.equal(up.cookies.get('xfsts'), 'SESSION'); +}); + +test('an unverified settings token cannot make a missing-session login pass', async (t) => { + const up = new DoodstreamUploader(); + t.mock.method(globalThis, 'fetch', async url => { + const target = new URL(url); + if (target.hostname === 'doodapi.co') return new Response(JSON.stringify({ status: 403 })); + if (target.searchParams.get('op') === 'login_ajax') return new Response(JSON.stringify({ status: 'success' })); + if (target.pathname === '/settings') return new Response(''); + return new Response(''); + }); + await assert.rejects(up.login('user', 'secret'), /sess_id nicht gefunden/); + assert.equal(up.apiKey, ''); +}); + +test('web server unavailability is a hoster failure rather than a missing login session', async () => { + const up = new DoodstreamUploader(); + up._fetch = async () => new Response(JSON.stringify({ success: false, message: 'No servers available for uploads' })); + await assert.rejects(up._getUploadServer(), error => error.hosterTransient === true && /No servers available/.test(error.message)); +}); + +test('parallel web uploads receive independent cookie and upload-session state', () => { + const up = new DoodstreamUploader(); + up.cookies.set('xfsts', 'AUTH_SESSION'); + up.sessId = 'UPLOAD_SESSION'; + const first = up.cloneSession(); + const second = up.cloneSession(); + first.cookies.set('xfsts', 'CHANGED'); + first.sessId = 'CHANGED'; + assert.equal(second.cookies.get('xfsts'), 'AUTH_SESSION'); + assert.equal(second.sessId, 'UPLOAD_SESSION'); + assert.equal(up.cookies.get('xfsts'), 'AUTH_SESSION'); +}); + test('getUploadServer: parses the current upload_get_srv response', async () => { const up = new DoodstreamUploader(); up._fetch = async (url) => { diff --git a/tests/upload-manager.test.js b/tests/upload-manager.test.js index ebf62d3..741ad04 100644 --- a/tests/upload-manager.test.js +++ b/tests/upload-manager.test.js @@ -70,6 +70,28 @@ describe('UploadManager', () => { assert.ok(events.length > 0, 'should emit at least one progress event'); }); + it('web-login uploads use the confirmed web session without API discovery or a second login', async () => { + let uploads = 0; + const mgr = new UploadManager({}, {}, {}, { + acquireDoodstreamSession: async task => { + assert.equal(task.username, 'user'); + return { + async login() { throw new Error('must not request another OTP'); }, + async upload(file) { + assert.equal(file, '/test/video.mp4'); + uploads++; + return { file_code: 'WEBFILE123', download_url: 'https://doodstream.com/d/WEBFILE123' }; + } + }; + } + }); + mgr._resolveDoodstreamApiKey = async () => { throw new Error('must not change web login to API'); }; + const result = await mgr._executeUpload({ hoster: 'doodstream.com', file: '/test/video.mp4', username: 'user', password: 'secret' }); + assert.equal(result.file_code, 'WEBFILE123'); + assert.equal(uploads, 1); + assert.equal(mockUploadFile.mock.callCount(), 0); + }); + it('emits job-settled after releasing job resources', async () => { const mgr = new UploadManager({}); let settled;