From e41848d8aac9a8c9894276194d9a770c092dbd0a Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 10:43:35 +0200 Subject: [PATCH 1/6] Added proxy URL functionality --- lib/src/client.dart | 45 ++++++++++++++++++--------------------------- 1 file changed, 18 insertions(+), 27 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index cebab1a..d3c6e1f 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -20,13 +20,13 @@ class ClientSettings { /// /// [credentials] must not be `null`. const ClientSettings({ - required this.credentials, + this.credentials, this.debug = false, this.loggerName = 'unsplash_client', }); /// The credentials used by the [UnsplashClient] to authenticate the app. - final AppCredentials credentials; + final AppCredentials? credentials; /// Whether to log debug information. @Deprecated( @@ -71,17 +71,19 @@ class UnsplashClient { /// [settings] must not be `null`. /// /// If no [httpClient] is provided, one is created. - UnsplashClient({ - required this.settings, - http.Client? httpClient, - }) : _http = httpClient ?? http.Client(), - logger = Logger(settings.loggerName); + /// + /// If no [proxyBaseUrl] is provided the Unsplash standard base URL + /// https://api.unsplash.com/ is used + UnsplashClient({required this.settings, http.Client? httpClient, Uri? proxyBaseUrl}) + : _http = httpClient ?? http.Client(), + logger = Logger(settings.loggerName), + baseUrl = proxyBaseUrl ?? Uri.parse('https://api.unsplash.com/'); /// The [Logger] used by this instance. final Logger logger; - /// The base url of the unsplash api. - final Uri baseUrl = Uri.parse('https://api.unsplash.com/'); + /// The base url of the unsplash api or its proxy. + final Uri baseUrl; /// The [ClientSettings] used by this client. final ClientSettings settings; @@ -206,9 +208,7 @@ class Request { // ignore: avoid_catches_without_on_clauses } - if (httpResponse.statusCode < 400 && - json != null && - bodyDeserializer != null) { + if (httpResponse.statusCode < 400 && json != null && bodyDeserializer != null) { data = bodyDeserializer!(json); } @@ -259,7 +259,9 @@ class Request { // Auth // TODO implement oauth assert(isPublicAction); - headers.addAll(_publicActionAuthHeader(client.settings.credentials)); + if (client.settings.credentials != null) { + headers.addAll(_publicActionAuthHeader(client.settings.credentials!)); + } return headers; } @@ -277,11 +279,7 @@ class Request { @override int get hashCode => - client.hashCode ^ - httpRequest.hashCode ^ - jsonBody.hashCode ^ - isPublicAction.hashCode ^ - bodyDeserializer.hashCode; + client.hashCode ^ httpRequest.hashCode ^ jsonBody.hashCode ^ isPublicAction.hashCode ^ bodyDeserializer.hashCode; @override String toString() { @@ -361,12 +359,7 @@ class Response { @override int get hashCode => - request.hashCode ^ - httpRequest.hashCode ^ - httpResponse.hashCode ^ - body.hashCode ^ - json.hashCode ^ - data.hashCode; + request.hashCode ^ httpRequest.hashCode ^ httpResponse.hashCode ^ body.hashCode ^ json.hashCode ^ data.hashCode; @override String toString() { @@ -410,7 +403,5 @@ ${_printHeaders(response.headers)} } String _printHeaders(Map headers) { - return headers.entries - .map((header) => '${header.key}: ${header.value}') - .join('\n'); + return headers.entries.map((header) => '${header.key}: ${header.value}').join('\n'); } From a6a4721b851c690158f0110cfe4252ae73aa1218 Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 13:04:50 +0200 Subject: [PATCH 2/6] Added test to ensure functionality works as expected without credentials --- lib/src/client.dart | 3 ++- test/client_test.dart | 44 +++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/lib/src/client.dart b/lib/src/client.dart index d3c6e1f..84bff5f 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -18,7 +18,7 @@ import 'users.dart'; class ClientSettings { /// Creates new [ClientSettings]. /// - /// [credentials] must not be `null`. + /// [credentials] must not be `null` if using no proxy URL. const ClientSettings({ this.credentials, this.debug = false, @@ -26,6 +26,7 @@ class ClientSettings { }); /// The credentials used by the [UnsplashClient] to authenticate the app. + /// These can be null in case a proxy for authentication is being used final AppCredentials? credentials; /// Whether to log debug information. diff --git a/test/client_test.dart b/test/client_test.dart index 60e9306..da673cb 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -2,8 +2,7 @@ import 'package:test/test.dart'; import 'package:unsplash_client/unsplash_client.dart'; void main() { - test('executing requests after client has been closed should throw', - () async { + test('executing requests after client has been closed should throw', () async { final client = UnsplashClient( settings: ClientSettings( credentials: AppCredentials( @@ -19,4 +18,45 @@ void main() { expect(req.go, throwsStateError); }); + + test('standard client functions work without app credentials as expected', () { + final clientWithSettings = UnsplashClient( + settings: const ClientSettings( + credentials: AppCredentials( + secretKey: '1234', + accessKey: '5678', + ), + ), + ); + + final clientWithoutSettings = UnsplashClient( + settings: const ClientSettings(), + ); + + expect(clientWithSettings.settings == clientWithSettings.settings, true); + expect(clientWithSettings.settings == clientWithoutSettings.settings, false); + expect(clientWithoutSettings.settings == clientWithSettings.settings, false); + expect(clientWithoutSettings.settings == clientWithoutSettings.settings, true); + + var clientWithSettingsHashCode = clientWithSettings.settings.hashCode; + expect(clientWithSettingsHashCode == 1043090474, true); + + var clientWithoutSettingsHashCode = clientWithoutSettings.settings.hashCode; + expect(clientWithoutSettingsHashCode == 346277, true); + + var clientWithSettingsString = clientWithSettings.settings.toString(); + expect( + clientWithSettingsString == + 'ClientSettings{credentials: Credentials{accessKey: 5678, ' + 'secretKey: HIDDEN}, ' + 'maxPageSize: 30}', + true); + + var clientWithoutSettingsString = clientWithoutSettings.settings.toString(); + expect( + clientWithoutSettingsString == + 'ClientSettings{credentials: null, ' + 'maxPageSize: 30}', + true); + }); } From 288442593a641eeb02b259953cea1abc14ddcfbf Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 13:09:53 +0200 Subject: [PATCH 3/6] Added section on authentication proxy to readme --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 5d86542..cff9acc 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,20 @@ final client = UnsplashClient( > :warning: When you are done using a client instance, make sure to call it's > `close` method. +### Moving authentication to an authentication proxy + +If you would like to use an authentication proxy and want to change the URL of the +Unsplash API endpoint you can use the following code: + +```dart +final client = UnsplashClient( + settings: const ClientSettings()), + proxyBaseUrl: Uri.parse('YOUR_PROXY_URL')); +); +``` +You can omit the `credentials` parameter and provide your own endpoint by +using the `proxyBaseUrl` parameter. + ## Get a random photo ```dart From 712f578205f9a2b5dcd5f8a6d3937cca3c69a9b0 Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 14:59:18 +0200 Subject: [PATCH 4/6] Added functionality to allow bearer token authentication --- README.md | 16 ++++++++++++++-- lib/src/client.dart | 28 ++++++++++++++++++++-------- test/client_test.dart | 36 +++++++++++++++++++----------------- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index cff9acc..7d3d7a6 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ final client = UnsplashClient( > :warning: When you are done using a client instance, make sure to call it's > `close` method. -### Moving authentication to an authentication proxy +### Moving Unsplash authentication to an authentication proxy If you would like to use an authentication proxy and want to change the URL of the -Unsplash API endpoint you can use the following code: +Unsplash API endpoint to your proxy URL you can use the following code: ```dart final client = UnsplashClient( @@ -64,6 +64,18 @@ final client = UnsplashClient( You can omit the `credentials` parameter and provide your own endpoint by using the `proxyBaseUrl` parameter. +If your authentication proxy requires authentication as well you can authenticate using a `bearer token` like so: +```dart +final client = UnsplashClient( + settings: const ClientSettings(bearerToken: 'YOUR_BEARER_TOKEN')), + proxyBaseUrl: Uri.parse('YOUR_PROXY_URL')); +); +``` +The bearer token will be assembled to a HTTP header like so: +``` +Authorization: Bearer YOUR_BEARER_TOKEN +``` + ## Get a random photo ```dart diff --git a/lib/src/client.dart b/lib/src/client.dart index 84bff5f..cf0b0f9 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -18,16 +18,20 @@ import 'users.dart'; class ClientSettings { /// Creates new [ClientSettings]. /// - /// [credentials] must not be `null` if using no proxy URL. + /// [unsplashCredentials] must not be `null` if using no proxy URL. + /// [bearerToken] must not be 'null' if using a proxy that requires authentication const ClientSettings({ - this.credentials, + this.unsplashCredentials, + this.bearerToken, this.debug = false, this.loggerName = 'unsplash_client', }); /// The credentials used by the [UnsplashClient] to authenticate the app. /// These can be null in case a proxy for authentication is being used - final AppCredentials? credentials; + final AppCredentials? unsplashCredentials; + + final String? bearerToken; /// Whether to log debug information. @Deprecated( @@ -48,15 +52,17 @@ class ClientSettings { identical(this, other) || other is ClientSettings && runtimeType == other.runtimeType && - credentials == other.credentials && + unsplashCredentials == other.unsplashCredentials && + bearerToken == other.bearerToken && maxPageSize == other.maxPageSize; @override - int get hashCode => credentials.hashCode ^ maxPageSize.hashCode; + int get hashCode => unsplashCredentials.hashCode ^ bearerToken.hashCode ^ maxPageSize.hashCode; @override String toString() { - return 'ClientSettings{credentials: $credentials, ' + return 'ClientSettings{credentials: $unsplashCredentials, ' + 'bearerToken: $bearerToken, ' 'maxPageSize: $maxPageSize}'; } } @@ -260,8 +266,10 @@ class Request { // Auth // TODO implement oauth assert(isPublicAction); - if (client.settings.credentials != null) { - headers.addAll(_publicActionAuthHeader(client.settings.credentials!)); + if (client.settings.unsplashCredentials != null) { + headers.addAll(_publicActionAuthHeader(client.settings.unsplashCredentials!)); + } else if (client.settings.bearerToken != null) { + headers.addAll(_bearerTokenAuthHeader(client.settings.bearerToken!)); } return headers; @@ -382,6 +390,10 @@ Map _publicActionAuthHeader(AppCredentials credentials) { return {'Authorization': 'Client-ID ${credentials.accessKey}'}; } +Map _bearerTokenAuthHeader(String bearerToken) { + return {'Authorization': 'Bearer $bearerToken'}; +} + Map _sanitizeHeaders(Map headers) { return headers.map((key, value) { final isAuthorization = key.toLowerCase() == 'authorization'; diff --git a/test/client_test.dart b/test/client_test.dart index da673cb..f71a683 100644 --- a/test/client_test.dart +++ b/test/client_test.dart @@ -5,7 +5,7 @@ void main() { test('executing requests after client has been closed should throw', () async { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), @@ -20,42 +20,44 @@ void main() { }); test('standard client functions work without app credentials as expected', () { - final clientWithSettings = UnsplashClient( + final clientWithUnsplashCredentials = UnsplashClient( settings: const ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '1234', accessKey: '5678', ), ), ); - final clientWithoutSettings = UnsplashClient( - settings: const ClientSettings(), + final clientWithoutUnsplashCredentials = UnsplashClient( + settings: const ClientSettings(bearerToken: '1234'), ); - expect(clientWithSettings.settings == clientWithSettings.settings, true); - expect(clientWithSettings.settings == clientWithoutSettings.settings, false); - expect(clientWithoutSettings.settings == clientWithSettings.settings, false); - expect(clientWithoutSettings.settings == clientWithoutSettings.settings, true); + expect(clientWithUnsplashCredentials.settings == clientWithUnsplashCredentials.settings, true); + expect(clientWithUnsplashCredentials.settings == clientWithoutUnsplashCredentials.settings, false); + expect(clientWithoutUnsplashCredentials.settings == clientWithUnsplashCredentials.settings, false); + expect(clientWithoutUnsplashCredentials.settings == clientWithoutUnsplashCredentials.settings, true); - var clientWithSettingsHashCode = clientWithSettings.settings.hashCode; - expect(clientWithSettingsHashCode == 1043090474, true); + var clientWithSettingsHashCode = clientWithUnsplashCredentials.settings.hashCode; + expect(clientWithSettingsHashCode == 1043090417, true); - var clientWithoutSettingsHashCode = clientWithoutSettings.settings.hashCode; - expect(clientWithoutSettingsHashCode == 346277, true); + var clientWithoutUnsplashCredentialsHashCode = clientWithoutUnsplashCredentials.settings.hashCode; + expect(clientWithoutUnsplashCredentialsHashCode == 759431771, true); - var clientWithSettingsString = clientWithSettings.settings.toString(); + var clientWithUnsplashCredentialsString = clientWithUnsplashCredentials.settings.toString(); expect( - clientWithSettingsString == + clientWithUnsplashCredentialsString == 'ClientSettings{credentials: Credentials{accessKey: 5678, ' 'secretKey: HIDDEN}, ' + 'bearerToken: null, ' 'maxPageSize: 30}', true); - var clientWithoutSettingsString = clientWithoutSettings.settings.toString(); + var clientWithoutUnsplashCredentialsString = clientWithoutUnsplashCredentials.settings.toString(); expect( - clientWithoutSettingsString == + clientWithoutUnsplashCredentialsString == 'ClientSettings{credentials: null, ' + 'bearerToken: 1234, ' 'maxPageSize: 30}', true); }); From f763067b69127aa5a122d3239689d5c8c52c9236 Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 15:02:15 +0200 Subject: [PATCH 5/6] Fixed usage of unsplashCredentials parameter --- test/integration/integration_test_utils.dart | 21 +++++++------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/test/integration/integration_test_utils.dart b/test/integration/integration_test_utils.dart index 44062eb..2f0076f 100644 --- a/test/integration/integration_test_utils.dart +++ b/test/integration/integration_test_utils.dart @@ -15,8 +15,7 @@ void setupIntegrationTests(String name) { } _hasSetupIntegrationTests = true; - _recordedExpectations = - File.fromUri(_recordedExpectationsDir.uri.resolve('$name.json')); + _recordedExpectations = File.fromUri(_recordedExpectationsDir.uri.resolve('$name.json')); _setupRecordedExpectationsTestHooks(); _setupTestClientTestHooks(); @@ -26,11 +25,9 @@ bool get isCI => Platform.environment['CI'] != null; // === Recorded Expectations =================================================== -bool get _updateRecordedExpectations => - Platform.environment['UPDATE_RECORDED_EXPECTATIONS'] != null; +bool get _updateRecordedExpectations => Platform.environment['UPDATE_RECORDED_EXPECTATIONS'] != null; -final _recordedExpectationsDir = - Directory('test/fixtures/recorded_expectations'); +final _recordedExpectationsDir = Directory('test/fixtures/recorded_expectations'); late File _recordedExpectations; late MockServer mockServer; @@ -49,8 +46,7 @@ void _setupRecordedExpectationsTestHooks() { 'There are no recorded expectations.', ); - final recordedExpectations = - decodeExpectations(await _recordedExpectations.readAsString()); + final recordedExpectations = decodeExpectations(await _recordedExpectations.readAsString()); // If the content of `User-Agent` changes we can still use the recorded // expectations, since the header does not change the expected response. @@ -87,8 +83,7 @@ void _setupRecordedExpectationsTestHooks() { await _recordedExpectations.parent.create(recursive: true); final jsonEncoder = const JsonEncoder.withIndent(' '); - await _recordedExpectations - .writeAsString(jsonEncoder.convert(recordedExpectations)); + await _recordedExpectations.writeAsString(jsonEncoder.convert(recordedExpectations)); } mockServer.close(); @@ -141,12 +136,10 @@ void _setupTestClientTestHooks() { setUpAll(() async { // In CI we run tests always against recorded responses and need not // credentials. - final credentials = isCI - ? AppCredentials(accessKey: '', secretKey: '') - : await getTestAppCredentials(); + final credentials = isCI ? AppCredentials(accessKey: '', secretKey: '') : await getTestAppCredentials(); client = UnsplashClient( - settings: ClientSettings(credentials: credentials), + settings: ClientSettings(unsplashCredentials: credentials), httpClient: IOClient(mockServer.createProxiedHttpClient()), ); }); From faa590ff67bd2f851a8d67bde826e0bec7880235 Mon Sep 17 00:00:00 2001 From: Oliver Gesch Date: Thu, 27 Jul 2023 15:02:28 +0200 Subject: [PATCH 6/6] Fixed usage of unsplashCredentials parameter --- example/lib/main.dart | 2 +- test/collections_test.dart | 2 +- test/photos_test.dart | 2 +- test/search_test.dart | 2 +- test/stats_test.dart | 2 +- test/topics_test.dart | 2 +- test/users_test.dart | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index adbdaa0..a73db1d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -17,7 +17,7 @@ void main(List args) async { // Create a client. final client = UnsplashClient( - settings: ClientSettings(credentials: appCredentials), + settings: ClientSettings(unsplashCredentials: appCredentials), ); // Fetch 5 random photos by calling `goAndGet` to execute the [Request] diff --git a/test/collections_test.dart b/test/collections_test.dart index 224e12b..c533f77 100644 --- a/test/collections_test.dart +++ b/test/collections_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/photos_test.dart b/test/photos_test.dart index a5aeebd..3365ede 100644 --- a/test/photos_test.dart +++ b/test/photos_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/search_test.dart b/test/search_test.dart index 6ec71b1..56d99ed 100644 --- a/test/search_test.dart +++ b/test/search_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/stats_test.dart b/test/stats_test.dart index 4c8ec02..330807f 100644 --- a/test/stats_test.dart +++ b/test/stats_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/topics_test.dart b/test/topics_test.dart index 92bf458..dcff861 100644 --- a/test/topics_test.dart +++ b/test/topics_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ), diff --git a/test/users_test.dart b/test/users_test.dart index 63050b0..65eb46e 100644 --- a/test/users_test.dart +++ b/test/users_test.dart @@ -6,7 +6,7 @@ import 'test_utils.dart'; void main() { final client = UnsplashClient( settings: ClientSettings( - credentials: AppCredentials( + unsplashCredentials: AppCredentials( secretKey: '', accessKey: '', ),