CodingAleCR / http_interceptor

A lightweight, simple plugin that allows you to intercept request and response objects and modify them if desired.
MIT License
133 stars 67 forks source link

How to test http client with interceptor? TimeoutException after ... Future not completed #118

Open macik1423 opened 1 year ago

macik1423 commented 1 year ago

Describe the bug I wonder how to test http client with intercepor. I get every time TimeoutException after 0:00:15.000000: Future not completed or Przekroczono limit czasu semafora. if I remove timeout in client.get(...). Below there are my classes, I hope you can easly reproduce issue.

To Reproduce Steps to reproduce the behavior: lib\ authentication_repository.dart

import 'dart:async';
import 'dart:convert';
import 'dart:developer' as developer;

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:testing_app/storage_service.dart';
import 'package:testing_app/token.dart';

enum AuthenticationStatus {
  unknown,
  authenticated,
  unauthenticated,
  sessionExpired
}

class AuthenticationRepository {
  final _controller = StreamController<AuthenticationStatus>();
  final API_URL = dotenv.env['API_URL']!;
  final SERVER_TIMEOUT_SECONDS = 15;
  final http.Client _httpClient;
  final IStorage storage;

  AuthenticationRepository(this.storage, {http.Client? httpClient})
      : _httpClient = httpClient ?? http.Client();

  Stream<AuthenticationStatus> get status async* {
    yield AuthenticationStatus.unauthenticated;
    yield* _controller.stream;
  }

  Future<void> logIn({
    required String username,
    required String password,
  }) async {
    final request = Uri.http(API_URL, '/api/authentication/login-user');

    try {
      final loginResponse = await _httpClient
          .post(
            request,
            body: jsonEncode(
                {"emailAddress": "${username}", "password": "${password}"}),
            headers: {
              "Access_Control_Allow_Methods": "POST, OPTIONS",
              "Content-Type": "application/json",
              "Access-Control-Allow-Origin": "*",
              "Access-Control-Allow-Credentials": 'true'
            },
            encoding: Encoding.getByName("utf-8"),
          )
          .timeout(
            Duration(
              seconds: SERVER_TIMEOUT_SECONDS,
            ),
          );
      if (loginResponse.statusCode != 200) {
        throw Exception();
      } else {
        final accessToken = jsonDecode(loginResponse.body)['token'];
        final expiresAt = jsonDecode(loginResponse.body)['expiresAt'];
        final refreshToken = jsonDecode(loginResponse.body)['refreshToken'];
        if (accessToken != null) {
          storage.writeSecureData(
            StorageItem(
              key: 'token',
              value: Token.serialize(Token(
                accessToken: accessToken,
                expiresAt: expiresAt,
                refreshToken: refreshToken,
              )),
            ),
          );
        }
        _controller.add(AuthenticationStatus.authenticated);
      }
    } on TimeoutException catch (e) {
      developer.log("TIMEOUT!!!");
      throw TimeoutException(e.message);
    } on Exception catch (e) {
      developer.log('${e}');
      throw Exception();
    }
  }

  Future<void> getRefreshToken({required Token token}) async {
    storage.deleteSecureData('token');
    final request = Uri.http(API_URL, '/api/Authentication/refresh-token');

    try {
      final response = await _httpClient
          .post(
            request,
            headers: {
              'Accept': 'application/json',
              'Content-Type': 'application/json',
            },
            body: jsonEncode(
              {
                'token': token.accessToken,
                'refreshToken': token.refreshToken,
              },
            ),
          )
          .timeout(Duration(seconds: SERVER_TIMEOUT_SECONDS));

      if (response.statusCode != 200) {
        throw Exception();
      } else {
        final accessToken = jsonDecode(response.body)['token'];
        final expiresAt = jsonDecode(response.body)['expiresAt'];
        final refreshToken = jsonDecode(response.body)['refreshToken'];
        if (accessToken != null) {
          storage.writeSecureData(
            StorageItem(
              key: 'token',
              value: Token.serialize(Token(
                accessToken: accessToken,
                expiresAt: expiresAt,
                refreshToken: refreshToken,
              )),
            ),
          );
        }
      }
    } on TimeoutException catch (e) {
      throw TimeoutException(e.message);
    } on Exception catch (_) {
      sessionExpired();
      throw Exception();
    }
  }

