test(doodstream): end-to-end integration test for the API upload + recovery path
Closes the gap between the unit-tested parseDoodstreamResult and the real
uploadFile orchestration. Mocks the undici transport (reassign undici.request +
refresh the hosters cache; mock.module needs an experimental flag npm test
doesn't pass) and global fetch, then drives the full doodstream API path against
the doc-verified response shapes:
- filecode returned directly in result[0].filecode → used.
- codeless 2xx → recovered by polling file/list and name-matching the title.
- codeless + file never appears → throws with err.hosterTransient=true (so the
account is not blacklisted).
Verified live this session: doodapi.co returns {"status":400,"msg":"Invalid
key"} for a bad key, so validation/list logic keys off status correctly.
Also makes the recovery poll count/delay tunable via __test.DOODSTREAM_POLL
(same 12 × 2.5 s defaults — non-behavioral) so the exhaustion test runs in ms.
Full suite 181/181.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
d2f903b8ba
commit
d24fd54e83
@ -468,6 +468,8 @@ async function _fetchDoodstreamFileList(apiKey, signal) {
|
||||
}
|
||||
}
|
||||
|
||||
const DOODSTREAM_POLL = { attempts: 12, delayMs: 2500 }; // test-tunable via __test
|
||||
|
||||
async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, signal) {
|
||||
// Same recovery byse uses: the upload POST returned no filecode, but the file
|
||||
// may register in the account a little later. Poll the list for a NEW file
|
||||
@ -475,8 +477,8 @@ async function _resolveDoodstreamUploadByName(apiKey, fileName, baselineCodes, s
|
||||
// (never "take the only new one") so parallel doodstream uploads can't claim
|
||||
// each other's files.
|
||||
const expected = _normalizeFileTitle(fileName);
|
||||
const POLL_ATTEMPTS = 12;
|
||||
const POLL_DELAY_MS = 2500;
|
||||
const POLL_ATTEMPTS = DOODSTREAM_POLL.attempts;
|
||||
const POLL_DELAY_MS = DOODSTREAM_POLL.delayMs;
|
||||
for (let i = 0; i < POLL_ATTEMPTS; i++) {
|
||||
if (signal && signal.aborted) return null;
|
||||
const list = await _fetchDoodstreamFileList(apiKey, signal);
|
||||
@ -638,6 +640,7 @@ module.exports = {
|
||||
extractUploadServerUrl,
|
||||
parseVoeResult,
|
||||
parseDoodstreamResult,
|
||||
parseByseResult
|
||||
parseByseResult,
|
||||
DOODSTREAM_POLL
|
||||
}
|
||||
};
|
||||
|
||||
105
tests/doodstream-api-upload.test.js
Normal file
105
tests/doodstream-api-upload.test.js
Normal file
@ -0,0 +1,105 @@
|
||||
const { test, before, after } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
// Mock the undici transport BEFORE requiring hosters so the destructured
|
||||
// `request` picks up our stub. apiGet (getUploadServer) uses global fetch, which
|
||||
// we override per-test. This exercises the FULL doodstream API upload + recovery
|
||||
// orchestration against the doc-verified response shapes — the gap between the
|
||||
// already-tested parseDoodstreamResult helper and the real uploadFile path.
|
||||
// (mock.module needs an experimental flag npm test doesn't pass, so we reassign
|
||||
// undici.request on the module object and refresh the hosters cache instead.)
|
||||
let requestRouter = async () => ({ statusCode: 200, headers: {}, body: { text: async () => '{}' } });
|
||||
const undici = require('undici');
|
||||
const _origUndiciRequest = undici.request;
|
||||
undici.request = (...a) => requestRouter(...a);
|
||||
delete require.cache[require.resolve('../lib/hosters')];
|
||||
const hostersMod = require('../lib/hosters');
|
||||
const { uploadFile } = hostersMod;
|
||||
|
||||
let tmpFile;
|
||||
let origFetch;
|
||||
before(() => {
|
||||
tmpFile = path.join(os.tmpdir(), `dood-itest-${process.pid}.mkv`);
|
||||
fs.writeFileSync(tmpFile, Buffer.alloc(2048, 7));
|
||||
origFetch = global.fetch;
|
||||
// Keep the "never appears" recovery test fast (real default is 12 × 2.5 s).
|
||||
hostersMod.__test.DOODSTREAM_POLL.attempts = 3;
|
||||
hostersMod.__test.DOODSTREAM_POLL.delayMs = 5;
|
||||
});
|
||||
after(() => {
|
||||
global.fetch = origFetch;
|
||||
undici.request = _origUndiciRequest; // restore real transport for other test files
|
||||
delete require.cache[require.resolve('../lib/hosters')];
|
||||
try { fs.unlinkSync(tmpFile); } catch {}
|
||||
});
|
||||
|
||||
// getUploadServer hits /api/upload/server via global fetch.
|
||||
function stubUploadServer() {
|
||||
global.fetch = async (url) => {
|
||||
if (/upload\/server/.test(String(url))) {
|
||||
return { status: 200, text: async () => JSON.stringify({ status: 200, result: 'https://node1.cloudatacdn.com/upload/01' }) };
|
||||
}
|
||||
return { status: 200, text: async () => '{"status":200}' };
|
||||
};
|
||||
}
|
||||
|
||||
// Build an undici-style router. uploadBody is the POST result; listBodies is a
|
||||
// queue consumed by successive /api/file/list calls (baseline, then polls).
|
||||
function routeWith(uploadBody, listBodies = []) {
|
||||
return async (url, opts) => {
|
||||
const u = String(url);
|
||||
if (/\/api\/file\/list/.test(u)) {
|
||||
const body = listBodies.length ? listBodies.shift() : '{"status":200,"result":{"files":[]}}';
|
||||
return { statusCode: 200, headers: {}, body: { text: async () => body } };
|
||||
}
|
||||
// Upload POST: drain the streamed body so the file handle closes.
|
||||
if (opts && opts.body && typeof opts.body[Symbol.asyncIterator] === 'function') {
|
||||
for await (const chunk of opts.body) { if (chunk && chunk.length === -1) break; }
|
||||
}
|
||||
return { statusCode: uploadBody.status, headers: { 'content-type': 'application/json' }, body: { text: async () => uploadBody.body } };
|
||||
};
|
||||
}
|
||||
|
||||
test('doodstream API upload: filecode returned directly is used', async () => {
|
||||
stubUploadServer();
|
||||
requestRouter = routeWith({
|
||||
status: 200,
|
||||
body: JSON.stringify({ status: 200, result: [{ filecode: 'DOODCODE1234', download_url: 'https://doodstream.com/d/DOODCODE1234', protected_embed: 'https://doodstream.com/e/DOODCODE1234' }] })
|
||||
});
|
||||
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
|
||||
assert.equal(res.file_code, 'DOODCODE1234');
|
||||
assert.equal(res.download_url, 'https://doodstream.com/d/DOODCODE1234');
|
||||
});
|
||||
|
||||
test('doodstream API upload: codeless result recovered via file-list name match', async () => {
|
||||
stubUploadServer();
|
||||
const fileName = path.basename(tmpFile).replace(/\.[^.]+$/, ''); // title doodstream stores
|
||||
requestRouter = routeWith(
|
||||
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) }, // codeless upload
|
||||
[
|
||||
'{"status":200,"result":{"files":[]}}', // baseline (pre-upload)
|
||||
`{"status":200,"result":{"files":[{"file_code":"RECOVER9999","title":"${fileName}"}]}}` // poll finds it
|
||||
]
|
||||
);
|
||||
const res = await uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null);
|
||||
assert.equal(res.file_code, 'RECOVER9999');
|
||||
assert.equal(res.download_url, 'https://doodstream.com/d/RECOVER9999');
|
||||
});
|
||||
|
||||
test('doodstream API upload: codeless + file never appears → throws hosterTransient (no account poison)', async () => {
|
||||
stubUploadServer();
|
||||
requestRouter = routeWith(
|
||||
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) },
|
||||
[] // every file/list returns empty
|
||||
);
|
||||
await assert.rejects(
|
||||
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null),
|
||||
(err) => {
|
||||
assert.equal(err.hosterTransient, true, 'codeless result must be tagged hosterTransient');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user