Skip to content
Merged
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 lib/core/dio/app_dio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:on_time_front/core/dio/interceptors/token_session_invalidator.da
import 'package:on_time_front/core/dio/transformers/logging_transformer.dart';
import 'package:on_time_front/data/data_sources/token_local_data_source.dart';

@Injectable(as: Dio)
@LazySingleton(as: Dio)
class AppDio with DioMixin implements Dio {
AppDio(
TokenLocalDataSource tokenLocalDataSource,
Expand Down
39 changes: 24 additions & 15 deletions lib/core/dio/interceptors/token_interceptor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ class TokenInterceptor implements InterceptorsWrapper {
static const _tokenUnavailableMessage =
'Authentication token is unavailable for a protected request';

static bool _isRefreshing = false;
static final _requestsNeedRetry = <_RequestNeedingRetry>[];

final Dio dio;
final TokenLocalDataSource tokenLocalDataSource;
final TokenSessionInvalidator _sessionInvalidator;
Expand All @@ -21,16 +24,6 @@ class TokenInterceptor implements InterceptorsWrapper {
required TokenSessionInvalidator sessionInvalidator,
}) : _sessionInvalidator = sessionInvalidator;

// when accessToken is expired & having multiple requests call
// this variable to lock others request to make sure only trigger call refresh token 01 times
// to prevent duplicate refresh call
bool _isRefreshing = false;

// when having multiple requests call at the same time, you need to store them in a list
// then loop this list to retry every request later, after call refresh token success
final _requestsNeedRetry =
<({RequestOptions options, ErrorInterceptorHandler handler})>[];

@override
void onRequest(
RequestOptions options,
Expand Down Expand Up @@ -62,7 +55,13 @@ class TokenInterceptor implements InterceptorsWrapper {
if (_shouldRefreshToken(err)) {
final response = err.response;
// if hasn't not refreshing yet, let's start it
_requestsNeedRetry.add((options: err.requestOptions, handler: handler));
_requestsNeedRetry.add(
_RequestNeedingRetry(
dio: dio,
options: err.requestOptions,
handler: handler,
),
);

if (!_isRefreshing) {
_isRefreshing = true;
Expand Down Expand Up @@ -167,16 +166,14 @@ class TokenInterceptor implements InterceptorsWrapper {
}
}

Future<void> _retryRequests(
List<({RequestOptions options, ErrorInterceptorHandler handler})> requests,
) async {
Future<void> _retryRequests(List<_RequestNeedingRetry> requests) async {
await Future.wait(
requests.map((requestNeedRetry) async {
final options = requestNeedRetry.options;
options.extra[_retryAfterRefreshKey] = true;

try {
final response = await dio.fetch(options);
final response = await requestNeedRetry.dio.fetch(options);
requestNeedRetry.handler.resolve(response);
} on DioException catch (error) {
requestNeedRetry.handler.reject(error);
Expand All @@ -198,3 +195,15 @@ class TokenInterceptor implements InterceptorsWrapper {
handler.next(response);
}
}

class _RequestNeedingRetry {
const _RequestNeedingRetry({
required this.dio,
required this.options,
required this.handler,
});

final Dio dio;
final RequestOptions options;
final ErrorInterceptorHandler handler;
}
72 changes: 60 additions & 12 deletions test/core/dio/interceptors/token_interceptor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,10 @@ void main() {
sessionInvalidator = _FakeTokenSessionInvalidator(tokenLocalDataSource);

adapter = _TokenRefreshAdapter();
dio = Dio(
BaseOptions(
baseUrl: 'https://example.com',
receiveDataWhenStatusError: true,
),
)..httpClientAdapter = adapter;
dio.interceptors.add(
TokenInterceptor(
dio,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
),
dio = _dioWithTokenInterceptor(
adapter,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
);
});

Expand Down Expand Up @@ -122,6 +114,41 @@ void main() {
);
});

test('shares refresh coordination across interceptor instances', () async {
final refreshCompleter = Completer<void>();
adapter = _TokenRefreshAdapter(refreshCompleter: refreshCompleter);
final firstDio = _dioWithTokenInterceptor(
adapter,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
);
final secondDio = _dioWithTokenInterceptor(
adapter,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
);

final firstRequest = firstDio.get<String>('/protected/one');
await _flushMicrotasks();
final secondRequest = secondDio.get<String>('/protected/two');
await _flushMicrotasks();

expect(adapter.refreshRequests, 1);
refreshCompleter.complete();

final responses = await Future.wait([firstRequest, secondRequest]);

expect(responses.map((response) => response.statusCode), everyElement(200));
expect(adapter.refreshRequests, 1);
expect(tokenLocalDataSource.storeTokensCallCount, 1);
expect(
adapter.protectedAuthorizationHeaders.where(
(header) => header == 'Bearer new-access-token',
),
hasLength(2),
);
});

test('rejects original request when retry after refresh fails', () async {
adapter = _TokenRefreshAdapter(retryStatusCode: 500);
dio.httpClientAdapter = adapter;
Expand Down Expand Up @@ -211,6 +238,27 @@ void main() {
);
}

Dio _dioWithTokenInterceptor(
HttpClientAdapter adapter, {
required TokenLocalDataSource tokenLocalDataSource,
required TokenSessionInvalidator sessionInvalidator,
}) {
final dio = Dio(
BaseOptions(
baseUrl: 'https://example.com',
receiveDataWhenStatusError: true,
),
)..httpClientAdapter = adapter;
dio.interceptors.add(
TokenInterceptor(
dio,
tokenLocalDataSource: tokenLocalDataSource,
sessionInvalidator: sessionInvalidator,
),
);
return dio;
}

Future<void> _flushMicrotasks() async {
for (var i = 0; i < 5; i++) {
await Future<void>.delayed(Duration.zero);
Expand Down
3 changes: 2 additions & 1 deletion test/presentation/home/components/month_calendar_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ void main() {
tester,
) async {
DateTime? selected;
final targetDate = DateTime.now().add(const Duration(days: 2));
final now = DateTime.now();
final targetDate = DateTime(now.year, now.month, 15);

await tester.pumpWidget(
_TestApp(
Expand Down
Loading