@@ -1335,6 +1335,218 @@ void main() {
13351335 );
13361336 });
13371337
1338+ // ============================================================
1339+ // onError: retry fails (separate Dio error handling)
1340+ // ============================================================
1341+ group ('onError: retry fails (separate Dio error handling)' , () {
1342+ test (
1343+ 'WHEN refresh succeeds but retry with new token gets 401\n '
1344+ 'THEN error is propagated via retry catch block\n '
1345+ 'AND no deadlock occurs' ,
1346+ () async {
1347+ authorizationInterceptors.setTokenAndAuthorityOidc (
1348+ newToken: OIDCFixtures .tokenOidcExpiredTime,
1349+ newConfig: OIDCFixtures .oidcConfiguration,
1350+ );
1351+
1352+ // Old token → 401
1353+ dioAdapter.onPost (
1354+ baseUrl,
1355+ (server) => server.throws (responseStatusCode401, makeDioError401 ()),
1356+ headers: {
1357+ HttpHeaders .authorizationHeader:
1358+ 'Bearer ${OIDCFixtures .tokenOidcExpiredTime .token }' ,
1359+ },
1360+ );
1361+ // Retry with new token → also 401
1362+ dioAdapter.onPost (
1363+ baseUrl,
1364+ (server) => server.throws (
1365+ responseStatusCode401,
1366+ makeDioError401 (),
1367+ ),
1368+ headers: {
1369+ HttpHeaders .authorizationHeader:
1370+ 'Bearer ${OIDCFixtures .newTokenOidc .token }' ,
1371+ },
1372+ );
1373+
1374+ when (authenticationClient.refreshingTokensOIDC (
1375+ OIDCFixtures .oidcConfiguration.clientId,
1376+ OIDCFixtures .oidcConfiguration.redirectUrl,
1377+ OIDCFixtures .oidcConfiguration.discoveryUrl,
1378+ OIDCFixtures .oidcConfiguration.scopes,
1379+ OIDCFixtures .tokenOidcExpiredTime.refreshToken,
1380+ )).thenAnswer ((_) async => OIDCFixtures .newTokenOidc);
1381+ stubAccountCache ();
1382+
1383+ await expectLater (
1384+ () => dio.post (baseUrl),
1385+ throwsA (isA <DioException >()),
1386+ );
1387+
1388+ // Refresh was called once
1389+ verify (authenticationClient.refreshingTokensOIDC (
1390+ OIDCFixtures .oidcConfiguration.clientId,
1391+ OIDCFixtures .oidcConfiguration.redirectUrl,
1392+ OIDCFixtures .oidcConfiguration.discoveryUrl,
1393+ OIDCFixtures .oidcConfiguration.scopes,
1394+ OIDCFixtures .tokenOidcExpiredTime.refreshToken,
1395+ )).called (1 );
1396+ },
1397+ );
1398+
1399+ test (
1400+ 'WHEN refresh succeeds but retry gets 500\n '
1401+ 'THEN error is propagated\n '
1402+ 'AND OIDC state is preserved' ,
1403+ () async {
1404+ authorizationInterceptors.setTokenAndAuthorityOidc (
1405+ newToken: OIDCFixtures .tokenOidcExpiredTime,
1406+ newConfig: OIDCFixtures .oidcConfiguration,
1407+ );
1408+
1409+ final dioError500 = DioException (
1410+ error: {'message' : 'Internal Server Error' },
1411+ requestOptions: RequestOptions (path: baseUrl, method: 'POST' ),
1412+ response: Response (
1413+ statusCode: responseStatusCode500,
1414+ requestOptions: RequestOptions (path: baseUrl),
1415+ ),
1416+ type: DioExceptionType .badResponse,
1417+ );
1418+
1419+ // Old token → 401
1420+ dioAdapter.onPost (
1421+ baseUrl,
1422+ (server) => server.throws (responseStatusCode401, makeDioError401 ()),
1423+ headers: {
1424+ HttpHeaders .authorizationHeader:
1425+ 'Bearer ${OIDCFixtures .tokenOidcExpiredTime .token }' ,
1426+ },
1427+ );
1428+ // Retry with new token → 500
1429+ dioAdapter.onPost (
1430+ baseUrl,
1431+ (server) => server.throws (responseStatusCode500, dioError500),
1432+ headers: {
1433+ HttpHeaders .authorizationHeader:
1434+ 'Bearer ${OIDCFixtures .newTokenOidc .token }' ,
1435+ },
1436+ );
1437+
1438+ when (authenticationClient.refreshingTokensOIDC (
1439+ OIDCFixtures .oidcConfiguration.clientId,
1440+ OIDCFixtures .oidcConfiguration.redirectUrl,
1441+ OIDCFixtures .oidcConfiguration.discoveryUrl,
1442+ OIDCFixtures .oidcConfiguration.scopes,
1443+ OIDCFixtures .tokenOidcExpiredTime.refreshToken,
1444+ )).thenAnswer ((_) async => OIDCFixtures .newTokenOidc);
1445+ stubAccountCache ();
1446+
1447+ await expectLater (
1448+ () => dio.post (baseUrl),
1449+ throwsA (isA <DioException >()),
1450+ );
1451+
1452+ // OIDC state should NOT be cleared (only 400 clears state)
1453+ expect (
1454+ authorizationInterceptors.authenticationType,
1455+ AuthenticationType .oidc,
1456+ );
1457+ },
1458+ );
1459+
1460+ test (
1461+ 'GIVEN 2 concurrent requests\n '
1462+ 'WHEN Request 1 refreshes and retries successfully\n '
1463+ 'AND Request 2 retries with new token but gets 500\n '
1464+ 'THEN Request 1 succeeds\n '
1465+ 'AND Request 2 error is propagated' ,
1466+ () async {
1467+ authorizationInterceptors.setTokenAndAuthorityOidc (
1468+ newToken: OIDCFixtures .tokenOidcExpiredTime,
1469+ newConfig: OIDCFixtures .oidcConfiguration,
1470+ );
1471+
1472+ final dioError500 = DioException (
1473+ error: {'message' : 'Internal Server Error' },
1474+ requestOptions: RequestOptions (path: '$baseUrl /2' , method: 'POST' ),
1475+ response: Response (
1476+ statusCode: responseStatusCode500,
1477+ requestOptions: RequestOptions (path: '$baseUrl /2' ),
1478+ ),
1479+ type: DioExceptionType .badResponse,
1480+ );
1481+
1482+ // Request 1: old token → 401
1483+ dioAdapter.onPost (
1484+ '$baseUrl /1' ,
1485+ (server) => server.reply (
1486+ responseStatusCode401,
1487+ {'error' : 'Unauthorized' },
1488+ ),
1489+ headers: {
1490+ HttpHeaders .authorizationHeader:
1491+ 'Bearer ${OIDCFixtures .tokenOidcExpiredTime .token }' ,
1492+ },
1493+ );
1494+ // Request 1 retry: new token → 200
1495+ dioAdapter.onPost (
1496+ '$baseUrl /1' ,
1497+ (server) =>
1498+ server.reply (responseStatusCode200, dataRequestSuccessfully),
1499+ headers: {
1500+ HttpHeaders .authorizationHeader:
1501+ 'Bearer ${OIDCFixtures .newTokenOidc .token }' ,
1502+ },
1503+ );
1504+
1505+ // Request 2: old token → 401
1506+ dioAdapter.onPost (
1507+ '$baseUrl /2' ,
1508+ (server) => server.reply (
1509+ responseStatusCode401,
1510+ {'error' : 'Unauthorized' },
1511+ ),
1512+ headers: {
1513+ HttpHeaders .authorizationHeader:
1514+ 'Bearer ${OIDCFixtures .tokenOidcExpiredTime .token }' ,
1515+ },
1516+ );
1517+ // Request 2 retry: new token → 500
1518+ dioAdapter.onPost (
1519+ '$baseUrl /2' ,
1520+ (server) => server.throws (responseStatusCode500, dioError500),
1521+ headers: {
1522+ HttpHeaders .authorizationHeader:
1523+ 'Bearer ${OIDCFixtures .newTokenOidc .token }' ,
1524+ },
1525+ );
1526+
1527+ when (authenticationClient.refreshingTokensOIDC (
1528+ OIDCFixtures .oidcConfiguration.clientId,
1529+ OIDCFixtures .oidcConfiguration.redirectUrl,
1530+ OIDCFixtures .oidcConfiguration.discoveryUrl,
1531+ OIDCFixtures .oidcConfiguration.scopes,
1532+ OIDCFixtures .tokenOidcExpiredTime.refreshToken,
1533+ )).thenAnswer ((_) async => OIDCFixtures .newTokenOidc);
1534+ stubAccountCache ();
1535+
1536+ final future1 = dio.post ('$baseUrl /1' );
1537+ final future2 = dio.post ('$baseUrl /2' );
1538+
1539+ final response1 = await future1;
1540+ expect (response1.statusCode, responseStatusCode200);
1541+
1542+ await expectLater (
1543+ () => future2,
1544+ throwsA (isA <DioException >()),
1545+ );
1546+ },
1547+ );
1548+ });
1549+
13381550 // ============================================================
13391551 // onError: token duplicate prevents infinite loop
13401552 // ============================================================
0 commit comments