packages feed

genvalidity-appendful-0.0.0.0: test/Data/Appendful/CollectionSpec.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Data.Appendful.CollectionSpec
  ( spec,
  )
where

import Control.Monad.State
import Data.Appendful.Collection
import Data.GenValidity.Appendful.Collection ()
import Data.GenValidity.UUID ()
import Data.List
import qualified Data.Map.Strict as M
import Data.UUID
import GHC.Generics (Generic)
import System.Random
import Test.Hspec
import Test.Validity
import Test.Validity.Aeson

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

spec :: Spec
spec = do
  genValidSpec @(ClientStore ClientId Int Int)
  jsonSpec @(ClientStore ClientId Int Int)
  genValidSpec @(SyncRequest ClientId Int Int)
  jsonSpec @(SyncRequest ClientId Int Int)
  genValidSpec @(SyncResponse ClientId Int Int)
  jsonSpec @(SyncResponse ClientId Int Int)
  genValidSpec @(ServerStore Int Int)
  jsonSpec @(ServerStore Int Int)
  describe "emptyStore" $ it "is valid" $ shouldBeValid (emptyClientStore @Int @Int @Int)
  describe "storeSize" $ do
    it "does not crash" $ producesValid (storeSize @Int @Int @Int)
    specify "adding an item makes the store bigger" $
      forAllValid $
        \store ->
          forAllValid $ \added -> do
            let size1 = storeSize (store :: ClientStore Int Int Int)
            let store' = addItemToClientStore added store
            let size2 = storeSize store'
            size2 `shouldBe` (size1 + 1)
  describe "addItemToClientStore" $
    it "produces valid stores" $
      producesValid2 (addItemToClientStore @Int @Int @Int)
  describe "emptySyncRequest" $
    it "is valid" $
      shouldBeValid (emptySyncRequest @Int @Int @Int)
  describe "makeSyncRequest" $
    it "produces valid sync requests" $
      producesValid (makeSyncRequest @Int @Int @Int)
  describe "mergeSyncResponse" $ do
    it "produces valid sync stores" $ producesValid2 (mergeSyncResponse @Int @Int @Int)
    it "adds the single item that the server tells it to add to an empty client store" $
      forAllValid $
        \cid ->
          forAllValid $ \a ->
            forAllValid $ \u -> do
              let cstore1 = emptyClientStore {clientStoreAdded = M.singleton (cid :: ClientId) (a :: Int)}
                  resp = emptySyncResponse {syncResponseClientAdded = M.singleton cid (u :: Int)}
                  cstore2 = mergeSyncResponse cstore1 resp
              clientStoreSynced cstore2 `shouldBe` M.singleton u a
  describe "processServerSync" $
    describe "deterministic UUIDs" $
      serverSyncSpec @Int evalDM $
        processServerSync genD

serverSyncSpec ::
  forall a si m.
  (Show si, Ord si, GenValid si, Show a, Ord a, GenValid a, MonadIO m) =>
  (forall r. m r -> IO r) ->
  (ServerStore si a -> SyncRequest ClientId si a -> m (SyncResponse ClientId si a, ServerStore si a)) ->
  Spec
