-- -- Minio Haskell SDK, (C) 2017 Minio, Inc. -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- import qualified Test.QuickCheck as Q import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck as QC import Lib.Prelude import System.Directory (getTemporaryDirectory) import qualified System.IO as SIO import qualified Control.Monad.Catch as MC import qualified Control.Monad.Trans.Resource as R import qualified Data.ByteString as BS import Data.Conduit (($$), yield) import qualified Data.Conduit as C import qualified Data.Conduit.Binary as CB import Data.Conduit.Combinators (sinkList) import Data.Default (Default(..)) import qualified Data.Text as T import System.Environment (lookupEnv) import Network.Minio import Network.Minio.Data import Network.Minio.Errors import Network.Minio.ListOps import Network.Minio.PutObject import Network.Minio.S3API import Network.Minio.Utils main :: IO () main = defaultMain tests tests :: TestTree tests = testGroup "Tests" [liveServerUnitTests] -- conduit that generates random binary stream of given length randomDataSrc :: MonadIO m => Int64 -> C.Producer m ByteString randomDataSrc s' = genBS s' where oneMiB = 1024*1024 concatIt bs n = BS.concat $ replicate (fromIntegral q) bs ++ [BS.take (fromIntegral r) bs] where (q, r) = n `divMod` fromIntegral (BS.length bs) genBS s = do w8s <- liftIO $ generate $ Q.vectorOf 64 (Q.choose (0, 255)) let byteArr64 = BS.pack w8s if s < oneMiB then yield $ concatIt byteArr64 s else do yield $ concatIt byteArr64 oneMiB genBS (s - oneMiB) mkRandFile :: R.MonadResource m => Int64 -> m FilePath mkRandFile size = do dir <- liftIO $ getTemporaryDirectory randomDataSrc size C.$$ CB.sinkTempFile dir "miniohstest.random" funTestBucketPrefix :: Text funTestBucketPrefix = "miniohstest-" funTestWithBucket :: TestName -> (([Char] -> Minio ()) -> Bucket -> Minio ()) -> TestTree funTestWithBucket t minioTest = testCaseSteps t $ \step -> do -- generate a random name for the bucket bktSuffix <- liftIO $ generate $ Q.vectorOf 10 (Q.choose ('a', 'z')) let b = T.concat [funTestBucketPrefix, T.pack bktSuffix] liftStep = liftIO . step connInfo <- maybe minioPlayCI (const def) <$> lookupEnv "MINIO_LOCAL" ret <- runMinio connInfo $ do liftStep $ "Creating bucket for test - " ++ t foundBucket <- bucketExists b liftIO $ foundBucket @?= False makeBucket b def minioTest liftStep b deleteBucket b isRight ret @? ("Functional test " ++ t ++ " failed => " ++ show ret) liveServerUnitTests :: TestTree liveServerUnitTests = testGroup "Unit tests against a live server" [ funTestWithBucket "Basic tests" $ \step bucket -> do step "getService works and contains the test bucket." buckets <- getService unless (length (filter (== bucket) $ map biName buckets) == 1) $ liftIO $ assertFailure ("The bucket " ++ show bucket ++ " was expected to exist.") step "makeBucket again to check if BucketAlreadyOwnedByYou exception is raised." mbE <- MC.try $ makeBucket bucket Nothing case mbE of Left exn -> liftIO $ exn @?= BucketAlreadyOwnedByYou _ -> return () step "makeBucket with an invalid bucket name and check for appropriate exception." invalidMBE <- MC.try $ makeBucket "invalidBucketName" Nothing case invalidMBE of Left exn -> liftIO $ exn @?= MErrVInvalidBucketName "invalidBucketName" _ -> return () step "getLocation works" region <- getLocation bucket liftIO $ region == "us-east-1" @? ("Got unexpected region => " ++ show region) step "singlepart putObject works" fPutObject bucket "lsb-release" "/etc/lsb-release" step "fPutObject onto a non-existent bucket and check for NoSuchBucket exception" fpE <- MC.try $ fPutObject "nosuchbucket" "lsb-release" "/etc/lsb-release" case fpE of Left exn -> liftIO $ exn @?= NoSuchBucket _ -> return () outFile <- mkRandFile 0 step "simple fGetObject works" fGetObject bucket "lsb-release" outFile step "fGetObject a non-existent object and check for NoSuchKey exception" resE <- MC.try $ fGetObject bucket "noSuchKey" outFile case resE of Left exn -> liftIO $ exn @?= NoSuchKey _ -> return () step "create new multipart upload works" uid <- newMultipartUpload bucket "newmpupload" [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "abort a new multipart upload works" abortMultipartUpload bucket "newmpupload" uid step "delete object works" deleteObject bucket "lsb-release" step "statObject test" let object = "sample" step "create an object" inputFile <- mkRandFile 0 fPutObject bucket object inputFile step "get metadata of the object" res <- statObject bucket object liftIO $ (oiSize res) @?= 0 step "delete object" deleteObject bucket object , funTestWithBucket "Multipart Tests" $ \step bucket -> do -- low-level multipart operation tests. let object = "newmpupload" mb15 = 15 * 1024 * 1024 step "Prepare for low-level multipart tests." step "create new multipart upload" uid <- newMultipartUpload bucket object [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") randFile <- mkRandFile mb15 step "put object parts 1 of 1" h <- liftIO $ SIO.openBinaryFile randFile SIO.ReadMode partInfo <- putObjectPart bucket object uid 1 [] $ PayloadH h 0 mb15 step "complete multipart" void $ completeMultipartUpload bucket object uid [partInfo] destFile <- mkRandFile 0 step $ "Retrieve the created object and check size" fGetObject bucket object destFile gotSize <- withNewHandle destFile getFileSize liftIO $ gotSize == Right (Just mb15) @? "Wrong file size of put file after getting" step $ "Cleanup actions" removeObject bucket object -- putObject test (conduit source, no size specified) let obj = "mpart" mb100 = 100 * 1024 * 1024 step "Prepare for putObject with from source without providing size." rFile <- mkRandFile mb100 step "Upload multipart file." putObject bucket obj (CB.sourceFile rFile) Nothing step "Retrieve and verify file size" destFile <- mkRandFile 0 fGetObject bucket obj destFile gotSize <- withNewHandle destFile getFileSize liftIO $ gotSize == Right (Just mb100) @? "Wrong file size of put file after getting" step $ "Cleanup actions" deleteObject bucket obj step "Prepare for putObjectInternal with non-seekable file, with size." step "Upload multipart file." void $ putObjectInternal bucket obj $ ODFile "/dev/zero" (Just mb100) step "Retrieve and verify file size" destFile <- mkRandFile 0 fGetObject bucket obj destFile gotSize <- withNewHandle destFile getFileSize liftIO $ gotSize == Right (Just mb100) @? "Wrong file size of put file after getting" step $ "Cleanup actions" removeObject bucket obj step "Prepare for putObjectInternal with large file as source." step "upload large object" void $ putObjectInternal bucket "big" (ODFile "/dev/zero" $ Just $ 1024*1024*100) step "cleanup" removeObject bucket "big" , funTestWithBucket "Listing Test" $ \step bucket -> do step "listObjects' test" step "put 10 objects" forM_ [1..10::Int] $ \s -> fPutObject bucket (T.concat ["lsb-release", T.pack (show s)]) "/etc/lsb-release" step "Simple list" res <- listObjects' bucket Nothing Nothing Nothing Nothing let expected = sort $ map (T.concat . ("lsb-release":) . (\x -> [x]) . T.pack . show) [1..10::Int] liftIO $ assertEqual "Objects match failed!" expected (map oiObject $ lorObjects res) step "Cleanup actions" forM_ [1..10::Int] $ \s -> deleteObject bucket (T.concat ["lsb-release", T.pack (show s)]) step "listIncompleteUploads' test" let object = "newmpupload" step "create 10 multipart uploads" forM_ [1..10::Int] $ \_ -> do uid <- newMultipartUpload bucket object [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "list incomplete multipart uploads" incompleteUploads <- listIncompleteUploads' bucket Nothing Nothing Nothing Nothing Nothing -- Minio server behaviour changed to list no incomplete uploads, -- so the check below reflects this; this test is expected to -- fail on AWS S3. liftIO $ (length $ lurUploads incompleteUploads) @?= 0 step "cleanup" forM_ (lurUploads incompleteUploads) $ \(_, uid, _) -> abortMultipartUpload bucket object uid step "Basic listIncompleteParts Test" let object = "newmpupload" mb15 = 15 * 1024 * 1024 step "create a multipart upload" uid <- newMultipartUpload bucket object [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "put object parts 1..10" inputFile <- mkRandFile mb15 h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode forM_ [1..10] $ \pnum -> putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb15 step "fetch list parts" listPartsResult <- listIncompleteParts' bucket object uid Nothing Nothing -- Minio server behaviour changed to list no incomplete uploads, -- so the check below reflects this; this test is expected to -- fail on AWS S3. liftIO $ (length $ lprParts listPartsResult) @?= 0 abortMultipartUpload bucket object uid step "High-level listObjects Test" step "put 3 objects" let expected = ["dir/o1", "dir/dir1/o2", "dir/dir2/o3"] forM_ expected $ \obj -> fPutObject bucket obj "/etc/lsb-release" step "High-level listing of objects" objects <- (listObjects bucket Nothing True) $$ sinkList liftIO $ assertEqual "Objects match failed!" (sort expected) (map oiObject objects) step "Cleanup actions" forM_ expected $ \obj -> removeObject bucket obj step "High-level listIncompleteUploads Test" let object = "newmpupload" step "create 10 multipart uploads" forM_ [1..10::Int] $ \_ -> do uid <- newMultipartUpload bucket object [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "High-level listing of incomplete multipart uploads" uploads <- (listIncompleteUploads bucket Nothing True) $$ sinkList -- Minio server behaviour changed to list no incomplete uploads, -- so the check below reflects this; this test is expected to -- fail on AWS S3. liftIO $ (length uploads) @?= 0 step "cleanup" forM_ uploads $ \(UploadInfo _ uid _ _) -> abortMultipartUpload bucket object uid step "High-level listIncompleteParts Test" let object = "newmpupload" mb15 = 15 * 1024 * 1024 step "create a multipart upload" uid <- newMultipartUpload bucket object [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "put object parts 1..10" inputFile <- mkRandFile mb15 h <- liftIO $ SIO.openBinaryFile inputFile SIO.ReadMode forM_ [1..10] $ \pnum -> putObjectPart bucket object uid pnum [] $ PayloadH h 0 mb15 step "fetch list parts" incompleteParts <- (listIncompleteParts bucket object uid) $$ sinkList -- Minio server behaviour changed to list no incomplete uploads, -- so the check below reflects this; this test is expected to -- fail on AWS S3. liftIO $ (length incompleteParts) @?= 0 step "cleanup" abortMultipartUpload bucket object uid , funTestWithBucket "copyObject related tests" $ \step bucket -> do step "copyObjectSingle basic tests" let object = "xxx" objCopy = "xxxCopy" size1 = 100 :: Int64 step "create server object to copy" inputFile <- mkRandFile size1 fPutObject bucket object inputFile step "copy object" let cps = def { cpSource = format "/{}/{}" [bucket, object] } (etag, modTime) <- copyObjectSingle bucket objCopy cps [] -- retrieve obj info to check ObjectInfo _ t e s <- headObject bucket objCopy let isMTimeDiffOk = abs (diffUTCTime modTime t) < 1.0 liftIO $ (s == size1 && e == etag && isMTimeDiffOk) @? "Copied object did not match expected." step "cleanup actions" removeObject bucket object removeObject bucket objCopy step "copyObjectPart basic tests" let srcObj = "XXX" copyObj = "XXXCopy" step "Prepare" let mb15 = 15 * 1024 * 1024 mb5 = 5 * 1024 * 1024 randFile <- mkRandFile mb15 fPutObject bucket srcObj randFile step "create new multipart upload" uid <- newMultipartUpload bucket copyObj [] liftIO $ (T.length uid > 0) @? ("Got an empty multipartUpload Id.") step "put object parts 1-3" let cps = def {cpSource = format "/{}/{}" [bucket, srcObj]} parts <- forM [1..3] $ \p -> do (etag, _) <- copyObjectPart bucket copyObj cps{ cpSourceRange = Just ((p-1)*mb5, (p-1)*mb5 + (mb5 - 1)) } uid (fromIntegral p) [] return $ (fromIntegral p, etag) step "complete multipart" void $ completeMultipartUpload bucket copyObj uid parts step "verify copied object size" (ObjectInfo _ _ _ s) <- headObject bucket copyObj liftIO $ (s == mb15) @? "Size failed to match" step $ "Cleanup actions" removeObject bucket srcObj removeObject bucket copyObj step "copyObject basic tests" let srcs = ["XXX", "XXXL"] copyObjs = ["XXXCopy", "XXXLCopy"] sizes = map (* (1024 * 1024)) [15, 65] step "Prepare" forM_ (zip srcs sizes) $ \(src, size) -> fPutObject bucket src =<< mkRandFile size step "make small and large object copy" forM_ (zip copyObjs srcs) $ \(cp, src) -> copyObject bucket cp def{cpSource = format "/{}/{}" [bucket, src]} step "verify uploaded objects" uploadedSizes <- fmap (fmap oiSize) $ forM copyObjs (headObject bucket) liftIO $ (sizes == uploadedSizes) @? "Uploaded obj sizes failed to match" forM_ (concat [srcs, copyObjs]) (removeObject bucket) step "copyObject with offset test " let src = "XXX" copyObj = "XXXCopy" size = 15 * 1024 * 1024 step "Prepare" fPutObject bucket src =<< mkRandFile size step "copy last 10MiB of object" copyObject bucket copyObj def{ cpSource = format "/{}/{}" [bucket, src] , cpSourceRange = Just (5 * 1024 * 1024, size - 1) } step "verify uploaded object" cSize <- oiSize <$> headObject bucket copyObj liftIO $ (cSize == 10 * 1024 * 1024) @? "Uploaded obj size mismatched!" forM_ [src, copyObj] (removeObject bucket) ]