fix(auth): request moderator OAuth scopes only on opt-in

Stop asking for mod scopes at normal login so existing core-only tokens
never hit a missing-permissions dialog. Gate moderated-channels Helix,
offer an Enable flow for mods/broadcasters, and upgrade atomically.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Tommy Chow 2026-08-09 15:33:06 -07:00
parent 2d65aeec29
commit cc772a7c90
15 changed files with 690 additions and 128 deletions

View file

@ -2,21 +2,49 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:frosty/utils/twitch_oauth_scopes.dart';
void main() {
group('scope sets', () {
test('core scopes exclude moderator scopes', () {
for (final scope in twitchModeratorScopes) {
expect(twitchUserScopes, isNot(contains(scope)));
}
expect(twitchAllUserScopes, containsAll(twitchUserScopes));
expect(twitchAllUserScopes, containsAll(twitchModeratorScopes));
});
});
group('hasModeratorScopes', () {
test('requires all three moderator scopes', () {
expect(hasModeratorScopes(twitchAllUserScopes), isTrue);
expect(
hasModeratorScopes([
...twitchUserScopes,
'user:read:moderated_channels',
'moderator:manage:chat_messages',
]),
isFalse,
);
expect(hasModeratorScopes(twitchUserScopes), isFalse);
});
});
group('missingTwitchScopes', () {
test('returns scopes not present on the token', () {
test('defaults to core scopes only', () {
final missing = missingTwitchScopes(
granted: const ['chat:read', 'chat:edit'],
);
expect(missing, contains('user:read:moderated_channels'));
expect(missing, contains('moderator:manage:chat_messages'));
expect(missing, contains('moderator:manage:banned_users'));
expect(missing, contains('user:read:follows'));
expect(missing, isNot(contains('user:read:moderated_channels')));
expect(missing, isNot(contains('chat:read')));
});
test('returns empty when all required scopes are granted', () {
expect(missingTwitchScopes(granted: twitchUserScopes), isEmpty);
expect(
missingTwitchScopes(granted: twitchUserScopes),
missingTwitchScopes(
granted: twitchAllUserScopes,
required: twitchModeratorScopes,
),
isEmpty,
);
});
@ -33,34 +61,34 @@ void main() {
);
});
test('lists human-readable missing permissions when logged in', () {
test('lists missing core permissions when logged in', () {
final message = unauthorizedDialogMessage(
isLoggedIn: true,
grantedScopes: const [
'chat:read',
'chat:edit',
'user:read:follows',
'user:read:blocked_users',
'user:manage:blocked_users',
'user:manage:chat_color',
],
grantedScopes: const ['chat:read', 'chat:edit'],
);
expect(message, contains('additional Twitch permissions'));
expect(message, contains('View channels you moderate'));
expect(message, contains('Delete chat messages as a moderator'));
expect(message, contains('Timeout and ban users as a moderator'));
expect(message, isNot(contains('Send chat messages')));
expect(message, contains('View followed channels'));
expect(message, isNot(contains('View channels you moderate')));
});
test('falls back when logged in but no missing scopes are known', () {
test('prefers session expired when core scopes are complete', () {
expect(
unauthorizedDialogMessage(
isLoggedIn: true,
grantedScopes: twitchUserScopes,
),
contains('missing permissions'),
contains('expired'),
);
});
});
group('enableModeratorToolsDialogMessage', () {
test('lists moderator permission labels', () {
final message = enableModeratorToolsDialogMessage();
expect(message, contains('View channels you moderate'));
expect(message, contains('Delete chat messages as a moderator'));
expect(message, contains('Timeout and ban users as a moderator'));
});
});
}