  void sessionExpired() {
    storage.deleteSecureData('token');
    _controller.add(AuthenticationStatus.sessionExpired);
  }

  void logOut() {
    storage.deleteSecureData('token');
    _controller.add(AuthenticationStatus.unauthenticated);
  }

  void dispose() => _controller.close();
}

material.dart

import 'package:json_annotation/json_annotation.dart';

part 'material.g.dart';

@JsonSerializable()
class Material {
  const Material({
    required this.symbol,
    required this.value,
  });

  factory Material.fromJson(Map<String, dynamic> json) =>
      _$MaterialFromJson(json);

  final int value;
  final String symbol;
}

material.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'material.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

Material _$MaterialFromJson(Map<String, dynamic> json) => Material(
      symbol: json['symbol'] as String,
      value: json['value'] as int,
    );

Map<String, dynamic> _$MaterialToJson(Material instance) => <String, dynamic>{
      'value': instance.value,
      'symbol': instance.symbol,
    };

storage_service.dart

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

abstract class IStorage {
  Future<void> writeSecureData(StorageItem newItem);
  Future<String?> readSecureData(String key);
  Future<void> deleteSecureData(String key);
  Future<bool> containsKeyInSecureData(String key);
  Future<void> deleteAllSecureData();
}

class StorageItem {
  StorageItem({required this.key, required this.value});

  final String key;
  final String value;
}

class StorageService implements IStorage {
  final FlutterSecureStorage flutterSecureStorage;

  StorageService({required this.flutterSecureStorage});

  AndroidOptions _getAndroidOptions() => const AndroidOptions(
        encryptedSharedPreferences: true,
      );

  @override
  Future<void> writeSecureData(StorageItem newItem) async {
    await flutterSecureStorage.write(key: newItem.key, value: newItem.value);
  }

  @override
  Future<String?> readSecureData(String key) async {
    return await flutterSecureStorage.read(
        key: key, aOptions: _getAndroidOptions());
  }

  @override
  Future<void> deleteSecureData(String key) async {
    await flutterSecureStorage.delete(key: key, aOptions: _getAndroidOptions());
  }

  @override
  Future<bool> containsKeyInSecureData(String key) async {
    return flutterSecureStorage.containsKey(
        key: key, aOptions: _getAndroidOptions());
  }

  @override
  Future<void> deleteAllSecureData() async {
    await flutterSecureStorage.deleteAll(aOptions: _getAndroidOptions());
  }
}

token.dart

import 'dart:convert';

class Token {
  final String accessToken;
  final String refreshToken;
  final String expiresAt;

  Token({
    required this.accessToken,
    required this.refreshToken,
    required this.expiresAt,
  });

  factory Token.fromJson(Map<String, dynamic> json) {
    return Token(
      accessToken: json['accessToken'] as String,
      refreshToken: json['refreshToken'] as String,
      expiresAt: json['expiresAt'] as String,
    );
  }

  static Map<String, dynamic> toMap(Token token) {
    return {
      'accessToken': token.accessToken,
      'refreshToken': token.refreshToken,
      'expiresAt': token.expiresAt,
    };
  }

  static String serialize(Token token) {
    return json.encode(toMap(token));
  }

  static Token deserialize(String? serialized) {
    return Token.fromJson(json.decode(serialized!));
  }
}

warehouse_api_client.dart

import 'dart:convert';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:http_interceptor/http/interceptor_contract.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:testing_app/authentication_repository.dart';
import 'package:testing_app/material.dart';
import 'package:testing_app/storage_service.dart';
import 'package:testing_app/token.dart';

