96 lines
3.7 KiB
TypeScript
96 lines
3.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { assertRequiredScopes, MissingScopesError, SCOPE_TABLE } from '../src/scopes';
|
|
|
|
const ALWAYS_ON_BOT_SCOPES = ['user:read:chat', 'user:write:chat', 'user:bot'];
|
|
const ALWAYS_ON_BROADCASTER_SCOPES = ['channel:bot', 'moderator:read:followers'];
|
|
|
|
void test('passes when all always-on B1 scopes are granted and both features are off', () => {
|
|
assert.doesNotThrow(() =>
|
|
assertRequiredScopes(
|
|
{ featureRedemptions: false, featurePredictions: false },
|
|
{ bot: ALWAYS_ON_BOT_SCOPES, broadcaster: ALWAYS_ON_BROADCASTER_SCOPES },
|
|
),
|
|
);
|
|
});
|
|
|
|
void test('throws MissingScopesError naming every missing scope, not just the first', () => {
|
|
assert.throws(
|
|
() =>
|
|
assertRequiredScopes(
|
|
{ featureRedemptions: false, featurePredictions: false },
|
|
{ bot: [], broadcaster: [] },
|
|
),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof MissingScopesError);
|
|
const scopes = error.missing.map((m) => m.scope);
|
|
assert.deepEqual(scopes.sort(), [...ALWAYS_ON_BOT_SCOPES, ...ALWAYS_ON_BROADCASTER_SCOPES].sort());
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
void test('redemption scopes are only required when featureRedemptions is on', () => {
|
|
assert.doesNotThrow(() =>
|
|
assertRequiredScopes(
|
|
{ featureRedemptions: false, featurePredictions: false },
|
|
{ bot: ALWAYS_ON_BOT_SCOPES, broadcaster: ALWAYS_ON_BROADCASTER_SCOPES },
|
|
),
|
|
);
|
|
|
|
assert.throws(
|
|
() =>
|
|
assertRequiredScopes(
|
|
{ featureRedemptions: true, featurePredictions: false },
|
|
{ bot: ALWAYS_ON_BOT_SCOPES, broadcaster: ALWAYS_ON_BROADCASTER_SCOPES },
|
|
),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof MissingScopesError);
|
|
const scopes = error.missing.map((m) => m.scope).sort();
|
|
assert.deepEqual(scopes, ['channel:manage:broadcast', 'channel:manage:redemptions', 'channel:read:redemptions']);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
void test('prediction scope is only required when featurePredictions is on', () => {
|
|
assert.throws(
|
|
() =>
|
|
assertRequiredScopes(
|
|
{ featureRedemptions: false, featurePredictions: true },
|
|
{ bot: ALWAYS_ON_BOT_SCOPES, broadcaster: ALWAYS_ON_BROADCASTER_SCOPES },
|
|
),
|
|
(error: unknown) => {
|
|
assert.ok(error instanceof MissingScopesError);
|
|
assert.deepEqual(
|
|
error.missing.map((m) => m.scope),
|
|
['channel:manage:predictions'],
|
|
);
|
|
return true;
|
|
},
|
|
);
|
|
});
|
|
|
|
void test('the error message names the token kind and what each missing scope is needed for', () => {
|
|
try {
|
|
assertRequiredScopes({ featureRedemptions: false, featurePredictions: false }, { bot: [], broadcaster: [] });
|
|
assert.fail('expected assertRequiredScopes to throw');
|
|
} catch (error) {
|
|
assert.ok(error instanceof MissingScopesError);
|
|
assert.match(error.message, /bot token missing "user:read:chat"/);
|
|
assert.match(error.message, /channel\.chat\.message over EventSub WS/);
|
|
}
|
|
});
|
|
|
|
void test('SCOPE_TABLE matches the B1 design doc table (every scope, correct token)', () => {
|
|
const byScope = new Map(SCOPE_TABLE.map((req) => [req.scope, req.token]));
|
|
assert.equal(byScope.get('user:read:chat'), 'bot');
|
|
assert.equal(byScope.get('user:write:chat'), 'bot');
|
|
assert.equal(byScope.get('user:bot'), 'bot');
|
|
assert.equal(byScope.get('channel:bot'), 'broadcaster');
|
|
assert.equal(byScope.get('channel:read:redemptions'), 'broadcaster');
|
|
assert.equal(byScope.get('channel:manage:redemptions'), 'broadcaster');
|
|
assert.equal(byScope.get('moderator:read:followers'), 'broadcaster');
|
|
assert.equal(byScope.get('channel:manage:broadcast'), 'broadcaster');
|
|
assert.equal(byScope.get('channel:manage:predictions'), 'broadcaster');
|
|
});
|