diff --git a/antiope-s3.cabal b/antiope-s3.cabal
--- a/antiope-s3.cabal
+++ b/antiope-s3.cabal
@@ -2,10 +2,10 @@
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: f9e791b7b53dd965cce315e225098b21908c0e5a7fbe783cb870fb8656dee283
+-- hash: a678db9abb851f5d8fb9f3a516d8675b821fec9552137d83dd8310c5118856d9
 
 name:           antiope-s3
-version:        6.0.3
+version:        6.0.4
 description:    Please see the README on Github at <https://github.com/arbor/antiope#readme>
 category:       Services
 homepage:       https://github.com/arbor/antiope#readme
@@ -26,6 +26,12 @@
   location: https://github.com/arbor/antiope
 
 library
+  exposed-modules:
+      Antiope.S3
+      Antiope.S3.Internal
+      Antiope.S3.Types
+  other-modules:
+      Paths_antiope_s3
   hs-source-dirs:
       src
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
@@ -43,21 +49,19 @@
     , http-types
     , lens
     , monad-logger
+    , mtl
     , network-uri
     , resourcet
     , text
     , unliftio-core
-  exposed-modules:
-      Antiope.S3
-      Antiope.S3.Internal
-      Antiope.S3.Types
-  other-modules:
-      Paths_antiope_s3
   default-language: Haskell2010
 
 test-suite antiope-s3-test
   type: exitcode-stdio-1.0
   main-is: Spec.hs
+  other-modules:
+      Antiope.S3Spec
+      Paths_antiope_s3
   hs-source-dirs:
       test
   default-extensions: BangPatterns GeneralizedNewtypeDeriving OverloadedStrings TupleSections
@@ -66,6 +70,7 @@
       amazonka >=1.6
     , amazonka-core >=1.6
     , amazonka-s3 >=1.6
+    , antiope-core
     , antiope-s3
     , base >=4.7 && <5
     , bytestring
@@ -73,13 +78,15 @@
     , conduit-extra
     , exceptions
     , generic-lens
+    , hedgehog >=0.5 && <0.7
+    , hspec >=2.4 && <2.6
     , http-types
+    , hw-hspec-hedgehog >=0.1 && <0.2
     , lens
     , monad-logger
+    , mtl
     , network-uri
     , resourcet
     , text
     , unliftio-core
-  other-modules:
-      Paths_antiope_s3
   default-language: Haskell2010
diff --git a/src/Antiope/S3.hs b/src/Antiope/S3.hs
--- a/src/Antiope/S3.hs
+++ b/src/Antiope/S3.hs
@@ -28,11 +28,11 @@
 import Control.Monad.Trans.Resource
 import Data.ByteString.Lazy         (ByteString, empty)
 import Data.Conduit
-import Data.Conduit.Binary          (sinkLbs)
 import Data.Conduit.Combinators     as CC (concatMap)
+import Data.Conduit.Lazy            (lazyConsume)
 import Data.Conduit.List            (unfoldM)
 import Data.Monoid                  ((<>))
-import Data.Text                    as T (Text, null, pack, unpack)
+import Data.Text                    as T (Text, pack, unpack)
 import Network.AWS                  (Error (..), MonadAWS, ServiceError (..))
 import Network.AWS.Data
 import Network.AWS.Data.Body        (_streamBody)
@@ -40,8 +40,9 @@
 import Network.HTTP.Types.Status    (Status (..))
 import Network.URI                  (URI (..), URIAuth (..), parseURI, unEscapeString)
 
-import qualified Data.ByteString as BS
-import qualified Network.AWS     as AWS
+import qualified Data.ByteString      as BS
+import qualified Data.ByteString.Lazy as LBS
+import qualified Network.AWS          as AWS
 
 chunkSize :: ChunkSize
 chunkSize = ChunkSize (1024 * 1024)
@@ -50,24 +51,19 @@
 fromS3Uri uri = do
   puri <- parseURI (unpack uri)
   auth <- puri & uriAuthority
-  let b = pack $ unEscapeString $ auth & uriRegName       -- URI lib is pretty weird
+  let b = pack $ auth & uriRegName       -- URI lib is pretty weird
   let k = pack $ unEscapeString $ drop 1 $ puri & uriPath
-  S3Uri <$> (BucketName <$> checkEmpty b)
-        <*> (ObjectKey <$> checkEmpty k)
-  where
-    checkEmpty t
-      | T.null t  = Nothing
-      | otherwise = Just t
+  pure $ S3Uri (BucketName b) (ObjectKey k)
 
-downloadLBS :: MonadAWS m
+downloadLBS :: (MonadAWS m, MonadResource m)
   => BucketName
   -> ObjectKey
   -> m ByteString
 downloadLBS bucketName objectKey = do
   resp <- AWS.send $ getObject bucketName objectKey
-  (resp ^. gorsBody) `sinkBody` sinkLbs
+  liftResourceT $ LBS.fromChunks <$> lazyConsume (_streamBody $ resp ^. gorsBody)
 
-downloadLBS' :: MonadAWS m
+downloadLBS' :: (MonadAWS m, MonadResource m)
   => BucketName
   -> ObjectKey
   -> m (Maybe ByteString)
@@ -79,7 +75,7 @@
     Right bs -> return (Just bs)
     Left _   -> return Nothing
 
-downloadS3Uri :: MonadAWS m
+downloadS3Uri :: (MonadAWS m, MonadResource m)
   => S3Uri
   -> m (Maybe ByteString)
 downloadS3Uri (S3Uri b k) = downloadLBS' b k
diff --git a/test/Antiope/S3Spec.hs b/test/Antiope/S3Spec.hs
new file mode 100644
--- /dev/null
+++ b/test/Antiope/S3Spec.hs
@@ -0,0 +1,30 @@
+module Antiope.S3Spec (spec) where
+
+import Antiope.Core
+import Antiope.Env
+import Antiope.S3
+import Control.Monad.IO.Class
+import Data.Maybe                  (fromJust)
+import HaskellWorks.Hspec.Hedgehog
+import Hedgehog
+import System.Environment          (lookupEnv)
+import Test.Hspec
+
+import qualified Data.ByteString.Lazy.Char8 as LBS
+
+{-# ANN module ("HLint: Ignore Redundant do" :: String) #-}
+
+spec :: Spec
+spec = describe "Antiope.S3Spec" $ do
+  it "downloadS3Uri" $ requireTest $ do
+    maybeRemoteS3Uri <- liftIO (lookupEnv "REMOTE_S3_URI")
+    case maybeRemoteS3Uri of
+      Just remoteS3Uri -> do
+        env <- liftIO $ mkEnv Oregon (const LBS.putStrLn)
+        liftIO $ runResourceT $ do
+          mlbs <- runAws env $ downloadS3Uri (read remoteS3Uri)
+          let lbs = fromJust mlbs
+          let !_ = LBS.take 10 lbs
+          return ()
+        True === True
+      Nothing -> True === True
diff --git a/test/Spec.hs b/test/Spec.hs
--- a/test/Spec.hs
+++ b/test/Spec.hs
@@ -1,2 +1,1 @@
-main :: IO ()
-main = putStrLn "Test suite not yet implemented"
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
