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
26 changes: 25 additions & 1 deletion chopper/lib/src/authenticator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,36 @@ import 'dart:async';

import 'package:chopper/chopper.dart';

/// This method should return a [Request] that includes credentials to satisfy an authentication challenge received in
/// 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.
///
/// Optionally, you can override either [onAuthenticationSuccessful] or
/// [onAuthenticationFailed] in order to listen to when a particular
/// authentication request succeeds or fails. You can also use it 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.
abstract class Authenticator {
FutureOr<Request?> authenticate(
Request request,
Response response, [
Request? originalRequest,
]);

// coverage:ignore-start
techouse marked this conversation as resolved.
Show resolved Hide resolved
FutureOr<void> onAuthenticationSuccessful(
Request request,
Response response, [
Request? originalRequest,
]) {}
techouse marked this conversation as resolved.
Show resolved Hide resolved
// coverage:ignore-end

// coverage:ignore-start
techouse marked this conversation as resolved.
Show resolved Hide resolved
FutureOr<void> onAuthenticationFailed(
Request request,
Response response, [
Request? originalRequest,
]) {}
// coverage:ignore-end
}
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(updatedRequest, res, request);
techouse marked this conversation as resolved.
Show resolved Hide resolved
return _processResponse(res);
} else {
res = await _handleErrorResponse<BodyType, InnerType>(res);

await authenticator!
.onAuthenticationFailed(updatedRequest, res, request);
techouse marked this conversation as resolved.
Show resolved Hide resolved
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