From 15bbc392987a2e4c462299a5710bd3999c10f0d0 Mon Sep 17 00:00:00 2001
From: xuty <xty50337@hotmail.com>
Date: Wed, 18 Aug 2021 13:26:09 +0000
Subject: [PATCH] Add more tests

---
 .gitignore                         |   3 +
 lib/src/minio.dart                 |  22 +-
 lib/src/minio_poller.dart          |   5 +-
 test/helpers.dart                  |  26 ++
 test/minio_dart_test.dart          | 263 -------------
 test/minio_helpers_test.dart       |  34 ++
 test/minio_models_test.dart        |  19 +
 test/minio_presigned_url_test.dart |  32 ++
 test/minio_stream_test.dart        |  30 ++
 test/minio_test.dart               | 602 +++++++++++++++++++++++++++++
 test/utils_test.dart               |  59 +++
 11 files changed, 822 insertions(+), 273 deletions(-)
 create mode 100644 test/helpers.dart
 delete mode 100644 test/minio_dart_test.dart
 create mode 100644 test/minio_helpers_test.dart
 create mode 100644 test/minio_models_test.dart
 create mode 100644 test/minio_presigned_url_test.dart
 create mode 100644 test/minio_stream_test.dart
 create mode 100644 test/minio_test.dart
 create mode 100644 test/utils_test.dart

diff --git a/.gitignore b/.gitignore
index 56260f1..c80df3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,6 @@ doc/api/
 
 # IDE directories
 .idea/
+
+# For debugging purpose
+test/main.dart
\ No newline at end of file
diff --git a/lib/src/minio.dart b/lib/src/minio.dart
index e2afb86..d4732f4 100644
--- a/lib/src/minio.dart
+++ b/lib/src/minio.dart
@@ -466,11 +466,11 @@ class Minio {
   }) {
     MinioInvalidBucketNameError.check(bucket);
 
-    final listener =
-        NotificationPoller(_client, bucket, prefix, suffix, events);
-    listener.start();
+    final poller = NotificationPoller(_client, bucket, prefix, suffix, events);
 
-    return listener;
+    poller.start();
+
+    return poller;
   }
 
   /// List of buckets created.
