Coalesce concurrent and repeated Doodstream login checks by credential identity, keep the challenged cookie session for OTP verification, and rate-limit explicit code resends. Prefer an existing Doodstream API key during health checks and add regression coverage for imports with duplicate accounts, concurrent checks, expired challenges, and session reuse.
This commit is contained in:
+118
-1
@@ -1,6 +1,6 @@
|
||||
const { test } = require('node:test');
|
||||
const assert = require('node:assert');
|
||||
const { selectUploadAuth } = require('../lib/account-auth');
|
||||
const { createDoodstreamOtpCoordinator, selectUploadAuth } = require('../lib/account-auth');
|
||||
|
||||
test('doodstream prefers the API key even when username/password are also set', () => {
|
||||
const auth = selectUploadAuth('doodstream.com', {
|
||||
@@ -42,3 +42,120 @@ test('null / non-object account does not throw', () => {
|
||||
assert.deepEqual(selectUploadAuth('doodstream.com', null), {});
|
||||
assert.deepEqual(selectUploadAuth('doodstream.com', undefined), {});
|
||||
});
|
||||
|
||||
function otpRequired(message = 'OTP erforderlich') {
|
||||
const error = new Error(message);
|
||||
error.otpRequired = true;
|
||||
return error;
|
||||
}
|
||||
|
||||
test('concurrent and repeated Doodstream checks request only one OTP', async () => {
|
||||
let loginCalls = 0;
|
||||
let releaseLogin;
|
||||
const coordinator = createDoodstreamOtpCoordinator({
|
||||
createUploader: () => ({
|
||||
login: () => new Promise((resolve, reject) => {
|
||||
loginCalls++;
|
||||
releaseLogin = () => reject(otpRequired('OTP gesendet'));
|
||||
})
|
||||
})
|
||||
});
|
||||
const first = coordinator.check({ username: 'user', password: 'secret' });
|
||||
const second = coordinator.check({ username: 'user', password: 'secret' });
|
||||
assert.equal(loginCalls, 1);
|
||||
releaseLogin();
|
||||
assert.deepEqual(await first, { status: 'otp_required', message: 'OTP gesendet' });
|
||||
assert.deepEqual(await second, { status: 'otp_required', message: 'OTP gesendet' });
|
||||
assert.deepEqual(await coordinator.check({ username: 'user', password: 'secret' }), { status: 'otp_required', message: 'OTP gesendet' });
|
||||
assert.equal(loginCalls, 1);
|
||||
});
|
||||
|
||||
test('Doodstream OTP verification reuses the challenged uploader session', async () => {
|
||||
let created = 0;
|
||||
const calls = [];
|
||||
const coordinator = createDoodstreamOtpCoordinator({
|
||||
createUploader: () => {
|
||||
created++;
|
||||
return {
|
||||
async login(_username, _password, otp) {
|
||||
calls.push(otp || '');
|
||||
if (!otp) throw otpRequired('OTP gesendet');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
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'
|
||||
});
|
||||
assert.equal(created, 1);
|
||||
assert.deepEqual(calls, ['', '123456']);
|
||||
});
|
||||
|
||||
test('a rejected OTP remains pending without requesting another code', async () => {
|
||||
let created = 0;
|
||||
let calls = 0;
|
||||
const coordinator = createDoodstreamOtpCoordinator({
|
||||
createUploader: () => {
|
||||
created++;
|
||||
return {
|
||||
async login(_username, _password, otp) {
|
||||
calls++;
|
||||
if (!otp) throw otpRequired('OTP gesendet');
|
||||
throw new Error('Code ungültig');
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
await coordinator.check({ username: 'user', password: 'secret' });
|
||||
assert.deepEqual(await coordinator.check({ username: 'user', password: 'secret', otp: '000000' }), {
|
||||
status: 'otp_required',
|
||||
message: 'Code ungültig'
|
||||
});
|
||||
assert.deepEqual(await coordinator.check({ username: 'user', password: 'secret' }), {
|
||||
status: 'otp_required',
|
||||
message: 'Code ungültig'
|
||||
});
|
||||
assert.equal(created, 1);
|
||||
assert.equal(calls, 2);
|
||||
});
|
||||
|
||||
test('explicit OTP resend is rate-limited and starts one fresh session', async () => {
|
||||
let currentTime = 1000;
|
||||
let created = 0;
|
||||
const coordinator = createDoodstreamOtpCoordinator({
|
||||
now: () => currentTime,
|
||||
resendCooldownMs: 60000,
|
||||
createUploader: () => {
|
||||
created++;
|
||||
return { login: async () => { throw otpRequired('OTP gesendet'); } };
|
||||
}
|
||||
});
|
||||
await coordinator.check({ username: 'user', password: 'secret' });
|
||||
await coordinator.check({ username: 'user', password: 'secret', requestNewChallenge: true });
|
||||
assert.equal(created, 1);
|
||||
currentTime += 60000;
|
||||
await coordinator.check({ username: 'user', password: 'secret', requestNewChallenge: true });
|
||||
assert.equal(created, 2);
|
||||
});
|
||||
|
||||
test('expired OTP submission cannot create a replacement challenge implicitly', async () => {
|
||||
let currentTime = 1000;
|
||||
let created = 0;
|
||||
const coordinator = createDoodstreamOtpCoordinator({
|
||||
now: () => currentTime,
|
||||
challengeTtlMs: 1000,
|
||||
createUploader: () => {
|
||||
created++;
|
||||
return { login: async () => { throw otpRequired('OTP gesendet'); } };
|
||||
}
|
||||
});
|
||||
await coordinator.check({ username: 'user', password: 'secret' });
|
||||
currentTime += 1001;
|
||||
assert.deepEqual(await coordinator.check({ username: 'user', password: 'secret', otp: '123456' }), {
|
||||
status: 'otp_required',
|
||||
message: 'OTP-Anfrage ist abgelaufen. Bitte einen neuen Code anfordern.'
|
||||
});
|
||||
assert.equal(created, 1);
|
||||
});
|
||||
|
||||
@@ -161,6 +161,43 @@ function fakeRes(body, { status = 200, ctype = 'text/html' } = {}) {
|
||||
return { status, headers: { get: (h) => (h.toLowerCase() === 'content-type' ? ctype : null) }, text: async () => body };
|
||||
}
|
||||
|
||||
test('OTP verification keeps the challenged cookie session without another bootstrap request', async () => {
|
||||
const up = new DoodstreamUploader();
|
||||
const originalFetch = globalThis.fetch;
|
||||
let bootstrapCalls = 0;
|
||||
let loginCalls = 0;
|
||||
up._fetch = async () => {
|
||||
bootstrapCalls++;
|
||||
return fakeRes(bootstrapCalls === 1 ? 'ok' : '<input type="hidden" name="sess_id" value="SESSION456">');
|
||||
};
|
||||
globalThis.fetch = async (_url, options) => {
|
||||
loginCalls++;
|
||||
if (loginCalls === 1) {
|
||||
assert.equal(options.headers.Cookie, undefined);
|
||||
return {
|
||||
status: 200,
|
||||
headers: { getSetCookie: () => ['otp_session=SESSION123; Path=/'], get: () => null },
|
||||
text: async () => JSON.stringify({ status: 'fail', message: 'OTP required' })
|
||||
};
|
||||
}
|
||||
assert.equal(options.headers.Cookie, 'otp_session=SESSION123');
|
||||
assert.match(options.body, /loginotp=123456/u);
|
||||
return {
|
||||
status: 302,
|
||||
headers: { getSetCookie: () => [], get: () => '/dashboard' },
|
||||
text: async () => ''
|
||||
};
|
||||
};
|
||||
try {
|
||||
await assert.rejects(() => up.login('user', 'secret'), error => error.otpRequired === true);
|
||||
await up.login('user', 'secret', '123456');
|
||||
assert.equal(bootstrapCalls, 2);
|
||||
assert.equal(loginCalls, 2);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test('getUploadServer: returns JSON result when present', async () => {
|
||||
const up = new DoodstreamUploader();
|
||||
up._fetch = async (url) => {
|
||||
|
||||
@@ -51,6 +51,8 @@ test('translates managed online backup controls in both directions', () => {
|
||||
test('translates the account check timestamp label', () => {
|
||||
assert.equal(translateText('geprüft', 'en'), 'checked');
|
||||
assert.equal(translateText('checked', 'de'), 'geprüft');
|
||||
assert.equal(translateText('Neuen Code anfordern', 'en'), 'Request new code');
|
||||
assert.equal(translateText('The OTP request has expired. Request a new code.', 'de'), 'OTP-Anfrage ist abgelaufen. Bitte einen neuen Code anfordern.');
|
||||
});
|
||||
|
||||
test('translates settings search result labels in both directions', () => {
|
||||
|
||||
@@ -45,7 +45,7 @@ test('public release verifier accepts only the exact source manifest and target
|
||||
assert.equal(baseline.status, 0, baseline.stderr);
|
||||
assert.equal(
|
||||
baseline.stdout,
|
||||
`public-release-source-ok files=159 denied-paths=0 internal-terms=0 version=${currentVersion} scripts=8 build-files=7 layout=exact screenshot=deferred\n`
|
||||
`public-release-source-ok files=160 denied-paths=0 internal-terms=0 version=${currentVersion} scripts=8 build-files=7 layout=exact screenshot=deferred\n`
|
||||
);
|
||||
|
||||
fs.writeFileSync(path.join(stage, 'tests', 'unexpected.json'), '{}');
|
||||
|
||||
Reference in New Issue
Block a user