notion-client-0.8.0.0: tasty/ObjectFieldTests.hs
-- | Object field gap tests: pages, blocks, property values, mentions, users,
-- file uploads and webhooks. Fixtures are transcribed from the official Notion
-- JS SDK types.
module ObjectFieldTests (tests) where
import Control.Exception (Exception, throwIO, try)
import Data.Aeson qualified as Aeson
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (parseEither)
import Data.ByteString.Char8 qualified as B8
import Data.ByteString.Lazy qualified as LBS
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Map qualified as Map
import Data.Text (Text)
import Data.Text.Encoding qualified as TE
import Data.Vector qualified as Vector
import Network.HTTP.Client qualified as HTTP
import Notion.V1 (Methods (..), makeMethods)
import Notion.V1.BlockContent
( BlockContent (..),
BlockUpdateContent (..),
MediaSourceUpdate (..),
MediaUpdate (..),
TableUpdate (..),
ToDoUpdate (..),
blockUpdateFromContent,
mkBlockUpdate,
parseBlockContent,
trashBlockUpdate,
)
import Notion.V1.Clearable (Clearable (..))
import Notion.V1.Common (CustomEmojiRef (..), Icon (..), NoticonColor (..), ObjectType (..), UUID (..))
import Notion.V1.FileUploads qualified as FU
import Notion.V1.Pages
( CreatePage (..),
InsertContentRequest (..),
InsertPosition (..),
MovePage (..),
MovePageParent (..),
PageMarkdown (..),
PropertyItemList (..),
PropertyItemResponse (..),
UpdatePage (..),
UpdatePageMarkdown (..),
UpdatePageTemplate (..),
mkUpdatePage,
)
import Notion.V1.PropertyValue
( Place (..),
PropertyValue (..),
RollupResult (..),
SelectOptionValue (..),
VerificationResult (..),
VerificationState (..),
unverifiedValue,
)
import Notion.V1.RichText (LinkMentionValue (..), MentionContent (..), RichText (..), RichTextContent (..))
import Notion.V1.Users (GroupObject (..), PeopleEntry (..), UserObject (..), UserType (..), UserValue (..))
import Notion.V1.Webhooks
( EntityType (..),
EventType (..),
PropertyAction (..),
UpdatedPropertySchema (..),
ViewField (..),
WebhookBlockRef (..),
WebhookEntity (..),
WebhookEvent (..),
WebhookEventData (..),
WebhookParent (..),
WebhookParentType (..),
WebhookRefType (..),
)
import Servant.Client qualified as Client
import Test.Tasty
import Test.Tasty.HUnit
tests :: TestTree
tests =
testGroup
"Object Field Gaps"
[ testGroup "Page and block requests" pageBlockRequestTests,
testGroup "Property values and mentions" propertyValueMentionTests,
testGroup "Users, file uploads, and object types" userFileUploadObjectTypeTests,
testGroup "Webhooks" webhookTests
]
------------------------------------------------------------------------------
-- Helpers
-- | Decode a JSON literal or fail the test with aeson's message. Fixtures are
-- 'Text' so that non-ASCII content is encoded as UTF-8.
decodeOrFail :: (Aeson.FromJSON a) => Text -> IO a
decodeOrFail t =
either (assertFailure . ("decode failed: " <>)) pure (Aeson.eitherDecode (LBS.fromStrict (TE.encodeUtf8 t)))
-- | Decode a JSON literal as a generic value.
value :: Text -> IO Aeson.Value
value = decodeOrFail
-- | Assert that a value encodes to exactly the given JSON.
encodesTo :: (Aeson.ToJSON a) => a -> Text -> Assertion
encodesTo x expected = do
e <- value expected
Aeson.toJSON x @?= e
data RequestCaptured = RequestCaptured
deriving stock (Show)
instance Exception RequestCaptured
-- | Run a 'Methods' call and capture the HTTP request it builds, aborting
-- before any network I/O happens.
captureRequest :: (Methods -> IO a) -> IO HTTP.Request
captureRequest call = do
ref <- newIORef Nothing
manager <- HTTP.newManager HTTP.defaultManagerSettings
let env0 = Client.mkClientEnv manager (Client.BaseUrl Client.Https "api.notion.com" 443 "/v1")
env =
env0
{ Client.makeClientRequest = \burl req -> do
built <- Client.defaultMakeClientRequest burl req
writeIORef ref (Just built)
throwIO RequestCaptured
}
_ <- try @RequestCaptured (call (makeMethods env "secret_test_token"))
readIORef ref >>= maybe (assertFailure "no request was built") pure
------------------------------------------------------------------------------
-- Milestone 1: page and block requests
emptyCreatePage :: CreatePage
emptyCreatePage =
CreatePage
{ parent = Nothing,
properties = Map.empty,
children = Nothing,
markdown = Nothing,
icon = Nothing,
cover = Nothing,
template = Nothing,
position = Nothing
}
trashOnlyUpdatePage :: UpdatePage
trashOnlyUpdatePage =
let UpdatePage {..} = mkUpdatePage Map.empty
in UpdatePage {inTrash = Just True, ..}
pageBlockRequestTests :: [TestTree]
pageBlockRequestTests =
[ testCase "CreatePage without parent or properties omits both keys" $ do
let CreatePage {..} = emptyCreatePage
CreatePage {markdown = Just "# こんにちは", ..} `encodesTo` "{\"markdown\":\"# こんにちは\"}",
testCase "UpdatePage with only in_trash encodes exactly that key" $
trashOnlyUpdatePage `encodesTo` "{\"in_trash\":true}",
testCase "UpdatePage clears icon and cover with null" $ do
let UpdatePage {..} = mkUpdatePage Map.empty
UpdatePage {icon = Clear, cover = Clear, ..} `encodesTo` "{\"icon\":null,\"cover\":null}",
testCase "UpdatePageTemplate by ID has no none variant" $
UpdateTemplateById (UUID "tpl-1") Nothing `encodesTo` "{\"type\":\"template_id\",\"template_id\":\"tpl-1\"}",
testCase "MovePage to a data source has no position" $
MovePage {parent = MoveToDataSource (UUID "ds-1")}
`encodesTo` "{\"parent\":{\"type\":\"data_source_id\",\"data_source_id\":\"ds-1\"}}",
testCase "insert_content at start encodes position" $
InsertContent (InsertContentRequest "- item" Nothing (Just InsertAtStart))
`encodesTo` "{\"type\":\"insert_content\",\"insert_content\":{\"content\":\"- item\",\"position\":{\"type\":\"start\"}}}",
testCase "createPageFiltered sends filter_properties as query parameters" $ do
req <- captureRequest $ \m -> createPageFiltered m ["title", "Xy12"] emptyCreatePage
assertBool
("query string: " <> B8.unpack (HTTP.queryString req))
("filter_properties=title&filter_properties=Xy12" `B8.isInfixOf` HTTP.queryString req)
HTTP.method req @?= "POST",
testCase "updatePageFiltered sends filter_properties as query parameters" $ do
req <- captureRequest $ \m -> updatePageFiltered m (UUID "p-1") ["title"] trashOnlyUpdatePage
assertBool
("query string: " <> B8.unpack (HTTP.queryString req))
("filter_properties=title" `B8.isInfixOf` HTTP.queryString req)
HTTP.method req @?= "PATCH",
testCase "to_do update with only checked" $
mkBlockUpdate (UpdateToDo (ToDoUpdate Nothing (Just True) Nothing))
`encodesTo` "{\"to_do\":{\"checked\":true}}",
testCase "table update carries only header flags" $
mkBlockUpdate (UpdateTable (TableUpdate (Just True) Nothing))
`encodesTo` "{\"table\":{\"has_column_header\":true}}",
testCase "blockUpdateFromContent drops table_width and children" $ do
let table = TableBlock 3 True False (Vector.singleton (TableRowBlock Vector.empty))
case blockUpdateFromContent table of
Just c -> mkBlockUpdate c `encodesTo` "{\"table\":{\"has_column_header\":true,\"has_row_header\":false}}"
Nothing -> assertFailure "tables are updatable",
testCase "trashBlockUpdate encodes in_trash only" $
trashBlockUpdate `encodesTo` "{\"in_trash\":true}",
testCase "image update via file upload" $
mkBlockUpdate (UpdateImage (MediaUpdate Nothing (Just (UpdateFileUploadSource (UUID "fu-1")))))
`encodesTo` "{\"image\":{\"file_upload\":{\"id\":\"fu-1\"}}}",
testCase "blockUpdateFromContent rejects child pages" $
blockUpdateFromContent (ChildPageBlock "x") @?= Nothing,
testCase "audio block decodes caption" $ do
empty <- value "{\"type\":\"external\",\"external\":{\"url\":\"https://example.com/a.mp3\"},\"caption\":[]}"
case parseEither (parseBlockContent "audio") empty of
Right AudioBlock {caption} -> Vector.length caption @?= 0
other -> assertFailure ("expected AudioBlock, got " <> show other)
one <-
value
"{\"type\":\"external\",\"external\":{\"url\":\"https://example.com/a.mp3\"},\"caption\":[{\"type\":\"text\",\"text\":{\"content\":\"録音\",\"link\":null},\"annotations\":{\"bold\":false,\"italic\":false,\"strikethrough\":false,\"underline\":false,\"code\":false,\"color\":\"default\"},\"plain_text\":\"録音\",\"href\":null}]}"
case parseEither (parseBlockContent "audio") one of
Right AudioBlock {caption} -> Vector.length caption @?= 1
other -> assertFailure ("expected AudioBlock, got " <> show other),
testCase "embed block decodes caption" $ do
v <- value "{\"url\":\"https://example.com\",\"caption\":[]}"
case parseEither (parseBlockContent "embed") v of
Right EmbedBlock {url} -> url @?= "https://example.com"
other -> assertFailure ("expected EmbedBlock, got " <> show other),
testCase "unsupported block keeps block_type" $ do
v <- value "{\"block_type\":\"form\"}"
parseEither (parseBlockContent "unsupported") v @?= Right (UnsupportedBlock (Just "form"))
]
------------------------------------------------------------------------------
-- Milestone 2: property values and mentions
-- | A rich-text item wrapping the given mention object.
mentionRichText :: Text -> Text
mentionRichText mention =
"{\"type\":\"mention\",\"mention\":"
<> mention
<> ",\"annotations\":{\"bold\":false,\"italic\":false,\"strikethrough\":false,\"underline\":false,\"code\":false,\"color\":\"default\"},\"plain_text\":\"@\",\"href\":null}"
-- | Decode a rich-text mention fixture and return its mention.
decodeMention :: Text -> IO MentionContent
decodeMention mention = do
rt <- decodeOrFail (mentionRichText mention)
case rt of
RichText {content = MentionContentWrapper m} -> pure m
other -> assertFailure ("expected a mention, got " <> show other)
propertyValueMentionTests :: [TestTree]
propertyValueMentionTests =
[ testCase "select option with description encodes" $
SelectValue "" (Just (SelectOptionValue Nothing "急ぎ" Nothing (Just "今日中")))
`encodesTo` "{\"select\":{\"name\":\"急ぎ\",\"description\":\"今日中\"}}",
testCase "people with partial user, full user and group decodes" $ do
pv <-
decodeOrFail
"{\"id\":\"p1\",\"type\":\"people\",\"people\":[{\"object\":\"user\",\"id\":\"u1\"},{\"object\":\"user\",\"id\":\"u2\",\"type\":\"person\",\"name\":\"Tanaka Hanako\",\"avatar_url\":null,\"person\":{\"email\":\"hanako@example.com\"}},{\"object\":\"group\",\"id\":\"g1\",\"name\":\"Design Team\"}]}"
case pv of
PeopleValue "p1" entries -> case Vector.toList entries of
[PersonEntry (PartialUser (UUID "u1")), PersonEntry (FullUser UserObject {name}), GroupEntry GroupObject {name = groupName}] -> do
name @?= Just "Tanaka Hanako"
groupName @?= Just "Design Team"
other -> assertFailure ("unexpected entries: " <> show other)
other -> assertFailure ("expected PeopleValue, got " <> show other),
testCase "group people entry encodes" $
GroupEntry (GroupObject (UUID "g1") (Just "Design Team"))
`encodesTo` "{\"object\":\"group\",\"id\":\"g1\",\"name\":\"Design Team\"}",
testCase "place decodes" $ do
pv <-
decodeOrFail
"{\"id\":\"p2\",\"type\":\"place\",\"place\":{\"lat\":35.6812,\"lon\":139.7671,\"name\":\"東京駅\",\"address\":null,\"google_place_id\":\"abc\"}}"
case pv of
PlaceValue _ (Just Place {lat, name, googlePlaceId}) -> do
lat @?= 35.6812
name @?= Just "東京駅"
googlePlaceId @?= Just "abc"
other -> assertFailure ("expected PlaceValue, got " <> show other),
testCase "verification decodes state, date and verifier" $ do
pv <-
decodeOrFail
"{\"id\":\"p3\",\"type\":\"verification\",\"verification\":{\"state\":\"expired\",\"date\":{\"start\":\"2026-01-01\",\"end\":null,\"time_zone\":null},\"verified_by\":{\"object\":\"user\",\"id\":\"u3\"}}}"
case pv of
VerificationValue _ (Just VerificationResult {state, verifiedBy}) -> do
state @?= Expired
verifiedBy @?= Just (PartialUser (UUID "u3"))
other -> assertFailure ("expected VerificationValue, got " <> show other)
unknown <- decodeOrFail "{\"id\":\"p4\",\"type\":\"verification\",\"verification\":{\"state\":\"pending_review\",\"date\":null,\"verified_by\":null}}"
case unknown of
VerificationValue _ (Just VerificationResult {state}) -> state @?= UnknownVerificationState "pending_review"
other -> assertFailure ("expected VerificationValue, got " <> show other),
testCase "unverifiedValue encodes the request shape" $
unverifiedValue `encodesTo` "{\"verification\":{\"state\":\"unverified\"}}",
testCase "rollup array decodes typed property values" $ do
pv <-
decodeOrFail
"{\"id\":\"p5\",\"type\":\"rollup\",\"rollup\":{\"type\":\"array\",\"function\":\"show_original\",\"array\":[{\"type\":\"number\",\"number\":3},{\"type\":\"title\",\"title\":[]}]}}"
case pv of
RollupValue _ (RollupArrayResult values _) -> case Vector.toList values of
[NumberValue "" (Just 3), TitleValue "" _] -> pure ()
other -> assertFailure ("unexpected rollup values: " <> show other)
other -> assertFailure ("expected RollupValue, got " <> show other),
testCase "unknown property type decodes to UnknownPropertyValue" $ do
pv <- decodeOrFail "{\"id\":\"p6\",\"type\":\"hologram\",\"hologram\":{\"x\":1}}"
case pv of
UnknownPropertyValue "p6" "hologram" _ -> pv `encodesTo` "{\"hologram\":{\"x\":1}}"
other -> assertFailure ("expected UnknownPropertyValue, got " <> show other),
testCase "paginated rollup property item decodes next_url and summary" $ do
r <-
decodeOrFail
"{\"object\":\"list\",\"type\":\"property_item\",\"results\":[],\"next_cursor\":null,\"has_more\":false,\"property_item\":{\"id\":\"r1\",\"type\":\"rollup\",\"next_url\":\"https://api.notion.com/v1/pages/x/properties/r1?start_cursor=abc\",\"rollup\":{\"type\":\"number\",\"number\":7,\"function\":\"count\"}}}"
case r of
PaginatedPropertyItems PropertyItemList {propertyType, propertyId, nextUrl, rollup} -> do
propertyType @?= "rollup"
propertyId @?= "r1"
nextUrl @?= Just "https://api.notion.com/v1/pages/x/properties/r1?start_cursor=abc"
case rollup of
Just (RollupNumberResult (Just 7) _) -> pure ()
other -> assertFailure ("unexpected rollup: " <> show other)
other -> assertFailure ("expected PaginatedPropertyItems, got " <> show other),
testCase "link_mention decodes" $ do
m <- decodeMention "{\"type\":\"link_mention\",\"link_mention\":{\"href\":\"https://github.com\",\"title\":\"GitHub\",\"padding_top\":12}}"
case m of
LinkMention LinkMentionValue {href, title, paddingTop} -> do
href @?= "https://github.com"
title @?= Just "GitHub"
paddingTop @?= Just 12
other -> assertFailure ("expected LinkMention, got " <> show other),
testCase "custom_emoji mention decodes and re-encodes" $ do
m <- decodeMention "{\"type\":\"custom_emoji\",\"custom_emoji\":{\"id\":\"e1\",\"name\":\"bufo\",\"url\":\"https://example.com/bufo.png\"}}"
m @?= CustomEmojiMention (CustomEmojiRef (UUID "e1") (Just "bufo") (Just "https://example.com/bufo.png"))
m `encodesTo` "{\"type\":\"custom_emoji\",\"custom_emoji\":{\"id\":\"e1\",\"name\":\"bufo\",\"url\":\"https://example.com/bufo.png\"}}",
testCase "user mention keeps the full user" $ do
m <- decodeMention "{\"type\":\"user\",\"user\":{\"object\":\"user\",\"id\":\"u9\",\"type\":\"person\",\"name\":\"Sato Kenji\",\"avatar_url\":null,\"person\":{}}}"
case m of
UserMention (FullUser UserObject {name}) -> name @?= Just "Sato Kenji"
other -> assertFailure ("expected a full user mention, got " <> show other)
partial <- decodeMention "{\"type\":\"user\",\"user\":{\"object\":\"user\",\"id\":\"u10\"}}"
partial @?= UserMention (PartialUser (UUID "u10"))
]
------------------------------------------------------------------------------
-- Milestone 3: users, file uploads, icons and object types
userFileUploadObjectTypeTests :: [TestTree]
userFileUploadObjectTypeTests =
[ testCase "custom emoji icon keeps name and url" $ do
let fixture = "{\"type\":\"custom_emoji\",\"custom_emoji\":{\"id\":\"e2\",\"name\":\"sakura\",\"url\":\"https://example.com/sakura.png\"}}"
i <- decodeOrFail fixture
i @?= CustomEmojiIcon (CustomEmojiRef (UUID "e2") (Just "sakura") (Just "https://example.com/sakura.png"))
i `encodesTo` fixture,
testCase "native icon with an unknown color" $ do
i <- decodeOrFail "{\"type\":\"icon\",\"icon\":{\"name\":\"pizza\",\"color\":\"teal\"}}"
i @?= NativeIcon "pizza" (Just (UnknownNoticonColor "teal")),
testCase "ObjectType new and unknown values" $ do
let known =
[ (FileUploadObjectType, "file_upload"),
(PageMarkdownObjectType, "page_markdown"),
(AsyncTaskObjectType, "async_task"),
(GroupObjectType, "group"),
(DataSource, "data_source")
]
mapM_
( \(ot, str) -> do
decoded <- decodeOrFail ("\"" <> str <> "\"")
decoded @?= ot
Aeson.toJSON ot @?= Aeson.String str
)
known
unknown <- decodeOrFail "\"meeting_room\""
unknown @?= UnknownObjectType "meeting_room",
testCase "PageMarkdown decodes object, with or without the key" $ do
md <- decodeOrFail "{\"object\":\"page_markdown\",\"id\":\"p1\",\"markdown\":\"# hi\",\"truncated\":false,\"unknown_block_ids\":[]}"
let PageMarkdown {object} = md
object @?= PageMarkdownObjectType
legacy <- decodeOrFail "{\"id\":\"p1\",\"markdown\":\"# hi\",\"truncated\":false,\"unknown_block_ids\":[]}"
let PageMarkdown {object = legacyObject} = legacy
legacyObject @?= PageMarkdownObjectType,
testCase "file upload with URLs and an agent creator" $ do
fu <-
decodeOrFail
"{\"object\":\"file_upload\",\"id\":\"fu1\",\"created_time\":\"2026-09-14T10:00:00.000Z\",\"last_edited_time\":\"2026-09-14T10:00:00.000Z\",\"created_by\":{\"id\":\"a1\",\"type\":\"agent\"},\"in_trash\":false,\"archived\":false,\"expiry_time\":null,\"status\":\"pending\",\"filename\":null,\"content_type\":null,\"content_length\":null,\"upload_url\":\"https://api.notion.com/v1/file_uploads/fu1/send\",\"complete_url\":\"https://api.notion.com/v1/file_uploads/fu1/complete\"}"
let FU.FileUploadObject {createdBy, uploadUrl, completeUrl} = fu
createdBy @?= FU.FileUploadCreator (UUID "a1") FU.CreatorAgent
uploadUrl @?= Just "https://api.notion.com/v1/file_uploads/fu1/send"
completeUrl @?= Just "https://api.notion.com/v1/file_uploads/fu1/complete",
testCase "CreateFileUpload encodes a typed mode" $
case Aeson.toJSON (FU.mkMultiPartUpload "動画.mp4" 3 Nothing) of
Aeson.Object o -> KeyMap.lookup "mode" o @?= Just (Aeson.String "multi_part")
other -> assertFailure ("expected object, got " <> show other),
testCase "bot user with an empty bot object decodes" $ do
u <- decodeOrFail "{\"object\":\"user\",\"id\":\"b1\",\"type\":\"bot\",\"name\":\"Kaizen Bot\",\"avatar_url\":null,\"bot\":{}}"
let UserObject {type_} = u
type_ @?= Bot
]
------------------------------------------------------------------------------
-- Milestone 4: webhooks
-- | Decode a webhook event built from a shared base payload plus the given
-- @type@, @entity@ and (optionally) @data@ JSON fragments.
decodeEvent :: Text -> Text -> Maybe Text -> IO WebhookEvent
decodeEvent eventType entity mData =
decodeOrFail $
"{\"id\":\"evt-1\",\"timestamp\":\"2026-09-14T10:00:00.000Z\",\"workspace_id\":\"ws-1\",\"workspace_name\":\"Yamada Lab\",\"subscription_id\":\"sub-1\",\"integration_id\":\"int-1\",\"authors\":[{\"id\":\"u1\",\"type\":\"person\"}],\"attempt_number\":1,\"api_version\":\"2026-03-11\",\"type\":\""
<> eventType
<> "\",\"entity\":"
<> entity
<> maybe "" (",\"data\":" <>) mData
<> "}"
webhookTests :: [TestTree]
webhookTests =
[ testCase "file_upload.upload_failed" $ do
e <-
decodeEvent
"file_upload.upload_failed"
"{\"id\":\"fu-1\",\"type\":\"file_upload\"}"
(Just "{\"file_import_result\":{\"type\":\"error\",\"imported_time\":\"2026-09-14T10:00:00.000Z\",\"error\":{\"type\":\"download_error\",\"code\":\"timeout\",\"message\":\"Download timed out\",\"parameter\":null,\"status_code\":504}}}")
let WebhookEvent {type_, entity = WebhookEntity {type_ = entityType}, workspaceName, apiVersion, data_} = e
type_ @?= FileUploadUploadFailed
entityType @?= FileUploadEntity
workspaceName @?= Just "Yamada Lab"
apiVersion @?= Just "2026-03-11"
case data_ of
Just (FileUploadFailedData FU.FileImportError {errorCode, errorStatusCode}) -> do
errorCode @?= "timeout"
errorStatusCode @?= Just 504
other -> assertFailure ("expected FileUploadFailedData, got " <> show other),
testCase "file_upload.created has no data" $ do
e <- decodeEvent "file_upload.created" "{\"id\":\"fu-1\",\"type\":\"file_upload\"}" Nothing
let WebhookEvent {type_, data_} = e
type_ @?= FileUploadCreated
case data_ of
Nothing -> pure ()
other -> assertFailure ("expected no data, got " <> show other),
testCase "page.transcription_block.transcript_deleted" $ do
e <-
decodeEvent
"page.transcription_block.transcript_deleted"
"{\"id\":\"b1\",\"type\":\"page\"}"
(Just "{\"target\":{\"id\":\"b1\",\"type\":\"block\"},\"transcript_id\":null}")
let WebhookEvent {type_, data_} = e
type_ @?= PageTranscriptBlockTranscriptDeleted
case data_ of
Just (TranscriptDeletedData ref tid) -> do
ref @?= WebhookBlockRef (UUID "b1") WebhookRefBlock
tid @?= Nothing
other -> assertFailure ("expected TranscriptDeletedData, got " <> show other),
testCase "database.content_updated on a linked database block" $ do
e <-
decodeEvent
"database.content_updated"
"{\"id\":\"d1\",\"type\":\"block\"}"
(Just "{\"parent\":{\"id\":\"p1\",\"type\":\"page\"},\"updated_blocks\":[{\"id\":\"b2\",\"type\":\"block\"}]}")
let WebhookEvent {entity = WebhookEntity {type_ = entityType}, data_} = e
entityType @?= BlockEntity
case data_ of
Just (ContentUpdatedData WebhookParent {type_ = parentType} refs) -> do
parentType @?= WebhookParentPage
Vector.toList refs @?= [WebhookBlockRef (UUID "b2") WebhookRefBlock]
other -> assertFailure ("expected ContentUpdatedData, got " <> show other),
testCase "data_source.schema_updated" $ do
e <-
decodeEvent
"data_source.schema_updated"
"{\"id\":\"ds1\",\"type\":\"data_source\"}"
(Just "{\"parent\":{\"id\":\"db1\",\"type\":\"database\",\"data_source_id\":\"ds1\"},\"updated_properties\":[{\"id\":\"abc\",\"name\":null,\"action\":\"deleted\"}]}")
case e of
WebhookEvent {data_ = Just (SchemaUpdatedData WebhookParent {dataSourceId} props)} -> do
dataSourceId @?= Just (UUID "ds1")
Vector.toList props @?= [UpdatedPropertySchema "abc" Nothing PropertyDeleted]
other -> assertFailure ("expected SchemaUpdatedData, got " <> show other),
testCase "page.properties_updated" $ do
e <-
decodeEvent
"page.properties_updated"
"{\"id\":\"p1\",\"type\":\"page\"}"
(Just "{\"parent\":{\"id\":\"s1\",\"type\":\"space\"},\"updated_properties\":[\"title\",\"xyz\"]}")
case e of
WebhookEvent {data_ = Just (PagePropertiesUpdatedData WebhookParent {type_ = parentType} props)} -> do
parentType @?= WebhookParentSpace
Vector.toList props @?= ["title", "xyz"]
other -> assertFailure ("expected PagePropertiesUpdatedData, got " <> show other),
testCase "view.updated" $ do
e <-
decodeEvent
"view.updated"
"{\"id\":\"v1\",\"type\":\"view\"}"
(Just "{\"parent\":{\"id\":\"db1\",\"type\":\"database\"},\"updated_fields\":[\"filter\",\"sorts\"]}")
case e of
WebhookEvent {data_ = Just (ViewUpdatedData _ fields)} -> Vector.toList fields @?= [ViewFieldFilter, ViewFieldSorts]
other -> assertFailure ("expected ViewUpdatedData, got " <> show other),
testCase "comment.created" $ do
e <-
decodeEvent
"comment.created"
"{\"id\":\"c1\",\"type\":\"comment\"}"
(Just "{\"parent\":{\"id\":\"p1\",\"type\":\"page\"},\"page_id\":\"p1\"}")
case e of
WebhookEvent {data_ = Just (CommentEventData ref pid)} -> do
ref @?= WebhookBlockRef (UUID "p1") WebhookRefPage
pid @?= UUID "p1"
other -> assertFailure ("expected CommentEventData, got " <> show other),
testCase "mismatched data falls back to RawEventData" $ do
e <- decodeEvent "page.created" "{\"id\":\"p1\",\"type\":\"page\"}" (Just "{\"unexpected\":true}")
case e of
WebhookEvent {data_ = Just (RawEventData raw)} -> raw `encodesTo` "{\"unexpected\":true}"
other -> assertFailure ("expected RawEventData, got " <> show other),
testCase "typed event data re-encodes to the wire shape" $
ContentUpdatedData (WebhookParent (UUID "p1") WebhookParentPage Nothing) (Vector.singleton (WebhookBlockRef (UUID "b2") WebhookRefBlock))
`encodesTo` "{\"parent\":{\"id\":\"p1\",\"type\":\"page\"},\"updated_blocks\":[{\"id\":\"b2\",\"type\":\"block\"}]}"
]