{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DerivingStrategies #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeOperators #-} module Properties ( spec ) where import Web.Libjwt import Generators import Libjwt.Classes import Libjwt.JsonByteString import Libjwt.JwtValidation ( runValidation ) import Libjwt.NumericDate ( minusSeconds ) import Control.Arrow ( left ) import Control.Exception ( displayException ) import Control.Monad.Trans.Reader ( runReader ) import Data.Aeson ( FromJSON , ToJSON , encode , decode ) import Data.Default import Data.Either.Validation as V import Data.Functor ( (<&>) ) import Data.List.NonEmpty ( NonEmpty ) import Data.Time.Calendar ( Day ) import Data.Time.Clock ( UTCTime ) import Data.UUID ( UUID ) import GHC.Generics import Test.Hspec import Test.Hspec.QuickCheck ( modifyArgs , prop ) import Test.QuickCheck import Test.QuickCheck.Instances ( ) import Prelude hiding ( exp ) spec :: Spec spec = modifyArgs quickcheckArgs $ do prop "enc-dec-roundtrip-reg-only" prop_encode_decode_roundtrip_reg_only prop "enc-dec-roundtrip" prop_encode_decode_roundtrip prop "enc-dec-roundtrip-ns" prop_encode_decode_roundtrip_ns prop "enc-dec-roundtrip-comp" prop_encode_decode_rountrip_comp prop "enc-dec-roundtrip-aeson" prop_encode_decode_roundtrip_aeson prop "from-to-generic" prop_from_to_generic prop "validity" prop_validity where quickcheckArgs args = args { maxSuccess = 4000 , maxDiscardRatio = 5 , maxSize = maxSize args `div` 2 , maxShrinks = 1000 } prop_encode_decode_roundtrip_poly :: (Decode (PrivateClaims pc ns), Encode (PrivateClaims pc ns), Show a, Eq a) => (Jwt pc ns -> a) -> (a -> Jwt pc ns) -> a -> Property prop_encode_decode_roundtrip_poly project embed a = let jwt = embed a in left displayException ( decodeByteString (alg $ header jwt) (getToken $ signJwt jwt) <&> project . getDecoded ) === pure a prop_encode_decode_roundtrip_reg_only :: Jwt Empty 'NoNs -> Property prop_encode_decode_roundtrip_reg_only = prop_encode_decode_roundtrip_poly id id prop_encode_decode_roundtrip :: Jwt '["intField" ->> Int, "dateField" ->> UTCTime, "textField" ->> JwtText, "optField" ->> Maybe JwtString, "arrayField" ->> [JwtText], "nonEmptyField" ->> NonEmpty ASCII] 'NoNs -> Property prop_encode_decode_roundtrip = prop_encode_decode_roundtrip_poly id id prop_encode_decode_roundtrip_ns :: Jwt '["dayField" ->> Day, "arrayField" ->> [Int], "stringField" ->> JwtString] ( 'SomeNs "https://example.com") -> Property prop_encode_decode_roundtrip_ns = prop_encode_decode_roundtrip_poly id id prop_encode_decode_rountrip_comp :: Jwt '["user_name" ->> JwtText, "is_root" ->> Bool, "type" ->> Flag AccountType, "client_id" ->> UUID, "created" ->> UTCTime, "accounts" ->> NonEmpty (UUID, JwtText)] 'NoNs -> Property prop_encode_decode_rountrip_comp = prop_encode_decode_roundtrip_poly id id data AccountType = Regular | Pro | Admin deriving stock (Show, Eq, Generic) instance AFlag AccountType instance Arbitrary AccountType where arbitrary = elements [Regular, Pro, Admin] data ClaimObj = MkClaimObj { userName :: JwtText, isRoot :: Bool, clientId :: UUID, createdAt :: UTCTime } deriving stock (Show, Eq, Generic) instance ToPrivateClaims ClaimObj instance FromPrivateClaims ClaimObj instance Arbitrary ClaimObj where arbitrary = MkClaimObj <$> arbitrary <*> arbitrary <*> arbitrary <*> arbitrary shrink MkClaimObj { userName, isRoot, clientId, createdAt } = MkClaimObj <$> shrink userName <*> pure isRoot <*> shrink clientId <*> shrink createdAt prop_from_to_generic :: ClaimObj -> Property prop_from_to_generic claimObj = forAllBlind genHeader $ \header -> prop_encode_decode_roundtrip_poly (fromPrivateClaims . privateClaims . payload) (\claimObj' -> Jwt { header , payload = def { privateClaims = toPrivateClaims claimObj' } } ) claimObj data Account = MkAccount { account_name :: JwtText, account_id :: UUID } deriving stock (Show, Eq, Generic) instance FromJSON Account instance ToJSON Account instance JwtRep JsonByteString Account where rep = Json . encode unRep = decode . toJson instance JsonBuilder Account instance JsonParser Account instance Arbitrary Account where arbitrary = MkAccount <$> arbitrary <*> arbitrary shrink (MkAccount name id) = MkAccount <$> shrink name <*> shrink id prop_encode_decode_roundtrip_aeson :: Jwt '["user_id" ->> Int, "accounts" ->> NonEmpty Account] 'NoNs -> Property prop_encode_decode_roundtrip_aeson = prop_encode_decode_roundtrip_poly id id prop_validity :: UTCTime -> ValidationSettings -> Payload Empty 'NoNs -> Property prop_validity time settings@Settings { leeway, appName } payload@ClaimsSet { exp, nbf, aud = Aud rs } | now >= validFrom && now < validTo && appropriate = label "valid" $ classify triviallyValid "trivial" $ success validate | otherwise = label "invalid" $ failure validate where now = fromUTC time validFrom = case nbf of Nbf Nothing -> minBound Nbf (Just sth) -> sth `minusSeconds` leeway validTo = case exp of Exp Nothing -> maxBound Exp (Just sth) -> sth `plusSeconds` leeway appropriate = null rs || maybe False (`elem` rs) appName triviallyValid = exp == Exp Nothing && nbf == Nbf Nothing && null rs validate = runReader (runValidation settings mempty payload) time success (V.Success _) = property True success errors = counterexample (show errors) False failure (V.Failure _) = property True failure other = counterexample (show other) False