packages feed

morley-client-0.1.0: test/Test/Util.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

-- | Module with various helpers that are used in morley-client mock tests.
module Test.Util
  ( chainOperationHandlers
  , dumbContractState
  , dumbImplicitContractState
  , dumbManagerKey
  , mapToContractStateBigMap
  , handleGetBigMapValue

    -- * TemplateHaskell test helpers
  , shouldCompileTo
  , shouldCompileIgnoringInstance
    -- * Internals
  , handleRunOperationInternal
  , assertHeadBlockId
  ) where

import Prelude hiding (Type)

import Control.Exception.Safe (throwString)
import Control.Lens (at, (?~))
import Data.Aeson (encode)
import Data.ByteArray (ScrubbedBytes)
import qualified Data.ByteString.Lazy as LBS (toStrict)
import qualified Data.Generics as SYB
import Data.Map as Map (elems, fromList, insert, lookup, toList)
import Data.Singletons (demote)
import Fmt ((+|), (|+))
import Language.Haskell.TH (pprint)
import Language.Haskell.TH.Syntax
  (Dec(..), Name, Q, TyVarBndr(..), Type(..), mkName, nameBase, runQ)
import Network.HTTP.Types.Status (status404)
import Network.HTTP.Types.Version (http20)
import Servant.Client.Core
  (BaseUrl(..), ClientError(..), RequestF(..), ResponseF(..), Scheme(..), defaultRequest)
import Test.Tasty.HUnit (Assertion, (@?=))
import Text.Hex (encodeHex)
import qualified Text.Show (show)

import Lorentz as L (compileLorentz, drop)
import Lorentz.Constraints
import Lorentz.Pack
import Morley.Client.RPC.Types
import Morley.Client.TezosClient.Types
import Morley.Micheline
import Morley.Michelson.Typed
import Morley.Tezos.Address
import Morley.Tezos.Core
import Morley.Tezos.Crypto
import qualified Morley.Tezos.Crypto.Ed25519 as Ed25519
import Morley.Util.ByteString
import TestM

-- | Function to convert given map to big map representation
-- used in mock state.
mapToContractStateBigMap
  :: forall k v. (NicePackedValue k, NicePackedValue v)
  => BigMapId k v -> Map k v -> ContractStateBigMap
mapToContractStateBigMap (BigMapId bigMapId) map' = ContractStateBigMap
  { csbmKeyType = toExpression $ demote @(ToT k)
  , csbmValueType = toExpression $ demote @(ToT v)
  , csbmMap = fromList $
    map (bimap (encodeBase58Check . valueToScriptExpr) lEncodeValue) $
    Map.toList map'
  , csbmId = bigMapId
  }

-- | Initial simple contract mock state.
dumbContractState :: ContractState
dumbContractState = ContractState
  { csCounter = 100500
  , csAlias = "genesis2"
  , csContractData = ContractData
      OriginationScript
        { osCode    = toExpression $ compileLorentz L.drop
        , osStorage = toExpression $ toVal ()
        }
      Nothing
  }

dumbImplicitContractState :: ContractState
dumbImplicitContractState = ContractState
  { csCounter = 100500
  , csAlias = "genesis1"
  , csContractData = ImplicitContractData Nothing
  }

-- | Mock handlers used for transaction sending and contract origination.
chainOperationHandlers :: Monad m => Handlers (TestT m)
chainOperationHandlers = defaultHandlers
  { hGetBlockHash = handleGetBlockHash
  , hGetCounter = handleGetCounter
  , hGetBlockConstants = handleGetBlockConstants
  , hGetProtocolParameters = handleGetProtocolParameters
  , hRunOperation = handleRunOperation
  , hPreApplyOperations = mapM . handlePreApplyOperation
  , hForgeOperation = \blkId arg -> do
      assertHeadBlockId blkId
      pure . HexJSONByteString . LBS.toStrict . encode $ arg
  , hInjectOperation = pure . OperationHash . (<> "_injected") . encodeHex . unHexJSONByteString
  , hGetContractScript = handleGetContractScript
  , hSignBytes =
    \_ _ -> pure . SignatureEd25519 . Ed25519.sign testSecretKey
  , hWaitForOperation = const pass
  , hGetAlias = handleGetAlias
  , hResolveAddressMaybe = handleResolveAddressMaybe
  , hRememberContract = handleRememberContract
  , hCalcTransferFee = \_ _ _ _ -> pure $ [TezosMutez $ toMutez 100500]
  , hCalcOriginationFee = \_ -> pure $ TezosMutez $ toMutez 100500
  , hGetKeyPassword = \_ -> pure Nothing
  , hGenKey = handleGenKey
  , hGetManagerKey = handleGetManagerKey
  , hRevealKey = handleRevealKey
  }
  where
    testSecretKey :: Ed25519.SecretKey
    testSecretKey = Ed25519.detSecretKey "\001\002\003\004"

