diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml
index 2363032..b75513e 100644
--- a/.github/workflows/dart.yml
+++ b/.github/workflows/dart.yml
@@ -24,8 +24,8 @@ jobs:
run: dart pub get
# Uncomment this step to verify the use of 'dart format' on each commit.
- # - name: Verify formatting
- # run: dart format --output=none --set-exit-if-changed .
+ - name: Verify formatting
+ run: dart format --output=none --set-exit-if-changed .
# Consider passing '--fatal-infos' for slightly stricter analysis.
- name: Analyze project source
@@ -36,3 +36,12 @@ jobs:
# want to change this to 'flutter test'.
- name: Run tests
run: dart test
+
+ - name: Generate coverage test
+ run: source scripts/coverage_helper.sh aws_request
+
+ - name: Collect coverage
+ run: dart test --coverage .
+ - uses: codecov/codecov-action@v1.0.2
+ with:
+ token: ${{ secrets.CODECOV_TOKEN }}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 54566c3..c320c0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -72,3 +72,5 @@ build/
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
+*.json
+test/coverage_helper_test.dart
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..3749f7c
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1 @@
+Zachary Merritt
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0a11156..19f38c0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+## [0.3.0] - 2022/01/08
+
+* Migrated from `universal_io` to `http`
+* Refactored project into discrete testable modules
+* Added unit tests for each piece
+* Added MockAwsRequest to mock requests for easier testing
+* Added AUTHORS file
+* Added static version of primary method
+* Updated documentation to illustrate new static call method
+* Added coverage
+* Fixed bug with allowing non String values in queryString
+
## [0.2.1] - 2022/01/08
* Fixed issue with rejected headers on web
diff --git a/README.md b/README.md
index b14a2b1..1c785fb 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,6 @@
-
-
-
-
-
-
@@ -21,6 +15,9 @@
+
+
+
@@ -79,38 +76,52 @@ headers: any required headers. Any non-default headers included in the signedHea
must be added here.
jsonBody: the body of the request, formatted as json
queryPath: the aws query path
-queryString: the aws query string, formatted like ('abc=123&def=456'). Must be url encoded
+queryString: the url query string as a Map
~~~
Supported HTTP methods are get, post, delete, patch, put.
-## Examples
+## Example 1
Here's an example of using aws_request to send a CloudWatch PutLogEvent request:
~~~dart
import 'package:aws_request/aws_request.dart';
-import 'dart:io';
+import 'package:http/http.dart';
-void sendCloudWatchLog(String logString) async {
+void awsRequestFunction(String logString) async {
AwsRequest request = new AwsRequest('awsAccessKey', 'awsSecretKey', 'region');
- String body = """
- {"logEvents":
- [{
- "timestamp":${DateTime
- .now()
- .toUtc()
- .millisecondsSinceEpoch},
- "message":"$logString"
- }],
- "logGroupName":"ExampleLogGroupName",
- "logStreamName":"ExampleLogStreamName"
- }""";
- HttpClientResponse result = await request.send(
+ Response result = await request.send(
AwsRequestType.POST,
- jsonBody: body,
+ jsonBody: "{'jsonKey': 'jsonValue'}",
+ target: 'Logs_20140328.PutLogEvents',
+ service: 'logs',
+ queryString: {'X-Amz-Expires': '10'},
+ headers: {'X-Amz-Security-Token': 'XXXXXXXXXXXX'},
+ );
+}
+~~~
+
+## Example 2
+
+There is also a static method if you find that more useful:
+
+~~~dart
+import 'package:aws_request/aws_request.dart';
+import 'package:http/http.dart';
+
+void awsRequestFunction(String logString) async {
+
+ Response result = await AwsRequest.staticSend(
+ awsAccessKey: 'awsAccessKey',
+ awsSecretKey: 'awsSecretKey',
+ region: 'region',
+ type: AwsRequestType.POST,
+ jsonBody: "{'jsonKey': 'jsonValue'}",
target: 'Logs_20140328.PutLogEvents',
service: 'logs',
+ queryString: {'X-Amz-Expires': '10'},
+ headers: {'X-Amz-Security-Token': 'XXXXXXXXXXXX'},
);
}
~~~
diff --git a/example/aws_request.dart b/example/aws_request.dart
index e116731..b6a7ec7 100644
--- a/example/aws_request.dart
+++ b/example/aws_request.dart
@@ -1,20 +1,14 @@
import 'package:aws_request/aws_request.dart';
+import 'package:http/http.dart';
-void sendCloudWatchLog(String logString) async {
+void awsRequestFunction(String logString) async {
AwsRequest request = new AwsRequest('awsAccessKey', 'awsSecretKey', 'region');
- String body = """
- {"logEvents":
- [{
- "timestamp":${DateTime.now().toUtc().millisecondsSinceEpoch},
- "message":"$logString"
- }],
- "logGroupName":"ExampleLogGroupName",
- "logStreamName":"ExampleLogStreamName"
- }""";
- await request.send(
+ Response result = await request.send(
AwsRequestType.POST,
- jsonBody: body,
- target: 'Logs_XXXXXXXX.PutLogEvents',
+ jsonBody: "{'jsonKey': 'jsonValue'}",
+ target: 'Logs_20140328.PutLogEvents',
service: 'logs',
+ queryString: {'X-Amz-Expires': '10'},
+ headers: {'X-Amz-Security-Token': 'XXXXXXXXXXXX'},
);
}
diff --git a/example/pubspec.lock b/example/pubspec.lock
index e17fdea..8c37bf2 100644
--- a/example/pubspec.lock
+++ b/example/pubspec.lock
@@ -14,7 +14,7 @@ packages:
name: aws_request
url: "https://pub.dartlang.org"
source: hosted
- version: "0.2.1"
+ version: "0.3.0"
boolean_selector:
dependency: transitive
description:
@@ -74,6 +74,20 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
+ http:
+ dependency: "direct main"
+ description:
+ name: http
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.13.4"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "4.0.0"
intl:
dependency: transitive
description:
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
index a51ae23..0ebe937 100644
--- a/example/pubspec.yaml
+++ b/example/pubspec.yaml
@@ -9,7 +9,8 @@ dependencies:
flutter:
sdk: flutter
- aws_request: 0.2.1
+ aws_request: 0.3.0
+ http: ^0.13.0
dev_dependencies:
flutter_test:
diff --git a/lib/aws_request.dart b/lib/aws_request.dart
index e9d58fc..4f36324 100644
--- a/lib/aws_request.dart
+++ b/lib/aws_request.dart
@@ -1,346 +1,8 @@
-library aws_request;
+// Copyright (c) 2021, Zachary Merritt. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// MIT license that can be found in the LICENSE file.
-import 'dart:convert';
+library AwsRequest;
-import 'package:crypto/crypto.dart';
-import 'package:intl/intl.dart';
-import 'package:universal_io/io.dart';
-
-class AwsRequestException implements Exception {
- String message;
- StackTrace stackTrace;
-
- /// A custom error to identify AwsRequest errors more easily
- ///
- /// message: the cause of the error
- /// stackTrace: the stack trace of the error
- AwsRequestException(this.message, this.stackTrace);
-}
-
-String _awsRequestType(AwsRequestType type) {
- switch (type) {
- case AwsRequestType.GET:
- return 'GET';
- case AwsRequestType.POST:
- return 'POST';
- case AwsRequestType.DELETE:
- return 'DELETE';
- case AwsRequestType.PATCH:
- return 'PATCH';
- case AwsRequestType.PUT:
- return 'PUT';
- }
-}
-
-enum AwsRequestType { GET, POST, DELETE, PATCH, PUT }
-
-class AwsRequest {
- // Public
- /// The aws service you are sending a request to
- String? service;
-
- /// The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
- String? target;
-
- // Private
- /// AWS access key
- String _awsAccessKey;
-
- /// AWS secret key
- String _awsSecretKey;
-
- /// The region to send the request to
- String _region;
-
- /// The timeout on the request
- Duration timeout;
- static const Map _defaultHeaders = {
- 'Accept': '*/*',
- 'Content-Type': 'application/x-amz-json-1.1',
- };
-
- AwsRequest(
- this._awsAccessKey,
- this._awsSecretKey,
- this._region, {
- this.service,
- this.target,
- this.timeout: const Duration(seconds: 10),
- });
-
- /// Builds, signs, and sends aws http requests.
- ///
- /// type: request type [GET, POST, PUT, etc]
- ///
- /// service: aws service you are sending request to
- ///
- /// target: The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
- ///
- /// signedHeaders: a list of headers aws requires in the signature.
- ///
- /// Default included signed headers are: [content-type, host, x-amz-date, x-amz-target]
- ///
- /// (You do not need to provide these in headers)
- ///
- /// headers: any required headers. Any non-default headers included in the signedHeaders must be added here.
- ///
- /// jsonBody: the body of the request, formatted as json
- ///
- /// queryPath: the aws query path
- ///
- /// queryString: the aws query string, formatted like ['abc=123&def=456']. Must be url encoded
- Future send(
- AwsRequestType type, {
- String? service,
- String? target,
- List? signedHeaders,
- Map headers = AwsRequest._defaultHeaders,
- String jsonBody: '',
- String queryPath: '/',
- Map queryString: const {},
- }) async {
- return _send(
- type: type,
- service: service,
- target: target,
- signedHeaders: signedHeaders,
- headers: headers,
- jsonBody: jsonBody,
- canonicalUri: queryPath,
- canonicalQuerystring: queryString,
- );
- }
-
- Map _getSignedHeaders(
- Map headers,
- List signedHeaderNames,
- String target,
- String host,
- String amzDate) {
- Map signedHeaders = {
- 'host': host,
- 'x-amz-date': amzDate,
- 'x-amz-target': target
- };
- for (String key in signedHeaderNames) {
- if (!signedHeaders.containsKey(key) && headers.containsKey(key)) {
- signedHeaders[key] = headers[key]!;
- } else {
- throw AwsRequestException(
- 'AwsRequest ERROR: Signed Header Not Found: '
- '$key could not be found in the included headers. '
- 'Provided header keys are ${headers.keys.toList()}'
- 'All headers besides [content-type, host, x-amz-date, x-amz-target] '
- 'that are included in signedHeaders must be included in headers',
- StackTrace.current);
- }
- }
- if (headers.containsKey('content-type')) {
- signedHeaders['content-type'] = headers['content-type']!;
- } else {
- signedHeaders['content-type'] = 'application/x-amz-json-1.1';
- }
- return signedHeaders;
- }
-
- dynamic _sign(List key, String msg, {bool? hex}) {
- if (hex != null && hex) {
- return Hmac(sha256, key).convert(utf8.encode(msg)).toString();
- } else {
- return Hmac(sha256, key).convert(utf8.encode(msg)).bytes;
- }
- }
-
- String _getSignature(
- String key,
- String dateStamp,
- String regionName,
- String serviceName,
- String stringToSign,
- ) {
- List kDate = _sign(utf8.encode('AWS4' + key), dateStamp);
- List kRegion = _sign(kDate, regionName);
- List kService = _sign(kRegion, serviceName);
- List kSigning = _sign(kService, 'aws4_request');
- return _sign(kSigning, stringToSign, hex: true);
- }
-
- String _getCanonicalRequest(
- String type,
- String requestBody,
- Map signedHeaders,
- String canonicalUri,
- String canonicalQuerystring) {
- List canonicalHeaders = [];
- signedHeaders.forEach((key, value) {
- canonicalHeaders.add('$key:$value\n');
- });
- canonicalHeaders.sort();
- String canonicalHeadersString = canonicalHeaders.join('');
- List keyList = signedHeaders.keys.toList();
- keyList.sort();
- String signedHeaderKeys = keyList.join(';');
- String payloadHash = sha256.convert(utf8.encode(requestBody)).toString();
- String canonicalRequest =
- '$type\n$canonicalUri\n$canonicalQuerystring\n$canonicalHeadersString\n'
- '$signedHeaderKeys\n$payloadHash';
- return canonicalRequest;
- }
-
- String _getAuth(
- String amzDate,
- String canonicalRequest,
- String region,
- String service,
- Map signedHeaders,
- ) {
- String algorithm = 'AWS4-HMAC-SHA256';
- String dateStamp = DateFormat('yyyyMMdd').format(DateTime.now().toUtc());
- String credentialScope = '$dateStamp/$region/$service/aws4_request';
- String stringToSign = '$algorithm\n$amzDate\n$credentialScope\n'
- '${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
- String signature = _getSignature(
- this._awsSecretKey,
- dateStamp,
- region,
- service,
- stringToSign,
- );
- List keyList = signedHeaders.keys.toList();
- keyList.sort();
- String signedHeaderKeys = keyList.join(';');
- return '$algorithm Credential=${this._awsAccessKey}/$credentialScope, '
- 'SignedHeaders=$signedHeaderKeys, '
- 'Signature=$signature';
- }
-
- Map _getHeaders(
- String host,
- String requestBody,
- Map headers,
- String target,
- String amzDate,
- String auth,
- ) {
- return {
- ..._defaultHeaders,
- 'Authorization': auth,
- 'X-Amz-Date': amzDate,
- 'x-amz-target': target,
- ...headers
- };
- }
-
- Map _validateRequest(
- String? service,
- String? target,
- ) {
- if (service == null) {
- return {
- 'valid': false,
- 'error':
- 'No Service Provided. Please pass in a service or set one with '
- 'AwsRequest.setService(String serviceName)'
- };
- }
- if (target == null) {
- return {
- 'valid': false,
- 'error': 'No Target Provided. Please pass in a service or set one with '
- 'AwsRequest.setTarget(String targetName)'
- };
- }
- return {'valid': true, 'error': null};
- }
-
- Future _getRequest(AwsRequestType type, Uri url) async {
- switch (type) {
- case AwsRequestType.GET:
- return await HttpClient().getUrl(url);
- case AwsRequestType.POST:
- return await HttpClient().postUrl(url);
- case AwsRequestType.DELETE:
- return await HttpClient().deleteUrl(url);
- case AwsRequestType.PATCH:
- return await HttpClient().patchUrl(url);
- case AwsRequestType.PUT:
- return await HttpClient().putUrl(url);
- }
- }
-
- Future _send({
- required AwsRequestType type,
- String? service,
- String? target,
- List? signedHeaders,
- required Map headers,
- required String jsonBody,
- required String canonicalUri,
- required Map canonicalQuerystring,
- }) async {
- service ??= this.service;
- target ??= this.target;
- signedHeaders ??= [];
-
- // validate request
- Map validation = _validateRequest(
- service,
- target,
- );
- if (!validation['valid']) {
- throw new AwsRequestException(
- 'AwsRequestException: ${validation['error']}', StackTrace.current);
- }
- // create needed variables
- String host = '$service.${this._region}.amazonaws.com';
- Uri url = Uri(
- scheme: 'https',
- host: host,
- path: canonicalUri,
- queryParameters: canonicalQuerystring,
- );
- String amzDate =
- DateFormat("yyyyMMdd'T'HHmmss'Z'").format(DateTime.now().toUtc());
- Map signedHeadersMap = _getSignedHeaders(
- headers,
- signedHeaders,
- target!,
- host,
- amzDate,
- );
-
- // generate canonical request, auth, and headers
- String canonicalRequest = _getCanonicalRequest(
- _awsRequestType(type),
- jsonBody,
- signedHeadersMap,
- canonicalUri,
- url.query,
- );
- String auth = _getAuth(
- amzDate,
- canonicalRequest,
- this._region,
- service!,
- signedHeadersMap,
- );
- Map updatedHeaders = _getHeaders(
- host,
- jsonBody,
- headers,
- target,
- amzDate,
- auth,
- );
-
- // generate request and add headers
- HttpClientRequest request = await _getRequest(type, url);
- updatedHeaders.forEach((key, value) {
- request.headers.set(key, value);
- });
-
- // encode body and send request
- request.add(utf8.encode(jsonBody));
- return await request.close().timeout(timeout);
- }
-}
+export 'src/aws_request.dart' show AwsRequest;
+export 'src/util.dart' show AwsRequestType, AwsRequestException;
diff --git a/lib/src/aws_request.dart b/lib/src/aws_request.dart
new file mode 100644
index 0000000..367a07c
--- /dev/null
+++ b/lib/src/aws_request.dart
@@ -0,0 +1,141 @@
+import 'package:http/http.dart';
+
+import 'request.dart';
+import 'util.dart';
+
+class AwsRequest {
+ /// The aws service you are sending a request to
+ String? service;
+
+ /// The api you are targeting
+ String? target;
+
+ /// AWS access key
+ String awsAccessKey;
+
+ /// AWS secret key
+ String awsSecretKey;
+
+ /// The region to send the request to
+ String region;
+
+ /// The timeout on the request
+ Duration timeout;
+
+ AwsRequest(
+ this.awsAccessKey,
+ this.awsSecretKey,
+ this.region, {
+ this.service,
+ this.target,
+ this.timeout: const Duration(seconds: 10),
+ });
+
+ /// Statically Builds, signs, and sends aws http requests.
+ ///
+ /// type: request type [GET, POST, PUT, etc]
+ ///
+ /// service: aws service you are sending request to
+ ///
+ /// target: The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
+ ///
+ /// signedHeaders: a list of headers aws requires in the signature.
+ ///
+ /// Default included signed headers are: [content-type, host, x-amz-date, x-amz-target]
+ ///
+ /// (You do not need to provide these in headers)
+ ///
+ /// headers: any required headers. Any non-default headers included in the signedHeaders must be added here.
+ ///
+ /// jsonBody: the body of the request, formatted as json
+ ///
+ /// queryPath: the aws query path
+ ///
+ /// queryString:the url query string as a Map
+ static Future staticSend({
+ required String awsAccessKey,
+ required String awsSecretKey,
+ required String region,
+ required String service,
+ required String target,
+ required AwsRequestType type,
+ List signedHeaders: const [],
+ Map headers: defaultHeaders,
+ String jsonBody: '',
+ String queryPath: '/',
+ Map? queryString,
+ Duration timeout: const Duration(seconds: 10),
+ }) async {
+ return AwsHttpRequest.send(
+ awsAccessKey: awsAccessKey,
+ awsSecretKey: awsSecretKey,
+ region: region,
+ type: type,
+ service: service,
+ target: target,
+ signedHeaders: signedHeaders,
+ headers: headers,
+ jsonBody: jsonBody,
+ canonicalUri: queryPath,
+ canonicalQuery: queryString,
+ timeout: timeout,
+ );
+ }
+
+ /// Builds, signs, and sends aws http requests.
+ ///
+ /// type: request type [GET, POST, PUT, etc]
+ ///
+ /// service: aws service you are sending request to
+ ///
+ /// target: The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
+ ///
+ /// signedHeaders: a list of headers aws requires in the signature.
+ ///
+ /// Default included signed headers are: [content-type, host, x-amz-date, x-amz-target]
+ ///
+ /// (You do not need to provide these in headers)
+ ///
+ /// headers: any required headers. Any non-default headers included in the signedHeaders must be added here.
+ ///
+ /// jsonBody: the body of the request, formatted as json
+ ///
+ /// queryPath: the aws query path
+ ///
+ /// queryString: the url query string as a Map
+ Future send(
+ AwsRequestType type, {
+ String? service,
+ String? target,
+ List signedHeaders: const [],
+ Map headers = defaultHeaders,
+ String jsonBody: '',
+ String queryPath: '/',
+ Map? queryString,
+ }) async {
+ // validate request
+ Map validation = validateRequest(
+ service ?? this.service,
+ target ?? this.target,
+ );
+ if (!validation['valid']) {
+ throw new AwsRequestException(
+ message: 'AwsRequestException: ${validation['error']}',
+ stackTrace: StackTrace.current);
+ }
+ return AwsHttpRequest.send(
+ awsAccessKey: awsAccessKey,
+ awsSecretKey: awsSecretKey,
+ region: region,
+ type: type,
+ service: service ?? this.service!,
+ target: target ?? this.target!,
+ signedHeaders: signedHeaders,
+ headers: headers,
+ jsonBody: jsonBody,
+ canonicalUri: queryPath,
+ canonicalQuery: queryString,
+ timeout: timeout,
+ );
+ }
+}
diff --git a/lib/src/mock_aws_request.dart b/lib/src/mock_aws_request.dart
new file mode 100644
index 0000000..9a448f5
--- /dev/null
+++ b/lib/src/mock_aws_request.dart
@@ -0,0 +1,150 @@
+import 'package:http/http.dart';
+
+import 'request.dart';
+import 'util.dart';
+
+class MockAwsRequest {
+ /// The aws service you are sending a request to
+ String? service;
+
+ /// The api you are targeting
+ String? target;
+
+ /// AWS access key
+ String awsAccessKey;
+
+ /// AWS secret key
+ String awsSecretKey;
+
+ /// The region to send the request to
+ String region;
+
+ /// The timeout on the request
+ Duration timeout;
+
+ /// The function used to specify responses
+ Future Function(Request) mockFunction;
+
+ MockAwsRequest(
+ this.awsAccessKey,
+ this.awsSecretKey,
+ this.region, {
+ required this.mockFunction,
+ this.service,
+ this.target,
+ this.timeout: const Duration(seconds: 10),
+ });
+
+ /// Statically Builds, signs, and sends aws http requests.
+ ///
+ /// type: request type [GET, POST, PUT, etc]
+ ///
+ /// service: aws service you are sending request to
+ ///
+ /// target: The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
+ ///
+ /// signedHeaders: a list of headers aws requires in the signature.
+ ///
+ /// Default included signed headers are: [content-type, host, x-amz-date, x-amz-target]
+ ///
+ /// (You do not need to provide these in headers)
+ ///
+ /// headers: any required headers. Any non-default headers included in the signedHeaders must be added here.
+ ///
+ /// jsonBody: the body of the request, formatted as json
+ ///
+ /// queryPath: the aws query path
+ ///
+ /// queryString: the aws query string, formatted like ['abc=123&def=456']. Must be url encoded
+ static Future staticSend({
+ required String awsAccessKey,
+ required String awsSecretKey,
+ required String region,
+ required String service,
+ required String target,
+ required AwsRequestType type,
+ required Future Function(Request) mockFunction,
+ List signedHeaders: const [],
+ Map headers: defaultHeaders,
+ String jsonBody: '',
+ String queryPath: '/',
+ Map? queryString,
+ Duration timeout: const Duration(seconds: 10),
+ }) async {
+ return AwsHttpRequest.send(
+ awsAccessKey: awsAccessKey,
+ awsSecretKey: awsSecretKey,
+ region: region,
+ type: type,
+ service: service,
+ target: target,
+ signedHeaders: signedHeaders,
+ headers: headers,
+ jsonBody: jsonBody,
+ canonicalUri: queryPath,
+ canonicalQuery: queryString,
+ timeout: timeout,
+ mockRequest: true,
+ mockFunction: mockFunction,
+ );
+ }
+
+ /// Builds, signs, and sends aws http requests.
+ ///
+ /// type: request type [GET, POST, PUT, etc]
+ ///
+ /// service: aws service you are sending request to
+ ///
+ /// target: The api you are targeting (ie Logs_XXXXXXXX.PutLogEvents)
+ ///
+ /// signedHeaders: a list of headers aws requires in the signature.
+ ///
+ /// Default included signed headers are: [content-type, host, x-amz-date, x-amz-target]
+ ///
+ /// (You do not need to provide these in headers)
+ ///
+ /// headers: any required headers. Any non-default headers included in the signedHeaders must be added here.
+ ///
+ /// jsonBody: the body of the request, formatted as json
+ ///
+ /// queryPath: the aws query path
+ ///
+ /// queryString: the url query string as a Map
+ Future send(
+ AwsRequestType type, {
+ String? service,
+ String? target,
+ List signedHeaders: const [],
+ Map headers = defaultHeaders,
+ String jsonBody: '',
+ String queryPath: '/',
+ Map? queryString,
+ }) async {
+ // validate request
+ Map validation = validateRequest(
+ service ?? this.service,
+ target ?? this.target,
+ );
+ if (!validation['valid']) {
+ throw new AwsRequestException(
+ message: 'AwsRequestException: ${validation['error']}',
+ stackTrace: StackTrace.current);
+ }
+ return AwsHttpRequest.send(
+ awsAccessKey: awsAccessKey,
+ awsSecretKey: awsSecretKey,
+ region: region,
+ type: type,
+ service: service ?? this.service!,
+ target: target ?? this.target!,
+ signedHeaders: signedHeaders,
+ headers: headers,
+ jsonBody: jsonBody,
+ canonicalUri: queryPath,
+ canonicalQuery: queryString,
+ timeout: timeout,
+ mockRequest: true,
+ mockFunction: mockFunction,
+ );
+ }
+}
diff --git a/lib/src/request.dart b/lib/src/request.dart
new file mode 100644
index 0000000..24b42af
--- /dev/null
+++ b/lib/src/request.dart
@@ -0,0 +1,274 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:crypto/crypto.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+import 'package:intl/intl.dart';
+
+import 'util.dart';
+
+class AwsHttpRequest {
+ static Map getSignedHeaders(
+ Map headers,
+ List signedHeaderNames,
+ String target,
+ String host,
+ String amzDate) {
+ Map signedHeaders = {
+ 'host': host,
+ 'x-amz-date': amzDate,
+ 'x-amz-target': target,
+ };
+ for (String key in signedHeaderNames) {
+ if (!signedHeaders.containsKey(key) && headers.containsKey(key)) {
+ signedHeaders[key] = headers[key]!;
+ } else {
+ throw AwsRequestException(
+ message: 'AwsRequest ERROR: Signed Header Not Found: '
+ '$key could not be found in the included headers. '
+ 'Provided header keys are ${headers.keys.toList()} '
+ 'All headers besides [content-type, host, x-amz-date, x-amz-target] '
+ 'that are included in signedHeaders must be included in headers',
+ stackTrace: StackTrace.current);
+ }
+ }
+ if (headers.containsKey('content-type')) {
+ signedHeaders['content-type'] = headers['content-type']!;
+ } else {
+ signedHeaders['content-type'] = 'application/x-amz-json-1.1';
+ }
+ return signedHeaders;
+ }
+
+ static dynamic sign(List key, String msg, {bool? hex}) {
+ if (hex != null && hex) {
+ return Hmac(sha256, key).convert(utf8.encode(msg)).toString();
+ } else {
+ return Hmac(sha256, key).convert(utf8.encode(msg)).bytes;
+ }
+ }
+
+ static String getSignature(
+ String key,
+ String dateStamp,
+ String regionName,
+ String serviceName,
+ String stringToSign,
+ ) {
+ List kDate = sign(utf8.encode('AWS4' + key), dateStamp);
+ List kRegion = sign(kDate, regionName);
+ List kService = sign(kRegion, serviceName);
+ List kSigning = sign(kService, 'aws4_request');
+ return sign(kSigning, stringToSign, hex: true);
+ }
+
+ static String getCanonicalRequest(
+ String type,
+ String requestBody,
+ Map signedHeaders,
+ String canonicalUri,
+ String canonicalQuerystring) {
+ List canonicalHeaders = [];
+ signedHeaders.forEach((key, value) {
+ canonicalHeaders.add('$key:$value\n');
+ });
+ canonicalHeaders.sort();
+ String canonicalHeadersString = canonicalHeaders.join('');
+ List keyList = signedHeaders.keys.toList();
+ keyList.sort();
+ String signedHeaderKeys = keyList.join(';');
+ String payloadHash = sha256.convert(utf8.encode(requestBody)).toString();
+ String canonicalRequest =
+ '$type\n$canonicalUri\n$canonicalQuerystring\n$canonicalHeadersString\n'
+ '$signedHeaderKeys\n$payloadHash';
+ return canonicalRequest;
+ }
+
+ static String getAuth(
+ String awsSecretKey,
+ String awsAccessKey,
+ String amzDate,
+ String canonicalRequest,
+ String region,
+ String service,
+ Map signedHeaders,
+ ) {
+ String algorithm = 'AWS4-HMAC-SHA256';
+ String dateStamp = DateFormat('yyyyMMdd').format(DateTime.now().toUtc());
+ String credentialScope = '$dateStamp/$region/$service/aws4_request';
+ String stringToSign = '$algorithm\n$amzDate\n$credentialScope\n'
+ '${sha256.convert(utf8.encode(canonicalRequest)).toString()}';
+ String signature = getSignature(
+ awsSecretKey,
+ dateStamp,
+ region,
+ service,
+ stringToSign,
+ );
+ List keyList = signedHeaders.keys.toList();
+ keyList.sort();
+ String signedHeaderKeys = keyList.join(';');
+ return '$algorithm Credential=$awsAccessKey/$credentialScope, '
+ 'SignedHeaders=$signedHeaderKeys, '
+ 'Signature=$signature';
+ }
+
+ static Map getHeaders(
+ String host,
+ String requestBody,
+ Map headers,
+ String target,
+ String amzDate,
+ String auth,
+ Duration timeout,
+ ) {
+ return {
+ ...defaultHeaders,
+ ...headers,
+ ...{
+ // We never want these keys overwritten
+ 'Authorization': auth,
+ 'X-Amz-Date': amzDate,
+ 'x-amz-target': target,
+ }
+ };
+ }
+
+ static Future getRequest(
+ AwsRequestType type,
+ Uri url,
+ Map headers,
+ String body,
+ Duration timeout, {
+ bool mockRequest: false,
+ Future Function(Request)? mockFunction,
+ }) async {
+ if (mockRequest && mockFunction == null) {
+ throw new AwsRequestException(
+ message: 'Mocking function request to mock AwsRequests',
+ stackTrace: StackTrace.current,
+ );
+ }
+ dynamic client = mockRequest ? MockClient(mockFunction!) : Client();
+ Future response;
+ switch (type) {
+ case AwsRequestType.GET:
+ response = client.get(
+ url,
+ headers: headers,
+ );
+ break;
+ case AwsRequestType.POST:
+ response = client.post(
+ url,
+ headers: headers,
+ body: utf8.encode(body),
+ );
+ break;
+ case AwsRequestType.DELETE:
+ response = client.delete(
+ url,
+ headers: headers,
+ body: utf8.encode(body),
+ );
+ break;
+ case AwsRequestType.PATCH:
+ response = client.patch(
+ url,
+ headers: headers,
+ body: utf8.encode(body),
+ );
+ break;
+ case AwsRequestType.PUT:
+ response = client.put(
+ url,
+ headers: headers,
+ body: utf8.encode(body),
+ );
+ break;
+ case AwsRequestType.HEAD:
+ response = client.head(
+ url,
+ headers: headers,
+ );
+ break;
+ }
+ return response.timeout(timeout, onTimeout: () {
+ client.close();
+ throw TimeoutException('AwsRequest Timed Out', timeout);
+ });
+ }
+
+ static Future send({
+ required String awsSecretKey,
+ required String awsAccessKey,
+ required AwsRequestType type,
+ required String service,
+ required String target,
+ required String region,
+ required Duration timeout,
+ List signedHeaders: const [],
+ required Map headers,
+ required String jsonBody,
+ required String canonicalUri,
+ Map? canonicalQuery,
+ bool mockRequest: false,
+ Future Function(Request)? mockFunction,
+ }) async {
+ String host = '$service.$region.amazonaws.com';
+ Uri url = Uri(
+ scheme: 'https',
+ host: host,
+ path: canonicalUri,
+ queryParameters: canonicalQuery,
+ );
+ String amzDate =
+ DateFormat("yyyyMMdd'T'HHmmss'Z'").format(DateTime.now().toUtc());
+ Map signedHeadersMap = getSignedHeaders(
+ headers,
+ signedHeaders,
+ target,
+ host,
+ amzDate,
+ );
+
+ // generate canonical request, auth, and headers
+ String canonicalRequest = getCanonicalRequest(
+ type.toString().split('.').last,
+ jsonBody,
+ signedHeadersMap,
+ canonicalUri,
+ url.query,
+ );
+ String auth = getAuth(
+ awsSecretKey,
+ awsAccessKey,
+ amzDate,
+ canonicalRequest,
+ region,
+ service,
+ signedHeadersMap,
+ );
+ Map updatedHeaders = getHeaders(
+ host,
+ jsonBody,
+ headers,
+ target,
+ amzDate,
+ auth,
+ timeout,
+ );
+
+ // generate request and add headers
+ return await getRequest(
+ type,
+ url,
+ updatedHeaders,
+ jsonBody,
+ timeout,
+ mockRequest: mockRequest,
+ mockFunction: mockFunction,
+ );
+ }
+}
diff --git a/lib/src/util.dart b/lib/src/util.dart
new file mode 100644
index 0000000..0570a7e
--- /dev/null
+++ b/lib/src/util.dart
@@ -0,0 +1,47 @@
+/// Special exception class to identify exceptions from AwsRequest
+class AwsRequestException implements Exception {
+ String message;
+ StackTrace stackTrace;
+
+ /// A custom error to identify AwsRequest errors more easily
+ ///
+ /// message: the cause of the error
+ /// stackTrace: the stack trace of the error
+ AwsRequestException({required this.message, required this.stackTrace});
+
+ /// AwsRequestException toString
+ String toString() {
+ return "AwsRequestException - message: $message";
+ }
+}
+
+/// Enum of supported HTTP methods
+enum AwsRequestType { GET, POST, DELETE, PATCH, PUT, HEAD }
+
+/// Default headers included automatically with every request.
+/// These can be overridden by passing in a Map with the same keys
+const Map defaultHeaders = {
+ 'Accept': '*/*',
+ 'Content-Type': 'application/x-amz-json-1.1',
+};
+
+Map validateRequest(
+ String? service,
+ String? target,
+) {
+ if (service == null) {
+ return {
+ 'valid': false,
+ 'error':
+ 'No Service Provided. Please pass in a service or set it in the constructor.'
+ };
+ }
+ if (target == null) {
+ return {
+ 'valid': false,
+ 'error':
+ 'No Target Provided. Please pass in a service or set it in the constructor.'
+ };
+ }
+ return {'valid': true, 'error': null};
+}
diff --git a/lib/testing.dart b/lib/testing.dart
new file mode 100644
index 0000000..387c425
--- /dev/null
+++ b/lib/testing.dart
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Zachary Merritt. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// MIT license that can be found in the LICENSE file.
+
+library MockAwsRequest;
+
+export 'src/mock_aws_request.dart' show MockAwsRequest;
+export 'src/util.dart' show AwsRequestType, AwsRequestException;
diff --git a/pubspec.lock b/pubspec.lock
index f790f45..ec28f0d 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -106,6 +106,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
+ http:
+ dependency: "direct main"
+ description:
+ name: http
+ url: "https://pub.dartlang.org"
+ source: hosted
+ version: "0.13.3"
http_multi_server:
dependency: transitive
description:
@@ -316,13 +323,6 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.0"
- universal_io:
- dependency: "direct main"
- description:
- name: universal_io
- url: "https://pub.dartlang.org"
- source: hosted
- version: "2.0.4"
vm_service:
dependency: transitive
description:
diff --git a/pubspec.yaml b/pubspec.yaml
index b919efb..25136be 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -10,7 +10,7 @@ dependencies:
intl: ^0.17.0 # Date Formatting
crypto: ^3.0.0 # Key Signing
- universal_io: ^2.0.0 # Web compatibility
+ http: ^0.13.0 # Send requests
dev_dependencies:
test: ^1.16.0
\ No newline at end of file
diff --git a/scripts/coverage_helper.sh b/scripts/coverage_helper.sh
new file mode 100644
index 0000000..87839ce
--- /dev/null
+++ b/scripts/coverage_helper.sh
@@ -0,0 +1,11 @@
+#!/bin/bash
+file=test/coverage_helper_test.dart
+printf "// Helper file to make coverage work for all dart files\n" > $file
+printf "// **************************************************************************\n" >> $file
+printf "// Because of this: https://github.com/flutter/flutter/issues/27997#issue-410722816\n" >> $file
+printf "// DO NOT EDIT THIS FILE USE: sh scripts/import_files_coverage.sh YOUR_PACKAGE_NAME\n" >> $file
+printf "// **************************************************************************\n" >> $file
+printf "\n" >> $file
+printf "// ignore_for_file: unused_import\n" >> $file
+find lib -type f \( -iname "*.dart" ! -iname "*.g.dart" ! -iname "*.freezed.dart" ! -iname "generated_plugin_registrant.dart" \) | cut -c4- | awk -v package="$1" '{printf "import '\''package:%s%s'\'';\n", package, $1}' >> $file
+printf "\nvoid main(){}" >> $file
\ No newline at end of file
diff --git a/test/aws_request_test.dart b/test/aws_request_test.dart
index 47e2ecf..6a4522b 100644
--- a/test/aws_request_test.dart
+++ b/test/aws_request_test.dart
@@ -1,4 +1,4 @@
-import 'package:aws_request/aws_request.dart';
+import 'package:aws_request/src/aws_request.dart';
import 'package:test/test.dart';
void main() {
diff --git a/test/mock_aws_request_test.dart b/test/mock_aws_request_test.dart
new file mode 100644
index 0000000..aacd60c
--- /dev/null
+++ b/test/mock_aws_request_test.dart
@@ -0,0 +1,22 @@
+import 'package:aws_request/src/mock_aws_request.dart';
+import 'package:http/http.dart';
+import 'package:test/test.dart';
+
+void main() {
+ test('tests getter / setter values', () {
+ final MockAwsRequest awsRequest = new MockAwsRequest(
+ '',
+ '',
+ '',
+ mockFunction: (Request) async {
+ return Response('body', 200);
+ },
+ );
+ expect(awsRequest.service == null, true);
+ expect(awsRequest.target == null, true);
+ awsRequest.service = 'testService';
+ awsRequest.target = 'testTarget';
+ expect(awsRequest.service == 'testService', true);
+ expect(awsRequest.target == 'testTarget', true);
+ });
+}
diff --git a/test/request_test.dart b/test/request_test.dart
new file mode 100644
index 0000000..7f0eb74
--- /dev/null
+++ b/test/request_test.dart
@@ -0,0 +1,617 @@
+import 'dart:convert';
+
+import 'package:aws_request/aws_request.dart';
+import 'package:aws_request/src/request.dart';
+import 'package:crypto/crypto.dart';
+import 'package:http/http.dart';
+import 'package:intl/intl.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('getSignedHeaders', () {
+ test('getSignedHeaders', () {
+ const Map correctSignedHeaders = {
+ 'host': 'host',
+ 'x-amz-date': 'amzDate',
+ 'x-amz-target': 'target',
+ 'signed_header': 'signed_header',
+ 'content-type': 'application/x-amz-json-1.1'
+ };
+ Map generatedSignedHeaders =
+ AwsHttpRequest.getSignedHeaders(
+ {
+ 'signed_header': 'signed_header',
+ 'unsigned_header': 'unsigned_header',
+ },
+ ['signed_header'],
+ 'target',
+ 'host',
+ 'amzDate',
+ );
+ expect(generatedSignedHeaders, correctSignedHeaders);
+ });
+
+ test('getSignedHeaders failure', () {
+ try {
+ AwsHttpRequest.getSignedHeaders(
+ {
+ 'signed_header': 'signed_header',
+ 'unsigned_header': 'unsigned_header',
+ },
+ ['signed_header', 'missing_header'],
+ 'target',
+ 'host',
+ 'amzDate',
+ );
+ } catch (e) {
+ expect(e, isA());
+ return;
+ }
+ fail("Something went wrong. The last line didn't cause an error");
+ });
+
+ test('getSignedHeaders content-type', () {
+ const Map correctSignedHeaders = {
+ 'host': 'host',
+ 'x-amz-date': 'amzDate',
+ 'x-amz-target': 'target',
+ 'signed_header': 'signed_header',
+ 'content-type': 'content-type'
+ };
+ Map generatedSignedHeaders =
+ AwsHttpRequest.getSignedHeaders(
+ {
+ 'signed_header': 'signed_header',
+ 'unsigned_header': 'unsigned_header',
+ 'content-type': 'content-type'
+ },
+ ['signed_header'],
+ 'target',
+ 'host',
+ 'amzDate',
+ );
+ expect(generatedSignedHeaders, correctSignedHeaders);
+ });
+ });
+
+ group('sign', () {
+ test('bytes', () {
+ List result =
+ AwsHttpRequest.sign(utf8.encode('AWS4KEY'), 'stringMessage');
+ expect([
+ 106,
+ 73,
+ 3,
+ 202,
+ 103,
+ 45,
+ 38,
+ 137,
+ 136,
+ 2,
+ 133,
+ 175,
+ 91,
+ 138,
+ 142,
+ 8,
+ 39,
+ 50,
+ 141,
+ 255,
+ 56,
+ 242,
+ 179,
+ 229,
+ 170,
+ 75,
+ 186,
+ 230,
+ 36,
+ 127,
+ 215,
+ 155
+ ], result);
+ });
+
+ test('hex', () {
+ String result2 = AwsHttpRequest.sign(
+ utf8.encode('AWS4KEY'),
+ 'stringMessage',
+ hex: true,
+ );
+ expect(
+ '6a4903ca672d2689880285af5b8a8e0827328dff38f2b3e5aa4bbae6247fd79b',
+ result2,
+ );
+ });
+ });
+
+ test('getSignature', () {
+ String signature = AwsHttpRequest.getSignature(
+ 'key',
+ 'dateStamp',
+ 'regionName',
+ 'serviceName',
+ 'stringToSign',
+ );
+ expect(
+ 'ba57e18ee959b8eb152d5ba3349eee5afd95812e0f2ff44b41dee9f8686c824d',
+ signature,
+ );
+ });
+
+ group('getCanonicalRequest', () {
+ test('getCanonicalRequest - 1', () {
+ String requestString = AwsHttpRequest.getCanonicalRequest(
+ 'type',
+ 'requestBody',
+ {'signedHeaderKey': 'signedHeaderValue'},
+ 'canonical/Uri',
+ 'canonicalQuerystring=canonicalQuerystring',
+ );
+ expect(
+ """type
+canonical/Uri
+canonicalQuerystring=canonicalQuerystring
+signedHeaderKey:signedHeaderValue
+
+signedHeaderKey
+fcf523fac03a2e3a814b7f97bf8c9533d657677c72ff3870afd69cef3b559c60""",
+ requestString,
+ );
+ });
+ test('getCanonicalRequest - 2', () {
+ String requestString = AwsHttpRequest.getCanonicalRequest(
+ 'type',
+ '',
+ {
+ 'signedHeaderKey': 'signedHeaderValue',
+ 'signedHeaderKey1': 'signedHeaderValue1',
+ 'signedHeaderKey2': 'signedHeaderValue2',
+ },
+ 'canonical/Uri',
+ '/',
+ );
+ expect(
+ """type
+canonical/Uri
+/
+signedHeaderKey1:signedHeaderValue1
+signedHeaderKey2:signedHeaderValue2
+signedHeaderKey:signedHeaderValue
+
+signedHeaderKey;signedHeaderKey1;signedHeaderKey2
+e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855""",
+ requestString,
+ );
+ });
+ });
+
+ group('getAuth', () {
+ String dateStamp = DateFormat('yyyyMMdd').format(DateTime.now().toUtc());
+
+ test('empty', () {
+ String auth = AwsHttpRequest.getAuth(
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ {},
+ );
+ String stringToSign = 'AWS4-HMAC-SHA256\n\n$dateStamp///aws4_request\n'
+ '${sha256.convert(utf8.encode('')).toString()}';
+ String signature = AwsHttpRequest.getSignature(
+ '',
+ dateStamp,
+ '',
+ '',
+ stringToSign,
+ );
+ expect(
+ 'AWS4-HMAC-SHA256 Credential=/$dateStamp///aws4_request, SignedHeaders=, Signature=$signature',
+ auth,
+ );
+ });
+ test('strings', () {
+ String auth = AwsHttpRequest.getAuth(
+ 'awsSecretKey',
+ 'awsAccessKey',
+ 'amzDate',
+ 'canonicalRequest',
+ 'region',
+ 'service',
+ {'signedHeaders': 'signedHeaders'},
+ );
+ String stringToSign =
+ 'AWS4-HMAC-SHA256\namzDate\n$dateStamp/region/service/aws4_request\n'
+ '${sha256.convert(utf8.encode('canonicalRequest')).toString()}';
+ String signature = AwsHttpRequest.getSignature(
+ 'awsSecretKey',
+ dateStamp,
+ 'region',
+ 'service',
+ stringToSign,
+ );
+ expect(
+ 'AWS4-HMAC-SHA256 Credential=awsAccessKey/$dateStamp/region/service/aws4_request, SignedHeaders=signedHeaders, Signature=$signature',
+ auth,
+ );
+ });
+ test('headers unsorted', () {
+ String auth = AwsHttpRequest.getAuth(
+ 'awsSecretKey',
+ 'awsAccessKey',
+ 'amzDate',
+ 'canonicalRequest',
+ 'region',
+ 'service',
+ {
+ 'c': 'c',
+ 'b': 'b',
+ 'a': 'a',
+ },
+ );
+ String stringToSign =
+ 'AWS4-HMAC-SHA256\namzDate\n$dateStamp/region/service/aws4_request\n'
+ '${sha256.convert(utf8.encode('canonicalRequest')).toString()}';
+ String signature = AwsHttpRequest.getSignature(
+ 'awsSecretKey',
+ dateStamp,
+ 'region',
+ 'service',
+ stringToSign,
+ );
+ expect(
+ 'AWS4-HMAC-SHA256 Credential=awsAccessKey/$dateStamp/region/service/aws4_request, SignedHeaders=a;b;c, Signature=$signature',
+ auth,
+ );
+ });
+ });
+
+ group('getHeaders', () {
+ test('strings', () {
+ Map res = AwsHttpRequest.getHeaders(
+ 'host',
+ 'requestBody',
+ {'c': 'c'},
+ 'target',
+ 'amzDate',
+ 'auth',
+ Duration(),
+ );
+ expect(
+ {
+ 'Accept': '*/*',
+ 'Content-Type': 'application/x-amz-json-1.1',
+ 'Authorization': 'auth',
+ 'X-Amz-Date': 'amzDate',
+ 'x-amz-target': 'target',
+ 'c': 'c',
+ },
+ res,
+ );
+ });
+ test('overwrite string', () {
+ Map res = AwsHttpRequest.getHeaders(
+ 'host',
+ 'requestBody',
+ {
+ 'Accept': '',
+ 'Content-Type': '',
+ 'Authorization': '',
+ 'X-Amz-Date': '',
+ 'x-amz-target': '',
+ },
+ 'target',
+ 'amzDate',
+ 'auth',
+ Duration(),
+ );
+ expect(
+ {
+ 'Accept': '',
+ 'Content-Type': '',
+ 'Authorization': 'auth',
+ 'X-Amz-Date': 'amzDate',
+ 'x-amz-target': 'target',
+ },
+ res,
+ );
+ });
+ test('increased timeout', () {
+ Map res = AwsHttpRequest.getHeaders(
+ 'host',
+ 'requestBody',
+ {},
+ 'target',
+ 'amzDate',
+ 'auth',
+ Duration(seconds: 123456789),
+ );
+ expect(
+ {
+ 'Accept': '*/*',
+ 'Content-Type': 'application/x-amz-json-1.1',
+ 'Authorization': 'auth',
+ 'X-Amz-Date': 'amzDate',
+ 'x-amz-target': 'target',
+ },
+ res,
+ );
+ });
+ test('increased timeout', () {
+ Map res = AwsHttpRequest.getHeaders(
+ 'host',
+ 'requestBody',
+ {},
+ 'target',
+ 'amzDate',
+ 'auth',
+ Duration(milliseconds: 10),
+ );
+ expect(
+ {
+ 'Accept': '*/*',
+ 'Content-Type': 'application/x-amz-json-1.1',
+ 'Authorization': 'auth',
+ 'X-Amz-Date': 'amzDate',
+ 'x-amz-target': 'target',
+ },
+ res,
+ );
+ });
+ });
+
+ group('getRequest', () {
+ test('AwsRequestType.DELETE', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.DELETE,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.DELETE, $err');
+ });
+ });
+ test('AwsRequestType.GET', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.GET,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.GET, $err');
+ });
+ });
+ test('AwsRequestType.HEAD', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.HEAD,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.HEAD, $err');
+ });
+ });
+ test('AwsRequestType.PATCH', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.PATCH,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.PATCH, $err');
+ });
+ });
+ test('AwsRequestType.POST', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.POST,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.POST, $err');
+ });
+ });
+ test('AwsRequestType.PUT', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.PUT,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ ).then((value) {}, onError: (err) {
+ fail('Missing type! AwsRequestType.PUT, $err');
+ });
+ });
+
+ test('failure', () {
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.GET,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ mockRequest: true,
+ ).then((val) {
+ fail('Mock client not detected!');
+ return; // needed for compiler
+ }, onError: (e) {
+ expect(e, isA());
+ });
+ });
+
+ test('mockFunction != null', () {
+ Future mockFunction(Request request) async {
+ return Response('', 500);
+ }
+
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.GET,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ mockFunction: mockFunction,
+ ).then((val) {}, onError: (err) {
+ fail(err);
+ });
+ });
+
+ test('check MockClient', () {
+ Future mockFunction(Request request) async {
+ return Response('', 500);
+ }
+
+ return AwsHttpRequest.getRequest(
+ AwsRequestType.GET,
+ Uri.parse('https://www.google.com'),
+ {},
+ '',
+ Duration(seconds: 10),
+ mockFunction: mockFunction,
+ mockRequest: true,
+ ).then((val) {
+ Request request = Request(
+ 'GET',
+ Uri.parse('https://www.google.com'),
+ );
+ expect(
+ val.request!.url,
+ request.url,
+ );
+ expect(
+ val.request!.method,
+ request.method,
+ );
+ expect(
+ val.statusCode,
+ 500,
+ );
+ });
+ });
+ });
+
+ group('send', () {
+ Future mockFunction(Request request) async {
+ return Response(request.method, 500);
+ }
+
+ test('GET', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.GET,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'GET');
+ });
+ });
+ test('POST', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.POST,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'POST');
+ });
+ });
+ test('DELETE', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.DELETE,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'DELETE');
+ });
+ });
+ test('PUT', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.PUT,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'PUT');
+ });
+ });
+ test('PATCH', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.PATCH,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'PATCH');
+ });
+ });
+ test('HEAD', () {
+ return AwsHttpRequest.send(
+ awsSecretKey: 'awsSecretKey',
+ awsAccessKey: 'awsAccessKey',
+ type: AwsRequestType.HEAD,
+ service: 'service',
+ target: 'target',
+ region: 'region',
+ timeout: Duration(),
+ headers: {},
+ jsonBody: 'jsonBody',
+ canonicalUri: 'canonicalUri',
+ mockRequest: true,
+ mockFunction: mockFunction,
+ ).then((val) {
+ expect(val.body, 'HEAD');
+ });
+ });
+ });
+}
diff --git a/test/util_test.dart b/test/util_test.dart
new file mode 100644
index 0000000..27538d1
--- /dev/null
+++ b/test/util_test.dart
@@ -0,0 +1,80 @@
+import 'package:aws_request/src/util.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('AwsRequestException', () {
+ test('constructor', () {
+ AwsRequestException exception = AwsRequestException(
+ message: '',
+ stackTrace: StackTrace.empty,
+ );
+ expect(exception.message, '');
+ expect(exception.stackTrace, StackTrace.empty);
+ });
+ test('toString - empty', () {
+ AwsRequestException exception = AwsRequestException(
+ message: '',
+ stackTrace: StackTrace.empty,
+ );
+ expect(exception.toString(), 'AwsRequestException - message: ');
+ });
+ test('toString - filled', () {
+ AwsRequestException exception = AwsRequestException(
+ message: 'test message',
+ stackTrace: StackTrace.current,
+ );
+ expect(
+ exception.toString(), 'AwsRequestException - message: test message');
+ });
+ });
+ group('AwsRequestType', () {
+ test('values', () {
+ expect(AwsRequestType.values, [
+ AwsRequestType.GET,
+ AwsRequestType.POST,
+ AwsRequestType.DELETE,
+ AwsRequestType.PATCH,
+ AwsRequestType.PUT,
+ AwsRequestType.HEAD
+ ]);
+ });
+ });
+ group('defaultHeaders', () {
+ test('values', () {
+ expect(defaultHeaders, {
+ 'Accept': '*/*',
+ 'Content-Type': 'application/x-amz-json-1.1',
+ });
+ });
+ });
+ group('validateRequest', () {
+ test('both null', () {
+ Map validation = validateRequest(null, null);
+ expect(validation, {
+ 'valid': false,
+ 'error':
+ 'No Service Provided. Please pass in a service or set it in the constructor.'
+ });
+ });
+ test('null service', () {
+ Map validation = validateRequest(null, 'null');
+ expect(validation, {
+ 'valid': false,
+ 'error':
+ 'No Service Provided. Please pass in a service or set it in the constructor.'
+ });
+ });
+ test('null target', () {
+ Map validation = validateRequest('null', null);
+ expect(validation, {
+ 'valid': false,
+ 'error':
+ 'No Target Provided. Please pass in a service or set it in the constructor.'
+ });
+ });
+ test('null target', () {
+ Map validation = validateRequest('null', 'null');
+ expect(validation, {'valid': true, 'error': null});
+ });
+ });
+}