diff --git a/CHANGELOG.md b/CHANGELOG.md index e9d68c68..48600ce7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Change Log ================================= +## Version 0.9.0 (2019/11/22) + +### Breaking changes: +* HTTP methods exposed through `Reddit` now only accept `Map` for + body arguments. + +### Other changes: +* Added support for uploading images for subreddit theming in `SubredditStyleSheet`. + ## Version 0.8.0 (2019/10/01) * Added support for Dart for Web and Flutter Web. diff --git a/lib/src/auth.dart b/lib/src/auth.dart index 29be57c7..3b6e9577 100644 --- a/lib/src/auth.dart +++ b/lib/src/auth.dart @@ -5,8 +5,10 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:typed_data'; import 'package:http/http.dart' as http; +import 'package:http_parser/http_parser.dart'; import 'package:oauth2/oauth2.dart' as oauth2; import "package:oauth2/src/handle_access_token_response.dart"; @@ -148,16 +150,18 @@ abstract class Authenticator { /// /// [path] is the destination URI and [body] contains the POST parameters /// that will be sent with the request. - Future post(Uri path, Map body) async { + Future post(Uri path, Map body, + {Map files, Map params}) async { _logger.info('POST: $path body: ${DRAWLoggingUtils.jsonify(body)}'); - return _request(_kPostRequest, path, body: body); + return _request(_kPostRequest, path, + body: body, files: files, params: params); } /// Make a simple `PUT` request. /// /// [path] is the destination URI and [body] contains the PUT parameters that /// will be sent with the request. - Future put(Uri path, {/* Map, String */ body}) async { + Future put(Uri path, {Map body}) async { _logger.info('PUT: $path body: ${DRAWLoggingUtils.jsonify(body)}'); return _request(_kPutRequest, path, body: body); } @@ -166,8 +170,7 @@ abstract class Authenticator { /// /// [path] is the destination URI and [body] contains the DELETE parameters /// that will be sent with the request. - Future delete(Uri path, - {/* Map, String */ body}) async { + Future delete(Uri path, {Map body}) async { _logger.info('DELETE: $path body: ${DRAWLoggingUtils.jsonify(body)}'); return _request(_kDeleteRequest, path, body: body); } @@ -178,8 +181,9 @@ abstract class Authenticator { /// request parameters. [body] is an optional parameter which contains the /// body fields for a POST request. Future _request(String type, Uri path, - {/* Map, String */ body, + {Map body, Map params, + Map files, bool followRedirects = false}) async { if (_client == null) { throw DRAWAuthenticationError( @@ -189,7 +193,7 @@ abstract class Authenticator { await refresh(); } final finalPath = path.replace(queryParameters: params); - final request = http.Request(type, finalPath); + final request = http.MultipartRequest(type, finalPath); // Some API requests initiate a redirect (i.e., random submission from a // subreddit) but the redirect doesn't forward the OAuth credentials @@ -198,11 +202,13 @@ abstract class Authenticator { request.followRedirects = followRedirects; if (body != null) { - if (body is Map) { - request.bodyFields = body; - } else { - request.body = body; - } + request.fields.addAll(body); + } + if (files != null) { + request.files.addAll([ + for (final key in files.keys) + http.MultipartFile.fromBytes(key, files[key], filename: 'filename') + ]); } http.StreamedResponse responseStream; try { diff --git a/lib/src/exceptions.dart b/lib/src/exceptions.dart index 1cd2e10a..b28f269b 100644 --- a/lib/src/exceptions.dart +++ b/lib/src/exceptions.dart @@ -109,3 +109,11 @@ class DRAWUnknownResponseException implements Exception { DRAWUnknownResponseException(this.status, this.message); String toString() => 'DRAWUnknownResponse: $message (status: $status)'; } + +/// Thrown if an image upload fails. +class DRAWImageUploadException implements Exception { + final String error; + final String message; + DRAWImageUploadException(this.error, this.message); + String toString() => 'DRAWImageUploadException: ($error) $message'; +} diff --git a/lib/src/image_file_reader.dart b/lib/src/image_file_reader.dart new file mode 100644 index 00000000..22dc8c3d --- /dev/null +++ b/lib/src/image_file_reader.dart @@ -0,0 +1,7 @@ +/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. +/// Please see the AUTHORS file for details. All rights reserved. +/// Use of this source code is governed by a BSD-style license that +/// can be found in the LICENSE file. + +export 'image_file_reader_unsupported.dart' + if (dart.library.io) 'image_file_reader_io.dart'; diff --git a/lib/src/image_file_reader_io.dart b/lib/src/image_file_reader_io.dart new file mode 100644 index 00000000..a41f0c85 --- /dev/null +++ b/lib/src/image_file_reader_io.dart @@ -0,0 +1,28 @@ +/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. +/// Please see the AUTHORS file for details. All rights reserved. +/// Use of this source code is governed by a BSD-style license that +/// can be found in the LICENSE file. + +import 'dart:io'; +import 'package:draw/src/models/subreddit.dart'; +import 'package:collection/collection.dart'; + +const String _kJpegHeader = '\xff\xd8\xff'; + +Future loadImage(Uri imagePath) async { + final image = File.fromUri(imagePath); + if (!await image.exists()) { + throw FileSystemException('File does not exist', imagePath.toString()); + } + final imageBytes = await image.readAsBytes(); + if (imageBytes.length < _kJpegHeader.length) { + throw FormatException('Invalid image format for file $imagePath.'); + } + final header = imageBytes.sublist(0, _kJpegHeader.length); + final isJpeg = + const IterableEquality().equals(_kJpegHeader.codeUnits, header); + return { + 'imageBytes': imageBytes, + 'imageType': isJpeg ? ImageFormat.jpeg : ImageFormat.png, + }; +} diff --git a/lib/src/image_file_reader_unsupported.dart b/lib/src/image_file_reader_unsupported.dart new file mode 100644 index 00000000..52f2068e --- /dev/null +++ b/lib/src/image_file_reader_unsupported.dart @@ -0,0 +1,7 @@ +/// Copyright (c) 2019, the Dart Reddit API Wrapper project authors. +/// Please see the AUTHORS file for details. All rights reserved. +/// Use of this source code is governed by a BSD-style license that +/// can be found in the LICENSE file. + +Future loadImage(Uri imagePath) async => + throw UnsupportedError('Loading images from disk is not supported on web.'); diff --git a/lib/src/models/redditor.dart b/lib/src/models/redditor.dart index 892b0e4f..1375b402 100644 --- a/lib/src/models/redditor.dart +++ b/lib/src/models/redditor.dart @@ -4,7 +4,6 @@ // can be found in the LICENSE file. import 'dart:async'; -import 'dart:convert'; import 'package:draw/src/api_paths.dart'; import 'package:draw/src/base_impl.dart'; @@ -147,7 +146,7 @@ class RedditorRef extends RedditBase Future friend({String note = ''}) async => _throwOnInvalidRedditor(() async => await reddit.put( apiPath['friend_v1'].replaceAll(_userRegExp, _name), - body: json.encode({'note': note}))); + body: {'note': note})); /// Returns a [Redditor] object with friend information populated. /// diff --git a/lib/src/models/subreddit.dart b/lib/src/models/subreddit.dart index 7e8526c5..0622982f 100644 --- a/lib/src/models/subreddit.dart +++ b/lib/src/models/subreddit.dart @@ -6,10 +6,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; +import 'dart:typed_data'; import 'package:draw/src/api_paths.dart'; import 'package:draw/src/base_impl.dart'; import 'package:draw/src/exceptions.dart'; +import 'package:draw/src/image_file_reader.dart'; import 'package:draw/src/listing/listing_generator.dart'; import 'package:draw/src/listing/mixins/base.dart'; import 'package:draw/src/listing/mixins/gilded.dart'; @@ -1406,12 +1408,16 @@ class StyleSheetImage { String toString() => name; } +enum ImageFormat { + jpeg, + png, +} + /// Provides a set of stylesheet functions to a [Subreddit]. class SubredditStyleSheet { - // TODO(bkonyi): implement _uploadImage // static const String _kJpegHeader = '\xff\xd8\xff'; - // static const String _kUploadType = 'upload_type'; - final Subreddit _subreddit; + static const String _kUploadType = 'upload_type'; + final SubredditRef _subreddit; SubredditStyleSheet(this._subreddit); /// Return the stylesheet for the [Subreddit]. @@ -1421,27 +1427,37 @@ class SubredditStyleSheet { return await _subreddit.reddit.get(uri); } -// TODO(bkonyi): uploading files requires some pretty large changes to the -// authenticator implementation. Will do later. -/* - Future _uploadImage(Uri imagePath, Map data) async { + Future _uploadImage(Uri imagePath, Uint8List imageBytes, + ImageFormat format, Map data) async { const kImgType = 'img_type'; - final image = File.fromUri(imagePath); - if (!await image.exists()) { - // TODO(bkonyi): throw - } - final imageRAF = await image.open(); - if (await imageRAF.length() < _kJpegHeader.length) { - // TODO(bkonyi): throw + if ((imagePath == null) && (imageBytes == null)) { + throw DRAWArgumentError( + 'Only one of imagePath or imageBytes can be provided.'); } - final header = (await imageRAF.read(_kJpegHeader.length)); - data[kImgType] = (header == _kJpegHeader.codeUnits) ? 'jpg' : 'png'; + if (imageBytes == null) { + final imageInfo = await loadImage(imagePath); + // ignore: parameter_assignments + format = imageInfo['imageType']; + // ignore: parameter_assignments + imageBytes = imageInfo['imageBytes']; + } + data[kImgType] = (format == ImageFormat.png) ? 'png' : 'jpeg'; + data['api_type'] = 'json'; final uri = apiPath['upload_image'] .replaceAll(SubredditRef._subredditRegExp, _subreddit.displayName); - //final response =_subreddit.reddit.post(uri, data, ) + final response = await _subreddit.reddit + .post(uri, data, files: {'file': imageBytes}, objectify: false) as Map; + const kImgSrc = 'img_src'; + const kErrors = 'errors'; + const kErrorsValues = 'errors_values'; + if (response[kImgSrc].isNotEmpty) { + return Uri.parse(response[kImgSrc]); + } else { + throw DRAWImageUploadException( + response[kErrors].first, response[kErrorsValues].first); + } } -*/ /// Remove the current header image for the [Subreddit]. Future deleteHeader() async { @@ -1496,48 +1512,81 @@ class SubredditStyleSheet { return await _subreddit.reddit.post(uri, data); } - // TODO(bkonyi): implement _uploadImage - // Upload an image to the [Subreddit]. - // - // `name` is the name to be used for the image. If an image already exists - // with this name, it will be replaced. - // - // `imagePath` is the path to a JPEG or PNG image. This path must be local - // and accessible from disk. - /* - Future upload(String name, Uri imagePath) async => - await _uploadImage(imagePath, { + /// Upload an image to the [Subreddit]. + /// + /// `name` is the name to be used for the image. If an image already exists + /// with this name, it will be replaced. + /// + /// `imagePath` is the path to a JPEG or PNG image. This path must be local + /// and accessible from disk. Will result in an [UnsupportedError] if provided + /// in a web application. + /// + /// `bytes` is a list of bytes representing an image. + /// + /// `format` is the format of the image defined by `bytes`. + /// + /// On success, the [Uri] for the uploaded image is returned. On failure, + /// [DRAWImageUploadException] is thrown. + Future upload(String name, + {Uri imagePath, Uint8List bytes, ImageFormat format}) async => + await _uploadImage(imagePath, bytes, format, { 'name': name, _kUploadType: 'img', }); - */ - // TODO(bkonyi): implement _uploadImage - // Upload an image to be used as the header image for the [Subreddit]. - /* - Future uploadHeader(Uri imagePath) async => - await _uploadImage(imagePath, { + /// Upload an image to be used as the header image for the [Subreddit]. + /// + /// `imagePath` is the path to a JPEG or PNG image. This path must be local + /// and accessible from disk. Will result in an [UnsupportedError] if provided + /// in a web application. + /// + /// `bytes` is a list of bytes representing an image. + /// + /// `format` is the format of the image defined by `bytes`. + /// + /// On success, the [Uri] for the uploaded image is returned. On failure, + /// [DRAWImageUploadException] is thrown. + Future uploadHeader( + {Uri imagePath, Uint8List bytes, ImageFormat format}) async => + await _uploadImage(imagePath, bytes, format, { _kUploadType: 'header', }); - */ - // TODO(bkonyi): implement _uploadImage - // Upload an image to be used as the mobile header image for the [Subreddit]. - /* - Future uploadMobileHeader(Uri imagePath) async => - await _uploadImage(imagePath, { + /// Upload an image to be used as the mobile header image for the [Subreddit]. + /// + /// `imagePath` is the path to a JPEG or PNG image. This path must be local + /// and accessible from disk. Will result in an [UnsupportedError] if provided + /// in a web application. + /// + /// `bytes` is a list of bytes representing an image. + /// + /// `format` is the format of the image defined by `bytes`. + /// + /// On success, the [Uri] for the uploaded image is returned. On failure, + /// [DRAWImageUploadException] is thrown. + Future uploadMobileHeader( + {Uri imagePath, Uint8List bytes, ImageFormat format}) async => + await _uploadImage(imagePath, bytes, format, { _kUploadType: 'banner', }); - */ - // TODO(bkonyi): implement _uploadImage - // Upload an image to be used as the mobile icon image for the [Subreddit]. - /* - Future uploadMobileIcon(Uri imagePath) async => - await _uploadImage(imagePath, { + /// Upload an image to be used as the mobile icon image for the [Subreddit]. + /// + /// `imagePath` is the path to a JPEG or PNG image. This path must be local + /// and accessible from disk. Will result in an [UnsupportedError] if provided + /// in a web application. + /// + /// `bytes` is a list of bytes representing an image. + /// + /// `format` is the format of the image defined by `bytes`. + /// + /// On success, the [Uri] for the uploaded image is returned. On failure, + /// [DRAWImageUploadException] is thrown. + Future uploadMobileIcon( + {Uri imagePath, Uint8List bytes, ImageFormat format}) async => + await _uploadImage(imagePath, bytes, format, { _kUploadType: 'icon', }); - */ } /// Provides a set of wiki functions to a [Subreddit]. diff --git a/lib/src/reddit.dart b/lib/src/reddit.dart index 5bd48536..4ffac195 100644 --- a/lib/src/reddit.dart +++ b/lib/src/reddit.dart @@ -4,6 +4,7 @@ // can be found in the LICENSE file. import 'dart:async'; +import 'dart:typed_data'; import 'package:draw/src/auth.dart'; import 'package:draw/src/draw_config_context.dart'; @@ -634,21 +635,23 @@ class Reddit { } Future post(String api, Map body, - {bool discardResponse = false, bool objectify = true}) async { + {Map files, + Map params, + bool discardResponse = false, + bool objectify = true}) async { if (!_initialized) { throw DRAWAuthenticationError( 'Cannot make requests using unauthenticated client.'); } final path = Uri.https(defaultOAuthApiEndpoint, api); - final response = await auth.post(path, body); + final response = await auth.post(path, body, files: files, params: params); if (discardResponse) { return null; } return objectify ? _objector.objectify(response) : response; } - Future put(String api, - {/* Map, String */ body}) async { + Future put(String api, {Map body}) async { if (!_initialized) { throw DRAWAuthenticationError( 'Cannot make requests using unauthenticated client.'); @@ -658,8 +661,7 @@ class Reddit { return _objector.objectify(response); } - Future delete(String api, - {/* Map, String */ body}) async { + Future delete(String api, {Map body}) async { if (!_initialized) { throw DRAWAuthenticationError( 'Cannot make requests using unauthenticated client.'); diff --git a/pubspec.yaml b/pubspec.yaml index 70efd9d8..23da940a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: draw -version: 0.8.0 +version: 0.9.0 description: 'A fully-featured Reddit API wrapper for Dart, inspired by PRAW.' homepage: 'https://github.com/draw-dev/DRAW' authors: diff --git a/test/images/10by3.jpg b/test/images/10by3.jpg new file mode 100644 index 00000000..2f05d96f Binary files /dev/null and b/test/images/10by3.jpg differ diff --git a/test/images/256.jpg b/test/images/256.jpg new file mode 100644 index 00000000..90462bac Binary files /dev/null and b/test/images/256.jpg differ diff --git a/test/images/bad.jpg b/test/images/bad.jpg new file mode 100644 index 00000000..2e65efe2 --- /dev/null +++ b/test/images/bad.jpg @@ -0,0 +1 @@ +a \ No newline at end of file diff --git a/test/images/dart_header.png b/test/images/dart_header.png new file mode 100644 index 00000000..1c4a9c89 Binary files /dev/null and b/test/images/dart_header.png differ diff --git a/test/redditor/lib_redditor_bad_friend.json b/test/redditor/lib_redditor_bad_friend.json index 1c8dacb0..b8af1de4 100644 --- a/test/redditor/lib_redditor_bad_friend.json +++ b/test/redditor/lib_redditor_bad_friend.json @@ -3,7 +3,7 @@ "always": false, "request": [ "https://oauth.reddit.com/api/v1/me/friends/drawapiofficial2", - "{\"note\":\"\"}" + "{note: }" ], "response": [ "DRAWNotFoundException", @@ -11,4 +11,4 @@ "Bad Request" ] } -] \ No newline at end of file +] diff --git a/test/redditor/lib_redditor_friend.json b/test/redditor/lib_redditor_friend.json index 35fdf828..adc14173 100644 --- a/test/redditor/lib_redditor_friend.json +++ b/test/redditor/lib_redditor_friend.json @@ -3,7 +3,7 @@ "always": false, "request": [ "https://oauth.reddit.com/api/v1/me/friends/XtremeCheese", - "{\"note\":\"My best friend!\"}" + "{note: My best friend!}" ], "response": { "date": 1503502054.0, @@ -81,4 +81,4 @@ } } } -] \ No newline at end of file +] diff --git a/test/subreddit/lib_subreddit_stylesheet_upload.json b/test/subreddit/lib_subreddit_stylesheet_upload.json new file mode 100644 index 00000000..566c42f1 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] \ No newline at end of file diff --git a/test/subreddit/lib_subreddit_stylesheet_upload_bytes.json b/test/subreddit/lib_subreddit_stylesheet_upload_bytes.json new file mode 100644 index 00000000..566c42f1 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload_bytes.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] \ No newline at end of file diff --git a/test/subreddit/lib_subreddit_stylesheet_upload_header.json b/test/subreddit/lib_subreddit_stylesheet_upload_header.json new file mode 100644 index 00000000..d9109750 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload_header.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: header, img_type: png, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png","errors_values":[]}}] \ No newline at end of file diff --git a/test/subreddit/lib_subreddit_stylesheet_upload_invalid_cases.json b/test/subreddit/lib_subreddit_stylesheet_upload_invalid_cases.json new file mode 100644 index 00000000..108f1a08 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload_invalid_cases.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{name: foobar, upload_type: img, img_type: png, api_type: json}"],"response":{"errors":["IMAGE_ERROR"],"img_src":"","errors_values":["Invalid image or general image error"]}}] \ No newline at end of file diff --git a/test/subreddit/lib_subreddit_stylesheet_upload_mobile_header.json b/test/subreddit/lib_subreddit_stylesheet_upload_mobile_header.json new file mode 100644 index 00000000..0d605f11 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload_mobile_header.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: banner, img_type: jpeg, api_type: json}"],"response":{"errors":[],"img_src":"https://a.thumbs.redditmedia.com/MkErrkhg6-Iou7zdTRxnpwOSNK4DPWXZ3xI35LiKTU0.png","errors_values":[]}}] \ No newline at end of file diff --git a/test/subreddit/lib_subreddit_stylesheet_upload_mobile_icon.json b/test/subreddit/lib_subreddit_stylesheet_upload_mobile_icon.json new file mode 100644 index 00000000..e0932831 --- /dev/null +++ b/test/subreddit/lib_subreddit_stylesheet_upload_mobile_icon.json @@ -0,0 +1 @@ +[{"always":false,"request":["https://oauth.reddit.com/r/drawapitesting/api/upload_sr_img","{upload_type: icon, img_type: jpeg, api_type: json}"],"response":{"errors":[],"img_src":"https://b.thumbs.redditmedia.com/GJSQRXiRY-2CH4PTgrrPNXqSPaQSKJYUikUr15m2n3Y.png","errors_values":[]}}] \ No newline at end of file diff --git a/test/subreddit/subreddit_test.dart b/test/subreddit/subreddit_test.dart index e871c105..abb4eddf 100644 --- a/test/subreddit/subreddit_test.dart +++ b/test/subreddit/subreddit_test.dart @@ -4,6 +4,7 @@ // can be found in the LICENSE file. import 'dart:async'; +import 'dart:io'; import 'package:draw/draw.dart'; import 'package:test/test.dart'; @@ -315,6 +316,87 @@ Future main() async { expect(stylesheet.stylesheet, kNewStyle); }); + test('lib/subreddit_stylesheet/upload', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + final uri = await stylesheet.upload('foobar', + imagePath: Uri.file('test/images/dart_header.png')); + expect(uri.toString(), + 'https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png'); + }); + + test('lib/subreddit_stylesheet/upload_bytes', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload_bytes.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + final imageBytes = + await File.fromUri(Uri.file('test/images/dart_header.png')) + .readAsBytes(); + final uri = await stylesheet.upload('foobar', + bytes: imageBytes, format: ImageFormat.png); + expect(uri.toString(), + 'https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png'); + }); + + test('lib/subreddit_stylesheet/upload_invalid_cases', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload_invalid_cases.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + + // Missing args. + await expectLater(() async => await stylesheet.upload('foobar'), + throwsA(TypeMatcher())); + + // Bad format. + await expectLater( + () async => await stylesheet.upload('foobar', + imagePath: Uri.file('test/test_utils.dart')), + throwsA(TypeMatcher())); + + // Too small. + await expectLater( + () async => await stylesheet.upload('foobar', + imagePath: Uri.file('test/images/bad.jpg')), + throwsA(TypeMatcher())); + + // File doesn't exist. + await expectLater( + () async => await stylesheet.upload('foobar', + imagePath: Uri.file('foobar.bad')), + throwsA(TypeMatcher())); + }); + + test('lib/subreddit_stylesheet/upload_header', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload_header.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + final uri = await stylesheet.uploadHeader( + imagePath: Uri.file('test/images/dart_header.png')); + expect(uri.toString(), + 'https://a.thumbs.redditmedia.com/MJGdqUs7bXLgG7-pYG5zVRdI_6qUQ6svvlZXURe5K98.png'); + }); + + test('lib/subreddit_stylesheet/upload_mobile_header', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload_mobile_header.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + final uri = await stylesheet.uploadMobileHeader( + imagePath: Uri.file('test/images/10by3.jpg')); + expect(uri.toString(), + 'https://a.thumbs.redditmedia.com/MkErrkhg6-Iou7zdTRxnpwOSNK4DPWXZ3xI35LiKTU0.png'); + }); + + test('lib/subreddit_stylesheet/upload_mobile_icon', () async { + final reddit = await createRedditTestInstance( + 'test/subreddit/lib_subreddit_stylesheet_upload_mobile_icon.json'); + final stylesheet = reddit.subreddit('drawapitesting').stylesheet; + final uri = await stylesheet.uploadMobileIcon( + imagePath: Uri.file('test/images/256.jpg')); + expect(uri.toString(), + 'https://b.thumbs.redditmedia.com/GJSQRXiRY-2CH4PTgrrPNXqSPaQSKJYUikUr15m2n3Y.png'); + }); + test('lib/subreddit_flair/call', () async { final reddit = await createRedditTestInstance( 'test/subreddit/lib_subreddit_flair_call.json'); diff --git a/test/test_authenticator.dart b/test/test_authenticator.dart index 712935a1..f94a0a8b 100644 --- a/test/test_authenticator.dart +++ b/test/test_authenticator.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'dart:typed_data'; import 'package:collection/collection.dart'; import 'package:reply/reply.dart'; @@ -127,14 +128,18 @@ class TestAuthenticator extends Authenticator { } @override - Future post(Uri path, Map body) async { + Future post(Uri path, Map body, + {Map files, Map params}) async { var result; if (isRecording) { + // Note: we ignore the files parameter for creating recordings, so tests + // which try to overwrite a remote file multiple times might have issues. result = _recording.reply([path.toString(), body.toString()]); _throwOnError(result); } else { try { - result = await _recordAuth.post(path, body); + result = + await _recordAuth.post(path, body, files: files, params: params); } catch (e) { // Throws. _recordException(path, body, e); @@ -148,8 +153,7 @@ class TestAuthenticator extends Authenticator { } @override - Future put(Uri path, - {/* Map, String */ body}) async { + Future put(Uri path, {Map body}) async { var result; if (isRecording) { result = _recording.reply([path.toString(), body.toString()]); @@ -170,8 +174,7 @@ class TestAuthenticator extends Authenticator { } @override - Future delete(Uri path, - {/* Map, String */ body}) async { + Future delete(Uri path, {Map body}) async { var result; if (isRecording) { result = _recording.reply([path.toString(), body.toString()]);