serverSyncSpec eval func = do
  describe "Single client" $ do
    describe "Single-item" $ do
      it "succesfully downloads a single item from the server for an empty client" $
        forAllValid $
          \u ->
            forAllValid $ \i ->
              eval $ do
                let sstore1 = emptyServerStore {serverStoreItems = M.singleton u i}
                let cstore1 = emptyClientStore
                let req = makeSyncRequest cstore1
                (resp, sstore2) <- func sstore1 req
                let cstore2 = mergeSyncResponse cstore1 resp
                liftIO $ do
                  sstore2 `shouldBe` sstore1
                  clientStoreSynced cstore2 `shouldBe` serverStoreItems sstore2
      it "succesfully uploads a single item to the server for an empty server" $
        forAllValid $
          \c ->
            forAllValid $ \i ->
              eval $ do
                let cstore1 = emptyClientStore {clientStoreAdded = M.singleton c i}
                let sstore1 = emptyServerStore
                let req = makeSyncRequest cstore1
                (resp, sstore2) <- func sstore1 req
                let cstore2 = mergeSyncResponse cstore1 resp
                liftIO $ do
                  clientStoreSynced cstore2 `shouldBe` serverStoreItems sstore2
                  sort (M.elems (clientStoreSynced cstore2))
                    `shouldBe` sort (M.elems $ M.singleton c i)
    describe "Multi-item" $ do
      it "succesfully downloads everything from the server for an empty client" $
        forAllValid $
          \sstore1 ->
            eval $ do
              let cstore1 = emptyClientStore
              let req = makeSyncRequest cstore1
              (resp, sstore2) <- func sstore1 req
              let cstore2 = mergeSyncResponse cstore1 resp
              liftIO $ do
                sstore2 `shouldBe` sstore1
                clientStoreSynced cstore2 `shouldBe` serverStoreItems sstore2
      it "succesfully uploads everything to the server for an empty server" $
        forAllValid $
          \items ->
            eval $ do
              let cstore1 = emptyClientStore {clientStoreAdded = items}
              let sstore1 = emptyServerStore
              let req = makeSyncRequest cstore1
              (resp, sstore2) <- func sstore1 req
              let cstore2 = mergeSyncResponse cstore1 resp
              liftIO $ do
                clientStoreSynced cstore2 `shouldBe` serverStoreItems sstore2
                sort (M.elems (clientStoreSynced cstore2)) `shouldBe` sort (M.elems items)
      it "is idempotent with one client" $
        forAllValid $
          \cstore1 ->
            forAllValid $ \sstore1 ->
              eval $ do
                let req1 = makeSyncRequest cstore1
                (resp1, sstore2) <- func sstore1 req1
                let cstore2 = mergeSyncResponse cstore1 resp1
                    req2 = makeSyncRequest cstore2
                (resp2, sstore3) <- func sstore2 req2
                let cstore3 = mergeSyncResponse cstore2 resp2
                liftIO $ do
                  cstore2 `shouldBe` cstore3
                  sstore2 `shouldBe` sstore3
  describe "Multiple clients" $ do
    describe "Single-item" $ do
      it "successfully syncs an addition accross to a second client" $
        forAllValid $
          \i ->
            eval $ do
              let cAstore1 = emptyClientStore {clientStoreAdded = M.singleton (ClientId 0) i}
              -- Client B is empty
              let cBstore1 = emptyClientStore
              -- The server is empty
              let sstore1 = emptyServerStore
              -- Client A makes sync request 1
              let req1 = makeSyncRequest cAstore1
              -- The server processes sync request 1
              (resp1, sstore2) <- func sstore1 req1
              let addedItems = syncResponseClientAdded resp1
              case M.toList addedItems of
                [(ClientId 0, clientAdditionId)] -> do
                  let items = M.singleton clientAdditionId i
                  liftIO $ sstore2 `shouldBe` (ServerStore {serverStoreItems = items})
                  -- Client A merges the response
                  let cAstore2 = mergeSyncResponse cAstore1 resp1
                  liftIO $ cAstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
                  -- Client B makes sync request 2
                  let req2 = makeSyncRequest cBstore1
                  -- The server processes sync request 2
                  (resp2, sstore3) <- func sstore2 req2
                  liftIO $ do
                    resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
                    sstore3 `shouldBe` sstore2
                  --  pPrint cBstore2
                  -- Client B merges the response
                  let cBstore2 = mergeSyncResponse cBstore1 resp2
                  liftIO $ cBstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
                  -- Client A and Client B now have the same store
                  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"
        $ forAllValid $
          \sis -> do
            let cs = ServerStore sis
            (sr, cs') <-
              eval $
                func cs $
                  SyncRequest
                    { syncRequestAdded = M.empty,
                      syncRequestMaximumSynced = fst <$> M.lookupMax sis
                    }
            cs' `shouldBe` cs
            sr
              `shouldBe` SyncResponse
                { syncResponseClientAdded = M.empty,
                  syncResponseServerAdded = M.empty
                }
      it "successfully syncs additions accross to a second client" $
        forAllValid $
          \is ->
            eval $ do
              let cAstore1 = emptyClientStore {clientStoreAdded = is}
              -- Client B is empty
              let cBstore1 = emptyClientStore
              -- The server is empty
              let sstore1 = emptyServerStore
              -- Client A makes sync request 1
              let req1 = makeSyncRequest cAstore1
              -- The server processes sync request 1
              (resp1, sstore2) <- func sstore1 req1
              -- Client A merges the response
              let cAstore2 = mergeSyncResponse cAstore1 resp1
              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
              let req2 = makeSyncRequest cBstore1
              -- The server processes sync request 2
              (resp2, sstore3) <- func sstore2 req2
              liftIO $ do
                resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
                sstore3 `shouldBe` sstore2
              -- Client B merges the response
              let cBstore2 = mergeSyncResponse cBstore1 resp2
              liftIO $ cBstore2 `shouldBe` (emptyClientStore {clientStoreSynced = items})
              -- Client A and Client B now have the same store
              liftIO $ cAstore2 `shouldBe` cBstore2
  describe "General properties" $ do
    it "produces valid results" $
      forAllValid $
        \cs ->
          forAllValid $ \sr -> do
            res <- eval $ func cs sr
            shouldBeValid res
    it "successfully syncs two clients using a central store" $
      forAllValid $
        \addedItems ->
          eval $ do
            let central = ServerStore M.empty
            let store1 = emptyClientStore {clientStoreAdded = addedItems}
            let store2 = emptyClientStore
            let sreq1 = makeSyncRequest store1
            (sresp1, central') <- func central sreq1
            let store1' = mergeSyncResponse store1 sresp1
            let sreq2 = makeSyncRequest store2
            (sresp2, central'') <- func central' sreq2
            let store2' = mergeSyncResponse store2 sresp2
            let sreq3 = makeSyncRequest store1'
            (sresp3, _) <- func central'' sreq3
            let store1'' = mergeSyncResponse store1' sresp3
            liftIO $ store1'' `shouldBe` store2'
    it "ensures that syncing is idempotent" $
      forAllValid $
        \central1 ->
          forAllValid $ \local1 ->
            eval $ do
              let sreq1 = makeSyncRequest local1
              (sresp1, central2) <- func central1 sreq1
              let local2 = mergeSyncResponse local1 sresp1
              let sreq2 = makeSyncRequest local2
              (sresp2, central3) <- func central2 sreq2
              let local3 = mergeSyncResponse local2 sresp2
              liftIO $ do
                local2 `shouldBe` local3
                central2 `shouldBe` central3

newtype D m a = D
  { unD :: StateT StdGen m a
  }
  deriving (Generic, Functor, Applicative, Monad, MonadState StdGen, MonadTrans, MonadIO)

evalDM :: Functor m => D m a -> m a
evalDM d = fst <$> runDM d (mkStdGen 42)

runDM :: D m a -> StdGen -> m (a, StdGen)
runDM = runStateT . unD

genD :: Monad m => D m UUID
genD = do
  r <- get
  let (u, r') = random r
  put r'
  pure u