Skip to content

Commit

Permalink
test: added tests for RestClientBase
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkkiller committed Apr 15, 2024
1 parent 99a5e09 commit 0fa9764
Show file tree
Hide file tree
Showing 6 changed files with 327 additions and 83 deletions.
73 changes: 42 additions & 31 deletions lib/src/core/rest_client/src/exception/rest_client_exception.dart
Original file line number Diff line number Diff line change
@@ -1,43 +1,36 @@
// ignore_for_file: overridden_fields

import 'package:meta/meta.dart';
import 'package:sizzle_starter/src/core/rest_client/rest_client.dart';

/// {@template rest_client_exception}
/// Base class for all rest client exceptions
/// {@endtemplate}
@immutable
abstract base class RestClientException implements Exception {
/// {@macro network_exception}
const RestClientException({
required this.message,
this.statusCode,
this.cause,
});

/// Message of the exception
final String message;

/// The status code of the response (if any)
final int? statusCode;

/// {@macro network_exception}
const RestClientException({required this.message, this.statusCode});
}

/// {@template rest_client_exception_with_cause}
/// Base class for all rest client exceptions that have a cause
/// {@endtemplate}
abstract base class RestClientExceptionWithCause extends RestClientException {
/// {@macro rest_client_exception_with_cause}
const RestClientExceptionWithCause({
required super.message,
required this.cause,
super.statusCode,
});

/// The cause of the exception
///
/// It is the inner exception that caused this exception to be thrown
/// It is the exception that caused this exception to be thrown.
///
/// If the exception is not caused by another exception, this field is `null`.
final Object? cause;
}

