packages feed

appendful-persistent-0.0.0.0: test/Data/Appendful/Persistent/TwoClientsSpec.hs

{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}

module Data.Appendful.Persistent.TwoClientsSpec
  ( spec,
  )
where

import Control.Monad
import Control.Monad.Reader
import Data.Appendful
import Data.GenValidity.Appendful ()
import qualified Data.Map as M
import Database.Persist.Sql
import Test.Hspec
import Test.Hspec.QuickCheck
import Test.Validity
import TestUtils

{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}

spec :: Spec
spec = modifyMaxShrinks (const 0) $
  twoClientsSpec $ do
    describe "sanity" $ do
      describe "setupClient & clientGetStore" $ do
        it "roundtrips" $ \te -> forAllValid $ \cstore -> runTest te $ do
          setupClient A cstore
          cstore' <- clientGetStore A
          liftIO $ cstore' `shouldBe` cstore
      describe "setupServer & serverGetStore" $ do
        it "roundtrips" $ \te -> forAllValid $ \sstore -> runTest te $ do
          setupServer sstore
          sstore' <- serverGetStore
          liftIO $ sstore' `shouldBe` sstore
    describe "Single item" $ do
      it "successfully syncs an addition accross to a second client" $
        \te ->
          forAllValid $ \st -> runTest te $ do
            setupUnsyncedClient A [st]
            setupUnsyncedClient B []
            setupServer emptyServerStore
            req1 <- clientMakeSyncRequest A
            resp1 <- serverProcessSync req1
            sstore2 <- serverGetStore
            case M.toList (syncResponseClientAdded resp1) of
              [(_, clientAdditionId)] -> do
                let items = M.singleton clientAdditionId st
                liftIO $ sstore2 `shouldBe` (ServerStore {serverStoreItems = items})
                clientMergeSyncResponse A resp1
                cAstore2 <- clientGetStore A
                liftIO $ cAstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
                req2 <- clientMakeSyncRequest B
                resp2 <- serverProcessSync req2
                sstore3 <- serverGetStore
                liftIO $ do
                  resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
                  sstore3 `shouldBe` sstore2
                clientMergeSyncResponse B resp2
                cBstore2 <- clientGetStore B
                liftIO $ cBstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
                liftIO $ cAstore2 `shouldBe` cBstore2
              _ -> liftIO $ expectationFailure "Should have found exactly one added item."
    describe "Multiple items" $ do
      it
        "makes no change if the sync request reflects the same local state with an empty sync response"
        $ \te ->
          forAllValid $ \sis -> runTest te $ do
            let cs = ServerStore sis
            setupServer cs
            sr <-
              serverProcessSync
                SyncRequest
                  { syncRequestAdded = M.empty,
                    syncRequestMaximumSynced = fst <$> M.lookupMax sis
                  }
            cs' <- serverGetStore
            liftIO $
              do
                cs' `shouldBe` cs
                sr
                  `shouldBe` SyncResponse
                    { syncResponseClientAdded = M.empty,
                      syncResponseServerAdded = M.empty
                    }
      it "successfully syncs additions accross to a second client" $
        \te -> forAllValid $ \is ->
          runTest te $ do
            setupClient A $ emptyClientStore {clientStoreAdded = is}
            -- Client B is empty
            setupClient B emptyClientStore
            -- The server is empty
            setupServer emptyServerStore
            -- Client A makes sync request 1
            req1 <- clientMakeSyncRequest A
            -- The server processes sync request 1
            resp1 <- serverProcessSync req1
            sstore2 <- serverGetStore
            -- Client A merges the response
            clientMergeSyncResponse A resp1
            cAstore2 <- clientGetStore A
            let items = clientStoreSynced cAstore2
            liftIO $ do
              clientStoreAdded cAstore2 `shouldBe` M.empty
              sstore2 `shouldBe` (ServerStore {serverStoreItems = items})
            liftIO $ cAstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
            -- Client B makes sync request 2
            req2 <- clientMakeSyncRequest B
            -- The server processes sync request 2
            resp2 <- serverProcessSync req2
            sstore3 <- serverGetStore
            liftIO $ do
              resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
              sstore3 `shouldBe` sstore2
            -- Client B merges the response
            clientMergeSyncResponse B resp2
            cBstore2 <- clientGetStore B
            liftIO $ cBstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
            -- Client A and Client B now have the same store
            liftIO $ cAstore2 `shouldBe` cBstore2
    describe "General properties" $
      it "successfully syncs two clients using a central store" $
        \te ->
          forAllValid $ \addedItems ->
            runTest te $
              do
                setupServer $ ServerStore M.empty
                let store1 = emptyClientStore {clientStoreAdded = addedItems}
                setupClient A store1
                setupClient B emptyClientStore
                void $ sync A
                (_, _, _, store2') <- sync B
                (_, _, _, store1'') <- sync A
                liftIO $ store1'' `shouldBe` store2'

type T a = ReaderT TestEnv IO a

runTest :: TestEnv -> T a -> IO a
runTest = flip runReaderT

runClientDB :: Client -> SqlPersistT IO a -> T a
runClientDB num func = do
  pool <- asks $ case num of
    A -> testEnvClient1Pool
    B -> testEnvClient2Pool
  liftIO $ runSqlPool func pool

runServerDB :: SqlPersistT IO a -> T a
runServerDB func = do
  pool <- asks testEnvServerPool
  liftIO $ runSqlPool func pool

type CS = ClientStore ClientThingId ServerThingId Thing

type SReq = SyncRequest ClientThingId ServerThingId Thing

type SS = ServerStore ServerThingId Thing

type SResp = SyncResponse ClientThingId ServerThingId Thing

sync :: Client -> T (CS, SS, SS, CS)
sync n = do
  cstore1 <- clientGetStore n
  req <- clientMakeSyncRequest n
  sstore1 <- serverGetStore
  resp <- serverProcessSync req
  sstore2 <- serverGetStore
  clientMergeSyncResponse n resp
  cstore2 <- clientGetStore n
  pure (cstore1, sstore1, sstore2, cstore2)

setupUnsyncedClient :: Client -> [Thing] -> T ()
setupUnsyncedClient n =
  runClientDB n . setupUnsyncedClientThingQuery

setupClient :: Client -> CS -> T ()
setupClient n = runClientDB n . setupClientThingQuery

setupServer :: SS -> T ()
setupServer = runServerDB . setupServerThingQuery

clientGetStore :: Client -> T CS
clientGetStore n = runClientDB n clientGetStoreThingQuery

clientMakeSyncRequest :: Client -> T SReq
clientMakeSyncRequest n = runClientDB n clientMakeSyncRequestThingQuery

serverGetStore :: T SS
serverGetStore = runServerDB serverGetStoreThingQuery

serverProcessSync :: SReq -> T SResp
serverProcessSync = runServerDB . serverProcessSyncThingQuery

clientMergeSyncResponse :: Client -> SResp -> T ()
clientMergeSyncResponse n = runClientDB n . clientMergeSyncResponseThingQuery

data Client = A | B
  deriving (Show, Eq)

data TestEnv = TestEnv
  { testEnvServerPool :: ConnectionPool,
    testEnvClient1Pool :: ConnectionPool,
    testEnvClient2Pool :: ConnectionPool
  }

twoClientsSpec :: SpecWith TestEnv -> Spec
twoClientsSpec = around withTestEnv

withTestEnv :: (TestEnv -> IO a) -> IO a
withTestEnv func =
  withServerPool $ \serverPool ->
    withClientPool $ \client1Pool ->
      withClientPool $ \client2Pool -> do
        let tenv =
              TestEnv
                { testEnvServerPool = serverPool,
                  testEnvClient1Pool = client1Pool,
                  testEnvClient2Pool = client2Pool
                }
        liftIO $ func tenv