fix: API interceptor 增加 token 刷新单飞机制防止并发刷新

This commit is contained in:
qzl
2026-03-10 17:43:43 +08:00
parent 2ec0965322
commit 5d839192ab
2 changed files with 171 additions and 5 deletions
@@ -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);
});
}