vcr-0.0.0: test/VCRSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE OverloadedRecordDot #-}
module VCRSpec (spec) where
import Imports
import Test.Hspec
import Test.HUnit.Lang
import Test.Mockery.Directory
import System.Timeout
import Control.Concurrent
import Control.Concurrent.Async
import Network.HTTP.Client qualified as Client
import Network.HTTP.Client.TLS (getGlobalManager)
import Network.HTTP.Types
import WebMock
import VCR
import VCR.Serialize (loadTape)
makeRequest :: String -> [Header] -> IO (Client.Response LazyByteString)
makeRequest url headers = do
manager <- getGlobalManager
request <- Client.parseUrlThrow url
Client.httpLbs request { Client.requestHeaders = headers } manager
httpException :: Client.HttpException -> Bool
httpException _ = True
hUnitFailure :: FailureReason -> HUnitFailure -> Bool
hUnitFailure expectd (HUnitFailure _ actual) = expectd == actual
expectedButGot :: Request -> Request -> HUnitFailure -> Bool
expectedButGot expected actual= hUnitFailure (ExpectedButGot Nothing (show expected) (show actual))
unexpectedRequest :: Request -> HUnitFailure -> Bool
unexpectedRequest = hUnitFailure . Reason . mappend "Unexpected HTTP request: " . show
infix 1 `shouldReturnStatus`
infix 1 `shouldReturnBody`
shouldReturnStatus :: HasCallStack => String -> Status -> IO ()
shouldReturnStatus url expected = do
Client.responseStatus <$> makeRequest url [] `shouldReturn` expected
shouldReturnBody :: HasCallStack => String -> LazyByteString -> IO ()
shouldReturnBody url expected = do
Client.responseBody <$> makeRequest url [] `shouldReturn` expected
authRequest :: Request
authRequest = "http://httpbin.org/status/200" {
requestHeaders = [(hAuthorization, "Bearer sk-RfAZfajzapKps4anC6ej8rhSnMxf5sLd")]
}
redactedAuthRequest :: Request
redactedAuthRequest = authRequest {
requestHeaders = [(hAuthorization, "********")]
}
makeAuthRequest :: IO ()
makeAuthRequest = void $ makeRequest authRequest.requestUrl authRequest.requestHeaders
respondWith :: Status -> Request -> IO Response
respondWith responseStatus _ = return $ "" { responseStatus }
spec :: Spec
spec = around_ inTempDirectory do
describe "with" do
context "when mode is AnyOrder" do
let tape = "tape.yaml"
it "records requests in the order they were made" do
mockRequestChain [respondWith status200, respondWith status202, respondWith status201] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status202
"http://httpbin.org/status/201" `shouldReturnStatus` status201
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/200", "" { responseStatus = status200 })
, ("http://httpbin.org/status/202", "" { responseStatus = status202 })
, ("http://httpbin.org/status/201", "" { responseStatus = status201 })
]
context "with repeated requests to a resource" do
it "records the first request, replays subsequent requests" do
mockRequestChain [\ _ -> return ""] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
length <$> loadTape tape.file `shouldReturn` 1
context "with an existing tape" do
it "replays existing requests from the tape" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
disableRequests do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
length <$> loadTape tape.file `shouldReturn` 1
it "records new requests to the tape" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
mockRequest "http://httpbin.org/status/201" "" { responseStatus = status201 } do
VCR.with tape do
"http://httpbin.org/status/201" `shouldReturnStatus` status201
length <$> loadTape tape.file `shouldReturn` 2
context "with concurrent requests" do
it "records the requests concurrently, without blocking" do
maybe (expectationFailure "<<timeout>>") return <=< timeout 100_000 . protectRequestAction $ do
unsafeMockRequest (\ _ -> threadDelay 10_000 >> return "")
VCR.with tape do
forConcurrently_ [200 .. 299 :: Int] $ \ status -> do
"http://httpbin.org/status/" <> show status `shouldReturnBody` ""
length <$> loadTape tape.file `shouldReturn` 100
context "on exception" do
it "writes the tape to disk" do
mockRequest "http://httpbin.org/status/500" "" { responseStatus = status500 } do
VCR.with tape (makeRequest "http://httpbin.org/status/500" [])
`shouldThrow` httpException
length <$> loadTape tape.file `shouldReturn` 1
context "with an Authorization header" do
it "redacts the Authorization header" do
mockRequest authRequest "" do
VCR.with tape makeAuthRequest
loadTape tape.file `shouldReturn` [(redactedAuthRequest, "")]
context "when mode is Sequential" do
let tape = "tape.yaml" { mode = Sequential }
context "with repeated requests to a resource" do
it "records all interactions" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
length <$> loadTape tape.file `shouldReturn` 3
context "with an existing tape" do
it "replays request in order" do
mockRequestChain [respondWith status200, respondWith status202, respondWith status201] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status202
"http://httpbin.org/status/201" `shouldReturnStatus` status201
disableRequests $ do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status202
"http://httpbin.org/status/201" `shouldReturnStatus` status201
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/200", "" { responseStatus = status200 })
, ("http://httpbin.org/status/202", "" { responseStatus = status202 })
, ("http://httpbin.org/status/201", "" { responseStatus = status201 })
]
context "with an out of order request" do
it "fails" do
mockRequestChain [respondWith status200, respondWith status201, respondWith status202] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status201
"http://httpbin.org/status/202" `shouldReturnStatus` status202
disableRequests $ do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status200
`shouldThrow`
expectedButGot
(Request "GET" "http://httpbin.org/status/201" [] "")
(Request "GET" "http://httpbin.org/status/202" [] "")
context "with a missing request" do
it "fails" do
mockRequestChain [respondWith status200, respondWith status201, respondWith status202] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status201
"http://httpbin.org/status/202" `shouldReturnStatus` status202
disableRequests $ do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status201
`shouldThrow` hUnitFailure (Reason "Expected 3 requests, but only received 2!")
context "with additional requests" do
it "records additional requests" do
mockRequestChain [respondWith status200, respondWith status201, respondWith status202] do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status201
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status201
"http://httpbin.org/status/202" `shouldReturnStatus` status202
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/200", "" { responseStatus = status200 })
, ("http://httpbin.org/status/201", "" { responseStatus = status201 })
, ("http://httpbin.org/status/202", "" { responseStatus = status202 })
]
context "on exception" do
it "writes the tape to disk" do
mockRequest "http://httpbin.org/status/500" "" { responseStatus = status500 } do
VCR.with tape (makeRequest "http://httpbin.org/status/500" [])
`shouldThrow` httpException
length <$> loadTape tape.file `shouldReturn` 1
context "with an Authorization header" do
it "redacts the Authorization header" do
mockRequest authRequest "" do
VCR.with tape makeAuthRequest
loadTape tape.file `shouldReturn` [(redactedAuthRequest, "")]
describe "record" do
context "when mode is AnyOrder" do
let tape = "tape.yaml"
context "with repeated requests to a resource" do
it "records only the last interaction" do
mockRequestChain [\ _ -> return "foo", \ _ -> return "bar", \ _ -> return "baz"] do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnBody` "foo"
"http://httpbin.org/status/200" `shouldReturnBody` "bar"
"http://httpbin.org/status/200" `shouldReturnBody` "baz"
loadTape tape.file `shouldReturn` [("http://httpbin.org/status/200", "baz")]
context "with an existing tape" do
it "records new requests to the tape" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
mockRequest "http://httpbin.org/status/201" "" { responseStatus = status201 } do
VCR.record tape do
"http://httpbin.org/status/201" `shouldReturnStatus` status201
length <$> loadTape tape.file `shouldReturn` 2
it "updates existing requests, keeping the original order" do
mockRequestChain (replicate 3 $ \ _ -> return "") do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status200
"http://httpbin.org/status/201" `shouldReturnStatus` status200
mockRequestChain [\ _ -> return "foo"] do
VCR.record tape do
"http://httpbin.org/status/202" `shouldReturnStatus` status200
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/200", "")
, ("http://httpbin.org/status/202", "foo")
, ("http://httpbin.org/status/201", "")
]
context "when mode is Sequential" do
let tape = "tape.yaml" { mode = Sequential }
context "with repeated requests to a resource" do
it "records all interactions" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.with tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
length <$> loadTape tape.file `shouldReturn` 3
context "with an existing tape" do
it "overwrites the existing tape" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/200", "")
]
mockRequest "http://httpbin.org/status/201" "" { responseStatus = status201 } do
VCR.record tape do
"http://httpbin.org/status/201" `shouldReturnStatus` status201
loadTape tape.file `shouldReturn` [
("http://httpbin.org/status/201", "" { responseStatus = status201 })
]
describe "play" do
context "when mode is AnyOrder" do
let tape = "tape.yaml"
it "replays existing requests from the tape" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
disableRequests do
VCR.play tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
length <$> loadTape tape.file `shouldReturn` 1
it "fails on new requests" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
mockRequest "http://httpbin.org/status/201" "" { responseStatus = status201 } do
VCR.play tape do
"http://httpbin.org/status/201" `shouldReturnStatus` status201
`shouldThrow` unexpectedRequest "http://httpbin.org/status/201"
context "when mode is Sequential" do
let tape = "tape.yaml" { mode = Sequential }
it "replays request in order" do
mockRequestChain [respondWith status200, respondWith status202, respondWith status201] do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status202
"http://httpbin.org/status/201" `shouldReturnStatus` status201
disableRequests $ do
VCR.play tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/202" `shouldReturnStatus` status202
"http://httpbin.org/status/201" `shouldReturnStatus` status201
context "when not all requests are used" do
it "fails" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
VCR.play tape do "http://httpbin.org/status/200" `shouldReturnStatus` status200
`shouldThrow` hUnitFailure (Reason "Expected 2 requests, but only received 1!")
context "with additional requests" do
it "fails" do
mockRequest "http://httpbin.org/status/200" "" do
VCR.record tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
mockRequestChain [\ _ -> return ""] do
VCR.play tape do
"http://httpbin.org/status/200" `shouldReturnStatus` status200
"http://httpbin.org/status/200" `shouldReturnStatus` status200
`shouldThrow` unexpectedRequest "http://httpbin.org/status/200"