[dart][dart-dio-next] Authentication fixes and improvements (#9975)

* [dart-dio-next] Update http mock test library

* [dart-dio-next] Fix authentication problems

* correctly map authentication by type and scheme
* add new authentication interceptor for `bearer` scheme - this currently does the same as the OAuth interceptor
* add tests for all authentication types except OAuth
* use temporary test library branch until new fixes and features get merge there

* Actually commit the fixed test library dependency

* Update http mock library

* Format :/
This commit is contained in:
Peter Leibiger
2021-07-27 04:04:05 +02:00
committed by GitHub
parent 5fb845a58f
commit 88e22b0a52
24 changed files with 237 additions and 154 deletions

View File

@@ -157,6 +157,7 @@ public class DartDioNextClientCodegen extends AbstractDartCodegen {
final String authFolder = srcFolder + File.separator + "auth";
supportingFiles.add(new SupportingFile("auth/api_key_auth.mustache", authFolder, "api_key_auth.dart"));
supportingFiles.add(new SupportingFile("auth/basic_auth.mustache", authFolder, "basic_auth.dart"));
supportingFiles.add(new SupportingFile("auth/bearer_auth.mustache", authFolder, "bearer_auth.dart"));
supportingFiles.add(new SupportingFile("auth/oauth.mustache", authFolder, "oauth.dart"));
supportingFiles.add(new SupportingFile("auth/auth.mustache", authFolder, "auth.dart"));

View File

@@ -73,7 +73,8 @@ class {{classname}} {
extra: <String, dynamic>{
'secure': <Map<String, String>>[{{^hasAuthMethods}}],{{/hasAuthMethods}}{{#hasAuthMethods}}
{{#authMethods}}{
'type': '{{type}}',
'type': '{{type}}',{{#scheme}}
'scheme': '{{scheme}}',{{/scheme}}
'name': '{{name}}',{{#isApiKey}}
'keyName': '{{keyParamName}}',
'where': '{{#isKeyInQuery}}query{{/isKeyInQuery}}{{#isKeyInHeader}}header{{/isKeyInHeader}}',{{/isApiKey}}

View File

@@ -4,6 +4,7 @@ import 'package:built_value/serializer.dart';
import 'package:{{pubName}}/src/serializers.dart';{{/useBuiltValue}}
import 'package:{{pubName}}/src/auth/api_key_auth.dart';
import 'package:{{pubName}}/src/auth/basic_auth.dart';
import 'package:{{pubName}}/src/auth/bearer_auth.dart';
import 'package:{{pubName}}/src/auth/oauth.dart';
{{#apiInfo}}{{#apis}}import 'package:{{pubName}}/src/api/{{classFilename}}.dart';
{{/apis}}{{/apiInfo}}
@@ -31,6 +32,7 @@ class {{clientName}} {
this.dio.interceptors.addAll([
OAuthInterceptor(),
BasicAuthInterceptor(),
BearerAuthInterceptor(),
ApiKeyAuthInterceptor(),
]);
} else {
@@ -44,6 +46,12 @@ class {{clientName}} {
}
}
void setBearerAuth(String name, String token) {
if (this.dio.interceptors.any((i) => i is BearerAuthInterceptor)) {
(this.dio.interceptors.firstWhere((i) => i is BearerAuthInterceptor) as BearerAuthInterceptor).tokens[name] = token;
}
}
void setBasicAuth(String name, String username, String password) {
if (this.dio.interceptors.any((i) => i is BasicAuthInterceptor)) {
(this.dio.interceptors.firstWhere((i) => i is BasicAuthInterceptor) as BasicAuthInterceptor).authInfo[name] = BasicAuthInfo(username, password);

View File

@@ -8,7 +8,7 @@ class ApiKeyAuthInterceptor extends AuthInterceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final authInfo = getAuthInfo(options, 'apiKey');
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'apiKey');
for (final info in authInfo) {
final authName = info['name'] as String;
final authKeyName = info['keyName'] as String;

View File

@@ -5,16 +5,10 @@ abstract class AuthInterceptor extends Interceptor {
/// Get auth information on given route for the given type.
/// Can return an empty list if type is not present on auth data or
/// if route doesn't need authentication.
List<Map<String, dynamic>> getAuthInfo(RequestOptions route, String type) {
List<Map<String, String>> getAuthInfo(RequestOptions route, bool Function(Map<String, String> secure) handles) {
if (route.extra.containsKey('secure')) {
final auth = route.extra['secure'] as List<Map<String, String>>;
final results = <Map<String, dynamic>>[];
for (final info in auth) {
if (info['type'] == type) {
results.add(info);
}
}
return results;
return auth.where((secure) => handles(secure)).toList();
}
return [];
}

View File

@@ -19,7 +19,7 @@ class BasicAuthInterceptor extends AuthInterceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) {
final metadataAuthInfo = getAuthInfo(options, 'basic');
final metadataAuthInfo = getAuthInfo(options, (secure) => (secure['type'] == 'http' && secure['scheme'] == 'basic') || secure['type'] == 'basic');
for (final info in metadataAuthInfo) {
final authName = info['name'] as String;
final basicAuthInfo = authInfo[authName];

View File

@@ -0,0 +1,23 @@
{{>header}}
import 'package:dio/dio.dart';
import 'package:{{pubName}}/src/auth/auth.dart';
class BearerAuthInterceptor extends AuthInterceptor {
final Map<String, String> tokens = {};
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'http' && secure['scheme'] == 'bearer');
for (final info in authInfo) {
final token = tokens[info['name']];
if (token != null) {
options.headers['Authorization'] = 'Bearer ${token}';
break;
}
}
super.onRequest(options, handler);
}
}

View File

@@ -10,7 +10,7 @@ class OAuthInterceptor extends AuthInterceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) {
final authInfo = getAuthInfo(options, 'oauth');
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'oauth' && secure['type'] == 'oauth2');
for (final info in authInfo) {
final token = tokens[info['name']];
if (token != null) {

View File

@@ -67,6 +67,7 @@ lib/src/api_util.dart
lib/src/auth/api_key_auth.dart
lib/src/auth/auth.dart
lib/src/auth/basic_auth.dart
lib/src/auth/bearer_auth.dart
lib/src/auth/oauth.dart
lib/src/date_serializer.dart
lib/src/model/additional_properties_class.dart

View File

@@ -7,6 +7,7 @@ import 'package:built_value/serializer.dart';
import 'package:openapi/src/serializers.dart';
import 'package:openapi/src/auth/api_key_auth.dart';
import 'package:openapi/src/auth/basic_auth.dart';
import 'package:openapi/src/auth/bearer_auth.dart';
import 'package:openapi/src/auth/oauth.dart';
import 'package:openapi/src/api/another_fake_api.dart';
import 'package:openapi/src/api/default_api.dart';
@@ -38,6 +39,7 @@ class Openapi {
this.dio.interceptors.addAll([
OAuthInterceptor(),
BasicAuthInterceptor(),
BearerAuthInterceptor(),
ApiKeyAuthInterceptor(),
]);
} else {
@@ -51,6 +53,12 @@ class Openapi {
}
}
void setBearerAuth(String name, String token) {
if (this.dio.interceptors.any((i) => i is BearerAuthInterceptor)) {
(this.dio.interceptors.firstWhere((i) => i is BearerAuthInterceptor) as BearerAuthInterceptor).tokens[name] = token;
}
}
void setBasicAuth(String name, String username, String password) {
if (this.dio.interceptors.any((i) => i is BasicAuthInterceptor)) {
(this.dio.interceptors.firstWhere((i) => i is BasicAuthInterceptor) as BasicAuthInterceptor).authInfo[name] = BasicAuthInfo(username, password);

View File

@@ -137,6 +137,7 @@ class FakeApi {
'secure': <Map<String, String>>[
{
'type': 'http',
'scheme': 'signature',
'name': 'http_signature_test',
},
],
@@ -988,6 +989,7 @@ class FakeApi {
'secure': <Map<String, String>>[
{
'type': 'http',
'scheme': 'basic',
'name': 'http_basic_test',
},
],
@@ -1178,6 +1180,7 @@ class FakeApi {
'secure': <Map<String, String>>[
{
'type': 'http',
'scheme': 'bearer',
'name': 'bearer_test',
},
],

View File

@@ -11,7 +11,7 @@ class ApiKeyAuthInterceptor extends AuthInterceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final authInfo = getAuthInfo(options, 'apiKey');
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'apiKey');
for (final info in authInfo) {
final authName = info['name'] as String;
final authKeyName = info['keyName'] as String;

View File

@@ -8,16 +8,10 @@ abstract class AuthInterceptor extends Interceptor {
/// Get auth information on given route for the given type.
/// Can return an empty list if type is not present on auth data or
/// if route doesn't need authentication.
List<Map<String, dynamic>> getAuthInfo(RequestOptions route, String type) {
List<Map<String, String>> getAuthInfo(RequestOptions route, bool Function(Map<String, String> secure) handles) {
if (route.extra.containsKey('secure')) {
final auth = route.extra['secure'] as List<Map<String, String>>;
final results = <Map<String, dynamic>>[];
for (final info in auth) {
if (info['type'] == type) {
results.add(info);
}
}
return results;
return auth.where((secure) => handles(secure)).toList();
}
return [];
}

View File

@@ -22,7 +22,7 @@ class BasicAuthInterceptor extends AuthInterceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) {
final metadataAuthInfo = getAuthInfo(options, 'basic');
final metadataAuthInfo = getAuthInfo(options, (secure) => (secure['type'] == 'http' && secure['scheme'] == 'basic') || secure['type'] == 'basic');
for (final info in metadataAuthInfo) {
final authName = info['name'] as String;
final basicAuthInfo = authInfo[authName];

View File

@@ -0,0 +1,26 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
import 'package:dio/dio.dart';
import 'package:openapi/src/auth/auth.dart';
class BearerAuthInterceptor extends AuthInterceptor {
final Map<String, String> tokens = {};
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) {
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'http' && secure['scheme'] == 'bearer');
for (final info in authInfo) {
final token = tokens[info['name']];
if (token != null) {
options.headers['Authorization'] = 'Bearer ${token}';
break;
}
}
super.onRequest(options, handler);
}
}

View File

@@ -13,7 +13,7 @@ class OAuthInterceptor extends AuthInterceptor {
RequestOptions options,
RequestInterceptorHandler handler,
) {
final authInfo = getAuthInfo(options, 'oauth');
final authInfo = getAuthInfo(options, (secure) => secure['type'] == 'oauth' && secure['type'] == 'oauth2');
for (final info in authInfo) {
final token = tokens[info['name']];
if (token != null) {

View File

@@ -49,14 +49,14 @@ packages:
name: built_collection
url: "https://pub.intern.sk"
source: hosted
version: "5.1.0"
version: "5.0.0"
built_value:
dependency: "direct dev"
description:
name: built_value
url: "https://pub.intern.sk"
source: hosted
version: "8.1.0"
version: "8.0.6"
charcode:
dependency: transitive
description:
@@ -154,7 +154,7 @@ packages:
name: http_mock_adapter
url: "https://pub.intern.sk"
source: hosted
version: "0.2.1"
version: "0.3.2"
http_multi_server:
dependency: transitive
description:
@@ -217,7 +217,7 @@ packages:
name: mockito
url: "https://pub.intern.sk"
source: hosted
version: "5.0.8"
version: "5.0.11"
node_preamble:
dependency: transitive
description:

View File

@@ -11,8 +11,8 @@ dev_dependencies:
built_collection: 5.0.0
built_value: 8.0.6
dio: 4.0.0
http_mock_adapter: 0.2.1
mockito: 5.0.8
http_mock_adapter: 0.3.2
mockito: 5.0.11
openapi:
path: ../petstore_client_lib_fake
test: 1.17.4

View File

@@ -0,0 +1,92 @@
import 'package:dio/dio.dart';
import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:openapi/openapi.dart';
import 'package:test/test.dart';
void main() {
Openapi client;
DioAdapter tester;
setUp(() {
client = Openapi(dio: Dio());
tester = DioAdapter(dio: client.dio);
});
tearDown(() {
tester.close();
});
group('Authentication', () {
test('http_basic', () async {
client.setBasicAuth('http_basic_test', 'foo', 'bar');
tester.onPost(
'/fake',
(server) => server.reply(200, null),
data: {
'number': '1',
'double': '1.1',
'pattern_without_delimiter': 'pattern',
'byte': '1',
},
headers: <String, dynamic>{
'content-type': 'application/x-www-form-urlencoded',
'content-length': Matchers.integer,
'authorization': Matchers.string,
},
);
final response = await client.getFakeApi().testEndpointParameters(
number: 1,
double_: 1.1,
patternWithoutDelimiter: 'pattern',
byte: '1',
);
expect(response.statusCode, 200);
});
test('bearer', () async {
client.setBearerAuth('bearer_test', 'foobar');
tester.onDelete(
'/fake',
(server) => server.reply(200, null),
headers: <String, dynamic>{
'required_boolean_group': 'false',
'authorization': Matchers.pattern('Bearer foobar'),
},
queryParameters: <String, dynamic>{
'required_string_group': 1,
'required_int64_group': 2,
},
);
final response = await client.getFakeApi().testGroupParameters(
requiredStringGroup: 1,
requiredBooleanGroup: false,
requiredInt64Group: 2,
);
expect(response.statusCode, 200);
});
test('api_key', () async {
client.setApiKey('api_key', 'SECRET_API_KEY');
tester.onGet(
'/store/inventory',
(server) => server.reply(200, {
'foo': 999,
}),
headers: <String, dynamic>{
'api_key': 'SECRET_API_KEY',
},
);
final response = await client.getStoreApi().getInventory();
expect(response.statusCode, 200);
});
});
}

View File

@@ -7,27 +7,25 @@ import 'package:http_mock_adapter/http_mock_adapter.dart';
import 'package:openapi/openapi.dart';
import 'package:test/test.dart';
import '../matcher/list_param_matcher.dart';
void main() {
Openapi client;
DioAdapter server;
DioAdapter tester;
setUp(() {
server = DioAdapter();
client = Openapi(dio: Dio()..httpClientAdapter = server);
client = Openapi(dio: Dio());
tester = DioAdapter(dio: client.dio);
});
tearDown(() {
server.close();
tester.close();
});
group(FakeApi, () {
group('testEndpointParameters', () {
test('complete', () async {
server.onPost(
tester.onPost(
'/fake',
(request) => request.reply(200, null),
(server) => server.reply(200, null),
data: {
'number': '3',
'double': '-13.57',
@@ -65,9 +63,9 @@ void main() {
});
test('minimal', () async {
server.onPost(
tester.onPost(
'/fake',
(request) => request.reply(200, null),
(server) => server.reply(200, null),
data: {
'byte': '0',
'double': '-13.57',
@@ -95,21 +93,21 @@ void main() {
test('in body data', () async {
// Not sure if this is correct, we are not sending
// form data in the body but some weird map
server.onGet(
tester.onGet(
'/fake',
(request) => request.reply(200, null),
(server) => server.reply(200, null),
data: {
'enum_form_string': 'formString',
'enum_form_string_array': ListParamMatcher(
expected: ListParam(
'enum_form_string_array': Matchers.listParam<String>(
ListParam(
['foo', 'bar'],
ListFormat.csv,
),
),
},
queryParameters: <String, dynamic>{
'enum_query_string_array': ListParamMatcher(
expected: ListParam(
'enum_query_string_array': Matchers.listParam<String>(
ListParam(
['a', 'b', 'c'],
ListFormat.multi,
),

View File

@@ -6,31 +6,28 @@ import 'package:http_parser/http_parser.dart';
import 'package:openapi/openapi.dart';
import 'package:test/test.dart';
import '../matcher/form_data_matcher.dart';
import '../matcher/list_param_matcher.dart';
void main() {
const photo1 = 'https://localhost/photo1.jpg';
const photo2 = 'https://localhost/photo2.jpg';
Openapi client;
DioAdapter server;
DioAdapter tester;
setUp(() {
server = DioAdapter();
client = Openapi(dio: Dio()..httpClientAdapter = server);
client = Openapi(dio: Dio());
tester = DioAdapter(dio: client.dio);
});
tearDown(() {
server.close();
tester.close();
});
group(PetApi, () {
group('getPetById', () {
test('complete', () async {
server.onGet(
tester.onGet(
'/pet/5',
(request) => request.reply(200, {
(server) => server.reply(200, {
'id': 5,
'name': 'Paula',
'status': 'sold',
@@ -53,9 +50,6 @@ void main() {
},
]
}),
headers: {
Headers.contentTypeHeader: Matchers.pattern('application/json'),
},
);
final response = await client.getPetApi().getPetById(petId: 5);
@@ -72,16 +66,13 @@ void main() {
});
test('minimal', () async {
server.onGet(
tester.onGet(
'/pet/5',
(request) => request.reply(200, {
(server) => server.reply(200, {
'id': 5,
'name': 'Paula',
'photoUrls': <String>[],
}),
headers: {
Headers.contentTypeHeader: Matchers.pattern('application/json'),
},
);
final response = await client.getPetApi().getPetById(petId: 5);
@@ -99,9 +90,9 @@ void main() {
group('addPet', () {
test('complete', () async {
server.onPost(
tester.onPost(
'/pet',
(request) => request.reply(200, ''),
(server) => server.reply(200, ''),
data: {
'id': 5,
'name': 'Paula',
@@ -125,7 +116,7 @@ void main() {
},
]
},
headers: {
headers: <String, dynamic>{
Headers.contentTypeHeader: Matchers.pattern('application/json'),
Headers.contentLengthHeader: Matchers.integer,
},
@@ -153,15 +144,15 @@ void main() {
});
test('minimal', () async {
server.onPost(
tester.onPost(
'/pet',
(request) => request.reply(200, ''),
(server) => server.reply(200, ''),
data: {
'id': 5,
'name': 'Paula',
'photoUrls': <String>[],
},
headers: {
headers: <String, dynamic>{
Headers.contentTypeHeader: Matchers.pattern('application/json'),
Headers.contentLengthHeader: Matchers.integer,
},
@@ -178,9 +169,9 @@ void main() {
group('getMultiplePets', () {
test('findByStatus', () async {
server.onRoute(
tester.onRoute(
'/pet/findByStatus',
(request) => request.reply(200, [
(server) => server.reply(200, [
{
'id': 5,
'name': 'Paula',
@@ -197,16 +188,13 @@ void main() {
request: Request(
method: RequestMethods.get,
queryParameters: <String, dynamic>{
'status': ListParamMatcher<dynamic>(
expected: ListParam<String>(
'status': Matchers.listParam<String>(
ListParam(
['available', 'sold'],
ListFormat.csv,
),
),
},
headers: <String, dynamic>{
Headers.contentTypeHeader: Matchers.pattern('application/json'),
},
),
);
@@ -237,9 +225,9 @@ void main() {
contentType: MediaType.parse('image/png'),
);
server.onRoute(
tester.onRoute(
'/fake/5/uploadImageWithRequiredFile',
(request) => request.reply(200, {
(server) => server.reply(200, {
'code': 200,
'type': 'success',
'message': 'File uploaded',
@@ -251,8 +239,8 @@ void main() {
Matchers.pattern('multipart/form-data'),
Headers.contentLengthHeader: Matchers.integer,
},
data: FormDataMatcher(
expected: FormData.fromMap(<String, dynamic>{
data: Matchers.formData(
FormData.fromMap(<String, dynamic>{
r'requiredFile': file,
}),
),
@@ -274,9 +262,9 @@ void main() {
contentType: MediaType.parse('image/png'),
);
server.onRoute(
tester.onRoute(
'/fake/3/uploadImageWithRequiredFile',
(request) => request.reply(200, {
(server) => server.reply(200, {
'code': 200,
'type': 'success',
'message': 'File uploaded',
@@ -288,8 +276,8 @@ void main() {
Matchers.pattern('multipart/form-data'),
Headers.contentLengthHeader: Matchers.integer,
},
data: FormDataMatcher(
expected: FormData.fromMap(<String, dynamic>{
data: Matchers.formData(
FormData.fromMap(<String, dynamic>{
'additionalMetadata': 'foo',
r'requiredFile': file,
}),

View File

@@ -5,41 +5,33 @@ import 'package:test/test.dart';
void main() {
Openapi client;
DioAdapter server;
DioAdapter tester;
setUp(() {
server = DioAdapter();
client = Openapi(dio: Dio()..httpClientAdapter = server);
client = Openapi(dio: Dio());
tester = DioAdapter(dio: client.dio);
});
tearDown(() {
server.close();
tester.close();
});
group(StoreApi, () {
group('getInventory', () {
test('with API key', () async {
client.setApiKey('api_key', 'SECRET_API_KEY');
test('getInventory', () async {
tester.onGet(
'/store/inventory',
(server) => server.reply(200, {
'foo': 5,
'bar': 999,
'baz': 0,
}),
);
server.onGet(
'/store/inventory',
(request) => request.reply(200, {
'foo': 5,
'bar': 999,
'baz': 0,
}),
headers: <String, dynamic>{
Headers.contentTypeHeader: Matchers.pattern('application/json'),
'api_key': 'SECRET_API_KEY',
},
);
final response = await client.getStoreApi().getInventory();
final response = await client.getStoreApi().getInventory();
expect(response.statusCode, 200);
expect(response.data, isNotNull);
expect(response.data.length, 3);
});
expect(response.statusCode, 200);
expect(response.data, isNotNull);
expect(response.data.length, 3);
});
});
}

View File

@@ -1,26 +0,0 @@
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import 'package:http_mock_adapter/src/matchers/matcher.dart';
class FormDataMatcher extends Matcher {
final FormData expected;
const FormDataMatcher({@required this.expected});
@override
bool matches(dynamic actual) {
if (actual is! FormData) {
return false;
}
final data = actual as FormData;
return MapEquality<String, String>().equals(
Map.fromEntries(expected.fields),
Map.fromEntries(data.fields),
) &&
MapEquality<String, MultipartFile>().equals(
Map.fromEntries(expected.files),
Map.fromEntries(data.files),
);
}
}

View File

@@ -1,20 +0,0 @@
import 'package:dio/src/parameter.dart';
import 'package:meta/meta.dart';
import 'package:collection/collection.dart';
import 'package:http_mock_adapter/src/matchers/matcher.dart';
class ListParamMatcher<T> extends Matcher {
final ListParam<T> expected;
const ListParamMatcher({@required this.expected});
@override
bool matches(dynamic actual) {
return actual is ListParam<T> &&
ListEquality<T>().equals(
actual.value,
expected.value,
) &&
actual.format == expected.format;
}
}