hstratus-drive-0.1.0.0: test/HStratus/Drive/NodeSpec.hs
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : HStratus.Drive.NodeSpec
Copyright : (c) 2026 Tim Emiola
Maintainer : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause
Tests for iCloud Drive node JSON decoding and data model.
-}
module HStratus.Drive.NodeSpec (spec) where
import Data.Aeson (Value, eitherDecode)
import Data.Aeson.Types (parseEither)
import qualified Data.ByteString.Lazy as LBS
import Data.Text (Text)
import Data.Time (UTCTime)
import Data.Time.Format.ISO8601 (iso8601ParseM)
import Network.HStratus.Internal.Drive.Node
( DriveNode (..)
, DriveNodeId (..)
, FileData (..)
, FolderData (..)
)
import Network.HStratus.Internal.Drive.NodeData
( parseChildrenResponse
, parseDownloadUrl
, parseNodeResponse
)
import Test.Hspec
spec :: Spec
spec = describe "Network.HStratus.Internal.Drive.NodeData" $ do
describe "parseNodeResponse" $ do
it "parses a root folder" $ do
v <- decodeOrFail rootFolderJson
parseEither parseNodeResponse v
`shouldBe` Right (DriveFolder rootFolderData)
it "parses APP_LIBRARY type as DriveFolder" $ do
v <- decodeOrFail appLibraryJson
case parseEither parseNodeResponse v of
Left err -> expectationFailure err
Right (DriveFile _) -> expectationFailure "expected DriveFolder for APP_LIBRARY"
Right (DriveFolder fd) ->
fnId fd `shouldBe` DriveNodeId "FOLDER::com.apple.Keynote::documents"
it "fails with an informative message for an unknown type" $ do
v <- decodeOrFail unknownTypeJson
case parseEither parseNodeResponse v of
Left err -> err `shouldContain` "unknown node type: SYMLINK"
Right _ -> expectationFailure "expected parse failure for unknown type"
describe "parseChildrenResponse" $ do
it "returns empty list when items field is absent" $ do
v <- decodeOrFail rootFolderJson
parseEither parseChildrenResponse v `shouldBe` Right []
it "returns all children" $ do
v <- decodeOrFail subfolderJson
case parseEither parseChildrenResponse v of
Left err -> expectationFailure err
Right nodes -> length nodes `shouldBe` 2
it "parses file children with extension, size, and dateModified" $ do
v <- decodeOrFail subfolderJson
case parseEither parseChildrenResponse v of
Left err -> expectationFailure err
Right (DriveFile fd : _) -> do
fdDocId fd `shouldBe` "33A41112-4131-4938-9691-7F356CE3C51D"
fdExtension fd `shouldBe` Just "pdf"
fdSize fd `shouldBe` Just 19876991
fdDateModified fd `shouldBe` parseTs "2020-04-27T21:37:36Z"
Right nodes ->
expectationFailure $ "expected file as first child, got: " <> show nodes
describe "parseDownloadUrl" $ do
it "extracts url from data_token" $ do
v <- decodeOrFail dataTokenJson
parseEither parseDownloadUrl v `shouldBe` Right dataTokenUrl
it "falls back to package_token when data_token is absent" $ do
v <- decodeOrFail pkgTokenJson
parseEither parseDownloadUrl v `shouldBe` Right pkgTokenUrl
it "fails when neither token field is present" $ do
v <- decodeOrFail "{\"document_id\":\"516C896C\"}"
case parseEither parseDownloadUrl v of
Left _ -> pure ()
Right _ -> expectationFailure "expected parse failure"
-- Fixtures
rootFolderData :: FolderData
rootFolderData =
FolderData
{ fnId = DriveNodeId "FOLDER::com.apple.CloudDocs::root"
, fnEtag = "31"
, fnName = ""
, fnZone = "com.apple.CloudDocs"
, fnDateCreated = Nothing
}
rootFolderJson :: LBS.ByteString
rootFolderJson =
"[{\"drivewsid\":\"FOLDER::com.apple.CloudDocs::root\"\
\,\"zone\":\"com.apple.CloudDocs\"\
\,\"name\":\"\"\
\,\"etag\":\"31\"\
\,\"type\":\"FOLDER\"\
\}]"
appLibraryJson :: LBS.ByteString
appLibraryJson =
"[{\"drivewsid\":\"FOLDER::com.apple.Keynote::documents\"\
\,\"zone\":\"com.apple.Keynote\"\
\,\"name\":\"Keynote\"\
\,\"etag\":\"2m\"\
\,\"type\":\"APP_LIBRARY\"\
\,\"dateCreated\":\"2019-12-12T14:33:55-08:00\"\
\}]"
unknownTypeJson :: LBS.ByteString
unknownTypeJson =
"[{\"drivewsid\":\"SYMLINK::com.apple.CloudDocs::ABC123\"\
\,\"zone\":\"com.apple.CloudDocs\"\
\,\"name\":\"link\"\
\,\"etag\":\"1a\"\
\,\"type\":\"SYMLINK\"\
\}]"
subfolderJson :: LBS.ByteString
subfolderJson =
"[{\"drivewsid\":\"FOLDER::com.apple.CloudDocs::D5AA0425-E84F-4501-AF5D-60F1D92648CF\"\
\,\"zone\":\"com.apple.CloudDocs\"\
\,\"name\":\"Test\"\
\,\"etag\":\"2z\"\
\,\"type\":\"FOLDER\"\
\,\"items\":[\
\{\"drivewsid\":\"FILE::com.apple.CloudDocs::33A41112-4131-4938-9691-7F356CE3C51D\"\
\,\"docwsid\":\"33A41112-4131-4938-9691-7F356CE3C51D\"\
\,\"zone\":\"com.apple.CloudDocs\"\
\,\"name\":\"Scan 2\"\
\,\"dateModified\":\"2020-04-27T21:37:36Z\"\
\,\"size\":19876991\
\,\"etag\":\"2k::2j\"\
\,\"extension\":\"pdf\"\
\,\"type\":\"FILE\"\
\}\
\,{\"drivewsid\":\"FILE::com.apple.CloudDocs::516C896C-6AA5-4A30-B30E-5502C2333DAE\"\
\,\"docwsid\":\"516C896C-6AA5-4A30-B30E-5502C2333DAE\"\
\,\"zone\":\"com.apple.CloudDocs\"\
\,\"name\":\"Scanned document 1\"\
\,\"dateModified\":\"2020-05-03T00:15:17Z\"\
\,\"size\":21644358\
\,\"etag\":\"32::2x\"\
\,\"extension\":\"pdf\"\
\,\"type\":\"FILE\"\
\}\
\]}]"
dataTokenUrl :: Text
dataTokenUrl = "https://cvws.icloud-content.com/B/sig1ref1/Scanned+document+1.pdf?o=obj&v=1"
dataTokenJson :: LBS.ByteString
dataTokenJson =
"{\"data_token\":{\"url\":\"https://cvws.icloud-content.com/B/sig1ref1/Scanned+document+1.pdf?o=obj&v=1\"\
\,\"token\":\"tok1\"\
\}}"
pkgTokenUrl :: Text
pkgTokenUrl = "https://cvws.icloud-content.com/B/sig2ref2/pkg.zip?o=obj&v=1"
pkgTokenJson :: LBS.ByteString
pkgTokenJson =
"{\"package_token\":{\"url\":\"https://cvws.icloud-content.com/B/sig2ref2/pkg.zip?o=obj&v=1\"\
\,\"token\":\"tok2\"\
\}}"
-- Helpers
decodeOrFail :: LBS.ByteString -> IO Value
decodeOrFail bs = case eitherDecode bs of
Left err -> fail err
Right v -> pure v
parseTs :: String -> Maybe UTCTime
parseTs = iso8601ParseM