packages feed

method-0.4.0.0: test/Test/Method/ProtocolSpec.hs

{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module Test.Method.ProtocolSpec where

import RIO (ByteString, Text, void)
import Test.Hspec
  ( Spec,
    anyErrorCall,
    before,
    context,
    describe,
    it,
    shouldReturn,
    shouldThrow,
  )
import Test.Method.Dynamic (ToDyn (toDyn), Typeable, dynArg)
import Test.Method.Label (deriveLabel)
import Test.Method.Protocol
  ( ProtocolM,
    decl,
    dependsOn,
    mockInterface,
    protocol,
    thenReturn,
    verify,
    whenArgs,
    withProtocol,
  )

type UserName = Text

type PlainPassword = ByteString

type HashedPassword = ByteString

type UserId = Int

data Env = Env

data User = User {userName :: UserName, userId :: UserId}
  deriving (Eq, Ord, Show)

data Service = Service
  { findUser :: UserName -> IO (Maybe User),
    findPassword :: UserName -> IO (Maybe HashedPassword),
    createUser :: UserName -> IO User,
    hashPassword :: PlainPassword -> IO HashedPassword,
    upsertAuth :: UserId -> HashedPassword -> IO ()
  }

deriveLabel ''Service

doService :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doService Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceHashFirst :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceHashFirst Service {..} usernm passwd = do
  hpasswd <- hashPassword passwd
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceWrongArgument :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongArgument Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      _ <- hashPassword passwd
      user <- createUser usernm
      upsertAuth (userId user) passwd
      pure $ Just user

doServiceWrongExtraCalls :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongExtraCalls Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      hpasswd' <- hashPassword hpasswd
      user <- createUser usernm
      upsertAuth (userId user) hpasswd'
      pure $ Just user

doServiceWrongMissingCalls :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongMissingCalls Service {..} usernm passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      _ <- hashPassword passwd
      user <- createUser usernm
      pure $ Just user

doServiceWrongOrder :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongOrder Service {..} usernm passwd = do
  user <- createUser usernm
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      hpasswd <- hashPassword passwd
      upsertAuth (userId user) hpasswd
      pure $ Just user

doServiceWrongUnspecifiedMethod :: Service -> UserName -> PlainPassword -> IO (Maybe User)
doServiceWrongUnspecifiedMethod Service {..} usernm _passwd = do
  mUser <- findUser usernm
  case mUser of
    Just _ -> pure Nothing
    Nothing -> do
      Just hpasswd <- findPassword usernm
      user <- createUser usernm
      upsertAuth (userId user) hpasswd
      pure $ Just user

serviceProtocol :: UserName -> PlainPassword -> HashedPassword -> UserId -> ProtocolM ServiceLabel ()
serviceProtocol usernm passwd hpasswd userid = do
  i1 <- decl $ whenArgs FindUser (== usernm) `thenReturn` Nothing
  i2 <- decl $ whenArgs CreateUser (== usernm) `thenReturn` User usernm userid `dependsOn` [i1]
  i3 <- decl $ whenArgs HashPassword (== passwd) `thenReturn` hpasswd
  void $ decl $ whenArgs UpsertAuth ((== userid), (== hpasswd)) `thenReturn` () `dependsOn` [i2, i3]

data DBService = DBService
  { query :: forall a. (Typeable a, Show a) => String -> IO [a],
    execute :: forall a. (Typeable a, Show a) => String -> a -> IO ()
  }

deriveLabel ''DBService

dbProtocol :: ProtocolM DBServiceLabel ()
dbProtocol = do
  i1 <-
    decl $
      whenArgs Query (== "SELECT user_id FROM user")
        `thenReturn` toDyn [1, 2, 3 :: Int]
  _ <-
    decl $
      whenArgs Execute ((== "INSERT INTO friend(user_id, other_id) VALUES (?,?)"), dynArg (== (1 :: Int, 2 :: Int)))
        `thenReturn` () `dependsOn` [i1]
  pure ()

doDBService :: DBService -> IO ()
doDBService DBService {..} = do
  (user1 : user2 : _) <- query "SELECT user_id FROM user"
  execute "INSERT INTO friend(user_id, other_id) VALUES (?,?)" (user1, user2 :: Int)

spec :: Spec
spec = describe "protocol" $ do
  let usernm = "user1"
      passwd = "password1"
      hpasswd = "hashed_password1"
      userid = 0
  let setup = do
        penv <- protocol $ serviceProtocol usernm passwd hpasswd userid
        pure (penv, mockInterface penv)
  before setup $ do
    context "accept valid impl" $ do
      it "accept valid impl findUser first" $ \(penv, service) -> do
        doService service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv
      it "accept valid impl hash first" $ \(penv, service) -> do
        doServiceHashFirst service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv
    context "reject invalid impl" $ do
      it "invalid argument" $ \(_, service) ->
        doServiceWrongArgument service usernm passwd `shouldThrow` anyErrorCall
      it "extra calls" $ \(_, service) ->
        doServiceWrongExtraCalls service usernm passwd `shouldThrow` anyErrorCall
      it "missing calls" $ \(penv, service) -> do
        doServiceWrongMissingCalls service usernm passwd `shouldReturn` Just (User usernm userid)
        verify penv `shouldThrow` anyErrorCall
      it "wrong order" $ \(_, service) -> do
        doServiceWrongOrder service usernm passwd `shouldThrow` anyErrorCall
      it "unspecified method call" $ \(_, service) -> do
        doServiceWrongUnspecifiedMethod service usernm passwd `shouldThrow` anyErrorCall
  context "dbservice" $ do
    it "mock polymorphic interface" $ do
      withProtocol dbProtocol $ \svc ->
        doDBService svc `shouldReturn` ()