packages feed

hstratus-drive-0.1.0.0: test/HStratus/Drive/MutationSpec.hs

{-# LANGUAGE OverloadedStrings #-}

{- |
Module      : HStratus.Drive.MutationSpec
Copyright   : (c) 2026 Tim Emiola
Maintainer  : Tim Emiola <adetokunbo@emio.la>
SPDX-License-Identifier: BSD-3-Clause

Tests for iCloud Drive mutation operations (rename, move, delete).
-}
module HStratus.Drive.MutationSpec (spec) where

import Data.Aeson (object)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import Network.HStratus.Drive
import Network.HStratus.Http (mkApiWith)
import Network.HStratus.Http.Endpoints (Endpoints (..))
import Network.HStratus.Session (AccountData (..), Credentials (..), Session (..), Webservice (..))
import Network.HTTP.Client
  ( Request (..)
  , defaultManagerSettings
  , defaultRequest
  , newManager
  )
import Network.HTTP.Types (HeaderName, hContentType, methodPost, status200, status400)
import Network.Wai (Application, rawPathInfo, responseLBS)
import Network.Wai.Handler.Warp (testWithApplication)
import System.IO.Temp (withSystemTempDirectory)
import Test.Hspec


spec :: Spec
spec = describe "Network.HStratus.Drive" $ do
  describe "createFolder" $ do
    it "returns () on success" $
      withMock createOkApp $ \da ->
        createFolder da rootNodeId "New Folder" `shouldReturn` ()
    it "raises an error on HTTP failure" $
      withMock errorApp $ \da ->
        createFolder da rootNodeId "New Folder" `shouldThrow` anyException

  describe "renameNode" $ do
    it "returns () when renaming a folder" $
      withMock renameOkApp $ \da ->
        renameNode da testFolderNode "Renamed Folder" `shouldReturn` ()
    it "returns () when renaming a file" $
      withMock renameOkApp $ \da ->
        renameNode da testFileNode "Renamed File" `shouldReturn` ()
    it "raises an error on HTTP failure" $
      withMock errorApp $ \da ->
        renameNode da testFolderNode "Renamed Folder" `shouldThrow` anyException

  describe "deleteNode" $ do
    it "returns () when deleting a folder" $
      withMock deleteOkApp $ \da ->
        deleteNode da testFolderNode `shouldReturn` ()
    it "returns () when deleting a file" $
      withMock deleteOkApp $ \da ->
        deleteNode da testFileNode `shouldReturn` ()
    it "raises an error on HTTP failure" $
      withMock errorApp $ \da ->
        deleteNode da testFolderNode `shouldThrow` anyException


-- Mock servers

withMock :: Application -> (DriveApi -> IO a) -> IO a
withMock app action =
  withSystemTempDirectory "icloud-drive-mutation" $ \tmpDir ->
    testWithApplication (pure app) $ \serverPort -> do
      da <- mkEpAndApi serverPort tmpDir
      action da


createOkApp :: Application
createOkApp req respond
  | "/createFolders" `BS.isSuffixOf` rawPathInfo req =
      respond $ responseLBS status200 jsonHeaders "{}"
  | otherwise =
      respond $ responseLBS status400 [] "unexpected path"


renameOkApp :: Application
renameOkApp req respond
  | "/renameItems" `BS.isSuffixOf` rawPathInfo req =
      respond $ responseLBS status200 jsonHeaders "{}"
  | otherwise =
      respond $ responseLBS status400 [] "unexpected path"


deleteOkApp :: Application
deleteOkApp req respond
  | "/moveItemsToTrash" `BS.isSuffixOf` rawPathInfo req =
      respond $ responseLBS status200 jsonHeaders "{}"
  | otherwise =
      respond $ responseLBS status400 [] "unexpected path"


errorApp :: Application
errorApp _req respond = respond $ responseLBS status400 [] "bad request"


mkEpAndApi :: Int -> FilePath -> IO DriveApi
mkEpAndApi serverPort tmpDir = do
  let baseUrl = Text.pack $ "http://127.0.0.1:" ++ show serverPort
  mgr <- newManager defaultManagerSettings
  api <- mkApiWith (testSession tmpDir) (testAuthEndpoints serverPort) mgr
  mkDriveApi (testAccountData baseUrl) (testSession tmpDir) api


-- Fixtures

testAccountData :: Text.Text -> AccountData
testAccountData baseUrl =
  AccountData
    { adHsaVersion = 2
    , adHsaChallengeRequired = False
    , adHsaTrustedBrowser = Just True
    , adWebservices = Map.fromList [("drivews", Webservice baseUrl Nothing), ("docws", Webservice baseUrl Nothing)]
    , adRaw = object []
    }


testSession :: FilePath -> Session
testSession topDir =
  Session
    { sessionCreds = Credentials{credAccountName = "test@example.com", credPassword = "test-pass"}
    , sessionTopDir = topDir
    , sessionClientId = "auth-test-client-id"
    }


testAuthEndpoints :: Int -> Endpoints
testAuthEndpoints serverPort =
  Endpoints
    { epHome = "http://127.0.0.1:" <> BS8.pack (show serverPort)
    , epAuth = dummyReq "/appleauth/auth"
    , epSetup = dummyReq "/setup/ws/1"
    , epWidgetKey = "test-widget-key"
    }
 where
  dummyReq reqPath =
    defaultRequest
      { host = "127.0.0.1"
      , port = serverPort
      , secure = False
      , method = methodPost
      , path = reqPath
      }


testFolderNode :: DriveNode
testFolderNode =
  DriveFolder
    FolderData
      { fnId = DriveNodeId "FOLDER::com.apple.CloudDocs::test-folder"
      , fnEtag = "1a"
      , fnName = "Test Folder"
      , fnZone = "com.apple.CloudDocs"
      , fnDateCreated = Nothing
      }


testFileNode :: DriveNode
testFileNode =
  DriveFile
    FileData
      { fdId = DriveNodeId "FILE::com.apple.CloudDocs::test-file"
      , fdDocId = "test-file-doc-id"
      , fdEtag = "2b"
      , fdName = "Test File"
      , fdExtension = Just "txt"
      , fdZone = "com.apple.CloudDocs"
      , fdSize = Just 100
      , fdDateCreated = Nothing
      , fdDateModified = Nothing
      }


jsonHeaders :: [(HeaderName, BS8.ByteString)]
jsonHeaders = [(hContentType, "application/json")]