mkRunOperationResult :: [Address] -> RunOperationResult
mkRunOperationResult originatedContracts = RunOperationResult
  { rrOperationContents =
    one $ OperationContent $ RunMetadata
      { rmOperationResult = OperationApplied $
        AppliedResult 100500 100500 100500 originatedContracts 0
      , rmInternalOperationResults = []
      }
  }

handleGetBlockHash :: Monad m => BlockId -> TestT m Text
handleGetBlockHash blkId = do
  assertHeadBlockId blkId
  MockState{..} <- get
  pure $ msHeadBlock

handleGetCounter
  :: ( MonadState MockState m
     , MonadThrow m
     )
  => BlockId -> Address -> m TezosInt64
handleGetCounter blk addr = do
  assertHeadBlockId blk
  MockState{..} <- get
  case lookup addr msContracts of
    Nothing -> throwM $ UnknownContract $ AddressResolved addr
    Just ContractState{..} -> pure $ csCounter

handleGetBlockConstants
  :: MonadState MockState m
  => anything -> m BlockConstants
handleGetBlockConstants _ = do
  MockState{..} <- get
  pure $ msBlockConstants

handleGetProtocolParameters
  :: (MonadState MockState m, MonadThrow m)
  => BlockId -> m ProtocolParameters
handleGetProtocolParameters blk = do
  assertHeadBlockId blk
  MockState{..} <- get
  pure $ msProtocolParameters

handleRunOperation :: Monad m => BlockId -> RunOperation -> TestT m RunOperationResult
handleRunOperation blk RunOperation{..} = do
  assertHeadBlockId blk
  MockState{..} <- get
  -- Ensure that passed chain id matches with one that mock state has
  unless (roChainId == bcChainId msBlockConstants) (throwM $ InvalidChainId)
  originatedContracts <- handleRunOperationInternal roOperation
  pure $ mkRunOperationResult originatedContracts

handlePreApplyOperation :: Monad m => BlockId -> PreApplyOperation -> TestT m RunOperationResult
handlePreApplyOperation blk PreApplyOperation{..} = do
  assertHeadBlockId blk
  MockState{..} <- get
  -- Ensure that passed protocol matches with one that mock state has
  unless (paoProtocol == bcProtocol msBlockConstants) (throwM $ InvalidProtocol)
  originatedContracts <- concatMapM handleTransactionOrOrigination paoContents
  pure $ mkRunOperationResult originatedContracts

handleRunOperationInternal :: Monad m => RunOperationInternal -> TestT m [Address]
handleRunOperationInternal RunOperationInternal{..} = do
  concatMapM handleTransactionOrOrigination roiContents

handleTransactionOrOrigination
  :: Monad m => Either TransactionOperation OriginationOperation -> TestT m [Address]
handleTransactionOrOrigination op = do
  MockState{..} <- get
  case op of
    -- Ensure that transaction sender exists
    Left TransactionOperation{..} -> case lookup codSource msContracts of
      Nothing -> throwM $ UnknownContract $ AddressResolved codSource
      Just ContractState{..} -> do
        -- Ensure that sender counter matches
        unless (csCounter + 1 == codCounter) (throwM CounterMismatch)
        case lookup toDestination msContracts of
          Nothing -> throwM $ UnknownContract $ AddressResolved toDestination
          Just _ -> pure []
      where
        CommonOperationData{..} = toCommonData
    -- Ensure that originator exists
    Right OriginationOperation{..} -> case lookup codSource msContracts of
      Nothing -> throwM $ UnknownContract $ AddressResolved codSource
      Just ContractState{..} -> do
        -- Ensure that originator counter matches
        unless (csCounter + 1 == codCounter) (throwM CounterMismatch)
        pure [dummyContractAddr]
      where
        CommonOperationData{..} = ooCommonData
        dummyContractAddr = unsafeParseAddress "KT1LZwEZqbqtLYhwzaidBp6So9LgYDpkpEv7"

