Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added success/failure callback hooks to Authenticator. #527

Merged
merged 6 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 52 additions & 2 deletions chopper/lib/src/authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,62 @@

import 'package:chopper/chopper.dart';

/// This method should return a [Request] that includes credentials to satisfy an authentication challenge received in
/// [response]. It should return `null` if the challenge cannot be satisfied.
///
/// Callback that is called when an authentication challenge is received
/// based on the given [request], [response], and optionally the
/// [originalRequest].
///
typedef AuthenticationCallback = FutureOr<void> Function(
Request request,
Response response, [
Request? originalRequest,
]);

///
/// Handles authentication challenges raised by the [ChopperClient].
///
/// Optionally, you can override either [onAuthenticationSuccessful] or
/// [onAuthenticationFailed] in order to listen to when a particular
/// authentication request succeeds or fails.
///
/// For example, you can use these in order to reset or mutate your
/// instance's internal state for the purposes of keeping track of
/// the number of retries made to authenticate a request.
///
/// Furthermore, you can use these callbacks to determine whether
/// your authentication [Request] from [authenticate] actually succeeded
/// or failed.
///
abstract class Authenticator {
///
/// Returns a [Request] that includes credentials to satisfy
/// an authentication challenge received in [response], based on
/// the incoming [request] or optionally, the [originalRequest]
/// (which was not modified with any previous [RequestInterceptor]s).
///
/// Otherwise, return `null` if the challenge cannot be satisfied.
///
FutureOr<Request?> authenticate(
Request request,
Response response, [
Request? originalRequest,
]);

///
/// Optional callback called by [ChopperClient] when the outgoing
/// request from [authenticate] was successful.
///
/// You can use this to determine whether that request actually succeeded
/// in authenticating the user.
///
AuthenticationCallback? get onAuthenticationSuccessful => null;

Check warning on line 53 in chopper/lib/src/authenticator.dart

View check run for this annotation

Codecov / codecov/patch

chopper/lib/src/authenticator.dart#L53

Added line #L53 was not covered by tests

///
/// Optional callback called by [ChopperClient] when the outgoing
/// request from [authenticate] failed to authenticate.
///
/// You can use this to determine whether that request failed to recover
/// the user's session.
///
AuthenticationCallback? get onAuthenticationFailed => null;

Check warning on line 62 in chopper/lib/src/authenticator.dart

View check run for this annotation

Codecov / codecov/patch

chopper/lib/src/authenticator.dart#L62

Added line #L62 was not covered by tests
}
5 changes: 4 additions & 1 deletion chopper/lib/src/base.dart
Original file line number Diff line number Diff line change
Expand Up @@ -320,10 +320,13 @@ base class ChopperClient {
);
// To prevent double call with typed response
if (_responseIsSuccessful(res.statusCode)) {
await authenticator!.onAuthenticationSuccessful
?.call(updatedRequest, res, request);
return _processResponse(res);
} else {
res = await _handleErrorResponse<BodyType, InnerType>(res);

await authenticator!.onAuthenticationFailed
?.call(updatedRequest, res, request);
return _processResponse(res);
}
}
Expand Down
255 changes: 255 additions & 0 deletions chopper/test/authenticator_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ void main() async {
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.get(
Uri(
path: '/test/get',
Expand All @@ -97,6 +98,59 @@ void main() async {
expect(response.statusCode, equals(200));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationSuccessfulCalled, isTrue);

httpClient.close();
});

test('unauthorized total failure', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/get?key=val'),
);
expect(request.method, equals('GET'));
expect(request.headers['foo'], equals('bar'));
expect(request.headers['int'], equals('42'));

if (!authenticated) {
tested['unauthenticated'] = true;
authenticated = true;

return http.Response('unauthorized', 401);
} else {
tested['authenticated'] = true;
expect(request.headers['authorization'], equals('some_fake_token'));
}

return http.Response('Access Denied', 403);
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.get(
Uri(
path: '/test/get',
queryParameters: {'key': 'val'},
),
headers: {'int': '42'},
);

expect(response.body, anyOf(isNull, isEmpty));
expect(response.statusCode, equals(403));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationFailedCalled, isTrue);

