packages feed

genvalidity-mergeful-0.0.0.0: test/Data/MergefulSpec.hs

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}

module Data.MergefulSpec
  ( spec
  ) where

import Data.Functor.Identity
import Data.List
import Data.Map (Map)
import qualified Data.Map as M
import qualified Data.Set as S
import Data.UUID
import GHC.Generics (Generic)
import System.Random
import Text.Show.Pretty

import Control.Monad.State

import Test.Hspec
import Test.Validity
import Test.Validity.Aeson

import Data.GenValidity.UUID ()

import Data.Mergeful
import Data.Mergeful.Timed

import Data.GenValidity.Mergeful ()
import Data.GenValidity.Mergeful.Item ()

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

spec :: Spec
spec = do
  genValidSpec @ClientId
  jsonSpecOnValid @ClientId
  genValidSpec @(ClientStore Int Int)
  jsonSpecOnValid @(ClientStore Int Int)
  genValidSpec @(ServerStore Int Int)
  jsonSpecOnValid @(ServerStore Int Int)
  genValidSpec @(SyncRequest Int Int)
  jsonSpecOnValid @(SyncRequest Int Int)
  genValidSpec @(SyncResponse Int Int)
  jsonSpecOnValid @(SyncResponse Int Int)
  describe "initialClientStore" $ it "is valid" $ shouldBeValid $ initialClientStore @Int @Int
  describe "addItemToClientStore" $
    it "produces valid stores" $ producesValidsOnValids2 (addItemToClientStore @Int @Int)
  describe "markItemDeletedInClientStore" $
    it "produces valid stores" $ producesValidsOnValids2 (markItemDeletedInClientStore @Int @Int)
  describe "changeItemInClientStore" $
    it "produces valid stores" $ producesValidsOnValids3 (changeItemInClientStore @Int @Int)
  describe "deleteItemFromClientStore" $
    it "produces valid stores" $ producesValidsOnValids2 (deleteItemFromClientStore @Int @Int)
  describe "initialServerStore" $ it "is valid" $ shouldBeValid $ initialServerStore @Int @Int
  describe "initialSyncRequest" $ it "is valid" $ shouldBeValid $ initialSyncRequest @Int @Int
  describe "emptySyncResponse" $ it "is valid" $ shouldBeValid $ emptySyncResponse @Int @Int
  describe "makeSyncRequest" $
    it "produces valid requests" $ producesValidsOnValids (makeSyncRequest @Int @Int)
  describe "mergeAddedItems" $
    it "produces valid results" $ producesValidsOnValids2 (mergeAddedItems @Int @Int)
  describe "mergeSyncedButChangedItems" $
    it "produces valid results" $ producesValidsOnValids2 (mergeSyncedButChangedItems @Int @Int)
  describe "mergeAddedItems" $
    it "produces valid results" $ producesValidsOnValids2 (mergeAddedItems @Int @Int)
  describe "mergeSyncedButChangedItems" $
    it "produces valid results" $ producesValidsOnValids2 (mergeSyncedButChangedItems @Int @Int)
  describe "mergeDeletedItems" $
    it "produces valid results" $ producesValidsOnValids2 (mergeDeletedItems @Int @Int)
  describe "mergeSyncResponseIgnoreProblems" $
    it "produces valid requests" $
    forAllValid $ \store ->
      forAllValid $ \response ->
        let res = mergeSyncResponseIgnoreProblems @Int @Int store response
         in case prettyValidate res of
              Right _ -> pure ()
              Left err ->
                expectationFailure $
                unlines
                  [ "Store:"
                  , ppShow store
                  , "Response:"
                  , ppShow response
                  , "Invalid result:"
                  , ppShow res
                  , "error:"
                  , err
                  ]
  describe "mergeSyncResponseFromServer" $ do
    it "only differs from mergeSyncResponseIgnoreProblems on conflicts" $
      forAllValid $ \cstore ->
        forAllValid $ \sresp@SyncResponse {..} -> do
          let cstoreA = mergeSyncResponseFromServer (cstore :: ClientStore (UUID) Int) sresp
              cstoreB = mergeSyncResponseIgnoreProblems cstore sresp
          if cstoreA == cstoreB
            then pure ()
            else unless
                   (or
                      [ not (M.null syncResponseConflicts)
                      , not (M.null syncResponseConflictsClientDeleted)
                      , not (S.null syncResponseConflictsServerDeleted)
                      ]) $
                 expectationFailure $
                 unlines
                   [ "There was a difference between mergeSyncResponseFromServer and mergeSyncResponseIgnoreProblems that was somehow unrelated to the conflicts:"
                   , "syncResponseConflicts:"
                   , ppShow syncResponseConflicts
                   , "syncResponseConflictsClientDeleted:"
                   , ppShow syncResponseConflictsClientDeleted
                   , "syncResponseConflictsServerDeleted:"
                   , ppShow syncResponseConflictsServerDeleted
                   , "client store after mergeSyncResponseFromServer:"
                   , ppShow cstoreA
                   , "client store after mergeSyncResponseIgnoreProblems:"
                   , ppShow cstoreB
                   ]
  describe "processServerSync with mergeSyncResponseIgnoreProblems" $ do
    it "produces valid tuples of a response and a store" $
      producesValidsOnValids2
        (\store request -> evalD $ processServerSync genD (store :: ServerStore (UUID) Int) request)
    describe "Single client" $ do
      describe "Multi-item" $ do
        it "succesfully downloads everything from the server for an empty client" $
          forAllValid $ \sstore1 ->
            evalDM $ do
              let cstore1 = initialClientStore :: ClientStore (UUID) Int
              let req = makeSyncRequest cstore1
              (resp, sstore2) <- processServerSync genD sstore1 req
              let cstore2 = mergeSyncResponseIgnoreProblems cstore1 resp
              lift $ do
                sstore2 `shouldBe` sstore1
                clientStoreSyncedItems cstore2 `shouldBe` serverStoreItems sstore2
        it "succesfully uploads everything to the server for an empty server" $
          forAllValid $ \items ->
            evalDM $ do
              let cstore1 = initialClientStore {clientStoreAddedItems = items}
              let sstore1 = initialServerStore :: ServerStore (UUID) Int
              let req = makeSyncRequest cstore1
              (resp, sstore2) <- processServerSync genD sstore1 req
              let cstore2 = mergeSyncResponseIgnoreProblems cstore1 resp
              lift $ do
                sort (M.elems (M.map timedValue (clientStoreSyncedItems cstore2))) `shouldBe`
                  sort (M.elems items)
                clientStoreSyncedItems cstore2 `shouldBe` serverStoreItems sstore2
        it "is idempotent with one client" $
          forAllValid $ \cstore1 ->
            forAllValid $ \sstore1 ->
              evalDM $ do
                let req1 = makeSyncRequest (cstore1 :: ClientStore (UUID) Int)
                (resp1, sstore2) <- processServerSync genD sstore1 req1
                let cstore2 = mergeSyncResponseIgnoreProblems cstore1 resp1
                    req2 = makeSyncRequest cstore2
                (resp2, sstore3) <- processServerSync genD sstore2 req2
                let cstore3 = mergeSyncResponseIgnoreProblems cstore2 resp2
                lift $ 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 ->
            evalDM $ do
              let cAstore1 = initialClientStore {clientStoreAddedItems = M.singleton (ClientId 0) i}
              -- Client B is empty
              let cBstore1 = initialClientStore :: ClientStore (UUID) Int
              -- The server is empty
              let sstore1 = initialServerStore
              -- Client A makes sync request 1
              let req1 = makeSyncRequest cAstore1
              -- The server processes sync request 1
              (resp1, sstore2) <- processServerSync genD sstore1 req1
              let addedItems = syncResponseClientAdded resp1
              case M.toList addedItems of
                [(ClientId 0, (uuid, st))] -> do
                  let time = initialServerTime
                  lift $ st `shouldBe` time
                  let items = M.singleton uuid (Timed i st)
                  lift $ sstore2 `shouldBe` (ServerStore {serverStoreItems = items})
                  -- Client A merges the response
                  let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1
                  lift $ cAstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})
                  -- Client B makes sync request 2
                  let req2 = makeSyncRequest cBstore1
                  -- The server processes sync request 2
                  (resp2, sstore3) <- processServerSync genD sstore2 req2
                  lift $ do
                    resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
                    sstore3 `shouldBe` sstore2
                  -- Client B merges the response
                  let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2
                  lift $ cBstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})
                  -- Client A and Client B now have the same store
                  lift $ cAstore2 `shouldBe` cBstore2
                _ -> lift $ expectationFailure "Should have found exactly one added item."
        it "successfully syncs a modification accross to a second client" $
          forAllValid $ \uuid ->
            forAllValid $ \i ->
              forAllValid $ \j ->
                forAllValid $ \time1 ->
                  evalDM $ do
                    let cAstore1 =
                          initialClientStore
                            { clientStoreSyncedItems =
                                M.singleton (uuid :: UUID) (Timed (i :: Int) time1)
                            }
                    -- Client B had synced that same item, but has since modified it
                    let cBstore1 =
                          initialClientStore
                            {clientStoreSyncedButChangedItems = M.singleton uuid (Timed j time1)}
                    -- The server is has the item that both clients had before
                    let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i time1)}
                    -- Client B makes sync request 1
                    let req1 = makeSyncRequest cBstore1
                    -- The server processes sync request 1
                    (resp1, sstore2) <- processServerSync genD sstore1 req1
                    let time2 = incrementServerTime time1
                    lift $ do
                      resp1 `shouldBe`
                        emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2}
                      sstore2 `shouldBe`
                        ServerStore {serverStoreItems = M.singleton uuid (Timed j time2)}
                    -- Client B merges the response
                    let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp1
                    lift $
                      cBstore2 `shouldBe`
                      initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}
                    -- Client A makes sync request 2
                    let req2 = makeSyncRequest cAstore1
                    -- The server processes sync request 2
                    (resp2, sstore3) <- processServerSync genD sstore2 req2
                    lift $ do
                      resp2 `shouldBe`
                        emptySyncResponse
                          {syncResponseServerChanged = M.singleton uuid (Timed j time2)}
                      sstore3 `shouldBe` sstore2
                    -- Client A merges the response
                    let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp2
                    lift $
                      cAstore2 `shouldBe`
                      initialClientStore {clientStoreSyncedItems = M.singleton uuid (Timed j time2)}
                    -- Client A and Client B now have the same store
                    lift $ cAstore2 `shouldBe` cBstore2
        it "succesfully syncs a deletion across to a second client" $
          forAllValid $ \uuid ->
            forAllValid $ \time1 ->
              forAllValid $ \i ->
                evalDM $ do
                  let cAstore1 =
                        initialClientStore
                          { clientStoreSyncedItems =
                              M.singleton (uuid :: UUID) (Timed (i :: Int) time1)
                          }
                  -- Client A has a synced item.
                  -- Client B had synced that same item, but has since deleted it.
                  let cBstore1 =
                        initialClientStore {clientStoreDeletedItems = M.singleton uuid time1}
                  -- The server still has the undeleted item
                  let sstore1 = ServerStore {serverStoreItems = M.singleton uuid (Timed i time1)}
                  -- Client B makes sync request 1
                  let req1 = makeSyncRequest cBstore1
                  -- The server processes sync request 1
                  (resp1, sstore2) <- processServerSync genD sstore1 req1
                  lift $ do
                    resp1 `shouldBe`
                      emptySyncResponse {syncResponseClientDeleted = S.singleton uuid}
                    sstore2 `shouldBe` initialServerStore
                  -- Client B merges the response
                  let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp1
                  lift $ cBstore2 `shouldBe` initialClientStore
                  -- Client A makes sync request 2
                  let req2 = makeSyncRequest cAstore1
                  -- The server processes sync request 2
                  (resp2, sstore3) <- processServerSync genD sstore2 req2
                  lift $ do
                    resp2 `shouldBe`
                      emptySyncResponse {syncResponseServerDeleted = S.singleton uuid}
                    sstore3 `shouldBe` sstore2
                  -- Client A merges the response
                  let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp2
                  lift $ cAstore2 `shouldBe` initialClientStore
                  -- Client A and Client B now have the same store
                  lift $ cAstore2 `shouldBe` cBstore2
        it "does not run into a conflict if two clients both try to sync a deletion" $
          forAllValid $ \uuid ->
            forAllValid $ \time1 ->
              forAllValid $ \i ->
                evalDM $ do
                  let cAstore1 =
                        initialClientStore
                          {clientStoreDeletedItems = M.singleton (uuid :: UUID) time1}
                  -- Both client a and client b delete an item.
                  let cBstore1 =
                        initialClientStore {clientStoreDeletedItems = M.singleton uuid time1}
                  -- The server still has the undeleted item
                  let sstore1 =
                        ServerStore {serverStoreItems = M.singleton uuid (Timed (i :: Int) time1)}
                  -- Client A makes sync request 1
                  let req1 = makeSyncRequest cAstore1
                  -- The server processes sync request 1
                  (resp1, sstore2) <- processServerSync genD sstore1 req1
                  lift $ do
                    resp1 `shouldBe`
                      (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})
                    sstore2 `shouldBe` (ServerStore {serverStoreItems = M.empty})
                  -- Client A merges the response
                  let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1
                  lift $ cAstore2 `shouldBe` initialClientStore
                  -- Client B makes sync request 2
                  let req2 = makeSyncRequest cBstore1
                  -- The server processes sync request 2
                  (resp2, sstore3) <- processServerSync genD sstore2 req2
                  lift $ do
                    resp2 `shouldBe`
                      (emptySyncResponse {syncResponseClientDeleted = S.singleton uuid})
                    sstore3 `shouldBe` sstore2
                  -- Client B merges the response
                  let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2
                  lift $ do
                    cBstore2 `shouldBe` initialClientStore
                    -- Client A and Client B now have the same store
                    cAstore2 `shouldBe` cBstore2
        it "does not lose data after a conflict occurs" $
          forAllValid $ \uuid ->
            forAllValid $ \time1 ->
              forAllValid $ \i1 ->
                forAllValid $ \i2 ->
                  forAllValid $ \i3 ->
                    evalDM $ do
                      let sstore1 =
                            ServerStore
                              { serverStoreItems =
                                  M.singleton (uuid :: UUID) (Timed (i1 :: Int) time1)
                              }
                      -- The server has an item
                      -- The first client has synced it, and modified it.
                      let cAstore1 =
                            initialClientStore
                              {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i2 time1)}
                      -- The second client has synced it too, and modified it too.
                      let cBstore1 =
                            initialClientStore
                              {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)}
                      -- Client A makes sync request 1
                      let req1 = makeSyncRequest cAstore1
                      -- The server processes sync request 1
                      (resp1, sstore2) <- processServerSync genD sstore1 req1
                      let time2 = incrementServerTime time1
                      -- The server updates the item accordingly
                      lift $ do
                        resp1 `shouldBe`
                          (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})
                        sstore2 `shouldBe`
                          (ServerStore {serverStoreItems = M.singleton uuid (Timed i2 time2)})
                      -- Client A merges the response
                      let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1
                      lift $
                        cAstore2 `shouldBe`
                        (initialClientStore
                           {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})
                      -- Client B makes sync request 2
                      let req2 = makeSyncRequest cBstore1
                      -- The server processes sync request 2
                      (resp2, sstore3) <- processServerSync genD sstore2 req2
                      -- The server reports a conflict and does not change its store
                      lift $ do
                        resp2 `shouldBe`
                          (emptySyncResponse
                             {syncResponseConflicts = M.singleton uuid (Timed i2 time2)})
                        sstore3 `shouldBe` sstore2
                      -- Client B merges the response
                      let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2
                      -- Client does not update, but keeps its conflict
                      lift $ do
                        cBstore2 `shouldBe`
                          (initialClientStore
                             {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)})
                      -- Client A and Client B now *do not* have the same store
      describe "Multiple items" $ do
        it "successfully syncs additions accross to a second client" $
          forAllValid $ \is ->
            evalDM $ do
              let cAstore1 = initialClientStore {clientStoreAddedItems = is}
              -- Client B is empty
              let cBstore1 = initialClientStore :: ClientStore (UUID) Int
              -- The server is empty
              let sstore1 = initialServerStore
              -- Client A makes sync request 1
              let req1 = makeSyncRequest cAstore1
              -- The server processes sync request 1
              (resp1, sstore2) <- processServerSync genD sstore1 req1
              let (rest, items) = mergeAddedItems is (syncResponseClientAdded resp1)
              lift $ do
                rest `shouldBe` M.empty
                sstore2 `shouldBe` (ServerStore {serverStoreItems = items})
              -- Client A merges the response
              let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1
              lift $ cAstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})
              -- Client B makes sync request 2
              let req2 = makeSyncRequest cBstore1
              -- The server processes sync request 2
              (resp2, sstore3) <- processServerSync genD sstore2 req2
              lift $ do
                resp2 `shouldBe` (emptySyncResponse {syncResponseServerAdded = items})
                sstore3 `shouldBe` sstore2
              -- Client B merges the response
              let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2
              lift $ cBstore2 `shouldBe` (initialClientStore {clientStoreSyncedItems = items})
              -- Client A and Client B now have the same store
              lift $ cAstore2 `shouldBe` cBstore2
        it "succesfully syncs deletions across to a second client" $
          forAllValid $ \items ->
            forAllValid $ \time1 ->
              evalDM $ do
                let syncedItems = M.map (\i -> Timed i time1) (items :: Map (UUID) Int)
                    itemTimes = M.map (const time1) items
                    itemIds = M.keysSet items
                let cAstore1 = initialClientStore {clientStoreSyncedItems = syncedItems}
                -- Client A has synced items
                -- Client B had synced the same items, but has since deleted them.
                let cBstore1 = initialClientStore {clientStoreDeletedItems = itemTimes}
                -- The server still has the undeleted item
                let sstore1 = ServerStore {serverStoreItems = syncedItems}
                -- Client B makes sync request 1
                let req1 = makeSyncRequest cBstore1
                -- The server processes sync request 1
                (resp1, sstore2) <- processServerSync genD sstore1 req1
                lift $ do
                  resp1 `shouldBe` emptySyncResponse {syncResponseClientDeleted = itemIds}
                  sstore2 `shouldBe` initialServerStore
                -- Client B merges the response
                let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp1
                lift $ cBstore2 `shouldBe` initialClientStore
                -- Client A makes sync request 2
                let req2 = makeSyncRequest cAstore1
                -- The server processes sync request 2
                (resp2, sstore3) <- processServerSync genD sstore2 req2
                lift $ do
                  resp2 `shouldBe` emptySyncResponse {syncResponseServerDeleted = itemIds}
                  sstore3 `shouldBe` sstore2
                -- Client A merges the response
                let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp2
                lift $ cAstore2 `shouldBe` initialClientStore
                -- Client A and Client B now have the same store
                lift $ cAstore2 `shouldBe` cBstore2
        it "does not run into a conflict if two clients both try to sync a deletion" $
          forAllValid $ \items ->
            forAllValid $ \time1 ->
              evalDM $ do
                let cAstore1 =
                      initialClientStore
                        {clientStoreDeletedItems = M.map (const time1) (items :: Map (UUID) Int)}
                -- Both client a and client b delete their items.
                let cBstore1 =
                      initialClientStore {clientStoreDeletedItems = M.map (const time1) items}
                -- The server still has the undeleted items
                let sstore1 = ServerStore {serverStoreItems = M.map (\i -> Timed i time1) items}
                -- Client A makes sync request 1
                let req1 = makeSyncRequest cAstore1
                -- The server processes sync request 1
                (resp1, sstore2) <- processServerSync genD sstore1 req1
                lift $ do
                  resp1 `shouldBe` (emptySyncResponse {syncResponseClientDeleted = M.keysSet items})
                  sstore2 `shouldBe` (ServerStore {serverStoreItems = M.empty}) -- TODO will probably need some sort of tombstoning.
                -- Client A merges the response
                let cAstore2 = mergeSyncResponseIgnoreProblems cAstore1 resp1
                lift $ cAstore2 `shouldBe` initialClientStore
                -- Client B makes sync request 2
                let req2 = makeSyncRequest cBstore1
                -- The server processes sync request 2
                (resp2, sstore3) <- processServerSync genD sstore2 req2
                lift $ do
                  resp2 `shouldBe` (emptySyncResponse {syncResponseClientDeleted = M.keysSet items})
                  sstore3 `shouldBe` sstore2
                -- Client B merges the response
                let cBstore2 = mergeSyncResponseIgnoreProblems cBstore1 resp2
                lift $ do
                  cBstore2 `shouldBe` initialClientStore
                  -- Client A and Client B now have the same store
                  cAstore2 `shouldBe` cBstore2
  describe "processServerSync with mergeSyncResponseFromServer" $ do
    describe "Multiple clients" $ do
      describe "Single-item" $ do
        it "does not diverge after a conflict occurs" $
          forAllValid $ \uuid ->
            forAllValid $ \time1 ->
              forAllValid $ \i1 ->
                forAllValid $ \i2 ->
                  forAllValid $ \i3 ->
                    evalDM $ do
                      let sstore1 =
                            ServerStore
                              { serverStoreItems =
                                  M.singleton (uuid :: UUID) (Timed (i1 :: Int) time1)
                              }
                      -- The server has an item
                      -- The first client has synced it, and modified it.
                      let cAstore1 =
                            initialClientStore
                              {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i2 time1)}
                      -- The second client has synced it too, and modified it too.
                      let cBstore1 =
                            initialClientStore
                              {clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)}
                      -- Client A makes sync request 1
                      let req1 = makeSyncRequest cAstore1
                      -- The server processes sync request 1
                      (resp1, sstore2) <- processServerSync genD sstore1 req1
                      let time2 = incrementServerTime time1
                      -- The server updates the item accordingly
                      lift $ do
                        resp1 `shouldBe`
                          (emptySyncResponse {syncResponseClientChanged = M.singleton uuid time2})
                        sstore2 `shouldBe`
                          (ServerStore {serverStoreItems = M.singleton uuid (Timed i2 time2)})
                      -- Client A merges the response
                      let cAstore2 = mergeSyncResponseFromServer cAstore1 resp1
                      lift $
                        cAstore2 `shouldBe`
                        (initialClientStore
                           {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})
                      -- Client B makes sync request 2
                      let req2 = makeSyncRequest cBstore1
                      -- The server processes sync request 2
                      (resp2, sstore3) <- processServerSync genD sstore2 req2
                      -- The server reports a conflict and does not change its store
                      lift $ do
                        resp2 `shouldBe`
                          (emptySyncResponse
                             {syncResponseConflicts = M.singleton uuid (Timed i2 time2)})
                        sstore3 `shouldBe` sstore2
                      -- Client B merges the response
                      let cBstore2 = mergeSyncResponseFromServer cBstore1 resp2
                      -- Client does not update, but keeps its conflict
                      lift $ do
                        cBstore2 `shouldBe`
                          (initialClientStore
                             {clientStoreSyncedItems = M.singleton uuid (Timed i2 time2)})
                        -- Client A and Client B now have the same store
                        cBstore2 `shouldBe` cAstore2

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

evalD :: D Identity a -> a
evalD = runIdentity . evalDM

-- runD :: D Identity a -> StdGen -> (a, StdGen)
-- runD = runState . unD
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