packages feed

github-rest-1.1.0: test/Endpoint.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}

module Endpoint (tests) where

import Data.Function (on)
import Data.List (nubBy)
import Data.Maybe (fromMaybe)

#if !MIN_VERSION_base(4,11,0)
import Data.Monoid ((<>))
import Data.Semigroup (Semigroup)
#endif
import Data.String (IsString (..))
import Data.Text (Text)
import qualified Data.Text as Text
import Network.HTTP.Types (StdMethod (..))
import Test.Tasty (TestTree)
import Test.Tasty.QuickCheck

import GitHub.REST.Endpoint
import GitHub.REST.KeyValue (KeyValue (..))

tests :: [TestTree]
tests =
  [ testProperty "endpointPath with no substitutions" $
      forAll (listOf genNoSubstitute) $ \endpointComponents ->
        let ghEndpoint = mkGHEndpoint endpointComponents []
         in endpointPath ghEndpoint === concatComponents endpointComponents
  , testProperty "endpointPath with substitutions" $
      forAll (listOf genNoSubstitute) $ \withoutColons ->
        forAll (listOf1 genSubstitute) $ \withColonAndVals' -> do
          -- we don't support duplicate substitutions
          let withColonAndVals = nubBy ((==) `on` fst) withColonAndVals'

          -- list of components before/after substituting
          let allComponents = map (\ec -> (ec, ec)) withoutColons ++ withColonAndVals
          (before, after) <- unzip <$> shuffle allComponents

          let ghEndpoint = mkGHEndpoint before $ map fromSubstitutePair withColonAndVals

          pure $ endpointPath ghEndpoint === concatComponents after
  ]

{- Helpers -}

mkGHEndpoint :: [EndpointComponent] -> EndpointVals -> GHEndpoint
mkGHEndpoint endpointComponents endpointVals =
  GHEndpoint
    { method = GET
    , endpoint = concatComponents endpointComponents
    , endpointVals
    , ghData = []
    }

concatComponents :: [EndpointComponent] -> Text
concatComponents = ("/" <>) . Text.intercalate "/" . map unComponent

-- | A component in an endpoint path; e.g. does not contain forward slashes.
newtype EndpointComponent = EndpointComponent {unComponent :: Text}
  deriving (Show, Eq, IsString, Monoid, Semigroup)

-- | Generate a non-substitutionable EndpointComponent.
genNoSubstitute :: Gen EndpointComponent
genNoSubstitute = fmap fromString $ listOf1 $ elements allowedUrlCharacters
  where
    -- https://stackoverflow.com/a/41353282
    allowedUrlCharacters = ['a' .. 'z'] ++ ['A' .. 'Z'] ++ ['0' .. '9'] ++ "-_.~"

-- | Generate a substitutionable EndpointComponent and a value to interpolate.
genSubstitute :: Gen (EndpointComponent, EndpointComponent)
genSubstitute = do
  component <- genNoSubstitute
  val <- genNoSubstitute
  pure (":" <> component, val)

fromSubstitutePair :: (EndpointComponent, EndpointComponent) -> KeyValue
fromSubstitutePair (EndpointComponent key, EndpointComponent val) = key' := val
  where
    key' = fromMaybe key $ Text.stripPrefix ":" key