httpClient.close();
});
Expand Down Expand Up @@ -177,6 +231,7 @@ void main() async {
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.post(
Uri(
path: '/test/post',
Expand All @@ -193,6 +248,72 @@ void main() async {
expect(response.statusCode, equals(200));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationSuccessfulCalled, isTrue);

httpClient.close();
});

test('unauthorized total failure', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/post?key=val'),
);
expect(request.method, equals('POST'));
expect(request.headers['foo'], equals('bar'));
expect(request.headers['int'], equals('42'));
expect(
request.body,
jsonEncode(
{
'name': 'john',
'surname': 'doe',
},
),
);

if (!authenticated) {
tested['unauthenticated'] = true;
authenticated = true;

return http.Response('unauthorized', 401);
} else {
tested['authenticated'] = true;
expect(request.headers['authorization'], equals('some_fake_token'));
}

return http.Response('Access Denied', 403);
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.post(
Uri(
path: '/test/post',
queryParameters: {'key': 'val'},
),
headers: {'int': '42'},
body: {
'name': 'john',
'surname': 'doe',
},
);

expect(response.body, anyOf(isNull, isEmpty));
expect(response.statusCode, equals(403));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationFailedCalled, isTrue);

httpClient.close();
});
Expand Down Expand Up @@ -273,6 +394,7 @@ void main() async {
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.put(
Uri(
path: '/test/put',
Expand All @@ -289,6 +411,72 @@ void main() async {
expect(response.statusCode, equals(200));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationSuccessfulCalled, isTrue);

httpClient.close();
});

test('unauthorized total failure', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/put?key=val'),
);
expect(request.method, equals('PUT'));
expect(request.headers['foo'], equals('bar'));
expect(request.headers['int'], equals('42'));
expect(
request.body,
jsonEncode(
{
'name': 'john',
'surname': 'doe',
},
),
);

if (!authenticated) {
tested['unauthenticated'] = true;
authenticated = true;

return http.Response('unauthorized', 401);
} else {
tested['authenticated'] = true;
expect(request.headers['authorization'], equals('some_fake_token'));
}

return http.Response('Access Denied', 403);
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.put(
Uri(
path: '/test/put',
queryParameters: {'key': 'val'},
),
headers: {'int': '42'},
body: {
'name': 'john',
'surname': 'doe',
},
);

expect(response.body, anyOf(isNull, isEmpty));
expect(response.statusCode, equals(403));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.onAuthenticationFailedCalled, isTrue);

httpClient.close();
});
Expand Down Expand Up @@ -369,6 +557,7 @@ void main() async {
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.patch(
Uri(
path: '/test/patch',
Expand All @@ -385,6 +574,72 @@ void main() async {
expect(response.statusCode, equals(200));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.onAuthenticationSuccessfulCalled, isTrue);

httpClient.close();
});

test('unauthorized total failure', () async {
final httpClient = MockClient((request) async {
expect(
request.url.toString(),
equals('$baseUrl/test/patch?key=val'),
);
expect(request.method, equals('PATCH'));
expect(request.headers['foo'], equals('bar'));
expect(request.headers['int'], equals('42'));
expect(
request.body,
jsonEncode(
{
'name': 'john',
'surname': 'doe',
},
),
);

if (!authenticated) {
tested['unauthenticated'] = true;
authenticated = true;

return http.Response('unauthorized', 401);
} else {
tested['authenticated'] = true;
expect(request.headers['authorization'], equals('some_fake_token'));
}

return http.Response('Access Denied', 403);
});

final chopper = buildClient(httpClient);
final authenticator = chopper.authenticator as FakeAuthenticator;
final response = await chopper.patch(
Uri(
path: '/test/patch',
queryParameters: {'key': 'val'},
),
headers: {'int': '42'},
body: {
'name': 'john',
'surname': 'doe',
},
);

expect(response.body, anyOf(isNull, isEmpty));
expect(response.statusCode, equals(403));
expect(tested['authenticated'], equals(true));
expect(tested['unauthenticated'], equals(true));
expect(authenticator.capturedRequest,
authenticator.capturedAuthenticateRequest);
expect(authenticator.capturedResponse, response);
expect(authenticator.capturedOriginalRequest,
authenticator.capturedAuthenticateOriginalRequest);
expect(authenticator.onAuthenticationFailedCalled, isTrue);

httpClient.close();
});
Expand Down
Loading