Paginate tag (and ASC) lists via GQL with integrity retry; mint follow integrity from a WebView because Dio /integrity tokens are bad_bot. Co-authored-by: Cursor <cursoragent@cursor.com>
129 lines
3.4 KiB
Dart
129 lines
3.4 KiB
Dart
import 'package:dio/dio.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:frosty/apis/base_api_client.dart';
|
|
import 'package:frosty/apis/twitch_gql_api.dart';
|
|
import 'package:http_mock_adapter/http_mock_adapter.dart';
|
|
|
|
void main() {
|
|
late Dio dio;
|
|
late DioAdapter dioAdapter;
|
|
late TwitchGqlApi api;
|
|
|
|
setUp(() {
|
|
dio = Dio(BaseOptions());
|
|
dioAdapter = DioAdapter(dio: dio);
|
|
api = TwitchGqlApi(dio);
|
|
});
|
|
|
|
Map<String, Object?> streamEdge({
|
|
required String id,
|
|
required String login,
|
|
required int viewers,
|
|
String? cursor,
|
|
}) {
|
|
return {
|
|
'cursor': cursor ?? 'cursor-$id',
|
|
'node': {
|
|
'id': id,
|
|
'title': '$login live',
|
|
'viewersCount': viewers,
|
|
'createdAt': '2026-01-01T00:00:00Z',
|
|
'language': 'en',
|
|
'game': {'id': '1', 'name': 'Game'},
|
|
'broadcaster': {
|
|
'id': id,
|
|
'login': login,
|
|
'displayName': login,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
group('getStreams', () {
|
|
test('parses freeform tag page and cursor', () async {
|
|
dioAdapter.onPost(
|
|
'https://gql.twitch.tv/gql',
|
|
(server) => server.reply(200, {
|
|
'data': {
|
|
'streams': {
|
|
'edges': [
|
|
streamEdge(id: '1', login: 'a', viewers: 10),
|
|
streamEdge(id: '2', login: 'b', viewers: 5),
|
|
],
|
|
'pageInfo': {'hasNextPage': true},
|
|
},
|
|
},
|
|
}),
|
|
data: Matchers.any,
|
|
);
|
|
|
|
final page = await api.getStreams(
|
|
freeformTags: ['Deutsch'],
|
|
sort: StreamViewerSort.viewersAsc,
|
|
);
|
|
|
|
expect(page.data.map((s) => s.userLogin), ['a', 'b']);
|
|
expect(page.data.map((s) => s.viewerCount), [10, 5]);
|
|
expect(page.pagination['cursor'], 'cursor-2');
|
|
});
|
|
|
|
test('throws IntegrityCheckException on integrity errors', () async {
|
|
dioAdapter.onPost(
|
|
'https://gql.twitch.tv/gql',
|
|
(server) => server.reply(200, {
|
|
'errors': [
|
|
{'message': 'failed integrity check'},
|
|
],
|
|
'data': {'streams': null},
|
|
}),
|
|
data: Matchers.any,
|
|
);
|
|
|
|
await expectLater(
|
|
api.getStreams(
|
|
freeformTags: ['Deutsch'],
|
|
cursor: 'page-2',
|
|
authToken: 'web',
|
|
integrityToken: 'stale',
|
|
deviceId: 'device',
|
|
),
|
|
throwsA(isA<IntegrityCheckException>()),
|
|
);
|
|
});
|
|
|
|
test('sends Client-Integrity and X-Device-Id when provided', () async {
|
|
dioAdapter.onPost(
|
|
'https://gql.twitch.tv/gql',
|
|
(server) => server.reply(200, {
|
|
'data': {
|
|
'streams': {
|
|
'edges': [streamEdge(id: '1', login: 'a', viewers: 1)],
|
|
'pageInfo': {'hasNextPage': false},
|
|
},
|
|
},
|
|
}),
|
|
data: Matchers.any,
|
|
headers: {
|
|
'Client-ID': Matchers.any,
|
|
'Content-Type': Matchers.any,
|
|
'User-Agent': Matchers.any,
|
|
'Authorization': 'OAuth web-token',
|
|
'X-Device-Id': 'dev-123',
|
|
'Client-Integrity': 'integrity-token',
|
|
},
|
|
);
|
|
|
|
final page = await api.getStreams(
|
|
freeformTags: ['English'],
|
|
sort: StreamViewerSort.viewersDesc,
|
|
cursor: 'after-1',
|
|
authToken: 'web-token',
|
|
integrityToken: 'integrity-token',
|
|
deviceId: 'dev-123',
|
|
);
|
|
|
|
expect(page.data, hasLength(1));
|
|
expect(page.pagination, isEmpty);
|
|
});
|
|
});
|
|
}
|