Real root cause from the 3.3.24 diagnostics: the failing upload used CDN "tr1128ve.cloudatacdn.com/upload/01" — character-for-character the hardcoded last-resort fallback in _getUploadServer(). The CDN form came back with only op=upload_result and NO fn/NO st, i.e. the bytes went into a stale node that returns an empty form. So _getUploadServer can no longer extract the current upload server (Doodstream likely changed the upload_server response/format) and silently fell back to a dead node — wasting ~90s/95MB per attempt. - Remove the silent hardcoded-node fallback; throw a clear error when discovery fails so the upload fails instantly instead of 90s later with a cryptic msg. - Embed the raw upload_server response (status, content-type, body) and upload-page URL hints in the error AND debug log, to pin the format change. - Tests: getUploadServer JSON path, srv_url HTML fallback, and the no-silent- fallback throw (asserts the hardcoded node never leaks into the error). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
102 lines
4.3 KiB
JavaScript
102 lines
4.3 KiB
JavaScript
const { test } = require('node:test');
|
|
const assert = require('node:assert');
|
|
const DoodstreamUploader = require('../lib/doodstream-upload');
|
|
|
|
// The CDN hands back an XFileSharing form. `fn` is the filecode, `st` is the
|
|
// status ("OK" on success, an error string when the backend refuses the file).
|
|
// These tests pin the parse/error behaviour of _parseUploadResponse without
|
|
// touching the network — _fetch is stubbed to return the upload_result page.
|
|
function cdnForm({ fn = '', st = 'OK' } = {}) {
|
|
return `<HTML><BODY><Form name='F1' action='https://cdn.example/' method='POST'>` +
|
|
`<textarea name="op">upload_result</textarea>` +
|
|
`<textarea name="fn">${fn}</textarea>` +
|
|
`<textarea name="st">${st}</textarea>` +
|
|
`</Form></BODY></HTML>`;
|
|
}
|
|
|
|
const EMPTY_RESULT = '<textarea id="copy_dl" readonly class="form-control" rows="5"></textarea>';
|
|
const LINK_RESULT = (code) => `<textarea id="copy_dl" readonly class="form-control" rows="5">https://myvidplay.com/d/${code}</textarea>`;
|
|
|
|
function uploaderWithResult(resultHtml) {
|
|
const up = new DoodstreamUploader();
|
|
up._lastUploadUrl = 'https://cdn.example/upload/01';
|
|
// Stub the second-step submit so no real request goes out.
|
|
up._fetch = async () => ({ text: async () => resultHtml });
|
|
return up;
|
|
}
|
|
|
|
test('rejected file: empty fn + non-OK st surfaces the real status', async () => {
|
|
const up = uploaderWithResult(EMPTY_RESULT);
|
|
await assert.rejects(
|
|
() => up._parseUploadResponse(cdnForm({ fn: '', st: 'Error: file already exists' })),
|
|
(err) => {
|
|
assert.match(err.message, /lehnt Datei ab/);
|
|
assert.match(err.message, /file already exists/);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test('empty fn + st OK: generic error still reports st, fn-state and CDN node', async () => {
|
|
const up = uploaderWithResult(EMPTY_RESULT);
|
|
await assert.rejects(
|
|
() => up._parseUploadResponse(cdnForm({ fn: '', st: 'OK' })),
|
|
(err) => {
|
|
assert.match(err.message, /kein Filecode/);
|
|
assert.match(err.message, /st=OK/);
|
|
assert.match(err.message, /fehlt\/leer/);
|
|
assert.match(err.message, /cdn\.example/);
|
|
return true;
|
|
}
|
|
);
|
|
});
|
|
|
|
test('valid fn but empty result page: still resolves via fn (no regression)', async () => {
|
|
const up = uploaderWithResult(EMPTY_RESULT);
|
|
const res = await up._parseUploadResponse(cdnForm({ fn: '7mnp8xna3123', st: 'OK' }));
|
|
assert.equal(res.file_code, '7mnp8xna3123');
|
|
assert.equal(res.download_url, 'https://doodstream.com/d/7mnp8xna3123');
|
|
});
|
|
|
|
test('happy path: link in result page wins', async () => {
|
|
const up = uploaderWithResult(LINK_RESULT('jjsuhr931ds9'));
|
|
const res = await up._parseUploadResponse(cdnForm({ fn: 'jjsuhr931ds9', st: 'OK' }));
|
|
assert.equal(res.file_code, 'jjsuhr931ds9');
|
|
});
|
|
|
|
// --- _getUploadServer: discovery must never fall back to a hardcoded node ---
|
|
function fakeRes(body, { status = 200, ctype = 'text/html' } = {}) {
|
|
return { status, headers: { get: (h) => (h.toLowerCase() === 'content-type' ? ctype : null) }, text: async () => body };
|
|
}
|
|
|
|
test('getUploadServer: returns JSON result when present', async () => {
|
|
const up = new DoodstreamUploader();
|
|
up._fetch = async (url) => {
|
|
assert.match(url, /op=upload_server/);
|
|
return fakeRes(JSON.stringify({ result: 'https://node42.cloudatacdn.com/upload/01' }), { ctype: 'application/json' });
|
|
};
|
|
assert.equal(await up._getUploadServer(), 'https://node42.cloudatacdn.com/upload/01');
|
|
});
|
|
|
|
test('getUploadServer: falls back to srv_url in upload-page HTML', async () => {
|
|
const up = new DoodstreamUploader();
|
|
up._fetch = async (url) => {
|
|
if (/op=upload_server/.test(url)) return fakeRes('<html>not json</html>');
|
|
return fakeRes('<script>var srv_url: "https://node7.cloudatacdn.com/upload/01";</script>');
|
|
};
|
|
assert.equal(await up._getUploadServer(), 'https://node7.cloudatacdn.com/upload/01');
|
|
});
|
|
|
|
test('getUploadServer: throws (no silent dead fallback) when discovery fails', async () => {
|
|
const up = new DoodstreamUploader();
|
|
up._fetch = async () => fakeRes('<html><body>login required</body></html>', { status: 200 });
|
|
await assert.rejects(
|
|
() => up._getUploadServer(),
|
|
(err) => {
|
|
assert.match(err.message, /konnte Upload-Server nicht ermitteln/);
|
|
assert.doesNotMatch(err.message, /tr1128ve\.cloudatacdn\.com/); // never the hardcoded node
|
|
return true;
|
|
}
|
|
);
|
|
});
|