fix: API interceptor 增加 token 刷新单飞机制防止并发刷新
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:social_app/core/api/api_interceptor.dart';
|
||||
import 'package:social_app/core/storage/token_storage.dart';
|
||||
|
||||
class MockTokenStorage extends Mock implements TokenStorage {}
|
||||
|
||||
class MockErrorInterceptorHandler extends Mock
|
||||
implements ErrorInterceptorHandler {}
|
||||
|
||||
void main() {
|
||||
late MockTokenStorage tokenStorage;
|
||||
late MockErrorInterceptorHandler handler;
|
||||
late ApiInterceptor interceptor;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(
|
||||
DioException(requestOptions: RequestOptions(path: '/fallback')),
|
||||
);
|
||||
registerFallbackValue(
|
||||
Response<dynamic>(requestOptions: RequestOptions(path: '/fallback')),
|
||||
);
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
tokenStorage = MockTokenStorage();
|
||||
handler = MockErrorInterceptorHandler();
|
||||
interceptor = ApiInterceptor(
|
||||
tokenStorage: tokenStorage,
|
||||
dio: Dio(),
|
||||
refreshFailureCooldown: const Duration(milliseconds: 80),
|
||||
);
|
||||
when(() => handler.next(any())).thenReturn(null);
|
||||
when(() => handler.resolve(any())).thenReturn(null);
|
||||
when(() => tokenStorage.getAccessToken()).thenAnswer((_) async => null);
|
||||
});
|
||||
|
||||
DioException _unauthorized(String path) {
|
||||
final requestOptions = RequestOptions(path: path);
|
||||
return DioException(
|
||||
requestOptions: requestOptions,
|
||||
response: Response<dynamic>(
|
||||
requestOptions: requestOptions,
|
||||
statusCode: 401,
|
||||
),
|
||||
type: DioExceptionType.badResponse,
|
||||
);
|
||||
}
|
||||
|
||||
test('401并发请求仅触发一次refresh', () async {
|
||||
var refreshCalls = 0;
|
||||
interceptor.onTokenRefresh = () async {
|
||||
refreshCalls += 1;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
return false;
|
||||
};
|
||||
|
||||
interceptor.onError(_unauthorized('/api/v1/agent/history'), handler);
|
||||
interceptor.onError(_unauthorized('/api/v1/agent/history'), handler);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 60));
|
||||
|
||||
expect(refreshCalls, 1);
|
||||
});
|
||||
|
||||
test('refresh接口401不应再次触发refresh', () async {
|
||||
var refreshCalls = 0;
|
||||
interceptor.onTokenRefresh = () async {
|
||||
refreshCalls += 1;
|
||||
return false;
|
||||
};
|
||||
|
||||
interceptor.onError(
|
||||
_unauthorized('/api/v1/auth/sessions/refresh'),
|
||||
handler,
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
expect(refreshCalls, 0);
|
||||
});
|
||||
|
||||
test('refresh接口带query时401也不应再次触发refresh', () async {
|
||||
var refreshCalls = 0;
|
||||
interceptor.onTokenRefresh = () async {
|
||||
refreshCalls += 1;
|
||||
return false;
|
||||
};
|
||||
|
||||
interceptor.onError(
|
||||
_unauthorized('/api/v1/auth/sessions/refresh?source=boot'),
|
||||
handler,
|
||||
);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
expect(refreshCalls, 0);
|
||||
});
|
||||
|
||||
test('refresh失败冷却期内不应重复触发refresh', () async {
|
||||
var refreshCalls = 0;
|
||||
interceptor.onTokenRefresh = () async {
|
||||
refreshCalls += 1;
|
||||
return false;
|
||||
};
|
||||
|
||||
interceptor.onError(_unauthorized('/api/v1/agent/history'), handler);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
interceptor.onError(_unauthorized('/api/v1/agent/history'), handler);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 20));
|
||||
|
||||
expect(refreshCalls, 1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user