hstratus-notes-0.1.0.0: test/HStratus/Notes/EndpointsSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : HStratus.Notes.EndpointsSpec
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
Tests for the Notes CloudKit endpoint and request-body builders in
'Network.HStratus.Internal.Notes.Endpoints'.
-}
module HStratus.Notes.EndpointsSpec (spec) where
import Control.Monad (forM_)
import Data.Aeson (Value (..), object)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map.Strict as Map
import Network.HStratus.Internal.Notes.Endpoints
( changesBody
, changesReq
, foldersBody
, lookupBody
, lookupReq
, mkNotesEndpoints
, queryReq
, recentsBody
)
import Network.HStratus.Session (AccountData (..), Credentials (..), Session (..), Webservice (..))
import Network.HTTP.Client (Request (..))
import Network.HTTP.Types (methodPost)
import Test.Hspec
spec :: Spec
spec = describe "Network.HStratus.Internal.Notes.Endpoints" $ do
ep <- runIO (mkNotesEndpoints testAccountData testSession)
let reqs =
[ ("queryReq", queryReq ep, "/records/query")
, ("lookupReq", lookupReq ep, "/records/lookup")
, ("changesReq", changesReq ep, "/changes/zone")
]
forM_ reqs $ \(name, req, suffix) ->
describe name $ do
it ("targets " <> suffix) $
path req `shouldSatisfy` BS.isSuffixOf (BS8.pack suffix)
it "uses POST" $
method req `shouldBe` methodPost
it "includes required query params" $ do
queryString req `shouldSatisfy` BS.isInfixOf "remapEnums=true"
queryString req `shouldSatisfy` BS.isInfixOf "getCurrentSyncToken=true"
queryString req `shouldSatisfy` BS.isInfixOf "clientId=auth-test-client-id"
describe "foldersBody" $ do
it "queries SearchIndexes with parentless filter" $
foldersBody 10 Nothing
`shouldSatisfy` lbsContains "\"recordType\":\"SearchIndexes\""
it "includes Notes zoneID with zoneType" $
foldersBody 10 Nothing
`shouldSatisfy` lbsContains "\"zoneType\":\"REGULAR_CUSTOM_ZONE\""
it "includes the requested resultsLimit" $
foldersBody 50 Nothing
`shouldSatisfy` lbsContains "\"resultsLimit\":50"
it "clamps resultsLimit to 200" $
foldersBody 999 Nothing
`shouldSatisfy` lbsContains "\"resultsLimit\":200"
it "includes continuationMarker when provided" $
foldersBody 10 (Just (String "test-marker"))
`shouldSatisfy` lbsContains "\"continuationMarker\""
describe "recentsBody" $ do
it "queries SearchIndexes records" $
recentsBody 10 Nothing
`shouldSatisfy` lbsContains "\"recordType\":\"SearchIndexes\""
it "includes Notes zoneID with zoneType" $
recentsBody 10 Nothing
`shouldSatisfy` lbsContains "\"zoneType\":\"REGULAR_CUSTOM_ZONE\""
it "includes the requested resultsLimit" $
recentsBody 42 Nothing
`shouldSatisfy` lbsContains "\"resultsLimit\":42"
it "clamps resultsLimit to 200" $
recentsBody 999 Nothing
`shouldSatisfy` lbsContains "\"resultsLimit\":200"
it "sorts by modTime descending" $
recentsBody 10 Nothing
`shouldSatisfy` lbsContains "\"fieldName\":\"modTime\""
describe "lookupBody" $ do
it "includes each record name" $
lookupBody ["Note/ABC", "Note/DEF"]
`shouldSatisfy` lbsContains "\"recordName\":\"Note/ABC\""
it "includes Notes zoneID with zoneType" $
lookupBody ["Note/ABC"]
`shouldSatisfy` lbsContains "\"zoneType\":\"REGULAR_CUSTOM_ZONE\""
describe "changesBody" $ do
it "includes REGULAR_CUSTOM_ZONE zoneType" $
changesBody Nothing
`shouldSatisfy` lbsContains "\"zoneType\":\"REGULAR_CUSTOM_ZONE\""
it "requests Note record type" $
changesBody Nothing
`shouldSatisfy` lbsContains "\"Note\""
it "includes syncToken when provided" $
changesBody (Just "test-sync-token")
`shouldSatisfy` lbsContains "\"syncToken\":\"test-sync-token\""
-- Helpers
lbsContains :: BS.ByteString -> LBS.ByteString -> Bool
lbsContains needle = BS.isInfixOf needle . LBS.toStrict
-- Fixtures
testAccountData :: AccountData
testAccountData =
AccountData
{ adHsaVersion = 2
, adHsaChallengeRequired = False
, adHsaTrustedBrowser = Just True
, adWebservices =
Map.fromList
[("ckdatabasews", Webservice "https://p31-ckdatabasews.icloud.com" Nothing)]
, adRaw = object []
}
testSession :: Session
testSession =
Session
{ sessionCreds = Credentials{credAccountName = "test@example.com", credPassword = "test-pass"}
, sessionTopDir = "/tmp/test"
, sessionClientId = "auth-test-client-id"
}