packages feed

genvalidity-mergeful-0.3.0.1: test/Data/Mergeful/ValueSpec.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Data.Mergeful.ValueSpec
  ( spec,
  )
where

import Autodocodec
import Autodocodec.Yaml
import Data.Data
import Data.GenValidity.Mergeful.Value ()
import Data.Mergeful.Timed
import Data.Mergeful.Value
import Data.Word
import Test.QuickCheck
import Test.Syd hiding (Timed (..))
import Test.Syd.Validity
import Test.Syd.Validity.Aeson
import Test.Syd.Validity.Utils
import Text.Colour

{-# 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
  let yamlSchemaSpec :: forall a. (Typeable a, GenValid a, HasCodec a) => FilePath -> Spec
      yamlSchemaSpec filePath = do
        it ("outputs the same schema as before for " <> nameOf @a) $
          pureGoldenTextFile
            ("test_resources/value/" <> filePath <> ".txt")
            (renderChunksText With24BitColours $ schemaChunksViaCodec @a)

  describe "ClientValue" $ do
    genValidSpec @(ClientValue Word8)
    jsonSpec @(ClientValue Word8)
    yamlSchemaSpec @(ClientValue Word8) "client"
  describe "ServerValue" $ do
    genValidSpec @(ServerValue Word8)
    jsonSpec @(ServerValue Word8)
    yamlSchemaSpec @(ServerValue Word8) "server"
  describe "ValueSyncRequest" $ do
    genValidSpec @(ValueSyncRequest Word8)
    jsonSpec @(ValueSyncRequest Word8)
    yamlSchemaSpec @(ValueSyncRequest Word8) "request"
  describe "ValueSyncResponse" $ do
    genValidSpec @(ValueSyncResponse Word8)
    jsonSpec @(ValueSyncResponse Word8)
    yamlSchemaSpec @(ValueSyncResponse Word8) "response"

  describe "makeValueSyncRequest" $
    it "produces valid requests" $
      producesValid (makeValueSyncRequest @Int)
  describe "mergeValueSyncResponseRaw" $
    it "produces valid client stores" $
      producesValid2 (mergeValueSyncResponseRaw @Int)
  describe "mergeValueSyncResponseIgnoreProblems" $
    it "produces valid client stores" $
      producesValid2 (mergeValueSyncResponseIgnoreProblems @Int)
  describe "processServerValueSync" $ do
    it "produces valid responses and stores" $ producesValid2 (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" $
      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" $
      it "tells the client that there is a modified item at the server side" $
        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" $
      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