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 @@ Pub Package - - Last Commit - - - Pull Requests - Open Issues @@ -21,6 +15,9 @@ License + + Coverage +

@@ -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}); + }); + }); +}