-- We don't pass non-head block anywhere in @morley-client@, this feature
-- exists only for external users.
assertHeadBlockId :: MonadThrow m => BlockId -> m ()
assertHeadBlockId blockId = unless (blockId == HeadId) $
  throwString "Accessing non-head block is not supported in tests"

handleGetContractScript
  :: ( MonadState MockState m
     , MonadThrow m
     )
  => BlockId
  -> Address
  -> m OriginationScript
handleGetContractScript blockId addr = do
  assertHeadBlockId blockId
  MockState{..} <- get
  case lookup addr msContracts of
    Nothing -> throwM $ err404 path
    Just ContractState{..} -> case csContractData of
      ImplicitContractData _ -> throwM $ UnexpectedImplicitContract addr
      ContractData script _ -> pure script
  where
    path = "/chains/main/blocks/head/context/contracts/" <> formatAddress addr <> "/script"

handleGetBigMapValue :: Monad m => BlockId -> Natural -> Text -> TestT m Expression
handleGetBigMapValue blockId bigMapId scriptExpr = do
  assertHeadBlockId blockId
  st <- get

  let allBigMaps :: [ContractStateBigMap] =
        catMaybes $
          Map.elems (msContracts st) <&> \cs -> case (csContractData cs) of
            ContractData _ bigMapMaybe -> bigMapMaybe
            ImplicitContractData _ -> Nothing

  -- Check if a big_map with the given ID exists and, if so, check
  -- whether the giv en key exists.
  case find (\bigMap -> csbmId bigMap == bigMapId) allBigMaps of
    Nothing -> throwM $ err404 path
    Just bigMap ->
      case lookup scriptExpr (csbmMap bigMap ) of
        Nothing -> throwM $ err404 path
        Just serializedValue -> pure $ decodeExpression serializedValue
  where
    path = "/chains/main/blocks/head/context/big_maps/" <> show bigMapId <> "/" <> scriptExpr

-- Here we have an alias with the prefix already added,
-- so we can use 'Alias' instead 'AliasHint'.
getAlias :: AliasOrAliasHint -> Alias
getAlias = \case
  AnAlias x -> x
  AnAliasHint hint -> unsafeCoerceAliasHintToAlias hint

handleRememberContract :: Monad m => Bool -> Address -> AliasOrAliasHint -> TestT m ()
handleRememberContract replaceExisting addr (getAlias -> alias) = do
  let
    cs = dumbContractState { csAlias = alias }
    remember addr' cs' MockState{..} =
      modify $ \s -> s { msContracts = insert addr' cs' msContracts }

  st@MockState{..} <- get
  case lookup addr msContracts of
    Nothing -> remember addr cs st
    _       -> bool pass (remember addr cs st) replaceExisting

handleGenKey :: Monad m => AliasOrAliasHint -> TestT m Address
handleGenKey (getAlias -> alias) = do
  let
    addr = detGenKeyAddress (encodeUtf8 $ unsafeGetAliasText alias)
    newContractState = dumbImplicitContractState { csAlias =  alias }
  modify $ \s ->
    s & msContractsL . at addr ?~ newContractState
  pure addr

handleGetAlias :: Monad m => AddressOrAlias -> TestT m Alias
handleGetAlias = \case
  AddressAlias alias -> pure alias
  AddressResolved addr -> do
    MockState{..} <- get
    case lookup addr msContracts of
      Nothing                -> throwM $ UnknownContract $ AddressResolved addr
      Just ContractState{..} -> pure $ csAlias

handleGetManagerKey :: (Monad m) => BlockId -> Address -> TestT m (Maybe PublicKey)
handleGetManagerKey blk addr = do
  assertHeadBlockId blk
  s <- get
  let mbCs = s ^. msContractsL . at addr
  case mbCs of
    Just ContractState{..} -> case csContractData of
      ImplicitContractData mbManagerKey -> pure mbManagerKey
      ContractData _ _ -> throwString "Only implicit account can have a manager key"
    Nothing -> throwM $ UnknownContract $ AddressResolved addr

-- In scenarios where the system under test checks for 404 errors, we
-- use this function to mock and simulate those errors.
err404 :: Text -> ClientError
err404 path = FailureResponse
  (defaultRequest { requestBody = Nothing, requestPath = (baseUrl , "") })
  response
  where
    baseUrl = BaseUrl
      { baseUrlScheme = Http
      , baseUrlHost = "localhost"
      , baseUrlPort = 8732
      , baseUrlPath = toString path
      }
    response = Response
      { responseStatusCode = status404
      , responseHeaders = mempty
      , responseHttpVersion = http20
      , responseBody = "Contract with given address not found"
      }