class AuthorizationInterceptor implements InterceptorContract {
  final IStorage storage;

  AuthorizationInterceptor({required this.storage});

  @override
  Future<RequestData> interceptRequest({required RequestData data}) async {
    try {
      final Token? token =
          Token.deserialize(await storage.readSecureData('token'));
      data.headers.clear();

      data.headers['authorization'] = 'Bearer ' + token!.accessToken;
      data.headers['content-type'] = 'application/json';
    } catch (e) {
      print(e);
    }

    return data;
  }

  @override
  Future<ResponseData> interceptResponse({required ResponseData data}) async {
    return data;
  }
}

class ExpiredTokenRetryPolicy extends RetryPolicy {
  final IStorage storage;
  final AuthenticationRepository authRepository;
  @override
  int maxRetryAttempts = 1;

  ExpiredTokenRetryPolicy(
      {required this.authRepository, required this.storage});

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    if (response.statusCode == 401) {
      await regenerateToken();
      return true;
    }
    return false;
  }

  Future<void> regenerateToken() async {
    final token = Token.deserialize(await storage.readSecureData('token'));

    await authRepository.getRefreshToken(token: token);

    final Token? newToken =
        Token.deserialize(await storage.readSecureData('token'));

    if (newToken == null) {
      authRepository.sessionExpired();
      throw Exception();
    }
  }
}

class WarehouseApiClient {
  final IStorage storage;
  final API_URL = dotenv.env['API_URL']!;
  final http.Client client;
  WarehouseApiClient(this.storage, this.authRepository)
      : client = InterceptedClient.build(
          interceptors: [AuthorizationInterceptor(storage: storage)],
          retryPolicy: ExpiredTokenRetryPolicy(
              authRepository: authRepository, storage: storage),
        );

  final AuthenticationRepository authRepository;

  Future<List<Material>> getMaterial() async {
    final request = Uri.http(API_URL, '/api/material');

    final token = await storage.readSecureData('token');

    final deserialized = Token.deserialize(token);

    final accessToken = deserialized.accessToken;

    http.Response response = await client.get(
      request,
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    ).timeout(Duration(seconds: 15));

    switch (response.statusCode) {
      case 200:
        final materialJson = jsonDecode(response.body) as List;
        final list = materialJson
            .map((item) => Material.fromJson(item as Map<String, dynamic>))
            .toList();
        return list;
      default:
        throw Exception();
    }
  }
}

test\my_test.dart

import 'dart:io';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:mocktail/mocktail.dart';
import 'package:testing_app/authentication_repository.dart';
import 'package:testing_app/storage_service.dart';
import 'package:testing_app/token.dart';
import 'package:testing_app/warehouse_api_client.dart';

class MockHttpClient extends Mock implements http.Client {}

class MockResponse extends Mock implements http.Response {}

class FakeUri extends Fake implements Uri {}

class MockAuthenticationRepository extends Mock
    implements AuthenticationRepository {}

class MockStorage extends Mock implements IStorage {}

