do-spaces-0.2: tests/Main.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Main where
import Conduit ( sourceLazy, withSourceFile )
import qualified Data.ByteString.Char8 as C
import qualified Data.CaseInsensitive as CI
import Data.Foldable ( Foldable(toList), for_ )
import Data.Generics.Labels ()
import Data.Maybe ( isJust )
import qualified Data.Sequence as S
import qualified Data.Text as T
import Data.Time ( UTCTime )
import Data.Time.Format.ISO8601 ( iso8601ParseM )
import Lens.Micro
import Network.DO.Spaces ( newSpaces )
import Network.DO.Spaces.Actions
import Network.DO.Spaces.Request
import Network.DO.Spaces.Types
import Network.HTTP.Types ( mkStatus )
import Test.Hspec
main :: IO ()
main = sequence_ [ requests
, errorResponse
, listAllBucketsResponse
, listBucket
, bucketLocationResponse
, objectInfoResponse
, copyObject
, beginMultipartResponse
, listPartsResponse
, bucketName
, bucketCORS
, bucketACLs
, bucketLifecycle
]
requests :: IO ()
requests = do
sp <- testSpaces
spacesRequest <- newSpacesRequest (testBuilder sp) testTime
hspec . describe "Network.DO.Spaces.Request" $ do
it "generates the canonical request"
$ (spacesRequest & canonicalRequest) `shouldBe` canonRequest
it "generates the string to sign"
$ mkStringToSign spacesRequest `shouldBe` strToSign
it "generates the signature"
$ mkSignature spacesRequest strToSign `shouldBe` sig
it "generates the authorization"
$ mkAuthorization spacesRequest strToSign `shouldBe` auth
where
bodyHash =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
canonRequest = Canonicalized
$ C.intercalate "\n"
[ "GET"
, "/"
, ""
, "host:a-bucket.sgp1.digitaloceanspaces.com"
, "x-amz-content-sha256:" <> bodyHash
, "x-amz-date:20210404T214315Z"
, ""
, "host;x-amz-content-sha256;x-amz-date"
, bodyHash
]
strToSign = StringToSign
$ C.intercalate "\n"
[ "AWS4-HMAC-SHA256"
, "20210404T214315Z"
, "20210404/sgp1/s3/aws4_request"
, "266d2fb56a251205c42c7e0deb7d2e370574cf190f366ecf53179c27697c8e38"
]
sig = Signature "3d0da77e916e588d05f0190f8c350eddb47337953897b1e0cfdb44075fd6b2b9"
auth = Authorization
$ mconcat [ "AWS4-HMAC-SHA256 Credential="
, "II5JDQBAN3JYM4DNEB6C"
, "/"
, "20210404/sgp1/s3/aws4_request, "
, "SignedHeaders="
, "host;x-amz-content-sha256;x-amz-date, "
, "Signature="
, uncompute sig
]
testTime = read @UTCTime "2021-04-04 21:43:15 +0000"
testBuilder spaces = SpacesRequestBuilder
{ spaces
, method = Nothing
, body = Nothing
, headers = mempty
, bucket = Just testBucket
, object = Nothing
, subresources = Nothing
, queryString = Nothing
, overrideRegion = Nothing
}
testBucket = Bucket "a-bucket"
testSpaces :: IO Spaces
testSpaces =
newSpaces (Explicit Singapore
(AccessKey "II5JDQBAN3JYM4DNEB6C")
(SecretKey "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"))
errorResponse :: IO ()
errorResponse = do
apiEx <- withSourceFile "./tests/data/error-response.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
parseErrorResponse status raw
hspec
. describe "Network.DO.Spaces.Actions.parseErrorResponse"
. it "parses Spaces XML error responses correctly"
$ apiEx
`shouldBe` APIException
{ code = "SignatureDoesNotMatch"
, requestID = "tx000012a832c-nyc3"
, hostID = "71f0230-nyc3a-nyc"
, status
}
where
status = mkStatus 403 ""
listAllBucketsResponse :: IO ()
listAllBucketsResponse = do
sp <- testSpaces
allBuckets
<- withSourceFile "./tests/data/list-all-buckets.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @ListAllBuckets raw) sp
d1 <- iso8601ParseM @_ @UTCTime "2017-06-23T18:37:48.157Z"
d2 <- iso8601ParseM @_ @UTCTime "2017-06-23T18:37:48.157Z"
hspec
. describe "Network.DO.Spaces.Actions.ListAllBuckets"
. it "parses the response correctly"
$ allBuckets
`shouldBe` ListAllBucketsResponse
{ owner = Owner (OwnerID 6174283) (OwnerID 6174283)
, buckets = S.fromList [ BucketInfo (Bucket "static-images") d1
, BucketInfo (Bucket "log-files") d2
]
}
listBucket :: IO ()
listBucket = do
sp <- testSpaces
bucketContents
<- withSourceFile "./tests/data/list-bucket.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @ListBucket raw) sp
d1 <- iso8601ParseM @_ @UTCTime "2017-07-13T18:40:46.777Z"
d2 <- iso8601ParseM @_ @UTCTime "2017-07-14T17:44:03.597Z"
hspec . describe "Network.DO.Spaces.Actions.ListBucket" $ do
it "parses the response correctly"
$ bucketContents `shouldBe` listBucketResp d1 d2
it "ensures maxKeys is within the correct range" $ do
let badReq = listBucketReq $ Just (-1)
badReq2 = listBucketReq $ Just 1001
runSpacesT (buildRequest badReq) sp
`shouldThrow` (InvalidRequest msg ==)
runSpacesT (buildRequest badReq2) sp
`shouldThrow` (InvalidRequest msg ==)
where
listBucketReq maxKeys = ListBucket
{ bucket = Bucket "some-bucket"
, delimiter = Nothing
, marker = Nothing
, prefix = Nothing
, maxKeys
}
msg = "ListBucket: maxKeys must be >= 0 && <= 1000"
listBucketResp d1 d2 = ListBucketResponse
{ bucket = Bucket "static-images"
, prefix = Nothing
, marker = Nothing
, nextMarker = Nothing
, maxKeys = 1000
, isTruncated = False
, objects =
S.fromList [ ObjectInfo
{ object = Object "example.txt"
, lastModified = d1
, etag = "b3a92f49e7ae64acbf6b3e76f2040f5e"
, size = 14
, owner =
Owner (OwnerID 6174283) (OwnerID 6174283)
}
, ObjectInfo
{ object = Object "sammy.png"
, lastModified = d2
, etag = "fb08934ef619f205f272b0adfd6c018c"
, size = 35369
, owner =
Owner (OwnerID 6174283) (OwnerID 6174283)
}
]
}
bucketLocationResponse :: IO ()
bucketLocationResponse = do
sp <- testSpaces
bucketContents
<- withSourceFile "./tests/data/bucket-location.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @GetBucketLocation raw) sp
hspec
. describe "Network.DO.Spaces.Actions.GetBucketLocation"
. it "parses the response correctly"
$ bucketContents
`shouldBe` GetBucketLocationResponse { locationConstraint = NewYork }
objectInfoResponse :: IO ()
objectInfoResponse = do
sp <- testSpaces
let body = sourceLazy mempty
headers = [ (CI.mk "Content-Type", "text/plain")
, (CI.mk "Content-Length", "14")
, (CI.mk "Etag", "b3a92f49e7ae64acbf6b3e76f2040f5e")
, (CI.mk "Last-Modified", "Thu, 13 Jul 2017 18:40:46 GMT")
]
raw = RawResponse { .. }
objectInfo <- runSpacesT (consumeResponse @_ @GetObjectInfo raw) sp
hspec
. describe "Network.DO.Spaces.Actions.GetObjectInfo"
. it "parses response headers correctly"
$ objectInfo
`shouldBe` ObjectMetadata
{ contentLength = 14
, contentType = "text/plain"
, etag = "b3a92f49e7ae64acbf6b3e76f2040f5e"
, lastModified = testTime
}
where
testTime = read @UTCTime "2017-07-13 18:40:46 +0000"
copyObject :: IO ()
copyObject = do
sp <- testSpaces
copyObjectDate <- iso8601ParseM @_ @UTCTime "2017-07-10T20:22:54.167Z"
copyObjectResp
<- withSourceFile "./tests/data/copy-object.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @CopyObject raw) sp
hspec . describe "Network.DO.Spaces.Actions.CopyObject" $ do
it "parses the response correctly"
$ copyObjectResp
`shouldBe` CopyObjectResponse
{ lastModified = copyObjectDate
, etag = "7967bfe102f83fb5fc7e5a02bf05e8fc"
}
it "ensures the correct metadataDirective is provided"
$ runSpacesT (buildRequest badReq) sp
`shouldThrow` (InvalidRequest msg ==)
where
badReq = CopyObject
{ srcBucket
, srcObject
, destBucket = srcBucket
, destObject = srcObject
, metadataDirective = Copy
, acl = Nothing
}
srcObject = Object "some-object"
srcBucket = Bucket "some-bucket"
msg = mconcat [ "CopyObject: "
, "Object cannot be copied to itself unless "
, "REPLACE directive is specified"
]
beginMultipartResponse :: IO ()
beginMultipartResponse = do
sp <- testSpaces
multipart
<- withSourceFile "./tests/data/begin-multipart.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @BeginMultipart raw) sp
hspec
. describe "Network.DO.Spaces.Actions.BeginMultipart"
. it "parses the response correctly"
$ multipart
`shouldBe` BeginMultipartResponse
{ session = MultipartSession
{ bucket = Bucket "static-images"
, object = Object "multipart-file.tar.gz"
, uploadID = "2~iCw_lDY8VoBhoRrIJbPMrUqnE3Z-3Qh"
}
}
listPartsResponse :: IO ()
listPartsResponse = do
sp <- testSpaces
d <- iso8601ParseM @_ @UTCTime "2017-08-14T18:45:01.601Z"
listParts <- withSourceFile "./tests/data/list-parts.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @ListParts raw) sp
hspec
. describe "Network.DO.Spaces.Actions.ListParts"
. it "parses the response correctly"
$ listParts `shouldBe` listPartsResp d
where
listPartsResp lastModified = ListPartsResponse
{ bucket = Bucket "my-new-bucket"
, object = Object "multipart-file.tar.gz"
, uploadID = "2~iCw_lDY8VoBhoRrIJbPMrUqnE3Z-3Qh"
, partMarker = 0
, nextPartMarker = 1
, maxParts = 1000
, isTruncated = False
, parts =
S.fromList [ Part
{ partNumber = 1
, etag = "d8d3ed3a4de016917a814a2cf5acad3c"
, size = 5242880
, lastModified
}
, Part
{ partNumber = 2
, etag = "adf5feafc0fe4632008d5cb30beb1c49"
, size = 5242880
, lastModified
}
]
}
bucketName :: IO ()
bucketName = hspec . describe "Network.DO.Spaces.Types.mkBucket" $ do
it "rejects invalid Bucket names"
. for_ [ "doc_example_bucket"
, "doc-example-bucket-"
, "do"
, T.replicate 64 "d"
]
$ \name -> mkBucket name `shouldThrow` matchErr
it "accepts valid Bucket names"
. for_ [ "docexamplebucket1"
, "log-delivery-march-2020"
, "my-hosted-content"
]
$ \name -> mkBucket name `shouldSatisfy` isJust
where
matchErr (OtherError _) = True
matchErr _ = False
bucketCORS :: IO ()
bucketCORS = do
sp <- testSpaces
cors <- withSourceFile "./tests/data/get-bucket-cors.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @GetBucketCORS raw) sp
hspec
. describe "Network.DO.Spaces.Actions.GetBucketCORS"
. it "parses the response correctly"
$ (cors ^. #rules & toList)
`shouldBe` [ CORSRule
{ allowedOrigin = "http://example.com"
, allowedMethods = [ PUT, DELETE, POST ]
, allowedHeaders = [ "*" ]
}
, CORSRule
{ allowedOrigin = "*"
, allowedMethods = [ GET ]
, allowedHeaders = mempty
}
]
bucketACLs :: IO ()
bucketACLs = do
sp <- testSpaces
acls <- withSourceFile "./tests/data/get-bucket-acls.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @GetBucketACLs raw) sp
hspec
. describe "Network.DO.Spaces.Actions.GetBucketACLs"
. it "parses the response correctly"
$ (acls ^. #accessControlList)
`shouldBe` [ Grant { permission = ReadOnly, grantee = Group }
, Grant
{ permission = FullControl
, grantee = CanonicalUser (Owner (OwnerID 6174283)
(OwnerID 6174283))
}
]
bucketLifecycle :: IO ()
bucketLifecycle = do
sp <- testSpaces
rs <- withSourceFile "./tests/data/get-bucket-lifecycle.xml" $ \body -> do
let headers = mempty
raw = RawResponse { .. }
runSpacesT (consumeResponse @_ @GetBucketLifecycle raw) sp
hspec
. describe "Network.DO.Spaces.Actions.GetBucketACLs"
. it "parses the response correctly"
$ (rs ^. #rules)
`shouldBe` [ LifecycleRule
{ lifecycleID = LifecycleID "Expire old logs"
, enabled = True
, expiration = Just (AfterDays 90)
, prefix = Just "logs/"
, abortIncomplete = Nothing
}
, LifecycleRule
{ lifecycleID =
LifecycleID "Remove uncompleted uploads"
, enabled = True
, abortIncomplete = Just 1
, expiration = Nothing
, prefix = Nothing
}
]