genvalidity-mergeful-0.0.0.0: test/Data/Mergeful/ValueSpec.hs
{-# LANGUAGE TypeApplications #-}
module Data.Mergeful.ValueSpec
( spec
) where
import Test.Hspec
import Test.QuickCheck
import Test.Validity
import Test.Validity.Aeson
import Data.Mergeful.Timed
import Data.Mergeful.Value
import Data.GenValidity.Mergeful.Value ()
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
forAllSubsequent :: Testable prop => ((ServerTime, ServerTime) -> prop) -> Property
forAllSubsequent func =
forAllValid $ \st ->
forAllShrink (genValid `suchThat` (> st)) (filter (> st) . shrinkValid) $ \st' -> func (st, st')
spec :: Spec
spec = do
genValidSpec @(ClientValue Int)
jsonSpecOnValid @(ClientValue Int)
genValidSpec @(ServerValue Int)
jsonSpecOnValid @(ServerValue Int)
genValidSpec @(ValueSyncRequest Int)
jsonSpecOnValid @(ValueSyncRequest Int)
genValidSpec @(ValueSyncResponse Int)
jsonSpecOnValid @(ValueSyncResponse Int)
describe "makeValueSyncRequest" $
it "produces valid requests" $ producesValidsOnValids (makeValueSyncRequest @Int)
describe "mergeValueSyncResponseRaw" $
it "produces valid client stores" $ producesValidsOnValids2 (mergeValueSyncResponseRaw @Int)
describe "mergeValueSyncResponseIgnoreProblems" $
it "produces valid client stores" $
producesValidsOnValids2 (mergeValueSyncResponseIgnoreProblems @Int)
describe "processServerValueSync" $ do
it "produces valid responses and stores" $ producesValidsOnValids2 (processServerValueSync @Int)
it "makes no changes if the sync request reflects the state of the server" $
forAllValid $ \i ->
forAllValid $ \st -> do
let store1 = ServerValue $ Timed i st
req = ValueSyncRequestKnown st
let (resp, store2) = processServerValueSync @Int store1 req
store2 `shouldBe` store1
resp `shouldBe` ValueSyncResponseInSync
describe "Client changes" $ do
it "changes the item that the client tells the server to change" $
forAllValid $ \i ->
forAllValid $ \j ->
forAllValid $ \st -> do
let store1 = ServerValue (Timed i st)
req = ValueSyncRequestKnownButChanged (Timed j st)
let (resp, store2) = processServerValueSync @Int store1 req
let time = incrementServerTime st
store2 `shouldBe` ServerValue (Timed j time)
resp `shouldBe` ValueSyncResponseClientChanged time
describe "Server changes" $ do
it "tells the client that there is a modified item at the server side" $ do
forAllValid $ \i ->
forAllSubsequent $ \(st, st') -> do
let store1 = ServerValue (Timed i st')
req = ValueSyncRequestKnown st
let (resp, store2) = processServerValueSync @Int store1 req
store2 `shouldBe` store1
resp `shouldBe` ValueSyncResponseServerChanged (Timed i st')
describe "Conflicts" $ do
it "notices a conflict if the client and server are trying to sync different items" $
forAllValid $ \i ->
forAllValid $ \j ->
forAllSubsequent $ \(st, st') -> do
let store1 = ServerValue (Timed i st')
req = ValueSyncRequestKnownButChanged (Timed j st)
let (resp, store2) = processServerValueSync @Int store1 req
store2 `shouldBe` store1
resp `shouldBe` ValueSyncResponseConflict (Timed i st')
describe "syncing" $ do
it "succesfully syncs a modification across to a second client" $
forAllValid $ \time1 ->
forAllValid $ \i ->
forAllValid $ \j -> do
let cAstore1 = ClientValue (Timed i time1) NotChanged
-- Client B had synced that same item, but has since modified it
let cBstore1 = ClientValue (Timed j time1) Changed
-- The server is has the item that both clients had before
let sstore1 = ServerValue (Timed i time1)
-- Client B makes sync request 1
let req1 = makeValueSyncRequest cBstore1
-- The server processes sync request 1
let (resp1, sstore2) = processServerValueSync @Int sstore1 req1
let time2 = incrementServerTime time1
resp1 `shouldBe` ValueSyncResponseClientChanged time2
sstore2 `shouldBe` ServerValue (Timed j time2)
-- Client B merges the response
let cBstore2 = mergeValueSyncResponseIgnoreProblems cBstore1 resp1
cBstore2 `shouldBe` ClientValue (Timed j time2) NotChanged
-- Client A makes sync request 2
let req2 = makeValueSyncRequest cAstore1
-- The server processes sync request 2
let (resp2, sstore3) = processServerValueSync sstore2 req2
resp2 `shouldBe` ValueSyncResponseServerChanged (Timed j time2)
sstore3 `shouldBe` ServerValue (Timed j time2)
-- Client A merges the response
let cAstore2 = mergeValueSyncResponseIgnoreProblems cAstore1 resp2
cAstore2 `shouldBe` ClientValue (Timed j time2) NotChanged
-- Client A and Client B now have the same store
cAstore2 `shouldBe` cBstore2
it "is idempotent with one client" $
forAllValid $ \cstore1 ->
forAllValid $ \sstore1 -> do
let req1 = makeValueSyncRequest (cstore1 :: ClientValue Int)
(resp1, sstore2) = processServerValueSync sstore1 req1
cstore2 = mergeValueSyncResponseIgnoreProblems cstore1 resp1
req2 = makeValueSyncRequest cstore2
(resp2, sstore3) = processServerValueSync sstore2 req2
cstore3 = mergeValueSyncResponseIgnoreProblems cstore2 resp2
cstore2 `shouldBe` cstore3
sstore2 `shouldBe` sstore3