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>
106 lines
4.7 KiB
JavaScript
106 lines
4.7 KiB
JavaScript
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;
|
||
}
|
||
);
|
||
});
|