mirror of
https://github.com/hansjone/dsh-im-ops.git
synced 2026-10-09 06:30:45 +08:00
120 lines
3.9 KiB
JavaScript
120 lines
3.9 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {
|
|
DingtalkDeviceAuth,
|
|
DingtalkDeviceAuthError,
|
|
} from '../../../src/channels/dingtalk/device-auth.mjs';
|
|
|
|
function fetchFixture(responses) {
|
|
const calls = [];
|
|
return {
|
|
calls,
|
|
fetch: async (url, init) => {
|
|
calls.push({ url, init });
|
|
return {
|
|
ok: true,
|
|
json: async () => responses.shift(),
|
|
};
|
|
},
|
|
};
|
|
}
|
|
|
|
test('device auth performs init then begin with the fixed DingTalk registration source', async () => {
|
|
const fixture = fetchFixture([
|
|
{ errcode: 0, nonce: 'nonce-private' },
|
|
{
|
|
errcode: 0,
|
|
device_code: 'device-private',
|
|
user_code: 'user-code',
|
|
verification_uri: 'https://oapi.dingtalk.com/verify',
|
|
verification_uri_complete: 'https://oapi.dingtalk.com/verify?user_code=user-code',
|
|
expires_in: 120,
|
|
interval: 3,
|
|
},
|
|
]);
|
|
const auth = new DingtalkDeviceAuth({ fetch: fixture.fetch, clock: () => 10_000 });
|
|
|
|
const result = await auth.start();
|
|
|
|
assert.deepEqual(fixture.calls.map((call) => call.url), [
|
|
'https://oapi.dingtalk.com/app/registration/init',
|
|
'https://oapi.dingtalk.com/app/registration/begin',
|
|
]);
|
|
assert.deepEqual(JSON.parse(fixture.calls[0].init.body), { source: 'DING_DWS_CLAW' });
|
|
assert.deepEqual(JSON.parse(fixture.calls[1].init.body), { nonce: 'nonce-private' });
|
|
assert.equal(fixture.calls[0].init.redirect, 'error');
|
|
assert.equal(result.deviceCode, 'device-private');
|
|
assert.equal(result.verificationUrl, 'https://oapi.dingtalk.com/verify?user_code=user-code');
|
|
assert.equal(result.expiresAt, 130_000);
|
|
assert.equal(result.pollIntervalMs, 3_000);
|
|
});
|
|
|
|
test('device auth polls with a host-only device code and normalizes credentials', async () => {
|
|
const fixture = fetchFixture([{
|
|
errcode: 0,
|
|
status: 'success',
|
|
client_id: 'ding-client',
|
|
client_secret: 'private-secret',
|
|
}]);
|
|
const auth = new DingtalkDeviceAuth({ fetch: fixture.fetch });
|
|
|
|
const result = await auth.poll({ deviceCode: 'device-private' });
|
|
|
|
assert.equal(fixture.calls[0].url, 'https://oapi.dingtalk.com/app/registration/poll');
|
|
assert.deepEqual(JSON.parse(fixture.calls[0].init.body), { device_code: 'device-private' });
|
|
assert.deepEqual(result, {
|
|
status: 'SUCCESS',
|
|
clientId: 'ding-client',
|
|
clientSecret: 'private-secret',
|
|
failReason: null,
|
|
});
|
|
});
|
|
|
|
test('device auth accepts only default-port HTTPS DingTalk registration hosts', () => {
|
|
const fetch = async () => assert.fail('fetch must not run');
|
|
for (const baseUrl of [
|
|
'http://oapi.dingtalk.com',
|
|
'https://oapi.dingtalk.com:8443',
|
|
'https://example.com',
|
|
'https://dingtalk.com@example.com',
|
|
]) {
|
|
assert.throws(
|
|
() => new DingtalkDeviceAuth({ fetch, baseUrl }),
|
|
/valid HTTPS URL/,
|
|
);
|
|
}
|
|
assert.doesNotThrow(
|
|
() => new DingtalkDeviceAuth({ fetch, baseUrl: 'https://staging.dingtalk.com/root/' }),
|
|
);
|
|
});
|
|
|
|
test('device auth API failures do not echo remote response details', async () => {
|
|
const fixture = fetchFixture([{
|
|
errcode: 400,
|
|
errmsg: 'do not leak device-private or client-secret-private',
|
|
}]);
|
|
const auth = new DingtalkDeviceAuth({ fetch: fixture.fetch });
|
|
|
|
await assert.rejects(
|
|
auth.start(),
|
|
(error) => {
|
|
assert.ok(error instanceof DingtalkDeviceAuthError);
|
|
assert.equal(error.code, 'api-error');
|
|
assert.doesNotMatch(error.message, /device-private|client-secret-private/);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
test('device auth bounds a stalled registration request with a local timeout', async () => {
|
|
const fetch = async (_url, { signal }) => new Promise((_resolve, reject) => {
|
|
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
|
});
|
|
const auth = new DingtalkDeviceAuth({ fetch, timeoutMs: 5 });
|
|
|
|
await assert.rejects(
|
|
auth.start(),
|
|
(error) => error instanceof DingtalkDeviceAuthError && error.code === 'timeout',
|
|
);
|
|
});
|