Future<void> main() async {
  await dotenv.load(fileName: '.env.development');
  group('WarehouseApiClient', () {
    late http.Client httpClient;
    late WarehouseApiClient apiClient;
    late MockAuthenticationRepository authenticationRepository;
    late MockStorage storage;
    final apiUrl = dotenv.env['API_URL']!;

    setUpAll(() {
      registerFallbackValue(FakeUri());
    });

    setUp(() {
      httpClient = MockHttpClient();
      authenticationRepository = MockAuthenticationRepository();
      storage = MockStorage();
      apiClient = WarehouseApiClient(
        storage,
        authenticationRepository,
      );
      when(() => authenticationRepository.status)
          .thenAnswer((_) => const Stream.empty());
    });

    group('getMaterial', () {
      test('makes correct http request', () async {
        final response = MockResponse();
        when(() => response.statusCode).thenReturn(200);
        when(() => response.body).thenReturn('{}');
        when(() => httpClient.get(any(), headers: any(named: 'headers')))
            .thenAnswer((_) async => response);
        when(() => storage.readSecureData(any())).thenAnswer(
          (_) async => """{
                "accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoibSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWVpZGVudGlmaWVyIjoiOSIsImVtYWlsIjoibSIsInN1YiI6Im0iLCJqdGkiOiI2YjdlZWRlNy1jMDJjLTRiNzMtYTMxYS01ZTYwNTA1NTkzMWYiLCJleHAiOjE2NjkxMDI2OTEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMSIsImF1ZCI6InVzZXIifQ.q14peCD-pfEhu9zm1JeAVps-WxHhriruGLadu3QNzeY",
                "refreshToken":"24e57093-ccac-489c-bc83-27cf4ca36285-29c13587-88e9-4f25-87f9-9d2134d845e2",
                "expiresAt":"2022-11-22T07:38:11Z"
              }""",
        );
        try {
          await apiClient.getMaterial();
        } catch (e) {
          print(e);
        }
        final uri = Uri.https(
          apiUrl,
          '/api/material',
        );
        verify(
          () => httpClient.get(uri),
        ).called(1);
      });
    });
  });
}

.env.development

API_URL=10.0.2.2:5001

pubspec.yaml

name: testing_app
description: A new Flutter project.

# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev

# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1

environment:
  sdk: '>=2.18.4 <3.0.0'

# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
  flutter:
    sdk: flutter
  http_interceptor: ^1.0.2
  flutter_secure_storage: ^6.0.0
  flutter_dotenv: ^5.0.2
  http: ^0.13.0

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2

dev_dependencies:
  flutter_test:
    sdk: flutter
  mocktail: ^0.3.0
  build_runner: ^2.0.0
  json_serializable: ^6.0.0

  # The "flutter_lints" package below contains a set of recommended lints to
  # encourage good coding practices. The lint set provided by the package is
  # activated in the `analysis_options.yaml` file located at the root of your
  # package. See that file for information about deactivating specific lint
  # rules and activating additional ones.
  flutter_lints: ^2.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

  # The following line ensures that the Material Icons font is
  # included with your application, so that you can use the icons in
  # the material Icons class.
  uses-material-design: true
  assets:
    - .env.development

  # To add assets to your application, add an assets section, like this:
  # assets:
  #   - images/a_dot_burr.jpeg
  #   - images/a_dot_ham.jpeg

  # An image asset can refer to one or more resolution-specific "variants", see
  # https://flutter.dev/assets-and-images/#resolution-aware

  # For details regarding adding assets from package dependencies, see
  # https://flutter.dev/assets-and-images/#from-packages

  # To add custom fonts to your application, add a fonts section here,
  # in this "flutter" section. Each entry in this list should have a
  # "family" key with the font family name, and a "fonts" key with a
  # list giving the asset and other descriptors for the font. For
  # example:
  # fonts:
  #   - family: Schyler
  #     fonts:
  #       - asset: fonts/Schyler-Regular.ttf
  #       - asset: fonts/Schyler-Italic.ttf
  #         style: italic
  #   - family: Trajan Pro
  #     fonts:
  #       - asset: fonts/TrajanPro.ttf
  #       - asset: fonts/TrajanPro_Bold.ttf
  #         weight: 700
  #
  # For details regarding fonts from package dependencies,
  # see https://flutter.dev/custom-fonts/#from-packages

Expected behavior I want to test my api where I use http client with interceptor but when I run test the async method takes few seconds and return error that I have mentioned above.

Please complete the following information): [√] Flutter (Channel stable, 3.3.8, on Microsoft Windows [Version 10.0.19042.2194], locale pl-PL) [!] Android toolchain - develop for Android devices (Android SDK version 33.0.0-rc4) ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses [√] Chrome - develop for the web [!] Visual Studio - develop for Windows (Visual Studio Community 2022 17.4.1) X Visual Studio is missing necessary components. Please re-run the Visual Studio installer for the "Desktop development with C++" workload, and include these components: MSVC v142 - VS 2019 C++ x64/x86 build tools

