genvalidity-mergeful-0.1.0.0: test/Data/Mergeful/CollectionSpec.hs
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeApplications #-}
module Data.Mergeful.CollectionSpec
( spec
) where
import Data.Functor.Identity
import Data.List
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.QuickCheck
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" $ do
it "produces valid stores" $ producesValidsOnValids2 (addItemToClientStore @Int @Int)
it "makes the client store one bigger" $
forAllValid $ \cs ->
forAllValid $ \a ->
clientStoreSize @Int @Int (addItemToClientStore a cs) `shouldBe` (clientStoreSize cs + 1)
it "ensures that the added item is in fact in the result" $
forAllValid $ \cs ->
forAllValid $ \a ->
let cs' = addItemToClientStore @Int @Int a cs
in a `elem` clientStoreAddedItems cs'
describe "markItemDeletedInClientStore" $ do
it "produces valid stores" $ producesValidsOnValids2 (markItemDeletedInClientStore @Int @Int)
it "makes the client store one smaller" $
let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0
in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->
forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->
(clientStoreSize @Int @Int (markItemDeletedInClientStore i cs) + 1) `shouldBe`
clientStoreSize cs
it "ensures that the added item is in fact not in the result" $
let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0
in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->
forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->
let cs' = markItemDeletedInClientStore @Int @Int i cs
in M.lookup (Right i) (clientStoreItems cs') `shouldBe` Nothing
describe "changeItemInClientStore" $ do
it "produces valid stores" $ producesValidsOnValids3 (changeItemInClientStore @Int @Int)
it "doesn't change the size of the client store" $
let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0
in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->
forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->
forAllValid $ \a ->
clientStoreSize @Int @Int (changeItemInClientStore i a cs) `shouldBe`
clientStoreSize cs
it "ensures that the changed item is in fact in the result" $
let p cs = S.size (clientStoreUndeletedSyncIdSet cs) > 0
in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->
forAll (elements $ S.toList $ clientStoreUndeletedSyncIdSet cs) $ \i ->
forAllValid $ \a ->
let cs' = changeItemInClientStore @Int @Int i a cs
in M.lookup (Right i) (clientStoreItems cs') `shouldBe` Just a
describe "deleteItemFromClientStore" $ do
it "produces valid stores" $ producesValidsOnValids2 (deleteItemFromClientStore @Int @Int)
it "makes the client store one smaller" $
let p cs = S.size (clientStoreClientIdSet cs) > 0
in forAllShrink (genValid `suchThat` p) (filter p . shrinkValid) $ \cs ->
forAll (elements $ S.toList $ clientStoreClientIdSet cs) $ \cid ->
(clientStoreSize @Int @Int (deleteItemFromClientStore cid cs) + 1) `shouldBe`
clientStoreSize cs
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 "processServerSync" $
it "produces valid tuples of a response and a store" $
producesValidsOnValids2
(\store request -> evalD $ processServerSync genD (store :: ServerStore UUID Int) request)
describe "Syncing with mergeSyncResponseIgnoreProblems" $ do
mergeFunctionSpec @Int mergeSyncResponseIgnoreProblems
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 $
cBstore2 `shouldBe`
(initialClientStore
{clientStoreSyncedButChangedItems = M.singleton uuid (Timed i3 time1)})
-- Client A and Client B now *do not* have the same store
describe "Syncing with mergeSyncResponseFromServer" $ do
mergeFunctionSpec @Int mergeSyncResponseFromServer
describe "mergeSyncResponseFromServer" $
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
]
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
describe "Syncing with mergeSyncResponseUsingStrategy with a GCounter" $
mergeFunctionSpec @Int $
mergeSyncResponseUsingStrategy
ItemMergeStrategy
{ itemMergeStrategyMergeChangeConflict =
\clientItem (Timed serverItem t) -> Timed (max clientItem serverItem) t
, itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem
, itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing
}
mergeFunctionSpec ::
forall a. (Show a, Ord a, GenValid a)
=> (forall i. Ord i =>
ClientStore i a -> SyncResponse i a -> ClientStore i a)
-> Spec
mergeFunctionSpec mergeFunc = do
describe "Single client" $
describe "Multi-item" $ do
it "succesfully downloads everything from the server for an empty client" $
forAllValid $ \sstore1 ->
evalDM $ do
let cstore1 = initialClientStore
let req = makeSyncRequest cstore1
(resp, sstore2) <- processServerSync genD sstore1 req
let cstore2 = mergeFunc 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
let req = makeSyncRequest cstore1
(resp, sstore2) <- processServerSync genD sstore1 req
let cstore2 = mergeFunc 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
(resp1, sstore2) <- processServerSync genD sstore1 req1
let cstore2 = mergeFunc cstore1 resp1
req2 = makeSyncRequest cstore2
(resp2, sstore3) <- processServerSync genD sstore2 req2
let cstore3 = mergeFunc 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
-- 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 = mergeFunc 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 = mergeFunc 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 (Timed i 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 = mergeFunc 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 = mergeFunc 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 (Timed i 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 = mergeFunc 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 = mergeFunc 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 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 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 = mergeFunc 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 = mergeFunc cBstore1 resp2
lift $ do
cBstore2 `shouldBe` initialClientStore
-- Client A and Client B now have the same store
cAstore2 `shouldBe` cBstore2
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
-- 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 = mergeFunc 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 = mergeFunc 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
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 = mergeFunc 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 = mergeFunc 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}
-- 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 = mergeFunc 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 = mergeFunc cBstore1 resp2
lift $ do
cBstore2 `shouldBe` initialClientStore
-- Client A and Client B now have the same store
cAstore2 `shouldBe` cBstore2
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