Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/analysis_baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"no_leading_underscores_for_local_identifiers": 5,
"non_constant_identifier_names": 1,
"overridden_fields": 1,
"prefer_final_fields": 4,
"prefer_final_fields": 3,
"prefer_function_declarations_over_variables": 1,
"prefer_interpolation_to_compose_strings": 5,
"prefer_is_empty": 1,
Expand Down
3 changes: 3 additions & 0 deletions app/lib/backend/http/shared.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'dart:async';

Check warning on line 1 in app/lib/backend/http/shared.dart

View workflow job for this annotation

GitHub Actions / PR Metadata Preflight

Large changed file

app/lib/backend/http/shared.dart is 845 lines; consider splitting files over 800 lines.

Check warning on line 1 in app/lib/backend/http/shared.dart

View workflow job for this annotation

GitHub Actions / Hygiene

Large changed file

app/lib/backend/http/shared.dart is 845 lines; consider splitting files over 800 lines.

Check warning on line 1 in app/lib/backend/http/shared.dart

View workflow job for this annotation

GitHub Actions / PR Metadata Preflight

Large changed file

app/lib/backend/http/shared.dart is 845 lines; consider splitting files over 800 lines.

Check warning on line 1 in app/lib/backend/http/shared.dart

View workflow job for this annotation

GitHub Actions / PR Metadata Preflight

Large changed file

app/lib/backend/http/shared.dart is 845 lines; consider splitting files over 800 lines.
import 'dart:convert';
import 'dart:io';

Expand Down Expand Up @@ -240,6 +240,9 @@
}
}

@visibleForTesting
Future<void> drainStreamedResponseForTesting(http.StreamedResponse response) => _drainStreamedResponse(response);

http.StreamedResponse _authUnavailableStreamedResponse() =>
http.StreamedResponse(const Stream<List<int>>.empty(), 401, reasonPhrase: 'Authentication unavailable');

Expand Down
39 changes: 39 additions & 0 deletions app/test/unit/backend/http/shared_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:convert';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';

Expand All @@ -11,6 +12,7 @@ import 'package:omi/backend/http/clock_skew_detector.dart';
import 'package:omi/backend/preferences.dart';
import 'package:omi/env/env.dart';
import 'package:omi/services/auth/auth_token_result.dart';
import 'package:omi/services/auth_service.dart';
import 'package:omi/utils/platform/platform_manager.dart';

Future<String> simulateGetAuthHeader({required bool isSignedIn, required String token}) async {
Expand Down Expand Up @@ -64,6 +66,26 @@ void main() {

expect(headers['Authorization'], equals('Bearer fresh-token'));
});

test('_drainStreamedResponse suppresses exceptions from aborted streams before replaying', () async {
var replayCount = 0;
final service = AuthService.forTesting(tokenGateway: _TestAuthTokenGateway(), refreshDelay: (_) async {});

final response = await refreshAndReplayAfter401(
firstResponse: http.StreamedResponse(_abortedResponseBody(), HttpStatus.unauthorized),
statusCode: (value) => value.statusCode,
disposeUnauthorizedResponse: drainStreamedResponseForTesting,
replay: () async {
replayCount++;
return http.StreamedResponse(const Stream<List<int>>.empty(), HttpStatus.ok);
},
expireTerminalSession: true,
authService: service,
);

expect(response.statusCode, HttpStatus.ok);
expect(replayCount, 1);
});
});

group('streaming clock-skew detection', () {
Expand Down Expand Up @@ -128,6 +150,23 @@ void main() {
});
}

Stream<List<int>> _abortedResponseBody() async* {
yield [1, 2, 3];
throw StateError('aborted response');
}

final class _TestAuthTokenGateway implements AuthTokenGateway {
@override
AuthUserSnapshot? get currentUser => const AuthUserSnapshot(uid: 'test-user');

@override
Future<RefreshedAuthToken?> forceRefresh() async =>
RefreshedAuthToken(token: 'fresh-token', expirationTime: DateTime.now().add(const Duration(hours: 1)));

@override
Future<void> signOut() async {}
}

class _TestEnvFields implements EnvFields {
String _requestBaseUrl = '';

Expand Down
Loading