packages feed

xrefcheck-0.3.0: tests/Test/Xrefcheck/TooManyRequestsSpec.hs

{- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io>
 -
 - SPDX-License-Identifier: MPL-2.0
 -}

module Test.Xrefcheck.TooManyRequestsSpec where

import Universum

import Data.CaseInsensitive qualified as CI
import Data.Set qualified as S
import Data.Time (addUTCTime, defaultTimeLocale, formatTime, getCurrentTime, rfc822DateFormat)
import Data.Time.Clock.POSIX (getPOSIXTime)
import Network.HTTP.Types (Status (..), ok200, serviceUnavailable503, tooManyRequests429)
import Network.HTTP.Types.Header (HeaderName, hRetryAfter)
import Network.Wai (requestMethod)
import Network.Wai qualified as Web
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, testCase, (@?=))
import Time (sec, (-:-))
import Web.Scotty qualified as Web

import Test.Xrefcheck.UtilRequests
import Xrefcheck.Core
import Xrefcheck.Progress
import Xrefcheck.Util
import Xrefcheck.Verify

test_tooManyRequests :: TestTree
test_tooManyRequests = testGroup "429 response tests"
  [ testCase "Returns 200 eventually" $ do
      setRef <- newIORef S.empty
      let prog = reportSuccess "" $ initProgress 1
      checkLinkAndProgressWithServerDefault setRef (5000, mock429 "1" ok200)
        "http://127.0.0.1:5000/429" prog $ VerifyResult []
  , testCase "Returns 503 eventually" $ do
      setRef <- newIORef S.empty
      let prog = reportError "" $ initProgress 1
      checkLinkAndProgressWithServerDefault setRef (5000, mock429 "1" serviceUnavailable503)
        "http://127.0.0.1:5000/429" prog $ VerifyResult
          [ ExternalHttpResourceUnavailable $
              Status { statusCode = 503, statusMessage = "Service Unavailable"}
          ]
  , testCase "Successfully updates the new retry-after value (as seconds)" $ do
      withServer (5000, mock429 "2" ok200) $ do
        now <- getPOSIXTime <&> posixTimeToTimeSecond
        setRef <- newIORef S.empty
        progressRef <- newIORef VerifyProgress
              { vrLocal = initProgress 0
              , vrExternal = setTaskTimestamp "" (sec 3) (now -:- sec 1.5)
                  . reportSuccess ""
                  $ initProgress 2
              }
        _ <- verifyReferenceWithProgressDefault
          (Reference "" (Position "") $ RIExternal $ ELUrl "http://127.0.0.1:5000/429")
          setRef
          progressRef
        progress <- vrExternal <$> readIORef progressRef
        let ttc = ttTimeToCompletion <$> getTaskTimestamp progress
        flip assertBool (ttc == Just (sec 2)) $
          "Expected time to completion be equal to " ++ show (Just $ sec 2) ++
          ", but instead it's " ++ show ttc
  , testCase "Successfully updates the new retry-after value (as date)" $ do
      utctime <- getCurrentTime
      let
        -- Set the @Retry-After@ response header value as (current datetime + 4 seconds)
        retryAfter = formatTime defaultTimeLocale rfc822DateFormat (addUTCTime 4 utctime)
        now = utcTimeToTimeSecond utctime
      withServer (5000, mock429 (fromString retryAfter) ok200) $ do
        setRef <- newIORef S.empty
        progressRef <- newIORef VerifyProgress
              { vrLocal = initProgress 0
              , vrExternal = setTaskTimestamp "" (sec 2) (now -:- sec 1.5)
                  . reportSuccess ""
                  $ initProgress 2
              }
        _ <- verifyReferenceWithProgressDefault
          (Reference "" (Position "") $ RIExternal $ ELUrl "http://127.0.0.1:5000/429")
          setRef
          progressRef
        progress <- vrExternal <$> readIORef progressRef
        let ttc = fromMaybe (sec 0) $ ttTimeToCompletion <$> getTaskTimestamp progress
        flip assertBool (sec 3 <= ttc && ttc <= sec 4) $
          "Expected time to completion be within range (seconds): 3 <= x <= 4" ++
          ", but instead it's " ++ show ttc
  , testCase "Sets the new retry-after to 0 seconds if\
                    \ its value is a date && has already passed" $ do
      utctime <- getCurrentTime
      let
        -- Set the @Retry-After@ response header value as (current datetime - 4 seconds)
        retryAfter = formatTime defaultTimeLocale rfc822DateFormat (addUTCTime (-4) utctime)
        now = utcTimeToTimeSecond utctime
      withServer (5000, mock429 (fromString retryAfter) ok200) $ do
        setRef <- newIORef S.empty
        progressRef <- newIORef VerifyProgress
              { vrLocal = initProgress 0
              , vrExternal = setTaskTimestamp "" (sec 1) (now -:- sec 1.5)
                  . reportSuccess ""
                  $ initProgress 2
              }
        _ <- verifyReferenceWithProgressDefault
          (Reference "" (Position "") $ RIExternal $ ELUrl "http://127.0.0.1:5000/429")
          setRef
          progressRef
        progress <- vrExternal <$> readIORef progressRef
        let ttc = ttTimeToCompletion <$> getTaskTimestamp progress
        flip assertBool (ttc == Just (sec 0)) $
          "Expected time to completion be 0 seconds" ++
          ", but instead it's " ++ show ttc
    , testCase "The GET request should not be attempted after catching a 429" $ do
      let
        mock429WithGlobalIORef :: IORef [(Text, Status)] -> IO Web.Application
        mock429WithGlobalIORef infoReverseAccumulatorRef = do
          callCountRef <- newIORef @_ @Int 0
          Web.scottyApp $
            Web.matchAny "/429grandfinale" $ do
              req <- Web.request
              let m = decodeUtf8 (requestMethod req)
              callCount <- atomicModifyIORef' callCountRef $ \cc -> (cc + 1, cc)
              atomicModifyIORef' infoReverseAccumulatorRef $ \lst ->
                ( ( m
                  , if | m == "GET"     -> ok200
                       | callCount == 0 -> tooManyRequests429
                       | otherwise      -> serviceUnavailable503
                  ) : lst
                , ()
                )
              if
                | m == "GET"     -> Web.status ok200
                | callCount == 0 -> do
                    Web.status tooManyRequests429
                    setHeader hRetryAfter "1"
                | otherwise      -> Web.status serviceUnavailable503
      infoReverseAccumulatorRef <- newIORef []
      setRef <- newIORef S.empty
      withServer (5000, mock429WithGlobalIORef infoReverseAccumulatorRef) $ do
        _ <- verifyLinkDefault setRef "http://127.0.0.1:5000/429grandfinale"
        infoReverseAccumulator <- readIORef infoReverseAccumulatorRef
        reverse infoReverseAccumulator @?=
          [ ("HEAD", tooManyRequests429)
          , ("HEAD", serviceUnavailable503)
          , ("GET", ok200)
          ]
  ]
  where
    -- When called for the first time, returns with a 429 and `Retry-After: @retryAfter@`.
    -- Subsequent calls will respond with @status@.
    mock429 :: Text -> Status -> IO Web.Application
    mock429 retryAfter status = do
      callCountRef <- newIORef @_ @Int 0
      Web.scottyApp $
        Web.matchAny "/429" $ do
          callCount <- atomicModifyIORef' callCountRef $ \cc -> (cc + 1, cc)
          if callCount == 0
          then do
            setHeader hRetryAfter retryAfter
            Web.status tooManyRequests429
          else do
            Web.status status

    setHeader :: HeaderName -> Text -> Web.ActionM ()
    setHeader hdr value = Web.setHeader (decodeUtf8 (CI.original hdr)) (fromStrict value)