xrefcheck-0.3.0: tests/Test/Xrefcheck/TimeoutSpec.hs
{- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io>
-
- SPDX-License-Identifier: MPL-2.0
-}
module Test.Xrefcheck.TimeoutSpec where
import Universum hiding ((.~))
import Control.Lens ((.~))
import Data.CaseInsensitive qualified as CI
import Data.Set qualified as S
import Network.HTTP.Types (ok200, tooManyRequests429)
import Network.HTTP.Types.Header (HeaderName, hRetryAfter)
import Network.Wai qualified as Web
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (testCase)
import Time (Second, Time, sec, threadDelay)
import Web.Scotty qualified as Web
import Test.Xrefcheck.UtilRequests
import Xrefcheck.Config
import Xrefcheck.Progress
import Xrefcheck.Verify
-- Here all the delays are doubled because we call sites
-- with HEAD first and then GET methods.
test_timeout :: TestTree
test_timeout = testGroup "Timeout tests"
[ testCase "Succeeds on one timeout if there were no 429 responses and no retries allowed" $
timeoutTestCase [Delay] True
(cNetworkingL . ncMaxTimeoutRetriesL .~ 0)
, testCase "Returns an error on two timeouts if there were no 429 responses" $
timeoutTestCase [Delay, Delay] False
(cNetworkingL . ncMaxTimeoutRetriesL .~ 2)
, testCase "Returns an error if there were 429 but no timeouts allowed" $
timeoutTestCase [Respond429, Delay, Delay] False
(cNetworkingL . ncMaxTimeoutRetriesL .~ 0)
, testCase "Succeeds if there were 429 and one timeout allowed" $
timeoutTestCase [Respond429, Delay, Delay] True
(cNetworkingL . ncMaxTimeoutRetriesL .~ 1)
, testCase "Fails on second timeout if there were 429 and one timeout allowed" $
timeoutTestCase [Respond429, Delay, Delay, Delay, Delay] False
(cNetworkingL . ncMaxTimeoutRetriesL .~ 1)
, testCase "Fails on maximum allowed errors achieved (mixed errors)" $
timeoutTestCase [Respond429, Delay, Delay, Respond429, Delay, Delay] False $ \c -> c
& cNetworkingL . ncMaxTimeoutRetriesL .~ 3
& cNetworkingL . ncMaxRetriesL .~ 3
, testCase "Fails on timeout if another domain returned 429" $ do
setRef <- newIORef S.empty
checkMultipleLinksWithServer
(5000, mockTimeout (sec 0.4) [Respond429, Ok, Delay, Delay])
setRef
[ VerifyLinkTestEntry
{ vlteConfigModifier = \c -> c
& cNetworkingL . ncMaxRetriesL .~ 1
& setAllowedTimeout
, vlteLink = "http://127.0.0.1:5000/timeout"
, vlteExpectedProgress = mkProgressWithOneTask True
, vlteExpectationErrors = VerifyResult []
}
, VerifyLinkTestEntry
{ vlteConfigModifier = \c -> c
& cNetworkingL . ncMaxTimeoutRetriesL .~ 0
& setAllowedTimeout
, vlteLink = "http://localhost:5000/timeout"
, vlteExpectedProgress = mkProgressWithOneTask False
, vlteExpectationErrors = VerifyResult
[ ExternalHttpTimeout (Just $ DomainName "localhost")
]
}
]
, testCase "Succeeds on timeout if another path of this domain returned 429" $ do
setRef <- newIORef S.empty
checkMultipleLinksWithServer
(5000, mockTimeout (sec 0.4) [Respond429, Ok, Delay, Delay])
setRef
[ VerifyLinkTestEntry
{ vlteConfigModifier = \c -> c
& cNetworkingL . ncMaxRetriesL .~ 1
& setAllowedTimeout
, vlteLink = "http://127.0.0.1:5000/timeout"
, vlteExpectedProgress = mkProgressWithOneTask True
, vlteExpectationErrors = VerifyResult []
}
, VerifyLinkTestEntry
{ vlteConfigModifier = \c -> c
& cNetworkingL . ncMaxTimeoutRetriesL .~ 1
& setAllowedTimeout
, vlteLink = "http://127.0.0.1:5000/timeoutother"
, vlteExpectedProgress = mkProgressWithOneTask True
, vlteExpectationErrors = VerifyResult []
}
]
]
where
setAllowedTimeout = cNetworkingL . ncExternalRefCheckTimeoutL .~ (sec 0.25)
mkProgressWithOneTask shouldSucceed = report "" $ initProgress 1
where
report =
if shouldSucceed
then reportSuccess
else reportError
timeoutTestCase mockResponses shouldSucceed configModifier = do
let prog = mkProgressWithOneTask shouldSucceed
setRef <- newIORef S.empty
checkLinkAndProgressWithServer
(\c -> c
& setAllowedTimeout
& configModifier)
setRef
(5000, mockTimeout (sec 0.4) mockResponses)
"http://127.0.0.1:5000/timeout" prog $
VerifyResult $
[ExternalHttpTimeout $ Just (DomainName "127.0.0.1") | not shouldSucceed]
-- When called for the first (N-1) times, waits for specified
-- amount of seconds and returns an arbitrary result.
-- When called N time returns the result immediately.
mockTimeout :: Time Second -> [MockTimeoutBehaviour] -> IO Web.Application
mockTimeout timeout behList = do
ref <- newIORef @_ behList
Web.scottyApp $ do
Web.matchAny "/timeout" $ handler ref
Web.matchAny "/timeoutother" $ handler ref
where
handler ref = do
mbCurrentAction <- atomicModifyIORef' ref $ \case
b : bs -> (bs, Just b)
[] -> ([], Nothing)
case mbCurrentAction of
Nothing -> Web.status ok200
Just Ok -> Web.status ok200
Just Delay -> do
threadDelay timeout
Web.status ok200
Just Respond429 -> do
setHeader hRetryAfter "1"
Web.status tooManyRequests429
setHeader :: HeaderName -> Text -> Web.ActionM ()
setHeader hdr value = Web.setHeader (decodeUtf8 (CI.original hdr)) (fromStrict value)
data MockTimeoutBehaviour = Respond429 | Delay | Ok