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

Dynamic proxy url #20

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
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
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,32 @@ final client = UnsplashClient(
> :warning: When you are done using a client instance, make sure to call it's
> `close` method.

### 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 to your proxy URL 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.

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
Expand Down
2 changes: 1 addition & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ void main(List<String> 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]
Expand Down
66 changes: 35 additions & 31 deletions lib/src/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,20 @@ import 'users.dart';
class ClientSettings {
/// Creates new [ClientSettings].
///
/// [credentials] must not be `null`.
/// [unsplashCredentials] must not be `null` if using no proxy URL.
/// [bearerToken] must not be 'null' if using a proxy that requires authentication
const ClientSettings({
required this.credentials,
this.unsplashCredentials,
this.bearerToken,
this.debug = false,
this.loggerName = 'unsplash_client',
});

/// The credentials used by the [UnsplashClient] to authenticate the app.
final AppCredentials credentials;
/// These can be null in case a proxy for authentication is being used
final AppCredentials? unsplashCredentials;

final String? bearerToken;

/// Whether to log debug information.
@Deprecated(
Expand All @@ -47,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}';
}
}
Expand All @@ -71,17 +78,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;
Expand Down Expand Up @@ -206,9 +215,7 @@ class Request<T> {
// 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);
}

Expand Down Expand Up @@ -259,7 +266,11 @@ class Request<T> {
// Auth
// TODO implement oauth
assert(isPublicAction);
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;
}
Expand All @@ -277,11 +288,7 @@ class Request<T> {

@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() {
Expand Down Expand Up @@ -361,12 +368,7 @@ class Response<T> {

@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() {
Expand All @@ -388,6 +390,10 @@ Map<String, String> _publicActionAuthHeader(AppCredentials credentials) {
return {'Authorization': 'Client-ID ${credentials.accessKey}'};
}

Map<String, String> _bearerTokenAuthHeader(String bearerToken) {
return {'Authorization': 'Bearer $bearerToken'};
}

Map<String, String> _sanitizeHeaders(Map<String, String> headers) {
return headers.map((key, value) {
final isAuthorization = key.toLowerCase() == 'authorization';
Expand All @@ -410,7 +416,5 @@ ${_printHeaders(response.headers)}
}

String _printHeaders(Map<String, String> headers) {
return headers.entries
.map((header) => '${header.key}: ${header.value}')
.join('\n');
return headers.entries.map((header) => '${header.key}: ${header.value}').join('\n');
}
48 changes: 45 additions & 3 deletions test/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ 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(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand All @@ -19,4 +18,47 @@ void main() {

expect(req.go, throwsStateError);
});

test('standard client functions work without app credentials as expected', () {
final clientWithUnsplashCredentials = UnsplashClient(
settings: const ClientSettings(
unsplashCredentials: AppCredentials(
secretKey: '1234',
accessKey: '5678',
),
),
);

final clientWithoutUnsplashCredentials = UnsplashClient(
settings: const ClientSettings(bearerToken: '1234'),
);

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 = clientWithUnsplashCredentials.settings.hashCode;
expect(clientWithSettingsHashCode == 1043090417, true);

var clientWithoutUnsplashCredentialsHashCode = clientWithoutUnsplashCredentials.settings.hashCode;
expect(clientWithoutUnsplashCredentialsHashCode == 759431771, true);

var clientWithUnsplashCredentialsString = clientWithUnsplashCredentials.settings.toString();
expect(
clientWithUnsplashCredentialsString ==
'ClientSettings{credentials: Credentials{accessKey: 5678, '
'secretKey: HIDDEN}, '
'bearerToken: null, '
'maxPageSize: 30}',
true);

var clientWithoutUnsplashCredentialsString = clientWithoutUnsplashCredentials.settings.toString();
expect(
clientWithoutUnsplashCredentialsString ==
'ClientSettings{credentials: null, '
'bearerToken: 1234, '
'maxPageSize: 30}',
true);
});
}
2 changes: 1 addition & 1 deletion test/collections_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down
21 changes: 7 additions & 14 deletions test/integration/integration_test_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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()),
);
});
Expand Down
2 changes: 1 addition & 1 deletion test/photos_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down
2 changes: 1 addition & 1 deletion test/search_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down
2 changes: 1 addition & 1 deletion test/stats_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down
2 changes: 1 addition & 1 deletion test/topics_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down
2 changes: 1 addition & 1 deletion test/users_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import 'test_utils.dart';
void main() {
final client = UnsplashClient(
settings: ClientSettings(
credentials: AppCredentials(
unsplashCredentials: AppCredentials(
secretKey: '',
accessKey: '',
),
Expand Down