hstratus-notes-0.1.0.0: test/HStratus/Notes/CloudKitSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : HStratus.Notes.CloudKitSpec
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
Tests for the CloudKit JSON response decoders in
'Network.HStratus.Internal.Notes.CloudKit'.
-}
module HStratus.Notes.CloudKitSpec (spec) where
import Data.Aeson (FromJSON, eitherDecode)
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Map.Strict as Map
import Data.Text (Text)
import Network.HStratus.Internal.Notes.CloudKit
( CKField (..)
, CKLookupResponse (..)
, CKQueryResponse (..)
, CKRecord (..)
, CKRecordRef (..)
, CKZoneChangesResponse (..)
, CKZoneChangesZone (..)
)
import Test.Hspec
spec :: Spec
spec = describe "Network.HStratus.Internal.Notes.CloudKit" $ do
describe "CKQueryResponse" $ do
it "parses a folders query response" $ do
r <- decodeOrFail queryFoldersJson :: IO CKQueryResponse
qrContinuationMarker r `shouldBe` Nothing
case qrRecords r of
[] -> expectationFailure "expected non-empty records"
rec : _ -> do
crName rec `shouldBe` "Folder/FOLDER-FIXTURE"
crType rec `shouldBe` Just "SearchIndexes"
Map.lookup "HasSubfolder" (crFields rec)
`shouldBe` Just (CKInt64Field 1)
it "parses an empty query response" $ do
r <- decodeOrFail emptyQueryJson :: IO CKQueryResponse
qrRecords r `shouldBe` []
describe "CKLookupResponse" $ do
it "parses a note lookup response" $ do
r <- decodeOrFail lookupNoteJson :: IO CKLookupResponse
lrSyncToken r `shouldBe` Just "notes-lookup-sync-token-fixture"
case lrRecords r of
[] -> expectationFailure "expected non-empty records"
rec : _ -> do
crName rec `shouldBe` "Note/NOTE-FIXTURE"
Map.lookup "TextDataEncrypted" (crFields rec)
`shouldBe` Just (CKEncryptedBytesField "c3ludGhldGljIG5vdGUgYm9keQ==")
Map.lookup "Folder" (crFields rec)
`shouldBe` Just (CKReferenceField (mkRef "Folder/FOLDER-FIXTURE"))
Map.lookup "Attachments" (crFields rec)
`shouldBe` Just (CKReferenceListField [mkRef "Attachment/ATTACHMENT-FIXTURE"])
it "parses an attachment lookup response" $ do
r <- decodeOrFail lookupAttachmentJson :: IO CKLookupResponse
lrSyncToken r `shouldBe` Nothing
case lrRecords r of
[] -> expectationFailure "expected non-empty records"
rec : _ -> do
crName rec `shouldBe` "Attachment/ATTACHMENT-FIXTURE"
Map.lookup "AttachmentUTI" (crFields rec)
`shouldBe` Just (CKStringField "public.url")
Map.lookup "Size" (crFields rec)
`shouldBe` Just (CKInt64Field 128)
describe "CKZoneChangesResponse" $ do
it "parses zone changes with records" $ do
r <- decodeOrFail zoneChangesJson :: IO CKZoneChangesResponse
case zcrZones r of
[] -> expectationFailure "expected non-empty zones"
zone : _ -> do
zczSyncToken zone `shouldBe` Just "notes-zone-sync-token-fixture"
zczMoreComing zone `shouldBe` Just False
case zczRecords zone of
[_, _, deleted] -> do
crName deleted `shouldBe` "Note/NOTE-DELETED-FIXTURE"
crDeleted deleted `shouldBe` Just True
recs -> expectationFailure $ "expected 3 records, got " <> show (length recs)
it "parses zone changes with no records" $ do
r <- decodeOrFail zoneChangesEmptyJson :: IO CKZoneChangesResponse
case zcrZones r of
[] -> expectationFailure "expected non-empty zones"
zone : _ -> do
zczRecords zone `shouldBe` []
zczSyncToken zone `shouldBe` Just "notes-changes-sync-token-fixture"
-- Helpers
decodeOrFail :: (FromJSON a) => LBS.ByteString -> IO a
decodeOrFail bs = either fail pure (eitherDecode bs)
mkRef :: Text -> CKRecordRef
mkRef name = CKRecordRef{rrRecordName = name, rrAction = "VALIDATE"}
-- Fixtures
queryFoldersJson :: LBS.ByteString
queryFoldersJson =
"{\"records\":[{\"recordName\":\"Folder/FOLDER-FIXTURE\"\
\,\"recordType\":\"SearchIndexes\"\
\,\"recordChangeTag\":\"folder-change-tag-fixture\"\
\,\"zoneID\":{\"zoneName\":\"Notes\",\"zoneType\":\"REGULAR_CUSTOM_ZONE\"}\
\,\"fields\":{\"TitleEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic Folder\",\"isEncrypted\":true}\
\,\"HasSubfolder\":{\"type\":\"INT64\",\"value\":1}}}]\
\,\"continuationMarker\":null}"
emptyQueryJson :: LBS.ByteString
emptyQueryJson = "{\"records\":[]}"
lookupNoteJson :: LBS.ByteString
lookupNoteJson =
"{\"records\":[{\"recordName\":\"Note/NOTE-FIXTURE\"\
\,\"recordType\":\"Note\"\
\,\"recordChangeTag\":\"note-change-tag-fixture\"\
\,\"created\":{\"timestamp\":1735689600000,\"userRecordName\":\"_synthetic_user\"}\
\,\"modified\":{\"timestamp\":1735776000000,\"userRecordName\":\"_synthetic_user\"}\
\,\"deleted\":false\
\,\"zoneID\":{\"zoneName\":\"Notes\",\"zoneType\":\"REGULAR_CUSTOM_ZONE\"}\
\,\"fields\":{\
\\"TitleEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic note\",\"isEncrypted\":true}\
\,\"SnippetEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic snippet\",\"isEncrypted\":true}\
\,\"TextDataEncrypted\":{\"type\":\"ENCRYPTED_BYTES\",\"value\":\"c3ludGhldGljIG5vdGUgYm9keQ==\"}\
\,\"ModificationDate\":{\"type\":\"TIMESTAMP\",\"value\":1735776000000}\
\,\"Deleted\":{\"type\":\"INT64\",\"value\":0}\
\,\"Folder\":{\"type\":\"REFERENCE\",\"value\":{\"recordName\":\"Folder/FOLDER-FIXTURE\",\"action\":\"VALIDATE\"}}\
\,\"Attachments\":{\"type\":\"REFERENCE_LIST\",\"value\":[{\"recordName\":\"Attachment/ATTACHMENT-FIXTURE\",\"action\":\"VALIDATE\"}]}\
\}}]\
\,\"syncToken\":\"notes-lookup-sync-token-fixture\"}"
lookupAttachmentJson :: LBS.ByteString
lookupAttachmentJson =
"{\"records\":[{\"recordName\":\"Attachment/ATTACHMENT-FIXTURE\"\
\,\"recordType\":\"Attachment\"\
\,\"recordChangeTag\":\"attachment-change-tag-fixture\"\
\,\"zoneID\":{\"zoneName\":\"Notes\",\"zoneType\":\"REGULAR_CUSTOM_ZONE\"}\
\,\"fields\":{\
\\"AttachmentIdentifier\":{\"type\":\"STRING\",\"value\":\"ATTACHMENT-ALIAS-FIXTURE\"}\
\,\"AttachmentUTI\":{\"type\":\"STRING\",\"value\":\"public.url\"}\
\,\"Filename\":{\"type\":\"STRING\",\"value\":\"synthetic-link.webloc\"}\
\,\"Size\":{\"type\":\"INT64\",\"value\":128}\
\,\"PrimaryAsset\":{\"type\":\"ASSETID\",\"value\":{\
\\"downloadURL\":\"https://example.test/notes/asset\"\
\,\"fileChecksum\":\"notes-asset-checksum-fixture\"\
\,\"referenceChecksum\":\"notes-asset-reference-fixture\"\
\,\"wrappingKey\":\"notes-asset-wrapping-key-fixture\"\
\,\"size\":128}}}}]}"
zoneChangesJson :: LBS.ByteString
zoneChangesJson =
"{\"zones\":[{\"zoneID\":{\"zoneName\":\"Notes\",\"zoneType\":\"REGULAR_CUSTOM_ZONE\"}\
\,\"syncToken\":\"notes-zone-sync-token-fixture\"\
\,\"moreComing\":false\
\,\"records\":[\
\{\"recordName\":\"Note/NOTE-FIXTURE\"\
\,\"recordType\":\"Note\"\
\,\"recordChangeTag\":\"note-change-tag-fixture\"\
\,\"fields\":{\
\\"TitleEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic note\",\"isEncrypted\":true}\
\,\"SnippetEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic snippet\",\"isEncrypted\":true}\
\,\"ModificationDate\":{\"type\":\"TIMESTAMP\",\"value\":1735776000000}\
\,\"Deleted\":{\"type\":\"INT64\",\"value\":0}\
\,\"Folder\":{\"type\":\"REFERENCE\",\"value\":{\"recordName\":\"Folder/FOLDER-FIXTURE\",\"action\":\"VALIDATE\"}}}}\
\,{\"recordName\":\"Folder/FOLDER-FIXTURE\"\
\,\"recordType\":\"SearchIndexes\"\
\,\"recordChangeTag\":\"folder-change-tag-fixture\"\
\,\"fields\":{\
\\"TitleEncrypted\":{\"type\":\"STRING\",\"value\":\"Synthetic Folder\",\"isEncrypted\":true}\
\,\"HasSubfolder\":{\"type\":\"INT64\",\"value\":1}}}\
\,{\"recordName\":\"Note/NOTE-DELETED-FIXTURE\",\"deleted\":true}\
\]}]}"
zoneChangesEmptyJson :: LBS.ByteString
zoneChangesEmptyJson =
"{\"zones\":[{\"zoneID\":{\"zoneName\":\"Notes\",\"zoneType\":\"REGULAR_CUSTOM_ZONE\"}\
\,\"records\":[]\
\,\"syncToken\":\"notes-changes-sync-token-fixture\"\
\,\"moreComing\":false}]}"