/// {@template client_exception}
/// [ClientException] is thrown if something went wrong on client side
/// {@endtemplate}
final class ClientException extends RestClientExceptionWithCause {
final class ClientException extends RestClientException {
/// {@macro client_exception}
const ClientException({
required super.message,
Expand All @@ -53,22 +46,40 @@ final class ClientException extends RestClientExceptionWithCause {
')';
}

/// {@template custom_backend_exception}
/// [CustomBackendException] is thrown if the backend returns an error
/// {@template structured_backend_exception}
/// Exception that is used for structured backend errors
///
/// [error] is a map that contains the error details
///
/// This exception is raised by [RestClientBase] when the response contains
/// 'error' field like the following:
/// ```json
/// {
/// "error": {
/// "message": "Some error message",
/// "code": 123
/// }
/// ```
///
/// This class exists to make handling of structured errors easier.
/// Basically, in data providers that use [RestClientBase], you can catch
/// this exception and convert it to a system-wide error. For example,
/// if backend returns an error with code 123 that means that the action
/// is not allowed, you can convert this exception to a NotAllowedException
/// and rethrow. This way, the rest of the application does not need to know
/// about the structure of the error and should only handle system-wide
/// exceptions.
/// {@endtemplate}
final class CustomBackendException extends RestClientException {
/// {@macro custom_backend_exception}
const CustomBackendException({
required super.message,
required this.error,
super.statusCode,
});
final class StructuredBackendException extends RestClientException {
/// {@macro structured_backend_exception}
const StructuredBackendException({required this.error, super.statusCode})
: super(message: 'Backend returned structured error');

/// The error returned by the backend
final Map<String, Object?> error;

@override
String toString() => 'CustomBackendException('
String toString() => 'StructuredBackendException('

Check warning on line 82 in lib/src/core/rest_client/src/exception/rest_client_exception.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/exception/rest_client_exception.dart#L82

Added line #L82 was not covered by tests
'message: $message,'
'error: $error,'
'statusCode: $statusCode,'
Expand Down Expand Up @@ -96,7 +107,7 @@ final class WrongResponseTypeException extends RestClientException {
/// {@template connection_exception}
/// [ConnectionException] is thrown if there are problems with the connection
/// {@endtemplate}
final class ConnectionException extends RestClientExceptionWithCause {
final class ConnectionException extends RestClientException {
/// {@macro connection_exception}
const ConnectionException({
required super.message,
Expand All @@ -115,7 +126,7 @@ final class ConnectionException extends RestClientExceptionWithCause {
/// {@template internal_server_exception}
/// If something went wrong on the server side
/// {@endtemplate}
final class InternalServerException extends RestClientExceptionWithCause {
final class InternalServerException extends RestClientException {
/// {@macro internal_server_exception}
const InternalServerException({
required super.message,
Expand Down
5 changes: 2 additions & 3 deletions lib/src/core/rest_client/src/http/rest_client_http.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import 'package:http/http.dart' as http;
import 'package:sizzle_starter/src/core/rest_client/rest_client.dart';

import 'package:sizzle_starter/src/core/rest_client/src/http/check_exception_io.dart'
if (dart.library.html) 'package:sizzle_starter/src/core/components/rest_client/src/http/check_exception_browser.dart';
if (dart.library.html) 'package:sizzle_starter/src/core/rest_client/src/http/check_exception_browser.dart';

/// {@template rest_client_http}
/// Rest client that uses [http] for making requests.
Expand Down Expand Up @@ -31,9 +30,9 @@ final class RestClientHttp extends RestClientBase {
Future<Map<String, Object?>?> send({
required String path,
required String method,
Map<String, String?>? queryParams,
Map<String, Object?>? body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
}) async {
try {
final uri = buildUri(path: path, queryParams: queryParams);
Expand Down
10 changes: 5 additions & 5 deletions lib/src/core/rest_client/src/rest_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,37 +6,37 @@ abstract class RestClient {
Future<Map<String, Object?>?> get(
String path, {
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});

/// Sends a POST request to the given [path].
Future<Map<String, Object?>?> post(
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});

/// Sends a PUT request to the given [path].
Future<Map<String, Object?>?> put(
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});

/// Sends a DELETE request to the given [path].
Future<Map<String, Object?>?> delete(
String path, {
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});

/// Sends a PATCH request to the given [path].
Future<Map<String, Object?>?> patch(
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});
}
111 changes: 68 additions & 43 deletions lib/src/core/rest_client/src/rest_client_base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ abstract base class RestClientBase implements RestClient {
required String method,
Map<String, Object?>? body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
});

@override

Check warning on line 29 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L29

Added line #L29 was not covered by tests
Future<Map<String, Object?>?> delete(
String path, {
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
}) =>
send(

Check warning on line 35 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L35

Added line #L35 was not covered by tests
path: path,
Expand All @@ -43,7 +43,7 @@ abstract base class RestClientBase implements RestClient {
Future<Map<String, Object?>?> get(
String path, {
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
}) =>
send(

Check warning on line 48 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L48

Added line #L48 was not covered by tests
path: path,
Expand All @@ -57,7 +57,7 @@ abstract base class RestClientBase implements RestClient {
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
}) =>
send(

Check warning on line 62 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L62

Added line #L62 was not covered by tests
path: path,
Expand All @@ -72,7 +72,7 @@ abstract base class RestClientBase implements RestClient {
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
}) =>
send(

Check warning on line 77 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L77

Added line #L77 was not covered by tests
path: path,
Expand All @@ -87,7 +87,7 @@ abstract base class RestClientBase implements RestClient {
String path, {
required Map<String, Object?> body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
Map<String, String?>? queryParams,
}) =>
send(

Check warning on line 92 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L92

Added line #L92 was not covered by tests
path: path,
Expand All @@ -105,7 +105,7 @@ abstract base class RestClientBase implements RestClient {
return _jsonUTF8.encode(body);
} on Object catch (e, stackTrace) {
Error.throwWithStackTrace(
ClientException(message: 'Error occured during encoding $e'),
ClientException(message: 'Error occured during encoding', cause: e),
stackTrace,
);
}
Expand All @@ -114,8 +114,8 @@ abstract base class RestClientBase implements RestClient {
/// Builds [Uri] from [path], [queryParams] and [baseUri]
@protected
@visibleForTesting
Uri buildUri({required String path, Map<String, Object?>? queryParams}) {
final finalPath = p.canonicalize(p.join(baseUri.path, path));
Uri buildUri({required String path, Map<String, String?>? queryParams}) {
final finalPath = p.join(baseUri.path, path);
return baseUri.replace(
path: finalPath,
queryParameters: {
Expand All @@ -125,54 +125,55 @@ abstract base class RestClientBase implements RestClient {
);
}

/// Decodes [body] from JSON \ UTF8
/// Decodes the response [body]
///
/// This method decodes the response body to a map and checks if the response
/// is an error or successful. If the response is an error, it throws a
/// [StructuredBackendException] with the error details.
///
/// If the response is successful, it returns the data from the response.
///
/// If the response is neither an error nor successful, it returns the decoded
/// body as is.
@protected
@visibleForTesting
FutureOr<Map<String, Object?>?> decodeResponse(
Object? body, {
Future<Map<String, Object?>?> decodeResponse(
/* String, Map<String, Object?>, List<int> */ Object? body, {
int? statusCode,
}) async {
if (body == null) return null;
try {
Map<String, Object?> result;
if (body is String) {
if (body.length > 1000) {
result = await Isolate.run(
() => json.decode(body) as Map<String, Object?>,
);
} else {
result = json.decode(body) as Map<String, Object?>;
}
} else if (body is Map<String, Object?>) {
result = body;
} else if (body is List<int>) {
if (body.length > 1000) {
result = await Isolate.run(
() => _jsonUTF8.decode(body)! as Map<String, Object?>,
);
} else {
result = _jsonUTF8.decode(body)! as Map<String, Object?>;
}
} else {
throw WrongResponseTypeException(
message: 'Unexpected response body type: ${body.runtimeType}',
statusCode: statusCode,
);
}

if (result case {'error': final Map<String, Object?> error}) {
throw CustomBackendException(
message: 'Backend returned custom error',
assert(
body is String || body is Map<String, Object?> || body is List<int>,
'Unexpected response body type: ${body.runtimeType}',

Check warning on line 148 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L148

Added line #L148 was not covered by tests
);

try {
final Map<String, Object?>? decodedBody = switch (body) {

Check warning on line 152 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L152

Added line #L152 was not covered by tests
final Map<String, Object?> map => map,
final String str => await _decodeString(str),
final List<int> bytes => await _decodeBytes(bytes),
_ => throw WrongResponseTypeException(
message: 'Unexpected response body type: ${body.runtimeType}',

Check warning on line 157 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L156-L157

Added lines #L156 - L157 were not covered by tests
statusCode: statusCode,
),
};

if (decodedBody case {'error': final Map<String, Object?> error}) {
throw StructuredBackendException(
error: error,
statusCode: statusCode,
);
}

if (result case {'data': final Map<String, Object?> data}) {
if (decodedBody case {'data': final Map<String, Object?> data}) {
return data;
}

return null;
// Simply return decoded body if it is not an error or data
// This is useful for responses that do not follow the structured response
// But generally, it is recommended to follow the structured response :)
return decodedBody;
} on RestClientException {
rethrow;
} on Object catch (e, stackTrace) {
Expand All @@ -186,4 +187,28 @@ abstract base class RestClientBase implements RestClient {
);
}
}

/// Decodes a [String] to a [Map<String, Object?>]
Future<Map<String, Object?>?> _decodeString(String str) async {
if (str.isEmpty) return null;

Check warning on line 193 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L192-L193

Added lines #L192 - L193 were not covered by tests

if (str.length > 1000) {
return Isolate.run(() => json.decode(str) as Map<String, Object?>);

Check warning on line 196 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L195-L196

Added lines #L195 - L196 were not covered by tests
}

return json.decode(str) as Map<String, Object?>;

Check warning on line 199 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L199

Added line #L199 was not covered by tests
}

/// Decodes a [List<int>] to a [Map<String, Object?>]
Future<Map<String, Object?>?> _decodeBytes(List<int> bytes) async {
if (bytes.isEmpty) return null;

if (bytes.length > 1000) {
return Isolate.run(
() => _jsonUTF8.decode(bytes)! as Map<String, Object?>,

Check warning on line 208 in lib/src/core/rest_client/src/rest_client_base.dart

View check run for this annotation

Codecov / codecov/patch

lib/src/core/rest_client/src/rest_client_base.dart#L207-L208

Added lines #L207 - L208 were not covered by tests
);
}

return _jsonUTF8.decode(bytes)! as Map<String, Object?>;
}
}
2 changes: 1 addition & 1 deletion lib/src/core/rest_client/src/rest_client_dio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ final class RestClientDio extends RestClientBase {
Future<Map<String, Object?>?> send({
required String path,
required String method,
Map<String, String?>? queryParams,
Map<String, Object?>? body,
Map<String, Object?>? headers,
Map<String, Object?>? queryParams,
}) async {
try {
final uri = buildUri(path: path, queryParams: queryParams);
Expand Down
Loading

0 comments on commit 0fa9764

Please sign in to comment.