LCOV - code coverage report
Current view: top level - lib/api/utils - api_base.dart Coverage Total Hit
Test: lcov.info Lines: 84.4 % 147 124
Test Date: 2025-05-10 20:26:13 Functions: - 0 0

            Line data    Source code
       1              : import 'dart:async';
       2              : import 'dart:convert';
       3              : import 'package:amadeus_proto/api/utils/api_cache.dart';
       4              : import 'package:amadeus_proto/api/utils/api_error.dart';
       5              : import 'package:amadeus_proto/api/utils/api_reactions.dart';
       6              : import 'package:amadeus_proto/api/utils/api_response.dart';
       7              : import 'package:either_dart/either.dart';
       8              : import 'package:flutter/material.dart';
       9              : import 'package:http/http.dart' as http;
      10              : import 'package:http/http.dart';
      11              : import 'package:provider/provider.dart';
      12              : 
      13              : /// Class simplifying Web API calls
      14              : class ApiBase extends ChangeNotifier {
      15              :   String? _accessToken;
      16              :   String? _refreshToken;
      17              :   final Client _client;
      18              :   String? _baseUrl;
      19              :   Map<String, String>? _defaultHeaders;
      20              :   BuildContext? _currentBuildContext;
      21              :   late final ApiReactions _apiReactions;
      22              : 
      23              :   ApiCache cache = ApiCache();
      24              : 
      25            0 :   BuildContext? get context => _currentBuildContext;
      26              : 
      27              :   /// Constructor for [ApiBase]
      28              :   /// If a [Client] isn't provided, the default [Client] is an [http.Client].
      29              :   /// If a [ApiReactions] isn't provided, one will be created.
      30            6 :   ApiBase({Client? client, ApiReactions? apiReactions})
      31            1 :       : _client = client ?? http.Client(),
      32            5 :         _apiReactions = apiReactions ?? ApiReactions();
      33              : 
      34              :   /// Set the access token
      35              :   ///
      36              :   /// If the token is invalid, one of those [Exception] will be throw:
      37              :   /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
      38              :   /// - `Unsupported signature algorithm: [algorithm]`
      39              :   /// - `Token expired at [DateTime]`
      40              :   /// - `Token issued in the future at  [DateTime]`
      41            2 :   set accessToken(String? value) {
      42              :     if (value != null) {
      43            2 :       _verifyJWTFormat(value);
      44              :     }
      45            2 :     _accessToken = value;
      46            2 :     notifyListeners();
      47              :   }
      48              : 
      49            2 :   String? get accessToken => _accessToken;
      50              : 
      51              :   /// Set the refresh token
      52              :   ///
      53              :   /// If the token is invalid, one of those [Exception] will be throw:
      54              :   /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
      55              :   /// - `Unsupported signature algorithm: [algorithm]`
      56              :   /// - `Token expired at [DateTime]`
      57              :   /// - `Token issued in the future at  [DateTime]`
      58            2 :   set refreshToken(String? value) {
      59              :     if (value != null) {
      60            2 :       _verifyJWTFormat(value);
      61              :     }
      62            2 :     _refreshToken = value;
      63              :   }
      64              : 
      65            6 :   String? get refreshToken => _refreshToken;
      66              : 
      67              :   /// Set base URL for Web API calls.
      68            6 :   set baseUrl(String url) {
      69            6 :     if (url.isEmpty) {
      70            1 :       throw Exception("Url can't be empty");
      71              :     }
      72            6 :     _baseUrl = url;
      73              :   }
      74              : 
      75            2 :   get defaultHeaders => _defaultHeaders;
      76              : 
      77              :   /// Reset the default headers
      78            1 :   void resetHeaders() {
      79            1 :     _defaultHeaders = null;
      80              :   }
      81              : 
      82              :   /// Add default headers for future HTTP requests
      83              :   ///
      84              :   /// [headers] is a Map containing all headers to add. If a keys is already present in
      85              :   ///  the default headers, its value will be overriden.
      86            4 :   void addHeaders(Map<String, String> headers) {
      87            4 :     if (_defaultHeaders == null) {
      88            4 :       _defaultHeaders = headers;
      89              :       return;
      90              :     }
      91            2 :     _defaultHeaders!.addAll(headers);
      92              :   }
      93              : 
      94              :   /// Delete default headers for future HTTP requests
      95              :   ///
      96              :   /// [keys] is a list containing all header keys to remove. If a key isn't present in
      97              :   ///  the default headers, it will be ignored.
      98            1 :   void deleteHeaders(List<String> keys) {
      99            2 :     for (final key in keys) {
     100            2 :       _defaultHeaders?.remove(key);
     101              :     }
     102              :   }
     103              : 
     104              :   /// Verify the format of the JWT [token]
     105              :   ///
     106              :   /// If the token is valid, nothing will happen, otherwise an [Exception] will be thrown.
     107              :   ///
     108              :   /// Exceptions thrown are:
     109              :   /// - `Invalid JWT format: Expected 3 parts, got [number of parts]`
     110              :   /// - `Unsupported signature algorithm: [algorithm]`
     111              :   /// - `Token expired at [DateTime]`
     112              :   /// - `Token issued in the future at  [DateTime]`
     113            2 :   void _verifyJWTFormat(String token) {
     114            2 :     List<String> parts = token.split('.');
     115              : 
     116            4 :     if (parts.length != 3) {
     117            1 :       throw Exception(
     118            2 :           'Invalid JWT format: Expected 3 parts, got ${parts.length}');
     119              :     }
     120              : 
     121            6 :     for (int i = 0; i < parts.length; i++) {
     122            8 :       if (parts[i].length % 4 != 0) {
     123           14 :         parts[i] += "=" * (4 - (parts[i].length % 4));
     124              :       }
     125              :     }
     126              : 
     127              :     Map<String, dynamic> header =
     128            8 :         jsonDecode(utf8.decode(base64Url.decode(parts[0])));
     129              : 
     130              :     Map<String, dynamic> payload =
     131            8 :         jsonDecode(utf8.decode(base64Url.decode(parts[1])));
     132              : 
     133            2 :     String alg = header['alg'] as String;
     134            4 :     if (!['HS256', 'RS256'].contains(alg)) {
     135            2 :       throw Exception('Unsupported signature algorithm: $alg');
     136              :     }
     137              : 
     138            2 :     DateTime? exp = payload.containsKey('exp')
     139            3 :         ? DateTime.fromMillisecondsSinceEpoch((payload['exp'] as int) * 1000)
     140              :         : null;
     141            2 :     if (exp != null && exp.isBefore(DateTime.now())) {
     142            2 :       throw Exception('Token expired at $exp');
     143              :     }
     144              :   }
     145              : 
     146              :   /// A wrapper around [context.watch], it should be used instead of it.
     147              :   ///
     148              :   /// Performs a `context.watch<ApiBase>()` to get [ApiBase] from the [context],
     149              :   /// set the current context of the [ApiBase] to [context] then return the [ApiBase]
     150            1 :   static ApiBase watch(BuildContext context) =>
     151            2 :       context.watch<ApiBase>().._currentBuildContext = context;
     152              : 
     153              :   /// A wrapper around [context.read], it should be used instead of it.
     154              :   ///
     155              :   /// Performs a `context.read<ApiBase>()` to get [ApiBase] from the [context],
     156              :   /// set the current context of the [ApiBase] to [context] then return the [ApiBase]
     157            6 :   static ApiBase read(BuildContext context) =>
     158           12 :       context.read<ApiBase>().._currentBuildContext = context;
     159              : 
     160              :   /// Transform the [promise] response into a [Future<Either<ApiError, Map<String, dynamic>>>]
     161              :   ///
     162              :   /// If the status of the [Response] is 200, the response will be returned as a right [Either] containing
     163              :   /// the body of the response decoded from json string with [jsonDecode].
     164              :   ///
     165              :   /// If the status of the [Response] isn't 200, the response will be return as left [Either] containing
     166              :   /// a [ApiError] containing the status coode of the response and the body as its message.
     167            6 :   Future<Either<ApiError, dynamic>> _handleApiResult(
     168              :       Future<Response> promise, [String? path]) async {
     169              :     try {
     170              :       final response = await promise;
     171            6 :       final apiResponse = ApiResponse.fromJson(
     172           24 :           response.body.isEmpty ? {} : jsonDecode(response.body));
     173            6 :       if (!apiResponse.success) {
     174           12 :         _apiReactions.executeReaction(
     175           12 :             _currentBuildContext!, apiResponse.statusCode, path);
     176              :         // if (response.statusCode != HttpStatus.ok) {
     177           12 :         return Left(ApiError(
     178            6 :             httpErrorCode: response.statusCode,
     179            6 :             message: apiResponse.message,
     180            6 :             apiStatusCode: apiResponse.statusCode));
     181              :         // ApiError(httpErrorCode: response.statusCode, message: response.body, apiStatusCode: "xxxxxx"));
     182              :       }
     183              :       // return Right(response.body.isEmpty ? {} : jsonDecode(response.body));
     184           13 :       return Right(apiResponse.data ?? {});
     185              :     } catch (e) {
     186              :       rethrow;
     187              :     }
     188              :   }
     189              : 
     190              :   /// Perform a GET HTTP request
     191              :   ///
     192              :   /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
     193              :   /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
     194              :   ///
     195              :   ///   Example of a [conversionFunc] for a example class:
     196              :   ///   ```dart
     197              :   ///   class Person {
     198              :   ///     Person({required this.firstName, required this.lastName});
     199              :   ///
     200              :   ///     String firstName;
     201              :   ///     String lastName;
     202              :   ///   }
     203              :   ///
     204              :   ///   Person fromJsonToPerson(Map<String, dynamic> json) {
     205              :   ///     return Person(firstName: json["firstName"], lastName: json["lastName"]);
     206              :   ///   }
     207              :   ///   ```
     208              :   ///
     209              :   /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
     210              :   ///  in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
     211              :   ///  has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
     212              :   ///  set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
     213              :   /// - [queryParameters] is a map containing the query parameters of the request
     214              :   /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
     215              :   ///  is set to 10 seconds.
     216              :   ///
     217              :   /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
     218              :   ///  transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
     219              :   ///  containing the status code and the body of the response as its message.
     220            6 :   Future<Either<ApiError, T>> get<T>(
     221              :       String path, T Function(dynamic json) conversionFunc,
     222              :       {Map<String, String>? headers,
     223              :       Map<String, String>? queryParameters,
     224              :       Duration? timeoutDuration}) async {
     225           12 :     final promise = _client.get(
     226           18 :       Uri.parse((_baseUrl ?? "") +
     227            6 :           path +
     228            2 :           (queryParameters?.entries
     229           10 :                   .map((entry) => "${entry.key}=${entry.value}")
     230            4 :                   .fold("?", (previous, current) {
     231            2 :                 if (previous == "?") {
     232            2 :                   return "?$current";
     233              :                 }
     234            2 :                 return "$previous&$current";
     235              :               }) ??
     236              :               "")),
     237            6 :       headers: <String, String>{
     238            6 :         ...?_defaultHeaders,
     239            9 :         if (_accessToken != null) "Authorization": "Bearer $_accessToken",
     240            1 :         ...?headers,
     241              :       },
     242            6 :     ).timeout(timeoutDuration ?? const Duration(seconds: 10));
     243           12 :     return _handleApiResult(promise, path).mapRight(conversionFunc);
     244              :   }
     245              : 
     246              :   /// Perform a PUT HTTP request
     247              :   ///
     248              :   /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
     249              :   /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
     250              :   ///
     251              :   ///   Example of a [conversionFunc] for a example class:
     252              :   ///   ```dart
     253              :   ///   class Person {
     254              :   ///     Person({required this.firstName, required this.lastName});
     255              :   ///
     256              :   ///     String firstName;
     257              :   ///     String lastName;
     258              :   ///   }
     259              :   ///
     260              :   ///   Person fromJsonToPerson(Map<String, dynamic> json) {
     261              :   ///     return Person(firstName: json["firstName"], lastName: json["lastName"]);
     262              :   ///   }
     263              :   ///   ```
     264              :   ///
     265              :   /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
     266              :   ///  in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
     267              :   ///  has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
     268              :   ///  set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
     269              :   /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
     270              :   /// - [queryParameters] is a map containing the query parameters of the request
     271              :   /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
     272              :   ///  is set to 10 seconds.
     273              :   ///
     274              :   /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
     275              :   ///  transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
     276              :   ///  containing the status code and the body of the response as its message.
     277            1 :   Future<Either<ApiError, T>> put<T>(
     278              :       String path, T Function(dynamic json) conversionFunc,
     279              :       {Map<String, String>? headers,
     280              :       Map<String, dynamic>? requestBody,
     281              :       Map<String, String>? queryParameters,
     282              :       Duration? timeoutDuration}) async {
     283            0 :     String? body = requestBody == null ? null : json.encode(requestBody);
     284            1 :     final promise = _client
     285            1 :         .put(
     286            3 :           Uri.parse((_baseUrl ?? "") +
     287            1 :               path +
     288            0 :               (queryParameters?.entries
     289            0 :                       .map((entry) => "${entry.key}=${entry.value}")
     290            0 :                       .fold("?", (previous, current) {
     291            0 :                     if (previous == "?") {
     292            0 :                       return "?$current";
     293              :                     }
     294            0 :                     return "$previous&$current";
     295              :                   }) ??
     296              :                   "")),
     297            1 :           headers: <String, String>{
     298            1 :             ...?_defaultHeaders,
     299            1 :             if (_accessToken != null) "Authorization": "Bearer $_accessToken",
     300            1 :             ...?headers,
     301              :           },
     302              :           body: body,
     303              :         )
     304            1 :         .timeout(timeoutDuration ?? const Duration(seconds: 10));
     305            2 :     return _handleApiResult(promise, path).mapRight(conversionFunc);
     306              :   }
     307              : 
     308              :   /// Perform a POST HTTP request
     309              :   ///
     310              :   /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
     311              :   /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
     312              :   ///
     313              :   ///   Example of a [conversionFunc] for a example class:
     314              :   ///   ```dart
     315              :   ///   class Person {
     316              :   ///     Person({required this.firstName, required this.lastName});
     317              :   ///
     318              :   ///     String firstName;
     319              :   ///     String lastName;
     320              :   ///   }
     321              :   ///
     322              :   ///   Person fromJsonToPerson(Map<String, dynamic> json) {
     323              :   ///     return Person(firstName: json["firstName"], lastName: json["lastName"]);
     324              :   ///   }
     325              :   ///   ```
     326              :   ///
     327              :   /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
     328              :   ///  in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
     329              :   ///  has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
     330              :   ///  set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
     331              :   /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
     332              :   /// - [queryParameters] is a map containing the query parameters of the request
     333              :   /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
     334              :   ///  is set to 10 seconds.
     335              :   ///
     336              :   /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
     337              :   ///  transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
     338              :   ///  containing the status code and the body of the response as its message.
     339            2 :   Future<Either<ApiError, T>> post<T>(
     340              :       String path, T Function(dynamic json) conversionFunc,
     341              :       {Map<String, String>? headers,
     342              :       Map<String, dynamic>? requestBody,
     343              :       Map<String, String>? queryParameters,
     344              :       Duration? timeoutDuration}) async {
     345            1 :     String? body = requestBody == null ? null : json.encode(requestBody);
     346            2 :     final promise = _client
     347            2 :         .post(
     348            6 :           Uri.parse((_baseUrl ?? "") +
     349            2 :               path +
     350            1 :               (queryParameters?.entries
     351            5 :                       .map((entry) => "${entry.key}=${entry.value}")
     352            2 :                       .fold("?", (previous, current) {
     353            1 :                     if (previous == "?") {
     354            1 :                       return "?$current";
     355              :                     }
     356            0 :                     return "$previous&$current";
     357              :                   }) ??
     358              :                   "")),
     359            2 :           headers: <String, String>{
     360            2 :             ...?_defaultHeaders,
     361            2 :             if (_accessToken != null) "Authorization": "Bearer $_accessToken",
     362            1 :             ...?headers
     363              :           },
     364              :           body: body,
     365              :         )
     366            2 :         .timeout(timeoutDuration ?? const Duration(seconds: 10));
     367            4 :     return _handleApiResult(promise, path).mapRight(conversionFunc);
     368              :   }
     369              : 
     370              :   /// Perform a PATCH HTTP request
     371              :   ///
     372              :   /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
     373              :   /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
     374              :   ///
     375              :   ///   Example of a [conversionFunc] for a example class:
     376              :   ///   ```dart
     377              :   ///   class Person {
     378              :   ///     Person({required this.firstName, required this.lastName});
     379              :   ///
     380              :   ///     String firstName;
     381              :   ///     String lastName;
     382              :   ///   }
     383              :   ///
     384              :   ///   Person fromJsonToPerson(Map<String, dynamic> json) {
     385              :   ///     return Person(firstName: json["firstName"], lastName: json["lastName"]);
     386              :   ///   }
     387              :   ///   ```
     388              :   ///
     389              :   /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
     390              :   ///  in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
     391              :   ///  has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
     392              :   ///  set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
     393              :   /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
     394              :   /// - [queryParameters] is a map containing the query parameters of the request
     395              :   /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
     396              :   ///  is set to 10 seconds.
     397              :   ///
     398              :   /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
     399              :   ///  transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
     400              :   ///  containing the status code and the body of the response as its message.
     401            1 :   Future<Either<ApiError, T>> patch<T>(
     402              :       String path, T Function(dynamic json) conversionFunc,
     403              :       {Map<String, String>? headers,
     404              :       Map<String, dynamic>? requestBody,
     405              :       Map<String, String>? queryParameters,
     406              :       Duration? timeoutDuration}) async {
     407            1 :     String? body = requestBody == null ? null : json.encode(requestBody);
     408            1 :     final promise = _client
     409            1 :         .patch(
     410            3 :           Uri.parse((_baseUrl ?? "") +
     411            1 :               path +
     412            0 :               (queryParameters?.entries
     413            0 :                       .map((entry) => "${entry.key}=${entry.value}")
     414            0 :                       .fold("?", (previous, current) {
     415            0 :                     if (previous == "?") {
     416            0 :                       return "?$current";
     417              :                     }
     418            0 :                     return "$previous&$current";
     419              :                   }) ??
     420              :                   "")),
     421            1 :           headers: <String, String>{
     422            1 :             ...?_defaultHeaders,
     423            1 :             if (_accessToken != null) "Authorization": "Bearer $_accessToken",
     424            0 :             ...?headers
     425              :           },
     426              :           body: body,
     427              :         )
     428            1 :         .timeout(timeoutDuration ?? const Duration(seconds: 10));
     429            2 :     return _handleApiResult(promise, path).mapRight(conversionFunc);
     430              :   }
     431              : 
     432              :   /// Perform a DELETE HTTP request
     433              :   ///
     434              :   /// - [path] is the endpoint of the request, it is added to the [baseUrl] of [ApiBase]
     435              :   /// - [conversionFunc] is a function used to transform the json (stored as a [Map]) into another object.
     436              :   ///
     437              :   ///   Example of a [conversionFunc] for a example class:
     438              :   ///   ```dart
     439              :   ///   class Person {
     440              :   ///     Person({required this.firstName, required this.lastName});
     441              :   ///
     442              :   ///     String firstName;
     443              :   ///     String lastName;
     444              :   ///   }
     445              :   ///
     446              :   ///   Person fromJsonToPerson(Map<String, dynamic> json) {
     447              :   ///     return Person(firstName: json["firstName"], lastName: json["lastName"]);
     448              :   ///   }
     449              :   ///   ```
     450              :   ///
     451              :   /// - [headers] are a map of headers to add to the [defaultHeaders], if a keys of [headers] is present
     452              :   ///  in the [defaultHeaders], its value is overridden by the one in [headers]. If the token of [ApiBase]
     453              :   ///  has been set, an `Authorization` header will be added, it will override if a `Authorization` header has been
     454              :   ///  set in the [defaultHeaders] but will be overriden if a `Authorization` header is present in the [headers]
     455              :   /// - [requestBody] is a [Map<String, dynamic>] containing the request body to send
     456              :   /// - [queryParameters] is a map containing the query parameters of the request
     457              :   /// - [timeoutDuration] is the [Duration] until a [TimeoutException] is thrown for the request. If not provided, the timeout duration
     458              :   ///  is set to 10 seconds.
     459              :   ///
     460              :   /// If the request is successful (status code 200), the [Future] returned by get will contain a right [Either] containing the data
     461              :   ///  transformed into an object of type [T] by [conversionFunc]. Else, it will contain a left [Either] containing the a [ApiError]
     462              :   ///  containing the status code and the body of the response as its message.
     463            1 :   Future<Either<ApiError, T>> delete<T>(
     464              :       String path, T Function(dynamic json) conversionFunc,
     465              :       {Map<String, String>? headers,
     466              :       Map<String, dynamic>? requestBody,
     467              :       Map<String, String>? queryParameters,
     468              :       Duration? timeoutDuration}) async {
     469            0 :     String? body = requestBody == null ? null : json.encode(requestBody);
     470            1 :     final promise = _client
     471            1 :         .delete(
     472            3 :           Uri.parse((_baseUrl ?? "") +
     473            1 :               path +
     474            0 :               (queryParameters?.entries
     475            0 :                       .map((entry) => "${entry.key}=${entry.value}")
     476            0 :                       .fold("?", (previous, current) {
     477            0 :                     if (previous == "?") {
     478            0 :                       return "?$current";
     479              :                     }
     480            0 :                     return "$previous&$current";
     481              :                   }) ??
     482              :                   "")),
     483            1 :           headers: <String, String>{
     484            1 :             ...?_defaultHeaders,
     485            1 :             if (_accessToken != null) "Authorization": "Bearer $_accessToken",
     486            1 :             ...?headers
     487              :           },
     488              :           body: body,
     489              :         )
     490            1 :         .timeout(timeoutDuration ?? const Duration(seconds: 10));
     491            2 :     return _handleApiResult(promise, path).mapRight(conversionFunc);
     492              :   }
     493              : }
        

Generated by: LCOV version 2.3-1