handleResolveAddressMaybe :: Monad m => AddressOrAlias -> TestT m (Maybe Address)
handleResolveAddressMaybe = \case
  AddressResolved addr -> pure (Just addr)
  AddressAlias alias -> do
    MockState{..} <- get
    case find checkAlias $ Map.toList msContracts of
      Just (addr, _) -> pure (Just addr)
      Nothing -> pure Nothing
    where
      checkAlias (_, ContractState { csAlias = alias' }) = alias == alias'

handleRevealKey :: Monad m => Alias -> Maybe ScrubbedBytes -> TestT m ()
handleRevealKey alias _ = do
  contracts <- gets (Map.toList . msContracts)
  let contracts' = filter (\(_, ContractState{..}) -> csAlias == alias) contracts
  case contracts' of
    []  -> throwM $ UnknownContract $ AddressAlias alias
    [(addr, cs@ContractState{..})] ->
      case (addr, csContractData) of
        (ContractAddress _, ContractData _ _) ->
          throwM $ CantRevealContract addr
        (KeyAddress _, ImplicitContractData (Just _)) ->
          throwM $ AlreadyRevealed addr
        (KeyAddress _, ImplicitContractData Nothing) ->
            -- We don't care about the public key itself, but only its presence.
            let newContractState = cs { csContractData = ImplicitContractData $ Just dumbManagerKey }
            in modify $ \s -> s & msContractsL . at addr ?~ newContractState
        _ -> error "Inconsitent mock state. This most likely a bug in tests."
    _   ->
      error $ "Multiple contracts have alias '" +| alias |+
        "'. This is most likely a bug in tests."

-- | Dummy public key used in mock tests.
dumbManagerKey :: PublicKey
dumbManagerKey = fromRight (error "impossible") $ parsePublicKey
  "edpkuwTWKgQNnhR5v17H2DYHbfcxYepARyrPGbf1tbMoGQAj8Ljr3V"

----------------------------------------------------------------------------
-- TemplateHaskell test helpers
----------------------------------------------------------------------------

shouldCompileTo :: HasCallStack => [Dec] -> Q [Dec] -> Assertion
shouldCompileTo actualDecs expectedQ = do
  expectedDecs <- runQ expectedQ
  PrettyDecs (normalizeDecs actualDecs) @?= PrettyDecs (normalizeDecs expectedDecs)

-- | Same as 'shouldCompileTo', but ignores instance declarations of the given class.
shouldCompileIgnoringInstance :: HasCallStack => Name -> [Dec] -> Q [Dec] -> Assertion
shouldCompileIgnoringInstance className actualDecs expectedQ = do
  expectedDecs <- runQ expectedQ
  let actualDecs' = filter (not . isInstance) actualDecs
  PrettyDecs (normalizeDecs actualDecs') @?= PrettyDecs (normalizeDecs expectedDecs)

  where
    isInstance :: Dec -> Bool
    isInstance = \case
      InstanceD _ _ (ConT t `AppT` _) _ | t == className -> True
      _ -> False

-- | Normalize ASTs to make them comparable.
--
-- By default, quoted ASTs and ASTs with names created using 'newName' will have
-- names with unique IDs.
-- For example:
--
-- > decs <- runQ [d|data D = D { f :: Int } |]
-- > putStrLn $ pprint decs
-- >
-- > -- Will generate this AST:
-- > data D_0 = D_1 { f_2 :: Int }
--
-- To be able to check if two ASTs are equivalent, we have to scrub the unique IDs off all names.
--
-- For convenience, to make the output easier to read, we also erase kind annotations when the kind is '*'.
normalizeDecs :: [Dec] -> [Dec]
normalizeDecs decs =
    SYB.everywhere
      (SYB.mkT fixName . SYB.mkT simplifyType . SYB.mkT simplifyTyVar)
      decs
  where
    fixName :: Name -> Name
    fixName = mkName . nameBase

    simplifyType :: Type -> Type
    simplifyType = \case
      SigT t StarT -> t
      t -> t

    simplifyTyVar :: TyVarBndr -> TyVarBndr
    simplifyTyVar = \case
     KindedTV name StarT -> PlainTV name
     tv -> tv

newtype PrettyDecs = PrettyDecs [Dec]
  deriving newtype Eq

instance Show PrettyDecs where
  show (PrettyDecs decs) = pprint decs