@@ -878,15 +878,15 @@ class Minio {
     metadata = prependXAMZMeta(metadata ?? <String, String>{});
 
     size ??= maxObjectSize;
-    size = _calculatePartSize(size);
+    final partSize = _calculatePartSize(size);
 
-    final chunker = BlockStream(size);
+    final chunker = BlockStream(partSize);
     final uploader = MinioUploader(
       this,
       _client,
       bucket,
       object,
-      size,
+      partSize,
       metadata,
     );
     final etag = await data.transform(chunker).pipe(uploader);
@@ -894,8 +894,12 @@ class Minio {
   }
 
   /// Remove all bucket notification
-  Future<void> removeAllBucketNotification(bucket) => setBucketNotification(
-      bucket, NotificationConfiguration(null, null, null));
+  Future<void> removeAllBucketNotification(String bucket) async {
+    await setBucketNotification(
+      bucket,
+      NotificationConfiguration(null, null, null),
+    );
+  }
 
   /// Remove a bucket.
   Future<void> removeBucket(String bucket) async {
diff --git a/lib/src/minio_poller.dart b/lib/src/minio_poller.dart
index 1801deb..ee20242 100644
--- a/lib/src/minio_poller.dart
+++ b/lib/src/minio_poller.dart
@@ -23,6 +23,10 @@ class NotificationPoller {
 
   bool _stop = true;
 
+  bool get isStarted {
+    return !_stop;
+  }
+
   /// Starts the polling.
   void start() async {
     _stop = false;
@@ -57,7 +61,6 @@ class NotificationPoller {
 
       final chunk = utf8.decode(resp);
       if (chunk.trim().isEmpty) continue;
-
       final data = json.decode(chunk);
       final records = List<Map<String, dynamic>>.from(data['Records']);
       await _eventStream.addStream(Stream.fromIterable(records));
diff --git a/test/helpers.dart b/test/helpers.dart
new file mode 100644
index 0000000..63ca173
--- /dev/null
+++ b/test/helpers.dart
@@ -0,0 +1,26 @@
+import 'package:minio/minio.dart';
+
+/// Initializes an instance of [Minio] with per default valid configuration.
+///
+/// Don't worry, these credentials for MinIO are publicly available and
+/// connect only to the MinIO demo server at `play.minio.io`.
+Minio getMinioClient({
+  String endpoint = 'play.minio.io',
+  int? port = 443,
+  bool useSSL = true,
+  String accessKey = 'Q3AM3UQ867SPQQA43P2F',
+  String secretKey = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
+  String sessionToken = '',
+  String region = 'us-east-1',
+  bool enableTrace = false,
+}) =>
+    Minio(
+      endPoint: endpoint,
+      port: port,
+      useSSL: useSSL,
+      accessKey: accessKey,
+      secretKey: secretKey,
+      sessionToken: sessionToken,
+      region: region,
+      enableTrace: enableTrace,
+    );
diff --git a/test/minio_dart_test.dart b/test/minio_dart_test.dart
deleted file mode 100644
index cf442c0..0000000
--- a/test/minio_dart_test.dart
+++ /dev/null
@@ -1,263 +0,0 @@
-import 'dart:io';
-
-import 'package:minio/io.dart';
-import 'package:minio/minio.dart';
-import 'package:test/test.dart';
-
-void main() {
-  group('listBuckets', () {
-    test('listBuckets() succeeds', () async {
-      final minio = _getClient();
-
-      expect(() async => await minio.listBuckets(), returnsNormally);
-    });
-
-    test('listBuckets() fails due to wrong access key', () async {
-      final minio = _getClient(accessKey: 'incorrect-access-key');
-
-      expect(
-        () async => await minio.listBuckets(),
-        throwsA(
-          isA<MinioError>().having(
-            (e) => e.message,
-            'message',
-            'The Access Key Id you provided does not exist in our records.',
-          ),
-        ),
-      );
-    });
-
-    test('listBuckets() fails due to wrong secret key', () async {
-      final minio = _getClient(secretKey: 'incorrect-secret-key');
-
-      expect(
-        () async => await minio.listBuckets(),
-        throwsA(
-          isA<MinioError>().having(
-            (e) => e.message,
-            'message',
-            'The request signature we calculated does not match the signature you provided. Check your key and signing method.',
-          ),
-        ),
-      );
-    });
-  });
-
-  group('bucketExists', () {
-    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
-
-    setUpAll(() async {
-      final minio = _getClient();
-      await minio.makeBucket(bucketName);
-    });
-
-    tearDownAll(() async {
-      final minio = _getClient();
-      await minio.removeBucket(bucketName);
-    });
-
-    test('bucketExists() returns true for an existing bucket', () async {
-      final minio = _getClient();
-      expect(await minio.bucketExists(bucketName), equals(true));
-    });
-
-    test('bucketExists() returns false for a non-existent bucket', () async {
-      final minio = _getClient();
-      expect(
-          await minio.bucketExists('non-existing-bucket-name'), equals(false));
-    });
-
-    test('bucketExists() fails due to wrong access key', () async {
-      final minio = _getClient(accessKey: 'incorrect-access-key');
-      expect(
-        () async => await minio.bucketExists(bucketName),
-        throwsA(
-          isA<MinioError>().having(
-            (e) => e.message,
-            'message',
-            'Forbidden',
-          ),
-        ),
-      );
-    });
-
-    test('bucketExists() fails due to wrong secret key', () async {
-      final minio = _getClient(secretKey: 'incorrect-secret-key');
-      expect(
-        () async => await minio.bucketExists(bucketName),
-        throwsA(
-          isA<MinioError>().having(
-            (e) => e.message,
-            'message',
-            'Forbidden',
-          ),
-        ),
-      );
-    });
-  });
-
-  group('fPutObject', () {
-    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
-    late Directory tempDir;
-    late File testFile;
-    final objectName = 'a.jpg';
-
-    setUpAll(() async {
-      tempDir = await Directory.systemTemp.createTemp();
-      testFile = await File('${tempDir.path}/$objectName').create();
-      await testFile.writeAsString('random bytes');
-
-      final minio = _getClient();
-      await minio.makeBucket(bucketName);
-    });
-
-    tearDownAll(() async {
-      final minio = _getClient();
-      await minio.removeObject(bucketName, objectName);
-      await tempDir.delete(recursive: true);
-    });
-
-    test('fPutObject() inserts content-type to metadata', () async {
-      final minio = _getClient();
-      await minio.fPutObject(bucketName, objectName, testFile.path);
-
-      final stat = await minio.statObject(bucketName, objectName);
-      expect(stat.metaData!['content-type'], equals('image/jpeg'));
-    });
-
-    test('fPutObject() adds user-defined object metadata w/ prefix', () async {
-      final prefix = 'x-amz-meta-';
-      final userDefinedMetadataKey = '${prefix}user-defined-metadata-key-1';
-      final userDefinedMetadataValue = 'custom value 1';
-      final metadata = {
-        userDefinedMetadataKey: userDefinedMetadataValue,
-      };
-
-      final minio = _getClient();
-      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);
-
-      final stat = await minio.statObject(bucketName, objectName);
-      expect(
-        stat.metaData![userDefinedMetadataKey.substring(prefix.length)],
-        equals(userDefinedMetadataValue),
-      );
-    });
-
-    test('fPutObject() adds user-defined object metadata w/o prefix', () async {
-      final userDefinedMetadataKey = 'user-defined-metadata-key-2';
-      final userDefinedMetadataValue = 'custom value 2';
-      final metadata = {
-        userDefinedMetadataKey: userDefinedMetadataValue,
-      };
-
-      final minio = _getClient();
-      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);
-
-      final stat = await minio.statObject(bucketName, objectName);
-      expect(stat.metaData![userDefinedMetadataKey],
-          equals(userDefinedMetadataValue));
-    });
-
-    test('fPutObject() with empty file', () async {
-      final objectName = 'empty.txt';
-      final emptyFile = await File('${tempDir.path}/$objectName').create();
-      await emptyFile.writeAsString('');
-
-      final minio = _getClient();
-      await minio.fPutObject(bucketName, objectName, emptyFile.path);
-
-      final stat = await minio.statObject(bucketName, objectName);
-      expect(stat.size, equals(0));
-    });
-  });
-
-  group(
-    'setObjectACL',
-    () {
-      late String bucketName;
-      late Directory tempDir;
-      File testFile;
-      final objectName = 'a.jpg';
-
-      setUpAll(() async {
-        bucketName = DateTime.now().millisecondsSinceEpoch.toString();
-
-        tempDir = await Directory.systemTemp.createTemp();
-        testFile = await File('${tempDir.path}/$objectName').create();
-        await testFile.writeAsString('random bytes');
-
-        final minio = _getClient();
-        await minio.makeBucket(bucketName);
-
-        await minio.fPutObject(bucketName, objectName, testFile.path);
-      });
-
-      tearDownAll(() async {
-        await tempDir.delete(recursive: true);
-      });
-
-      test('setObjectACL() set objects acl', () async {
-        final minio = _getClient();
-        await minio.setObjectACL(bucketName, objectName, 'public-read');
-      });
-    },
-  );
-
-  group(
-    'getObjectACL',
-    () {
-      late String bucketName;
-      late Directory tempDir;
-      File testFile;
-      final objectName = 'a.jpg';
-
-      setUpAll(() async {
-        bucketName = DateTime.now().millisecondsSinceEpoch.toString();
-
-        tempDir = await Directory.systemTemp.createTemp();
-        testFile = await File('${tempDir.path}/$objectName').create();
-        await testFile.writeAsString('random bytes');
-
-        final minio = _getClient();
-        await minio.makeBucket(bucketName);
-
-        await minio.fPutObject(bucketName, objectName, testFile.path);
-      });
-
-      tearDownAll(() async {
-        await tempDir.delete(recursive: true);
-      });
-
-      test('getObjectACL() fetch objects acl', () async {
-        final minio = _getClient();
-        var acl = await minio.getObjectACL(bucketName, objectName);
-        expect(acl.grants!.permission, equals(null));
-      });
-    },
-  );
-}
-
-/// Initializes an instance of [Minio] with per default valid configuration.
-///
-/// Don't worry, these credentials for MinIO are publicly available and
-/// connect only to the MinIO demo server at `play.minio.io`.
-Minio _getClient({
-  String endpoint = 'play.minio.io',
-  int port = 443,
-  bool useSSL = true,
-  String accessKey = 'Q3AM3UQ867SPQQA43P2F',
-  String secretKey = 'zuf+tfteSlswRu7BJ86wekitnifILbZam1KYY3TG',
-  String sessionToken = '',
-  String region = 'us-east-1',
-  bool enableTrace = false,
-}) =>
-    Minio(
-      endPoint: endpoint,
-      port: port,
-      useSSL: useSSL,
-      accessKey: accessKey,
-      secretKey: secretKey,
-      sessionToken: sessionToken,
-      region: region,
-      enableTrace: enableTrace,
-    );
diff --git a/test/minio_helpers_test.dart b/test/minio_helpers_test.dart
new file mode 100644
index 0000000..a73f55f
--- /dev/null
+++ b/test/minio_helpers_test.dart
@@ -0,0 +1,34 @@
+import 'package:minio/src/minio_helpers.dart';
+import 'package:test/test.dart';
+
+void main() {
+  group('helpers', () {
+    test('should validate for s3 endpoint', () {
+      expect(isValidEndpoint('s3.amazonaws.com'), isTrue);
+    });
+    test('should validate for s3 china', () {
+      expect(isValidEndpoint('s3.cn-north-1.amazonaws.com.cn'), isTrue);
+    });
+    test('should validate for us-west-2', () {
+      expect(isValidEndpoint('s3-us-west-2.amazonaws.com'), isTrue);
+    });
+    test('should fail for invalid endpoint characters', () {
+      expect(isValidEndpoint('111.#2.11'), isFalse);
+    });
+    test('should validate for valid ip', () {
+      expect(isValidIPv4('1.1.1.1'), isTrue);
+    });
+    test('should fail for invalid ip', () {
+      expect(isValidIPv4('1.1.1'), isFalse);
+    });
+    test('should make date short', () {
+      final date = DateTime.parse('2012-12-03T17:25:36.331Z');
+      expect(makeDateShort(date), '20121203');
+    });
+    test('should make date long', () {
+      final date = DateTime.parse('2017-08-11T17:26:34.935Z');
+      expect(makeDateLong(date), '20170811T172634Z');
+    });
+  });
+  ;
+}
diff --git a/test/minio_models_test.dart b/test/minio_models_test.dart
new file mode 100644
index 0000000..1e2d07c
--- /dev/null
+++ b/test/minio_models_test.dart
@@ -0,0 +1,19 @@
+import 'package:minio/models.dart';
+import 'package:test/test.dart';
+
+void main() {
+  final date = DateTime.utc(2017, 8, 11, 19, 34, 18);
+  final dateString = 'Fri, 11 Aug 2017 19:34:18 GMT';
+
+  test('CopyConditions.setModified() works', () {
+    final cc = CopyConditions();
+    cc.setModified(date);
+    expect(cc.modified, equals(dateString));
+  });
+
+  test('CopyConditions.setUnmodified() works', () {
+    final cc = CopyConditions();
+    cc.setUnmodified(date);
+    expect(cc.unmodified, dateString);
+  });
+}
diff --git a/test/minio_presigned_url_test.dart b/test/minio_presigned_url_test.dart
new file mode 100644
index 0000000..7821e50
--- /dev/null
+++ b/test/minio_presigned_url_test.dart
@@ -0,0 +1,32 @@
+import 'package:minio/minio.dart';
+import 'package:test/test.dart';
+
+import 'helpers.dart';
+
+void main() {
+  test('Minio.presignedGetObject() works', () async {
+    final minio = getMinioClient();
+    await minio.presignedGetObject('bucket', 'object');
+  });
+
+  test('Minio.presignedGetObject() throws when [expires] < 0', () async {
+    final minio = getMinioClient();
+    expect(
+      () => minio.presignedGetObject('bucket', 'object', expires: -1),
+      throwsA(isA<MinioError>()),
+    );
+  });
+
+  test('Minio.presignedPutObject() works', () async {
+    final minio = getMinioClient();
+    await minio.presignedPutObject('bucket', 'object');
+  });
+
+  test('Minio.presignedPutObject() throws when [expires] < 0', () async {
+    final minio = getMinioClient();
+    expect(
+      () => minio.presignedPutObject('bucket', 'object', expires: -1),
+      throwsA(isA<MinioError>()),
+    );
+  });
+}
diff --git a/test/minio_stream_test.dart b/test/minio_stream_test.dart
new file mode 100644
index 0000000..8c74cd8
--- /dev/null
+++ b/test/minio_stream_test.dart
@@ -0,0 +1,30 @@
+import 'package:test/test.dart';
+
+import 'helpers.dart';
+
+void main() {
+  group('MinioByteStream', () {
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    final objectName = 'content-length-test';
+    final testData = [1, 2, 3, 4, 5];
+
+    setUpAll(() async {
+      final minio = getMinioClient();
+      await minio.makeBucket(bucketName);
+      await minio.putObject(bucketName, objectName, Stream.value(testData));
+    });
+
+    tearDownAll(() async {
+      final minio = getMinioClient();
+      await minio.removeObject(bucketName, objectName);
+      await minio.removeBucket(bucketName);
+    });
+
+    test('contains content length', () async {
+      final minio = getMinioClient();
+      final stream = await minio.getObject(bucketName, objectName);
+      expect(stream.contentLength, equals(testData.length));
+      await stream.drain();
+    });
+  });
+}
diff --git a/test/minio_test.dart b/test/minio_test.dart
new file mode 100644
index 0000000..fdf5770
--- /dev/null
+++ b/test/minio_test.dart
@@ -0,0 +1,602 @@
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:minio/io.dart';
+import 'package:minio/minio.dart';
+import 'package:minio/src/minio_models_generated.dart';
+import 'package:test/test.dart';
+
+import 'helpers.dart';
+
+void main() {
+  // testConstruct();
+  // testListBuckets();
+  // testBucketExists();
+  // testFPutObject();
+  // testGetObjectACL();
+  // testSetObjectACL();
+  // testGetObject();
+  // testPutObject();
+  // testGetBucketNotification();
+  // testSetBucketNotification();
+  // testRemoveAllBucketNotification();
+  // testListenBucketNotification();
+  // testStatObject();
+  // testMakeBucket();
+  // testRemoveBucket();
+  // testRemoveObject();
+}
+
+void testConstruct() {
+  test('Minio() implies http port', () {
+    final client = getMinioClient(port: null, useSSL: false);
+    expect(client.port, equals(80));
+  });
+
+  test('Minio() implies https port', () {
+    final client = getMinioClient(port: null, useSSL: true);
+    expect(client.port, equals(443));
+  });
+
+  test('Minio() overrides port with http', () {
+    final client = getMinioClient(port: 1234, useSSL: false);
+    expect(client.port, equals(1234));
+  });
+
+  test('Minio() overrides port with https', () {
+    final client = getMinioClient(port: 1234, useSSL: true);
+    expect(client.port, equals(1234));
+  });
+
+  test('Minio() throws when endPoint is url', () {
+    expect(
+      () => getMinioClient(endpoint: 'http://play.min.io'),
+      throwsA(isA<MinioError>()),
+    );
+  });
+
+  test('Minio() throws when port is invalid', () {
+    expect(
+      () => getMinioClient(port: -1),
+      throwsA(isA<MinioError>()),
+    );
+
+    expect(
+      () => getMinioClient(port: 65536),
+      throwsA(isA<MinioError>()),
+    );
+  });
+}
+
+void testListBuckets() {
+  test('listBuckets() succeeds', () async {
+    final minio = getMinioClient();
+
+    expect(() async => await minio.listBuckets(), returnsNormally);
+  });
+
+  test('listBuckets() can list buckets', () async {
+    final minio = getMinioClient();
+    final bucketName1 = DateTime.now().millisecondsSinceEpoch.toString();
+    await minio.makeBucket(bucketName1);
+
+    final bucketName2 = DateTime.now().millisecondsSinceEpoch.toString();
+    await minio.makeBucket(bucketName2);
+
+    final buckets = await minio.listBuckets();
+    expect(buckets.any((b) => b.name == bucketName1), isTrue);
+    expect(buckets.any((b) => b.name == bucketName2), isTrue);
+
+    await minio.removeBucket(bucketName1);
+    await minio.removeBucket(bucketName2);
+  });
+
+  test('listBuckets() fails due to wrong access key', () async {
+    final minio = getMinioClient(accessKey: 'incorrect-access-key');
+
+    expect(
+      () async => await minio.listBuckets(),
+      throwsA(
+        isA<MinioError>().having(
+          (e) => e.message,
+          'message',
+          'The Access Key Id you provided does not exist in our records.',
+        ),
+      ),
+    );
+  });
+
+  test('listBuckets() fails due to wrong secret key', () async {
+    final minio = getMinioClient(secretKey: 'incorrect-secret-key');
+
+    expect(
+      () async => await minio.listBuckets(),
+      throwsA(
+        isA<MinioError>().having(
+          (e) => e.message,
+          'message',
+          'The request signature we calculated does not match the signature you provided. Check your key and signing method.',
+        ),
+      ),
+    );
+  });
+}
+
+void testBucketExists() {
+  group('bucketExists', () {
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      final minio = getMinioClient();
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      final minio = getMinioClient();
+      await minio.removeBucket(bucketName);
+    });
+
+    test('bucketExists() returns true for an existing bucket', () async {
+      final minio = getMinioClient();
+      expect(await minio.bucketExists(bucketName), equals(true));
+    });
+
+    test('bucketExists() returns false for a non-existent bucket', () async {
+      final minio = getMinioClient();
+      expect(
+          await minio.bucketExists('non-existing-bucket-name'), equals(false));
+    });
+
+    test('bucketExists() fails due to wrong access key', () async {
+      final minio = getMinioClient(accessKey: 'incorrect-access-key');
+      expect(
+        () async => await minio.bucketExists(bucketName),
+        throwsA(
+          isA<MinioError>().having(
+            (e) => e.message,
+            'message',
+            'Forbidden',
+          ),
+        ),
+      );
+    });
+
+    test('bucketExists() fails due to wrong secret key', () async {
+      final minio = getMinioClient(secretKey: 'incorrect-secret-key');
+      expect(
+        () async => await minio.bucketExists(bucketName),
+        throwsA(
+          isA<MinioError>().having(
+            (e) => e.message,
+            'message',
+            'Forbidden',
+          ),
+        ),
+      );
+    });
+  });
+}
+
+void testFPutObject() {
+  group('fPutObject', () {
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    late Directory tempDir;
+    late File testFile;
+    final objectName = 'a.jpg';
+
+    setUpAll(() async {
+      tempDir = await Directory.systemTemp.createTemp();
+      testFile = await File('${tempDir.path}/$objectName').create();
+      await testFile.writeAsString('random bytes');
+
+      final minio = getMinioClient();
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      final minio = getMinioClient();
+      await minio.removeObject(bucketName, objectName);
+      await tempDir.delete(recursive: true);
+    });
+
+    test('fPutObject() inserts content-type to metadata', () async {
+      final minio = getMinioClient();
+      await minio.fPutObject(bucketName, objectName, testFile.path);
+
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(stat.metaData!['content-type'], equals('image/jpeg'));
+    });
+
+    test('fPutObject() adds user-defined object metadata w/ prefix', () async {
+      final prefix = 'x-amz-meta-';
+      final userDefinedMetadataKey = '${prefix}user-defined-metadata-key-1';
+      final userDefinedMetadataValue = 'custom value 1';
+      final metadata = {
+        userDefinedMetadataKey: userDefinedMetadataValue,
+      };
+
+      final minio = getMinioClient();
+      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);
+
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(
+        stat.metaData![userDefinedMetadataKey.substring(prefix.length)],
+        equals(userDefinedMetadataValue),
+      );
+    });
+
+    test('fPutObject() adds user-defined object metadata w/o prefix', () async {
+      final userDefinedMetadataKey = 'user-defined-metadata-key-2';
+      final userDefinedMetadataValue = 'custom value 2';
+      final metadata = {
+        userDefinedMetadataKey: userDefinedMetadataValue,
+      };
+
+      final minio = getMinioClient();
+      await minio.fPutObject(bucketName, objectName, testFile.path, metadata);
+
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(stat.metaData![userDefinedMetadataKey],
+          equals(userDefinedMetadataValue));
+    });
+
+    test('fPutObject() with empty file', () async {
+      final objectName = 'empty.txt';
+      final emptyFile = await File('${tempDir.path}/$objectName').create();
+      await emptyFile.writeAsString('');
+
+      final minio = getMinioClient();
+      await minio.fPutObject(bucketName, objectName, emptyFile.path);
+
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(stat.size, equals(0));
+    });
+  });
+}
+
+void testSetObjectACL() {
+  group('setObjectACL', () {
+    late String bucketName;
+    late Directory tempDir;
+    File testFile;
+    final objectName = 'a.jpg';
+
+    setUpAll(() async {
+      bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+      tempDir = await Directory.systemTemp.createTemp();
+      testFile = await File('${tempDir.path}/$objectName').create();
+      await testFile.writeAsString('random bytes');
+
+      final minio = getMinioClient();
+      await minio.makeBucket(bucketName);
+
+      await minio.fPutObject(bucketName, objectName, testFile.path);
+    });
+
+    tearDownAll(() async {
+      await tempDir.delete(recursive: true);
+    });
+
+    test('setObjectACL() set objects acl', () async {
+      final minio = getMinioClient();
+      await minio.setObjectACL(bucketName, objectName, 'public-read');
+    });
+  });
+}
+
+void testGetObjectACL() {
+  group('getObjectACL', () {
+    late String bucketName;
+    late Directory tempDir;
+    File testFile;
+    final objectName = 'a.jpg';
+
+    setUpAll(() async {
+      bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+      tempDir = await Directory.systemTemp.createTemp();
+      testFile = await File('${tempDir.path}/$objectName').create();
+      await testFile.writeAsString('random bytes');
+
+      final minio = getMinioClient();
+      await minio.makeBucket(bucketName);
+
+      await minio.fPutObject(bucketName, objectName, testFile.path);
+    });
+
+    tearDownAll(() async {
+      await tempDir.delete(recursive: true);
+    });
+
+    test('getObjectACL() fetch objects acl', () async {
+      final minio = getMinioClient();
+      var acl = await minio.getObjectACL(bucketName, objectName);
+      expect(acl.grants!.permission, equals(null));
+    });
+  });
+}
+
+void testGetObject() {
+  group('getObject()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+    final objectData = Uint8List.fromList([1, 2, 3]);
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+      await minio.putObject(bucketName, objectName, Stream.value(objectData));
+    });
+
+    tearDownAll(() async {
+      await minio.removeObject(bucketName, objectName);
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      final stream = await minio.getObject(bucketName, objectName);
+      final buffer = BytesBuilder();
+      await stream.forEach((data) => buffer.add(data));
+      expect(stream.contentLength, equals(objectData.length));
+      expect(buffer.takeBytes(), equals(objectData));
+    });
+
+    test('fails on invalid bucket', () {
+      expect(
+        () async => await minio.getObject('$bucketName-invalid', objectName),
+        throwsA(isA<MinioError>()),
+      );
+    });
+
+    test('fails on invalid object', () {
+      expect(
+        () async => await minio.getObject(bucketName, '$objectName-invalid'),
+        throwsA(isA<MinioError>()),
+      );
+    });
+  });
+}
+
+void testPutObject() {
+  group('putObject()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+    final objectData = Uint8List.fromList([1, 2, 3]);
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      await minio.putObject(bucketName, objectName, Stream.value(objectData));
+      final stat = await minio.statObject(bucketName, objectName);
+      expect(stat.size, equals(objectData.length));
+      await minio.removeObject(bucketName, objectName);
+    });
+  });
+}
+
+void testGetBucketNotification() {
+  group('getBucketNotification()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      await minio.getBucketNotification(bucketName);
+    });
+  });
+}
+
+void testSetBucketNotification() {
+  group('setBucketNotification()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      await minio.setBucketNotification(
+        bucketName,
+        NotificationConfiguration(null, null, null),
+      );
+    });
+  });
+}
+
+void testRemoveAllBucketNotification() {
+  group('removeAllBucketNotification()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      await minio.removeAllBucketNotification(bucketName);
+    });
+  });
+}
+
+void testListenBucketNotification() {
+  group('listenBucketNotification()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    // final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      final poller = minio.listenBucketNotification(bucketName);
+      expect(poller.isStarted, isTrue);
+      poller.stop();
+    });
+
+    // test('can receive notification', () async {
+    //   final poller = minio.listenBucketNotification(
+    //     bucketName,
+    //     events: ['s3:ObjectCreated:*'],
+    //   );
+
+    //   final receivedEvents = [];
+    //   poller.stream.listen((event) => receivedEvents.add(event));
+    //   expect(receivedEvents, isEmpty);
+
+    //   await minio.putObject(bucketName, objectName, Stream.value([0]));
+    //   await minio.removeObject(bucketName, objectName);
+
+    //   // FIXME: Needs sleep here
+    //   expect(receivedEvents, isNotEmpty);
+
+    //   poller.stop();
+    // });
+  });
+}
+
+void testStatObject() {
+  group('statObject()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+    final data = [1, 2, 3, 4, 5];
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+      await minio.putObject(bucketName, objectName, Stream.value(data));
+    });
+
+    tearDownAll(() async {
+      await minio.removeObject(bucketName, objectName);
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      final stats = await minio.statObject(bucketName, objectName);
+      expect(stats.lastModified, isNotNull);
+      expect(stats.lastModified!.isBefore(DateTime.now()), isTrue);
+      expect(stats.size, isNotNull);
+      expect(stats.size, equals(data.length));
+    });
+
+    test('fails on invalid bucket', () {
+      expect(
+        () async => await minio.statObject('$bucketName-invalid', objectName),
+        throwsA(isA<MinioError>()),
+      );
+    });
+
+    test('fails on invalid object', () {
+      expect(
+        () async => await minio.statObject(bucketName, '$objectName-invalid'),
+        throwsA(isA<MinioError>()),
+      );
+    });
+  });
+}
+
+void testMakeBucket() {
+  group('makeBucket()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      final buckets = await minio.listBuckets();
+      final bucketNames = buckets.map((b) => b.name).toList();
+      expect(bucketNames, contains(bucketName));
+    });
+  });
+}
+
+void testRemoveBucket() {
+  group('removeBucket()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+
+    test('succeeds', () async {
+      await minio.makeBucket(bucketName);
+      await minio.removeBucket(bucketName);
+    });
+
+    test('fails on invalid bucket name', () {
+      expect(
+        () async => await minio.removeBucket('$bucketName-invalid'),
+        throwsA(isA<MinioError>()),
+      );
+    });
+  });
+}
+
+void testRemoveObject() {
+  group('removeObject()', () {
+    final minio = getMinioClient();
+    final bucketName = DateTime.now().millisecondsSinceEpoch.toString();
+    final objectName = DateTime.now().microsecondsSinceEpoch.toString();
+    final data = [1, 2, 3, 4, 5];
+
+    setUpAll(() async {
+      await minio.makeBucket(bucketName);
+    });
+
+    tearDownAll(() async {
+      await minio.removeBucket(bucketName);
+    });
+
+    test('succeeds', () async {
+      await minio.putObject(bucketName, objectName, Stream.value(data));
+      await minio.removeObject(bucketName, objectName);
+
+      await for (var chunk in minio.listObjects(bucketName)) {
+        expect(chunk.objects.contains(objectName), isFalse);
+      }
+    });
+
+    test('fails on invalid bucket', () {
+      expect(
+        () async => await minio.removeObject('$bucketName-invalid', objectName),
+        throwsA(isA<MinioError>()),
+      );
+    });
+
+    test('does not throw on invalid object', () async {
+      await minio.removeObject(bucketName, '$objectName-invalid');
+    });
+  });
+}
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..a37d9fe
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,59 @@
+import 'package:minio/src/utils.dart';
+import 'package:test/test.dart';
+
+void main() {
+  testRfc7231Time();
+  testBlockStream();
+}
+
+void testRfc7231Time() {
+  final time = DateTime(2017, 8, 11, 19, 34, 18);
+  final timeString = 'Fri, 11 Aug 2017 19:34:18';
+
+  final timeUtc = DateTime.utc(2017, 8, 11, 19, 34, 18);
+  final timeStringUtc = 'Fri, 11 Aug 2017 19:34:18 GMT';
+
+  group('parseRfc7231Time', () {
+    test('works', () {
+      expect(parseRfc7231Time(timeString), equals(time));
+      expect(parseRfc7231Time(timeString).isUtc, isFalse);
+    });
+
+    test('works for GMT time', () {
+      expect(parseRfc7231Time(timeStringUtc), equals(timeUtc));
+      expect(parseRfc7231Time(timeStringUtc).isUtc, isTrue);
+    });
+  });
+
+  group('toRfc7231Time', () {
+    test('works', () {
+      expect(toRfc7231Time(time), equals(timeString));
+    });
+
+    test('works for GMT time', () {
+      expect(toRfc7231Time(timeUtc), equals(timeStringUtc));
+    });
+  });
+}
+
+void testBlockStream() {
+  test('BlockStream can split chunks to blocks', () async {
+    final streamData = [
+      [1, 2, 3],
+      [4, 5, 6],
+      [7, 8, 9],
+      [10, 11, 12],
+    ];
+
+    final stream = Stream.fromIterable(streamData).transform(BlockStream(5));
+
+    expect(
+      await stream.toList(),
+      equals([
+        [1, 2, 3, 4, 5],
+        [6, 7, 8, 9, 10],
+        [11, 12]
+      ]),
+    );
+  });
+}
-- 
GitLab