! Doctor found issues in 2 categories.

CodingAleCR commented 1 year ago

Hi, have you tried sending a http.Client instance to your interceptor builder? You can use that to mock the internal client and thus have your requests do whatever you want. The library is designed to wrap a Client and provide proper support of interceptors.

InterceptedClient.build(
  interceptors: [AuthorizationInterceptor(storage: storage)],
  retryPolicy: ExpiredTokenRetryPolicy(
      authRepository: authRepository, 
      storage: storage,
  ),
  client: MockClient(),
);

Let me know if that helps. Cheers from CR 🇨🇷

macik1423 commented 1 year ago

Hi, thanks for your reply! Thanks to your hint I have created something like this: warehouse_api_client.dart

import 'dart:convert';

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:http/http.dart' as http;
import 'package:http_interceptor/http/interceptor_contract.dart';
import 'package:http_interceptor/http_interceptor.dart';
import 'package:testing_app/authentication_repository.dart';
import 'package:testing_app/material.dart';
import 'package:testing_app/storage_service.dart';
import 'package:testing_app/token.dart';

class AuthorizationInterceptor implements InterceptorContract {
  final IStorage storage;

  AuthorizationInterceptor({required this.storage});

  @override
  Future<RequestData> interceptRequest({required RequestData data}) async {
    try {
      final Token? token =
          Token.deserialize(await storage.readSecureData('token'));
      data.headers.clear();

      data.headers['authorization'] = 'Bearer ' + token!.accessToken;
      data.headers['content-type'] = 'application/json';
    } catch (e) {
      print(e);
    }

    return data;
  }

  @override
  Future<ResponseData> interceptResponse({required ResponseData data}) async {
    return data;
  }
}

class ExpiredTokenRetryPolicy extends RetryPolicy {
  final IStorage storage;
  final AuthenticationRepository authRepository;
  @override
  int maxRetryAttempts = 1;

  ExpiredTokenRetryPolicy(
      {required this.authRepository, required this.storage});

  @override
  Future<bool> shouldAttemptRetryOnResponse(ResponseData response) async {
    if (response.statusCode == 401) {
      await regenerateToken();
      return true;
    }
    return false;
  }

  Future<void> regenerateToken() async {
    final token = Token.deserialize(await storage.readSecureData('token'));

    await authRepository.getRefreshToken(token: token);

    final Token? newToken =
        Token.deserialize(await storage.readSecureData('token'));

    if (newToken == null) {
      authRepository.sessionExpired();
      throw Exception();
    }
  }
}

class WarehouseApiClient {
  final IStorage storage;
  final API_URL = dotenv.env['API_URL']!;
  final http.Client _client;
  WarehouseApiClient(this.storage, this.authRepository, client)
      : _client = client ??
            InterceptedClient.build(
              interceptors: [AuthorizationInterceptor(storage: storage)],
              retryPolicy: ExpiredTokenRetryPolicy(
                  authRepository: authRepository, storage: storage),
            );

  final AuthenticationRepository authRepository;

