packages feed

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

{-# LANGUAGE OverloadedStrings #-}

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

Tests for the iCloud Drive file upload workflow.
-}
module HStratus.Drive.UploadSpec (spec) where

import Data.Aeson (object)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.ByteString.Lazy as LBS
import qualified Data.ByteString.Lazy.Char8 as LBS8
import Data.IORef (newIORef, readIORef, writeIORef)
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 "uploadFile" $ do
    it "returns () on success" $
      withUploadMock uploadOkApp $ \da ->
        uploadFile da testFolderData "hello.txt" "hello world" `shouldReturn` ()
    it "raises an error when the token request fails" $
      withUploadMock errorApp $ \da ->
        uploadFile da testFolderData "hello.txt" "hello world" `shouldThrow` anyException
    it "raises an error when the content upload fails" $
      withUploadMock contentErrorApp $ \da ->
        uploadFile da testFolderData "hello.txt" "hello world" `shouldThrow` anyException
    it "raises an error when the commit request fails" $
      withUploadMock commitErrorApp $ \da ->
        uploadFile da testFolderData "hello.txt" "hello world" `shouldThrow` anyException


-- Mock servers

withUploadMock :: (Int -> Application) -> (DriveApi -> IO a) -> IO a
withUploadMock mkApp action =
  withSystemTempDirectory "icloud-drive-upload" $ \tmpDir -> do
    portRef <- newIORef 0
    testWithApplication (pure (dynApp portRef mkApp)) $ \serverPort -> do
      writeIORef portRef serverPort
      da <- mkEpAndApi serverPort tmpDir
      action da
 where
  dynApp portRef mk req respond = do
    serverPort <- readIORef portRef
    mk serverPort req respond


uploadOkApp :: Int -> Application
uploadOkApp serverPort req respond
  | "/upload/web" `BS.isSuffixOf` rawPathInfo req =
      let url = "http://127.0.0.1:" ++ show serverPort ++ "/upload/content"
          body = LBS8.pack $ "[{\"document_id\":\"test-doc-id\",\"url\":\"" ++ url ++ "\"}]"
       in respond $ responseLBS status200 jsonHeaders body
  | rawPathInfo req == "/upload/content" =
      respond $ responseLBS status200 jsonHeaders receiptJson
  | "/update/documents" `BS.isSuffixOf` rawPathInfo req =
      respond $ responseLBS status200 jsonHeaders "{}"
  | otherwise =
      respond $ responseLBS status400 [] "unexpected path"


errorApp :: Int -> Application
errorApp _port _req respond = respond $ responseLBS status400 [] "bad request"


contentErrorApp :: Int -> Application
contentErrorApp _port req respond
  | "/upload/web" `BS.isSuffixOf` rawPathInfo req =
      respond $ responseLBS status200 jsonHeaders badTokenJson
  | otherwise =
      respond $ responseLBS status400 [] "bad request"
 where
  badTokenJson = "[{\"document_id\":\"x\",\"url\":\"http://127.0.0.1:1/no-such\"}]"


commitErrorApp :: Int -> Application
commitErrorApp serverPort req respond
  | "/upload/web" `BS.isSuffixOf` rawPathInfo req =
      let url = "http://127.0.0.1:" ++ show serverPort ++ "/upload/content"
          body = LBS8.pack $ "[{\"document_id\":\"test-doc-id\",\"url\":\"" ++ url ++ "\"}]"
       in respond $ responseLBS status200 jsonHeaders body
  | rawPathInfo req == "/upload/content" =
      respond $ responseLBS status200 jsonHeaders receiptJson
  | otherwise =
      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
      }


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


receiptJson :: LBS.ByteString
receiptJson =
  "{\"singleFile\":\
  \{\"fileChecksum\":\"chk\"\
  \,\"wrappingKey\":\"wk\"\
  \,\"referenceChecksum\":\"rc\"\
  \,\"size\":11\
  \,\"receipt\":\"rcpt\"\
  \}}"


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