fix: harden hoster confirmation and recovery
Require every successful upload to expose a validated HTTPS result and rebuild all Doodstream and DSVPlay output from the confirmed file code. Keep failed baselines distinct from empty accounts, reject stale, foreign, and ambiguous recovery candidates across Doodstream, Byse, VOE, and Vidmoly, and preserve exact filename recovery with normalized extensions. Emit bounded structured transport diagnostics without raw response bodies or tokenized URLs, and remove sensitive values from Doodstream debug traces. Tests: node --test tests/upload-confirmation.test.js tests/hosters.test.js tests/doodstream-api-upload.test.js tests/doodstream-upload.test.js tests/byse-reject-recovery.test.js tests/hoster-recovery-provenance.test.js tests/suspect-reject-alternates.test.js Lint: eslint lib/hoster-transport-error.js lib/hosters.js lib/doodstream-upload.js lib/voe-upload.js lib/vidmoly-upload.js lib/upload-confirmation.js
This commit is contained in:
@@ -171,6 +171,94 @@ test('byse empty filecode WITHOUT explicit rejection still polls recovery', asyn
|
||||
assert.ok(listCalls >= 2, 'recovery polling must run when there is no explicit rejection');
|
||||
});
|
||||
|
||||
test('byse never recovers an old file after a failed baseline', async () => {
|
||||
stubByseUploadServer();
|
||||
const abort = new AbortController();
|
||||
const fileName = path.basename(tmpFile);
|
||||
let listCalls = 0;
|
||||
requestRouter = async (url, opts) => {
|
||||
if (/\/file\/list/.test(String(url))) {
|
||||
listCalls++;
|
||||
if (listCalls === 1) {
|
||||
return {
|
||||
statusCode: 503,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
body: { text: async () => '<html>baseline-token=SYNTHETIC_BYSE_BASELINE</html>' }
|
||||
};
|
||||
}
|
||||
abort.abort();
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { text: async () => JSON.stringify({ status: 200, result: { files: [{ file_code: 'OLD_BYSE_123', title: fileName }] } }) }
|
||||
};
|
||||
}
|
||||
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: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { text: async () => JSON.stringify({ status: 200, msg: 'OK' }) }
|
||||
};
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => uploadFile('byse.sx', tmpFile, 'VALIDKEY', null, abort.signal, null),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_BYSE_BASELINE/);
|
||||
assert.equal(err.diagnostic.phase, 'recovery-baseline');
|
||||
assert.equal(err.diagnostic.http, 503);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.equal(listCalls, 1);
|
||||
});
|
||||
|
||||
test('byse recovery rejects ambiguous same-title candidates', async () => {
|
||||
stubByseUploadServer();
|
||||
const abort = new AbortController();
|
||||
const fileName = path.basename(tmpFile);
|
||||
let listCalls = 0;
|
||||
requestRouter = async (url, opts) => {
|
||||
if (/\/file\/list/.test(String(url))) {
|
||||
listCalls++;
|
||||
if (listCalls === 1) {
|
||||
return { statusCode: 200, headers: {}, body: { text: async () => '{"status":200,"result":{"files":[]}}' } };
|
||||
}
|
||||
abort.abort();
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: {},
|
||||
body: {
|
||||
text: async () => JSON.stringify({
|
||||
status: 200,
|
||||
result: {
|
||||
files: [
|
||||
{ file_code: 'PARALLEL_A', title: fileName },
|
||||
{ file_code: 'PARALLEL_B', title: fileName }
|
||||
]
|
||||
}
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
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: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { text: async () => JSON.stringify({ status: 200, msg: 'OK' }) }
|
||||
};
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => uploadFile('byse.sx', tmpFile, 'VALIDKEY', null, abort.signal, null),
|
||||
(err) => err.hosterTransient === true
|
||||
);
|
||||
});
|
||||
|
||||
function stubBysePost(response) {
|
||||
requestRouter = async (url, opts) => {
|
||||
const u = String(url);
|
||||
|
||||
@@ -59,7 +59,7 @@ function routeWith(uploadBody, listBodies = []) {
|
||||
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 } };
|
||||
return { statusCode: uploadBody.status, headers: { 'content-type': uploadBody.contentType || 'application/json' }, body: { text: async () => uploadBody.body } };
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,3 +103,95 @@ test('doodstream API upload: codeless + file never appears → throws hosterTran
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('doodstream API upload never recovers an old file after a failed baseline', async () => {
|
||||
stubUploadServer();
|
||||
const abort = new AbortController();
|
||||
const fileName = path.basename(tmpFile);
|
||||
let listCalls = 0;
|
||||
requestRouter = async (url, opts) => {
|
||||
if (/\/api\/file\/list/.test(String(url))) {
|
||||
listCalls++;
|
||||
if (listCalls === 1) {
|
||||
return {
|
||||
statusCode: 503,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
body: { text: async () => '<html>baseline-token=SYNTHETIC_BASELINE_SECRET</html>' }
|
||||
};
|
||||
}
|
||||
abort.abort();
|
||||
return {
|
||||
statusCode: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { text: async () => JSON.stringify({ status: 200, result: { files: [{ file_code: 'OLD_DOOD_123', title: fileName }] } }) }
|
||||
};
|
||||
}
|
||||
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: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: { text: async () => JSON.stringify({ status: 200, msg: 'OK' }) }
|
||||
};
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, abort.signal, null),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_BASELINE_SECRET/);
|
||||
assert.equal(err.diagnostic.phase, 'recovery-baseline');
|
||||
assert.equal(err.diagnostic.http, 503);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
assert.equal(listCalls, 1);
|
||||
});
|
||||
|
||||
test('doodstream API recovery rejects ambiguous same-title candidates', async () => {
|
||||
stubUploadServer();
|
||||
const fileName = path.basename(tmpFile);
|
||||
requestRouter = routeWith(
|
||||
{ status: 200, body: JSON.stringify({ status: 200, msg: 'OK' }) },
|
||||
[
|
||||
'{"status":200,"result":{"files":[]}}',
|
||||
JSON.stringify({
|
||||
status: 200,
|
||||
result: {
|
||||
files: [
|
||||
{ file_code: 'PARALLEL_A', title: fileName },
|
||||
{ file_code: 'PARALLEL_B', title: fileName }
|
||||
]
|
||||
}
|
||||
})
|
||||
]
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null),
|
||||
(err) => err.hosterTransient === true
|
||||
);
|
||||
});
|
||||
|
||||
test('doodstream API upload errors expose safe structured diagnostics', async () => {
|
||||
stubUploadServer();
|
||||
requestRouter = routeWith({
|
||||
status: 502,
|
||||
contentType: 'text/html; charset=utf-8',
|
||||
body: '<html>upstream-token=SYNTHETIC_UPLOAD_SECRET https://node.invalid/upload?session=SYNTHETIC_SESSION</html>'
|
||||
});
|
||||
|
||||
await assert.rejects(
|
||||
() => uploadFile('doodstream.com', tmpFile, 'VALIDKEY', null, null, null),
|
||||
(err) => {
|
||||
assert.equal(err.transientNetwork, true);
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_UPLOAD_SECRET|SYNTHETIC_SESSION|<html>/);
|
||||
assert.equal(err.diagnostic.phase, 'upload-response');
|
||||
assert.equal(err.diagnostic.http, 502);
|
||||
assert.equal(err.diagnostic.contentType, 'text/html; charset=utf-8');
|
||||
assert.equal(err.diagnostic.responseKind, 'html');
|
||||
assert.doesNotMatch(err.diagnostic.payloadSnippet, /SYNTHETIC_UPLOAD_SECRET|SYNTHETIC_SESSION/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -64,6 +64,39 @@ test('happy path: link in result page wins', async () => {
|
||||
assert.equal(res.file_code, 'jjsuhr931ds9');
|
||||
});
|
||||
|
||||
test('JSON results rebuild canonical Doodstream URLs from the file code', () => {
|
||||
const up = new DoodstreamUploader();
|
||||
assert.deepEqual(
|
||||
up._extractFromJson({
|
||||
status: 200,
|
||||
result: {
|
||||
filecode: 'CANONICAL123',
|
||||
download_url: 'http://edge.dsvplay.com/result/CANONICAL123?token=SYNTHETIC_SECRET',
|
||||
protected_embed: 'https://dood.to/arbitrary/CANONICAL123'
|
||||
}
|
||||
}),
|
||||
{
|
||||
file_code: 'CANONICAL123',
|
||||
download_url: 'https://doodstream.com/d/CANONICAL123',
|
||||
embed_url: 'https://doodstream.com/e/CANONICAL123'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('invalid web upload results expose safe structured diagnostics', async () => {
|
||||
const up = new DoodstreamUploader();
|
||||
await assert.rejects(
|
||||
() => up._parseUploadResponse('<html><input name="api_key" value="SYNTHETIC_WEB_SECRET"> https://doodstream.com/?session=SYNTHETIC_WEB_SESSION</html>'),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_WEB_SECRET|SYNTHETIC_WEB_SESSION|<html>/);
|
||||
assert.equal(err.diagnostic.phase, 'upload-result');
|
||||
assert.equal(err.diagnostic.responseKind, 'html');
|
||||
assert.doesNotMatch(err.diagnostic.payloadSnippet, /SYNTHETIC_WEB_SECRET|SYNTHETIC_WEB_SESSION/);
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
// --- _parseUploadFormFields: replicate the current upload form faithfully ---
|
||||
test('_parseUploadFormFields extracts the real form fields and excludes the file input', () => {
|
||||
const up = new DoodstreamUploader();
|
||||
@@ -212,3 +245,26 @@ test('getUploadServer: throws (no silent dead fallback) when discovery fails', a
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('getUploadServer: failures expose safe structured diagnostics without response secrets', async () => {
|
||||
const up = new DoodstreamUploader();
|
||||
up._fetch = async (url) => {
|
||||
if (/op=upload_server/.test(url)) {
|
||||
return fakeRes('<html>upstream-token=SYNTHETIC_DISCOVERY_SECRET</html>', { status: 503, ctype: 'text/html; charset=utf-8' });
|
||||
}
|
||||
return fakeRes('<input name="sess_id" value="SYNTHETIC_DISCOVERY_SESSION"><a href="https://node.invalid/upload?token=SYNTHETIC_QUERY">x</a>');
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
() => up._getUploadServer(),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_DISCOVERY_SECRET|SYNTHETIC_DISCOVERY_SESSION|SYNTHETIC_QUERY|<html>/);
|
||||
assert.equal(err.diagnostic.phase, 'upload-server');
|
||||
assert.equal(err.diagnostic.http, 503);
|
||||
assert.equal(err.diagnostic.contentType, 'text/html; charset=utf-8');
|
||||
assert.equal(err.diagnostic.safeEndpointHost, 'doodstream.com');
|
||||
assert.equal(err.diagnostic.responseKind, 'html');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const VoeUploader = require('../lib/voe-upload');
|
||||
const VidmolyUploader = require('../lib/vidmoly-upload');
|
||||
|
||||
function response(body, status = 200, contentType = 'application/json') {
|
||||
return {
|
||||
status,
|
||||
headers: { get: (name) => name.toLowerCase() === 'content-type' ? contentType : null },
|
||||
text: async () => body
|
||||
};
|
||||
}
|
||||
|
||||
test('VOE recovery rejects an unrelated singleton candidate', async () => {
|
||||
const uploader = new VoeUploader();
|
||||
uploader._fetchFileList = async () => [{ file_code: 'OTHER999', title: 'foreign-upload' }];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.equal(await uploader._resolveUploadedFile('wanted-video.mkv', new Set(), null), null);
|
||||
});
|
||||
|
||||
test('VOE recovery rejects ambiguous exact-title candidates', async () => {
|
||||
const uploader = new VoeUploader();
|
||||
uploader._fetchFileList = async () => [
|
||||
{ file_code: 'VOE_FIRST', title: 'wanted-video' },
|
||||
{ file_code: 'VOE_SECOND', title: 'wanted-video' }
|
||||
];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.equal(await uploader._resolveUploadedFile('wanted-video.mkv', new Set(), null), null);
|
||||
});
|
||||
|
||||
test('VOE recovery accepts one new exact-title candidate with a file extension', async () => {
|
||||
const uploader = new VoeUploader();
|
||||
uploader._fetchFileList = async () => [{ file_code: 'VOE_EXACT', title: 'wanted-video.mkv' }];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.deepEqual(await uploader._resolveUploadedFile('wanted-video.mkv', new Set(), null), {
|
||||
file_code: 'VOE_EXACT',
|
||||
download_url: 'https://voe.sx/VOE_EXACT',
|
||||
embed_url: 'https://voe.sx/e/VOE_EXACT'
|
||||
});
|
||||
});
|
||||
|
||||
test('VOE preserves a failed recovery baseline as a safe structured error', async () => {
|
||||
const uploader = new VoeUploader();
|
||||
uploader._fetch = async () => response(
|
||||
'<html>api_key=SYNTHETIC_VOE_SECRET https://voe.sx/list?session=SYNTHETIC_VOE_SESSION</html>',
|
||||
503,
|
||||
'text/html'
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => uploader._captureFileCodes(),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_VOE_SECRET|SYNTHETIC_VOE_SESSION|<html>/);
|
||||
assert.equal(err.diagnostic.phase, 'recovery-baseline');
|
||||
assert.equal(err.diagnostic.http, 503);
|
||||
assert.equal(err.diagnostic.responseKind, 'html');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('Vidmoly recovery rejects a matching code already present in the baseline', async () => {
|
||||
const uploader = new VidmolyUploader();
|
||||
uploader._fetchVmList = async () => [{ file_code: ' OLDVID123456 ', full_title: 'wanted-video' }];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.equal(
|
||||
await uploader._resolveUploadedFileFromVmApi('wanted-video.mkv', new Set(['OLDVID123456']), null),
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
test('Vidmoly recovery rejects ambiguous exact-title candidates', async () => {
|
||||
const uploader = new VidmolyUploader();
|
||||
uploader._fetchVmList = async () => [
|
||||
{ file_code: 'NEWVID123456', full_title: 'wanted-video' },
|
||||
{ file_code: 'NEWVID654321', full_title: 'wanted-video' }
|
||||
];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.equal(await uploader._resolveUploadedFileFromVmApi('wanted-video.mkv', new Set(), null), null);
|
||||
});
|
||||
|
||||
test('Vidmoly recovery accepts one new exact-title candidate with a file extension', async () => {
|
||||
const uploader = new VidmolyUploader();
|
||||
uploader._fetchVmList = async () => [{ file_code: 'NEWVID123456', full_title: 'wanted-video.mkv' }];
|
||||
uploader._sleep = async () => {};
|
||||
|
||||
assert.deepEqual(await uploader._resolveUploadedFileFromVmApi('wanted-video.mkv', new Set(), null), {
|
||||
file_code: 'NEWVID123456',
|
||||
download_url: 'https://vidmoly.me/w/NEWVID123456',
|
||||
embed_url: 'https://vidmoly.me/embed-NEWVID123456.html'
|
||||
});
|
||||
});
|
||||
|
||||
test('Vidmoly preserves a failed recovery baseline as a safe structured error', async () => {
|
||||
const uploader = new VidmolyUploader();
|
||||
uploader._fetch = async () => response(
|
||||
'<html>sess_id=SYNTHETIC_VIDMOLY_SECRET https://vidmoly.me/?token=SYNTHETIC_VIDMOLY_SESSION</html>',
|
||||
503,
|
||||
'text/html'
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => uploader._captureVmFileCodes(),
|
||||
(err) => {
|
||||
assert.doesNotMatch(err.message, /SYNTHETIC_VIDMOLY_SECRET|SYNTHETIC_VIDMOLY_SESSION|<html>/);
|
||||
assert.equal(err.diagnostic.phase, 'recovery-baseline');
|
||||
assert.equal(err.diagnostic.http, 503);
|
||||
assert.equal(err.diagnostic.responseKind, 'html');
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
@@ -46,12 +46,13 @@ describe('hosters helpers', () => {
|
||||
it('parseDoodstreamResult handles result-as-array and result-as-object', () => {
|
||||
const arr = __test.parseDoodstreamResult({ result: [{ filecode: 'AB1', protected_dl: 'https://x/1', protected_embed: 'https://x/e/1' }] });
|
||||
assert.equal(arr.file_code, 'AB1');
|
||||
assert.equal(arr.download_url, 'https://x/1');
|
||||
assert.equal(arr.embed_url, 'https://x/e/1');
|
||||
assert.equal(arr.download_url, 'https://doodstream.com/d/AB1');
|
||||
assert.equal(arr.embed_url, 'https://doodstream.com/e/AB1');
|
||||
|
||||
const obj = __test.parseDoodstreamResult({ result: { filecode: 'OBJ1', download_url: 'https://x/2' } });
|
||||
assert.equal(obj.file_code, 'OBJ1');
|
||||
assert.equal(obj.download_url, 'https://x/2');
|
||||
assert.equal(obj.download_url, 'https://doodstream.com/d/OBJ1');
|
||||
assert.equal(obj.embed_url, 'https://doodstream.com/e/OBJ1');
|
||||
});
|
||||
|
||||
it('parseByseResult tolerates null/non-object payload without throwing', () => {
|
||||
|
||||
@@ -3,9 +3,15 @@ const assert = require('node:assert/strict');
|
||||
|
||||
const { assertUploadConfirmation } = require('../lib/upload-confirmation');
|
||||
|
||||
test('accepts a host-confirmed file code without a public URL', () => {
|
||||
const result = { file_code: 'AB1', download_url: null, embed_url: null };
|
||||
assert.equal(assertUploadConfirmation(result, 'doodstream.com'), result);
|
||||
test('materializes canonical Doodstream URLs from a confirmed file code', () => {
|
||||
assert.deepEqual(
|
||||
assertUploadConfirmation({ file_code: 'AB1', download_url: null, embed_url: null }, 'doodstream.com'),
|
||||
{
|
||||
file_code: 'AB1',
|
||||
download_url: 'https://doodstream.com/d/AB1',
|
||||
embed_url: 'https://doodstream.com/e/AB1'
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test('accepts upload URLs for every supported hoster and its subdomains', () => {
|
||||
@@ -18,7 +24,15 @@ test('accepts upload URLs for every supported hoster and its subdomains', () =>
|
||||
];
|
||||
for (const [hoster, downloadUrl] of cases) {
|
||||
const result = { file_code: 'abc123', download_url: downloadUrl };
|
||||
assert.equal(assertUploadConfirmation(result, hoster), result);
|
||||
const confirmed = assertUploadConfirmation(result, hoster);
|
||||
if (hoster === 'doodstream.com') {
|
||||
assert.deepEqual(confirmed, {
|
||||
...result,
|
||||
embed_url: 'https://doodstream.com/e/abc123'
|
||||
});
|
||||
} else {
|
||||
assert.equal(confirmed, result);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,6 +62,40 @@ test('accepts the Doodstream result domain returned by the current upload servic
|
||||
});
|
||||
});
|
||||
|
||||
test('rebuilds every accepted Doodstream transport URL from the file code', () => {
|
||||
const variants = [
|
||||
'http://dsvplay.com/d/DOODCODE1234?token=SYNTHETIC_SECRET#fragment',
|
||||
'https://edge.dsvplay.com/result/DOODCODE1234?session=SYNTHETIC_SESSION',
|
||||
'https://dood.to/e/DOODCODE1234',
|
||||
'https://dood.la/arbitrary/DOODCODE1234'
|
||||
];
|
||||
|
||||
for (const downloadUrl of variants) {
|
||||
assert.deepEqual(
|
||||
assertUploadConfirmation({ file_code: 'DOODCODE1234', download_url: downloadUrl }, 'doodstream.com'),
|
||||
{
|
||||
file_code: 'DOODCODE1234',
|
||||
download_url: 'https://doodstream.com/d/DOODCODE1234',
|
||||
embed_url: 'https://doodstream.com/e/DOODCODE1234'
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test('rejects code-only confirmations for hosters without canonical materialization', () => {
|
||||
assert.throws(
|
||||
() => assertUploadConfirmation({ file_code: 'BYSE123' }, 'byse.sx'),
|
||||
/Upload zu byse\.sx wurde nicht bestätigt/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects non-HTTPS public URLs outside Doodstream transport normalization', () => {
|
||||
assert.throws(
|
||||
() => assertUploadConfirmation({ file_code: 'VOE123', download_url: 'http://voe.sx/VOE123' }, 'voe.sx'),
|
||||
/Upload zu voe\.sx wurde nicht bestätigt/
|
||||
);
|
||||
});
|
||||
|
||||
test('rejects an upload URL from a different domain', () => {
|
||||
assert.throws(
|
||||
() => assertUploadConfirmation({ file_code: 'abc123', download_url: 'https://attacker.invalid/file/abc123' }, 'voe.sx'),
|
||||
|
||||
Reference in New Issue
Block a user