  Future<List<Material>> getMaterial() async {
    final request = Uri.http(API_URL, '/api/material');

    final token = await storage.readSecureData('token');

    final deserialized = Token.deserialize(token);

    final accessToken = deserialized.accessToken;

    http.Response response = await _client.get(
      request,
      headers: {
        'Authorization': 'Bearer $accessToken',
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    );
    // .timeout(Duration(seconds: 15));

    switch (response.statusCode) {
      case 200:
        final materialJson = jsonDecode(response.body) as List;
        final list = materialJson
            .map((item) => Material.fromJson(item as Map<String, dynamic>))
            .toList();
        return list;
      default:
        throw Exception();
    }
  }
}

my_test.dart

import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http_interceptor/http/http.dart';
import 'package:mocktail/mocktail.dart';
import 'package:testing_app/authentication_repository.dart';
import 'package:testing_app/storage_service.dart';
import 'package:testing_app/warehouse_api_client.dart';

class MockHttpClient extends Mock implements http.Client {}

class MockResponse extends Mock implements http.Response {}

class FakeUri extends Fake implements Uri {}

class MockAuthenticationRepository extends Mock
    implements AuthenticationRepository {}

class MockStorage extends Mock implements IStorage {}

Future<void> main() async {
  await dotenv.load(fileName: '.env.development');
  group('WarehouseApiClient', () {
    late http.Client httpClient;
    late WarehouseApiClient apiClient;
    late MockAuthenticationRepository authenticationRepository;
    late MockStorage storage;
    final apiUrl = dotenv.env['API_URL']!;

    setUpAll(() {
      registerFallbackValue(FakeUri());
    });

    setUp(() {
      httpClient = MockHttpClient();
      authenticationRepository = MockAuthenticationRepository();
      storage = MockStorage();
      apiClient = WarehouseApiClient(
        storage,
        authenticationRepository,
        InterceptedClient.build(
          interceptors: [AuthorizationInterceptor(storage: storage)],
          retryPolicy: ExpiredTokenRetryPolicy(
            authRepository: authenticationRepository,
            storage: storage,
          ),
          client: httpClient,
        ),
      );
      when(() => authenticationRepository.status)
          .thenAnswer((_) => const Stream.empty());
    });

    group('getMaterial', () {
      test('makes correct http request', () async {
        final response = MockResponse();
        when(() => response.statusCode).thenReturn(200);
        when(() => response.body).thenReturn('{}');
        when(() => httpClient.get(any(), headers: any(named: 'headers')))
            .thenAnswer((_) async => response);
        when(() => storage.readSecureData(any())).thenAnswer(
          (_) async => """{
                "accessToken":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoibSIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWVpZGVudGlmaWVyIjoiOSIsImVtYWlsIjoibSIsInN1YiI6Im0iLCJqdGkiOiI2YjdlZWRlNy1jMDJjLTRiNzMtYTMxYS01ZTYwNTA1NTkzMWYiLCJleHAiOjE2NjkxMDI2OTEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NTAwMSIsImF1ZCI6InVzZXIifQ.q14peCD-pfEhu9zm1JeAVps-WxHhriruGLadu3QNzeY",
                "refreshToken":"24e57093-ccac-489c-bc83-27cf4ca36285-29c13587-88e9-4f25-87f9-9d2134d845e2",
                "expiresAt":"2022-11-22T07:38:11Z"
              }""",
        );
        try {
          await apiClient.getMaterial();
        } catch (e) {
          print(e);
        }
        final uri = Uri.https(
          apiUrl,
          '/api/material',
        );
        verify(
          () => httpClient.get(uri),
        ).called(1);
      });
    });
  });
}

previously errors with timeout is disapear, but now I have a

type 'Null' is not a subtype of type 'Future<StreamedResponse>'
package:test_api                           fail
package:mocktail/src/mocktail.dart 722:7   _VerifyCall._checkWith
package:mocktail/src/mocktail.dart 515:18  _makeVerify.<fn>
test\my_test.dart 77:15                    main.<fn>.<fn>.<fn>

No matching calls. All calls: MockHttpClient.send(GET http://10.0.2.2:5001/api/material)
(If you called `verify(...).called(0);`, please instead use `verifyNever(...);`.)

error. Maybe I should mock http server somehow?

CodingAleCR commented 1 year ago

I will take a closer look with mocktail. Will get back to you as soon as I can 😄

macik1423 commented 1 year ago

Any updates? :)

stale[bot] commented 1 year ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

CodingAleCR commented 1 year ago

mocktail

Hi sorry, hectic months. I. haven't gotten around to trying tests with mocktail. But it's getting to the top of my list too so I'm hoping to dive into it soon.

stale[bot] commented 10 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.