{-# LANGUAGE NoImplicitPrelude #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TemplateHaskell #-} {-# LANGUAGE PackageImports #-} module Test.IO.Mismi.S3.Commands where import Control.Concurrent (threadDelay) import Control.Monad.Catch (catchAll, throwM) import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (runExceptT) import "cryptohash" Crypto.Hash import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.Either (isRight, isLeft) import qualified Data.List as L import qualified Data.Text as T import qualified Data.Text.Encoding as T import qualified Data.Text.IO as T import Control.Lens ((^.), to) import Control.Monad (replicateM_) import Hedgehog import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range import Mismi.S3 import qualified Mismi.S3.Amazonka as A import qualified Mismi.S3.Unsafe as Unsafe import P import qualified System.Directory as D import System.FilePath (()) import qualified System.FilePath as F import System.IO (withFile, IOMode (..), hFileSize, putStrLn) import System.IO.Error (userError) import Test.Mismi.Amazonka (sendMultipart, newMultipart) import Test.Mismi.S3 import qualified Test.Mismi.S3.Core.Gen as Gen import Mismi.S3.Internal.Parallel (RunError (..)) prop_exists :: Property prop_exists = withTests 2 . property . liftAWS $ do a <- newAddress lift $ Unsafe.write a "" result <- lift $ exists a result === True prop_exists_empty :: Property prop_exists_empty = withTests 2 . property . liftAWS $ do a <- newAddress result <- lift $ exists a result === False prop_exists_failure :: Property prop_exists_failure = withTests 2 . property . liftAWS $ do a <- newAddress e <- lift $ exists a e === False prop_exists_prefix :: Property prop_exists_prefix = withTests 2 . property . liftAWS $ do k <- forAll $ Gen.genKey a <- newAddress lift $ Unsafe.write (withKey (// k) a) "" e <- lift $ existsPrefix a e === True prop_exists_prefix_missing :: Property prop_exists_prefix_missing = withTests 2 . property . liftAWS $ do a <- newAddress lift $ Unsafe.write a "" e <- lift $ existsPrefix a e === False prop_exists_prefix_key :: Property prop_exists_prefix_key = withTests 2 . property . liftAWS $ do a <- newAddress e <- lift $ existsPrefix a e === False prop_headObject :: Property prop_headObject = withTests 2 . property . liftAWS $ do a <- newAddress h <- lift $ headObject a h === Nothing prop_getObjects_empty :: Property prop_getObjects_empty = withTests 2 . property . liftAWS $ do a <- newAddress objs <- lift $ getObjectsRecursively $ a objs === [] prop_getObjectsR :: Property prop_getObjectsR = -- d p1 p2 = p1 /= p2 ==> testAWS $ do withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.constant 5 15) Gen.alphaNum items <- fmap toList . forAll $ Gen.set (Range.constant 2 2) Gen.genKey (p1, p2) <- case items of p1 : p2 : [] -> pure (p1, p2) _ -> annotate "Invariant generator." >> failure root <- newAddress let keys = [p1, p2 // p1, p2 // p2] lift . forM_ keys $ \k -> Unsafe.write (withKey (// k) root) d objs <- lift $ getObjectsRecursively root on (===) L.sort ((^. A.oKey . to A.toText) <$> objs) (unKey . (//) (key root) <$> keys) -- TODO This is incredible slow prop_pagination_list :: Property prop_pagination_list = -- TODO withTests 0 . property . liftAWS $ do m <- forAll $ Gen.text (Range.linear 10 15) Gen.alphaNum n <- forAll $ Gen.int (Range.linear 1000 1500) a <- newAddress lift . forM_ [1..n] $ \n' -> Unsafe.write (withKey(// Key (m <> T.pack (show n'))) a) "" r' <- lift $ list a length r' === n prop_size :: Property prop_size = withTests 10 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.write a d i <- lift $ size a i === (Just . fromIntegral . BS.length $ T.encodeUtf8 d) prop_size_failure :: Property prop_size_failure = withTests 2 . property . liftAWS $ do a <- newAddress i <- lift $ size a i === Nothing prop_size_recursively :: Property prop_size_recursively = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.write a d r <- lift $ sizeRecursively (a { key = dirname $ key a }) r === [Sized (fromIntegral . BS.length $ T.encodeUtf8 d) a] -- TODO This is incredible slow prop_concat :: Property prop_concat = withTests 1 . property . liftAWS $ do a <- newAddress b <- newAddress c <- newAddress f <- newFilePath let s = f T.unpack "fred" d = f T.unpack "down" bs10k = BS.concat $ L.replicate 10000 "fred" liftIO $ withFile s WriteMode $ \h -> replicateM_ 1000 (BS.hPut h bs10k) lift $ Unsafe.upload s a lift $ Unsafe.upload s b r <- lift . runExceptT $ concatMultipart Fail 1 [a, b] c () <- either (fail . show . renderConcatError) pure r lift $ Unsafe.download c d s' <- liftIO $ LBS.readFile s d' <- liftIO $ LBS.readFile d sha1 (LBS.concat [s', s']) === sha1 d' prop_concat_empty_input :: Property prop_concat_empty_input = withTests 2 . property . liftAWS $ do a <- newAddress r <- lift . runExceptT $ concatMultipart Fail 1 [] a case r of Left NoInputFiles -> success _ -> annotate "concat didn't fail correctly" >> failure prop_concat_empty_input_files :: Property prop_concat_empty_input_files = withTests 2 . property . liftAWS $ do a <- newAddress b <- newAddress lift $ Unsafe.write a "" r <- lift . runExceptT $ concatMultipart Fail 1 [a] b case r of Left NoInputFilesWithData -> success _ -> annotate "concat didn't fail correctly" >> failure prop_copy :: Property prop_copy = withTests 2 . property . liftAWS $ do t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress b <- newAddress lift $ Unsafe.write a t lift $ either (fail . T.unpack . renderCopyError) pure =<< runExceptT (copy a b) a' <- lift $ read a b' <- lift $ read b a' === b' prop_copy_missing :: Property prop_copy_missing = withTests 2 . property . liftAWS $ do a <- newAddress r <- lift . runExceptT $ copy a a case r of Left (CopySourceMissing b) -> a === b _ -> annotate "Copy didn't fail correctly" >> failure prop_copy_overwrite :: Property prop_copy_overwrite = withTests 2 . property . liftAWS $ do t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum t' <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress b <- newAddress lift $ Unsafe.write a t lift $ Unsafe.write b t' lift $ either (fail . T.unpack . renderCopyError) pure =<< runExceptT (copyWithMode Overwrite a b) b' <- lift $ read b b' === Just t prop_copy_fail :: Property prop_copy_fail = withTests 2 . property . liftAWS $ do t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress b <- newAddress lift $ Unsafe.write a t lift $ Unsafe.write b t r <- lift . runExceptT $ copyWithMode Fail a b case r of Left (CopyDestinationExists z) -> b === z _ -> annotate "Copy didn't failure correctly" >> failure prop_copy_multipart :: Property prop_copy_multipart = withTests 1 . property . liftAWS $ do c <- forAll $ Gen.text (Range.linear 10 20) Gen.alphaNum m <- forAll $ Gen.text (Range.linear 20 30) Gen.alphaNum bs <- forAll $ Gen.utf8 (Range.linear 1 100) Gen.unicodeAll f <- newFilePath a' <- newAddress let a = withKey (// Key c) a' b = withKey (// Key m) a' s = f T.unpack c d = f T.unpack m -- create large file to copy liftIO $ D.createDirectoryIfMissing True f liftIO $ withFile s WriteMode $ \h -> replicateM_ 1000 (LBS.hPut h (LBS.fromChunks . return $ (BS.concat . L.replicate 10000 $ bs))) liftIO . putStrLn $ "Generated file" lift $ Unsafe.upload s a liftIO . putStrLn $ "Uploaded file" liftIO . putStrLn $ "Running copy ..." lift $ either (fail . T.unpack . renderCopyError) pure =<< runExceptT (copy a b) liftIO . putStrLn $ "Done copy" -- compare lift $ either (fail . show) pure =<< runExceptT (download b d) liftIO . putStrLn $ "Done download" s' <- liftIO $ LBS.readFile s d' <- liftIO $ LBS.readFile d sha1 s' === sha1 d' prop_move :: Property prop_move = withTests 2 . property . liftAWS $ do t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum s <- newAddress d <- newAddress lift $ Unsafe.write s t lift $ either (fail . T.unpack . renderCopyError) pure =<< runExceptT (move s d) es <- lift $ exists s ed <- lift $ exists d (es, ed) === (False, True) prop_upload_mode :: Property prop_upload_mode = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath m <- forAll $ Gen.genWriteMode p <- newFilePath a <- newAddress let t = p localPath l liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO $ T.writeFile t d lift $ Unsafe.uploadWithMode m t a r <- lift $ read a r === Just d prop_upload_overwrite :: Property prop_upload_overwrite = withTests 2 . property . liftAWS $ do d1 <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum d2 <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO $ T.writeFile t d1 lift $ Unsafe.uploadWithMode Fail t a liftIO $ T.writeFile t d2 lift $ Unsafe.uploadWithMode Overwrite t a r <- lift $ read a r === Just d2 prop_upload_fail :: Property prop_upload_fail = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO $ T.writeFile t d lift $ Unsafe.uploadWithMode Fail t a r <- lift . runExceptT $ uploadWithMode Fail t a case r of Left (UploadDestinationExists _) -> success _ -> annotate "Upload succeded but should have failed" >> failure prop_upload :: Property prop_upload = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO $ T.writeFile t d lift $ Unsafe.upload t a r <- lift $ read a r === Just d prop_upload_multipart :: Property prop_upload_multipart = withTests 2 . property . liftAWS $ do bs <- forAll $ Gen.utf8 (Range.linear 0 100) Gen.unicodeAll l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO $ withFile t WriteMode $ \h -> replicateM_ 1000 (LBS.hPut h (LBS.fromChunks . return $ (BS.concat . L.replicate 10000 $ bs))) lift $ Unsafe.upload t a result <- lift $ exists a assert result prop_abort_multipart :: Property prop_abort_multipart = withTests 2 . property . liftAWS $ do (a, i) <- newMultipart lift $ sendMultipart "" a 1 i l <- lift $ listMultiparts (bucket a) let abortCheck :: Text -> Bucket -> Int -> A.MultipartUpload -> AWS () abortCheck x b n u = do abortMultipart b u r <- listMultiparts b unless (n <= (0 :: Int) || L.null (findMultiparts x r)) $ do liftIO $ threadDelay 500000 abortCheck x b (n-1) u lift . forM_ (findMultiparts i l) $ abortCheck i (bucket a) 3 r <- lift $ listMultiparts (bucket a) (L.filter (== Just i) . fmap (^. A.muUploadId) $ l) === [Just i] findMultiparts i r === [] prop_list_multipart :: Property prop_list_multipart = withTests 2 . property . liftAWS $ do (a, i) <- newMultipart lift $ sendMultipart "" a 1 i l <- lift $ listMultiparts (bucket a) multipartExists i l prop_list_parts :: Property prop_list_parts = withTests 2 . property . liftAWS $ do (a, i) <- newMultipart lift $ sendMultipart "" a 1 i l2 <- lift $ listMultipartParts a i length l2 === 1 multipartExists :: Monad m => Text -> [A.MultipartUpload] -> PropertyT m () multipartExists uploadId multiparts = L.length (L.filter (findMultipart uploadId) multiparts) === 1 findMultiparts :: Text -> [A.MultipartUpload] -> [A.MultipartUpload] findMultiparts uploadId = L.filter (findMultipart uploadId) findMultipart :: Text -> A.MultipartUpload -> Bool findMultipart uploadId m = m ^. A.muUploadId == Just uploadId prop_list :: Property prop_list = withTests 2 . property . liftAWS $ do m <- forAll $ Gen.text (Range.linear 10 20) Gen.alphaNum s <- forAll $ Gen.text (Range.linear 20 30) Gen.alphaNum a <- newAddress lift $ Unsafe.write (withKey (// Key m) a) "" lift $ Unsafe.write (withKey (// (Key s // Key m)) a) "" r' <- lift $ list a (Just . Key <$> [m, s <> "/"]) === (removeCommonPrefix a <$> r') prop_listObjects :: Property prop_listObjects = withTests 2 . property . liftAWS $ do m <- forAll $ Gen.text (Range.linear 10 20) Gen.alphaNum s <- forAll $ Gen.text (Range.linear 20 30) Gen.alphaNum a <- newAddress lift $ Unsafe.write (withKey (// Key m) a) "" lift $ Unsafe.write (withKey (// (Key s // Key m)) a) "" (p, k) <- lift $ listObjects a ([Just . Key $ s <> "/"], [Just $ Key m]) === (removeCommonPrefix a <$> p, removeCommonPrefix a <$> k) prop_list_recursively :: Property prop_list_recursively = withTests 2 . property . liftAWS $ do a <- newAddress lift $ Unsafe.write a "" r' <- lift $ listRecursively (a { key = dirname $ key a }) assert $ a `elem` r' prop_list_forbidden_bucket :: Property prop_list_forbidden_bucket = withTests 1 . property . liftAWS $ do _ <- lift $ write (Address (Bucket "ambiata-dev-view") (Key "")) "" success prop_download :: Property prop_download = withTests 1 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 10 20) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l lift $ Unsafe.write a d r <- lift . runExceptT $ download a t res <- liftIO $ T.readFile t assert $ isRight r res === d prop_download_multipart :: Property prop_download_multipart = withTests 1 . property . liftAWS $ do c <- forAll $ Gen.text (Range.linear 10 20) Gen.alphaNum m <- forAll $ Gen.text (Range.linear 20 30) Gen.alphaNum bs <- forAll $ Gen.utf8 (Range.linear 1 100) Gen.unicodeAll p <- newFilePath a <- newAddress let t = p T.unpack c let o = p T.unpack m liftIO . D.createDirectoryIfMissing True $ F.takeDirectory t liftIO . D.createDirectoryIfMissing True $ F.takeDirectory o liftIO $ withFile t WriteMode $ \h -> replicateM_ 1000 (LBS.hPut h (LBS.fromChunks . return $ (BS.concat . L.replicate 10000 $ bs))) sz <- liftIO . withFile t ReadMode $ hFileSize lift $ Unsafe.upload t a let ten :: Integer = 10 r <- lift . runExceptT $ multipartDownload a o (fromInteger sz) ten 100 b <- liftIO $ LBS.readFile t let b' = sha1 b o' <- liftIO $ LBS.readFile o let o'' = sha1 o' assert $ isRight r b' === o'' prop_write_download_overwrite :: Property prop_write_download_overwrite = withTests 2 . property . liftAWS $ do old <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum new <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l lift $ Unsafe.write a old x <- lift . runExceptT $ downloadWithMode Fail a t lift $ Unsafe.writeWithMode Overwrite a new y <- lift . runExceptT $ downloadWithMode Overwrite a t r <- liftIO $ T.readFile t assert $ isRight x assert $ isRight y r === new prop_write_download_fail :: Property prop_write_download_fail = withTests 2 . property . liftAWS $ do old <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum new <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum l <- forAll $ genLocalPath p <- newFilePath a <- newAddress let t = p localPath l lift $ Unsafe.write a old x <- lift . runExceptT $ downloadWithMode Fail a t lift $ Unsafe.writeWithMode Overwrite a new y <- lift . runExceptT $ downloadWithMode Fail a t assert $ isRight x assert $ isLeft y prop_delete :: Property prop_delete = withTests 2 . property . liftAWS $ do w <- forAll Gen.genWriteMode a <- newAddress lift $ Unsafe.writeWithMode w a "" x <- lift $ exists a lift $ delete a y <- lift $ exists a assert x assert $ not y prop_delete_empty :: Property prop_delete_empty = withTests 2 . property . liftAWS $ do a <- newAddress result <- (True <$ lift (delete a)) `catchAll` (const . pure $ False) assert result prop_read_write :: Property prop_read_write = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.write a d r <- lift $ read a r === Just d prop_write_failure :: Property prop_write_failure = withTests 2 . property . liftAWS $ do d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.write a d r <- lift $ write a d r === WriteDestinationExists a prop_write_overwrite :: Property prop_write_overwrite = withTests 2 . property . liftAWS $ do let gen = Gen.text (Range.constant 5 15) Gen.alphaNum items <- fmap toList . forAll $ Gen.set (Range.constant 2 2) gen (x, y) <- case items of x : y : [] -> pure (x, y) _ -> annotate "Invariant generator." >> failure a <- newAddress lift $ Unsafe.writeWithMode Fail a x lift $ Unsafe.writeWithMode Overwrite a y r <- lift $ read a r === Just y prop_sync_overwrite :: Property prop_sync_overwrite = withTests 2 . property . liftAWS $ do m <- forAll $ Gen.text (Range.linear 5 20) Gen.alphaNum a <- newAddress b <- newAddress createSmallFiles a m 10 x <- lift . runExceptT $ syncWithMode OverwriteSync a b 1 y <- lift . runExceptT $ syncWithMode OverwriteSync a b 1 lift . forM_ (files b m 10) $ \e -> exists e >>= \e' -> when (e' == False) $ (throwM . userError $ "Output files do not exist") assert $ isRight x assert $ isRight y prop_sync_fail :: Property prop_sync_fail = withTests 2 . property . liftAWS $ do m <- forAll $ Gen.text (Range.linear 5 20) Gen.alphaNum a <- newAddress b <- newAddress createSmallFiles a m 1 x <- lift . runExceptT $ syncWithMode FailSync a b 1 y <- lift . runExceptT $ syncWithMode FailSync a b 1 case y of (Left (SyncError (WorkerError (OutputExists q)))) -> q === withKey (// Key (m <> "-1")) b _ -> failure assert $ isRight x -- | If the object does not exist, then the behaviour should be invariant with the WriteMode prop_write_nonexisting :: Property prop_write_nonexisting = withTests 2 . property . liftAWS $ do w <- forAll Gen.genWriteMode d <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.writeWithMode w a d r <- lift $ read a r === Just d prop_write_grant :: Property prop_write_grant = withTests 2 . property . liftAWS $ do t <- forAll $ Gen.text (Range.linear 0 100) Gen.alphaNum a <- newAddress lift $ Unsafe.write a t lift . grantReadAccess a $ ReadGrant "id=e3abd0cceaecbd471c3eaaa47bb722bf199296c5e41c9ee4222877cc91b536fc" r <- lift $ read a r === Just t prop_read_empty :: Property prop_read_empty = withTests 1 . property $ do k <- forAll Gen.genKey bucket' <- liftIO testBucket t <- liftIO . runAWSDefaultRegion . read $ Address bucket' k t === Nothing prop_download_recursive :: Property prop_download_recursive = withTests 1 . property . liftAWS $ do let name1 = "first name" name2 = "second name" name3 = "third name" tmpdir <- newFilePath addr <- withKey (// Key "top") <$> newAddress lift $ do Unsafe.write (withKey (// Key "a") addr) name1 Unsafe.write (withKey (// Key "b/c") addr) name2 Unsafe.write (withKey (// Key "c/d/e") addr) name3 lift $ either (fail . show) pure =<< runExceptT (downloadRecursive addr tmpdir) a <- liftIO $ T.readFile (tmpdir "a") c <- liftIO $ T.readFile (tmpdir "b" "c") e <- liftIO $ T.readFile (tmpdir "c" "d" "e") a === name1 c === name2 e === name3 prop_upload_recursive :: Property prop_upload_recursive = withTests 1 . property . liftAWS $ do let name1 = "first name" name2 = "second name" name3 = "third name" tmpdir <- newFilePath liftIO $ do D.createDirectoryIfMissing True (tmpdir "b") D.createDirectoryIfMissing True (tmpdir "c" "d") T.writeFile (tmpdir "a") name1 T.writeFile (tmpdir "b" "c") name2 T.writeFile (tmpdir "c" "d" "e") name3 addr <- withKey (// Key "top") <$> newAddress lift $ either (fail . show) pure =<< runExceptT (uploadRecursive tmpdir addr 2) a <- lift $ read (withKey (// Key "a") addr) c <- lift $ read (withKey (// Key "b/c") addr) e <- lift $ read (withKey (// Key "c/d/e") addr) a === Just name1 c === Just name2 e === Just name3 prop_on_status_ok :: Property prop_on_status_ok = withTests 2 . property . liftAWS $ do let handler _ = Just 2 r <- lift $ onStatus_ (1 :: Int) handler (void (exists (Address (Bucket "ambiata-dev-view") (Key "")))) r === 1 prop_on_status_ko :: Property prop_on_status_ko = withTests 2 . property . liftAWS $ do let handler _ = Just 2 r <- lift $ onStatus_ (1 :: Int) handler (void (write missingAddress "text")) r === 2 ---------- -- HELPERS ---------- missingAddress :: Address missingAddress = Address (Bucket "ambiata-missing") (Key "m") sha1 :: LBS.ByteString -> Digest SHA1 sha1 = hashlazy tests :: IO Bool tests = checkParallel $$(discover)