todoist-sdk-0.1.2.1: integration-test/CommentIntegrationSpec.hs
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
module CommentIntegrationSpec (spec) where
import Helpers
( assertSucceeds
, buildTestTask
, generateUniqueName
, getTestConfig
, liftTodoist
)
import Web.Todoist.Domain.Comment
( CommentCreate
, CommentId (..)
, CommentParam (..)
, Content (..)
, TodoistCommentM (..)
, newCommentBuilder
, updateCommentBuilder
)
import qualified Web.Todoist.Domain.Comment as C
import qualified Web.Todoist.Domain.Project as P
import Web.Todoist.Domain.Task (NewTask (..), TodoistTaskM (..))
import Web.Todoist.Domain.Types (ProjectId (..), TaskId (..))
import Web.Todoist.Internal.Config (TodoistConfig)
import Web.Todoist.Internal.Error (TodoistError)
import Web.Todoist.Lens ((^.))
import Web.Todoist.Runner (todoist)
import Web.Todoist.Util.Builder (runBuilder, withProjectId, withTaskId)
import Control.Applicative (pure)
import Control.Exception (bracket)
import Control.Monad (forM_, mapM, void)
import Control.Monad.IO.Class (liftIO)
import Control.Monad.Trans.Except (ExceptT)
import Data.Bool (Bool (..))
import Data.Function (($))
import Data.Functor ((<$>))
import Data.Int (Int)
import qualified Data.List as L
import Data.Maybe (Maybe (..))
import Data.Ord ((>=))
import Data.Semigroup ((<>))
import Data.Text (Text, pack)
import GHC.Base (mempty)
import System.IO (IO, putStrLn)
import Test.Hspec (Spec, describe, it, pendingWith, runIO, shouldBe, shouldSatisfy)
import Text.Show (show)
spec :: Spec
spec = do
maybeConfig <- runIO getTestConfig
case maybeConfig of
Nothing ->
it "requires TODOIST_TEST_API_TOKEN" $
pendingWith "TODOIST_TEST_API_TOKEN not set"
Just config -> do
commentOnProjectLifecycleSpec config
commentOnTaskLifecycleSpec config
updateCommentSpec config
getCommentsSpec config
getSingleCommentSpec config
commentOnProjectLifecycleSpec :: TodoistConfig -> Spec
commentOnProjectLifecycleSpec config = describe "Comment on project lifecycle (create, get, delete)" $ do
it "creates, retrieves, and deletes a comment on a project" $ do
-- Generate unique names
projectName <- pack <$> generateUniqueName "IntegTest-CommentProject-Project"
commentContent <- pack <$> generateUniqueName "IntegTest-CommentProject-Comment"
-- Use withTestProjectComment for automatic cleanup
withTestProjectComment config projectName commentContent $ \projectId commentId -> do
-- Verify we can retrieve the comment
comment <- liftTodoist config (getComment commentId)
-- Extract comment fields for verification using lenses
let retrievedId = comment ^. C.commentId
retrievedContent = comment ^. C.commentContent
retrievedProjectId = comment ^. C.commentProjectId
retrievedTaskId = comment ^. C.commentTaskId
-- Verify comment ID matches
liftIO $ retrievedId `shouldBe` commentId
-- Verify content matches
liftIO $ retrievedContent `shouldBe` Content commentContent
-- Verify project ID matches and task ID is Nothing
liftIO $ retrievedProjectId `shouldBe` Just projectId
liftIO $ retrievedTaskId `shouldBe` Nothing
-- Test explicit delete (cleanup will handle if this fails)
liftTodoist config (deleteComment commentId)
commentOnTaskLifecycleSpec :: TodoistConfig -> Spec
commentOnTaskLifecycleSpec config = describe "Comment on task lifecycle (create, get, delete)" $ do
it "creates, retrieves, and deletes a comment on a task" $ do
-- Generate unique names
projectName <- pack <$> generateUniqueName "IntegTest-CommentTask-Project"
taskContent <- pack <$> generateUniqueName "IntegTest-CommentTask-Task"
commentContent <- pack <$> generateUniqueName "IntegTest-CommentTask-Comment"
-- Use withTestTaskComment for automatic cleanup
withTestTaskComment config projectName taskContent commentContent $ \taskId commentId -> do
-- Verify we can retrieve the comment
comment <- liftTodoist config (getComment commentId)
-- Extract comment fields for verification using lenses
let retrievedId = comment ^. C.commentId
retrievedContent = comment ^. C.commentContent
retrievedProjectId = comment ^. C.commentProjectId
retrievedTaskId = comment ^. C.commentTaskId
-- Verify comment ID matches
liftIO $ retrievedId `shouldBe` commentId
-- Verify content matches
liftIO $ retrievedContent `shouldBe` Content commentContent
-- Verify task ID matches and project ID is Nothing
liftIO $ retrievedTaskId `shouldBe` Just taskId
liftIO $ retrievedProjectId `shouldBe` Nothing
-- Test explicit delete (cleanup will handle if this fails)
liftTodoist config (deleteComment commentId)
updateCommentSpec :: TodoistConfig -> Spec
updateCommentSpec config = describe "Update comment" $ do
it "creates a comment, updates its content, verifies changes" $ do
-- Generate unique names
projectName <- pack <$> generateUniqueName "IntegTest-UpdateComment-Project"
originalContent <- pack <$> generateUniqueName "IntegTest-UpdateComment-Original"
withTestProjectComment config projectName originalContent $ \_ commentId -> do
-- Verify initial state
comment1 <- liftTodoist config (getComment commentId)
let initialContent = comment1 ^. C.commentContent
liftIO $ initialContent `shouldBe` Content originalContent
-- Update the comment
let updatedContent = originalContent <> "-Updated"
let commentUpdate = runBuilder (updateCommentBuilder updatedContent) mempty
updatedComment <- liftTodoist config (updateComment commentUpdate commentId)
-- Verify the response contains updated value
let responseContent = updatedComment ^. C.commentContent
liftIO $ responseContent `shouldBe` Content updatedContent
-- Fetch the comment again to verify persistence
comment2 <- liftTodoist config (getComment commentId)
let persistedContent = comment2 ^. C.commentContent
liftIO $ persistedContent `shouldBe` Content updatedContent
getCommentsSpec :: TodoistConfig -> Spec
getCommentsSpec config = describe "Get multiple comments" $ do
it "creates 3 comments on a project, retrieves them via getComments" $ do
-- Generate unique names
projectName <- pack <$> generateUniqueName "IntegTest-GetComments-Project"
baseName <- generateUniqueName "IntegTest-GetComments-Comment"
let commentContent1 = pack $ baseName <> "-Comment1"
let commentContent2 = pack $ baseName <> "-Comment2"
let commentContent3 = pack $ baseName <> "-Comment3"
let commentContents = [commentContent1, commentContent2, commentContent3]
withTestProjectComments config projectName commentContents $ \projectId commentIds -> do
-- Get all comments for this project
let ProjectId {getProjectId = projIdText} = projectId
let commentParam =
CommentParam
{ project_id = Just projIdText
, task_id = Nothing
, cursor = Nothing
, limit = Nothing
, public_key = Nothing
}
comments <- liftTodoist config (getComments commentParam)
-- Verify we got at least 3 comments (there might be more from previous tests)
let commentCount = L.length comments
liftIO $ commentCount `shouldSatisfy` (\n -> n >= (3 :: Int))
-- Extract comment IDs and contents from results
let commentIdsResult = L.map (^. C.commentId) comments
let commentContentsResult = L.map (^. C.commentContent) comments
-- Verify all 3 comment IDs are present
let [expectedId1, expectedId2, expectedId3] = commentIds
liftIO $ (expectedId1 `L.elem` commentIdsResult) `shouldBe` True
liftIO $ (expectedId2 `L.elem` commentIdsResult) `shouldBe` True
liftIO $ (expectedId3 `L.elem` commentIdsResult) `shouldBe` True
-- Verify all 3 comment contents are present
liftIO $ (Content commentContent1 `L.elem` commentContentsResult) `shouldBe` True
liftIO $ (Content commentContent2 `L.elem` commentContentsResult) `shouldBe` True
liftIO $ (Content commentContent3 `L.elem` commentContentsResult) `shouldBe` True
-- Verify each comment has the correct project_id
forM_ comments $ \comment -> do
let commentProjId = comment ^. C.commentProjectId
commentTaskId = comment ^. C.commentTaskId
case (commentProjId, commentTaskId) of
(Just pid, Nothing) -> liftIO $ pid `shouldBe` ProjectId projIdText
_ -> pure () -- Skip comments that don't match our filter
getSingleCommentSpec :: TodoistConfig -> Spec
getSingleCommentSpec config = describe "Get single comment by ID" $ do
it "creates a comment and retrieves it by ID" $ do
-- Generate unique names
projectName <- pack <$> generateUniqueName "IntegTest-GetSingleComment-Project"
commentContent <- pack <$> generateUniqueName "IntegTest-GetSingleComment-Comment"
withTestProjectComment config projectName commentContent $ \projectId commentId -> do
-- Get the comment by ID
comment <- liftTodoist config (getComment commentId)
-- Verify the comment
-- Extract comment fields for verification using lenses
let retrievedId = comment ^. C.commentId
retrievedContent = comment ^. C.commentContent
retrievedProjectId = comment ^. C.commentProjectId
liftIO $ retrievedId `shouldBe` commentId
liftIO $ retrievedContent `shouldBe` Content commentContent
liftIO $ retrievedProjectId `shouldBe` Just projectId
{- | Create a test project and comment, run an action with the comment ID, then clean up
Uses bracket to ensure cleanup happens even if the action fails
Comments are deleted before the project
-}
withTestProjectComment ::
TodoistConfig ->
Text -> -- project name
Text -> -- comment content
(ProjectId -> CommentId -> ExceptT TodoistError IO a) ->
IO ()
withTestProjectComment config projectName commentContent action = do
let createResources = do
liftIO $ putStrLn $ "Creating test project: " <> show projectName
projectId <- liftTodoist config (P.addProject $ runBuilder (P.createProjectBuilder projectName) mempty)
liftIO $ putStrLn $ "Creating test comment: " <> show commentContent
let ProjectId {getProjectId = projIdText} = projectId
let commentCreate = buildTestCommentForProject commentContent projIdText
createdComment <- liftTodoist config (addComment commentCreate)
let commentId = createdComment ^. C.commentId
pure (projectId, commentId)
let deleteResources (projectId, commentId) = do
liftIO $ putStrLn $ "Cleaning up test comment: " <> show commentContent
void $ todoist config (deleteComment commentId)
liftIO $ putStrLn $ "Cleaning up test project: " <> show projectName
void $ todoist config (P.deleteProject projectId)
let runAction (projectId, commentId) = void $ assertSucceeds $ action projectId commentId
bracket (assertSucceeds createResources) deleteResources runAction
{- | Create a test project, task, and comment on the task, run an action, then clean up
Uses bracket to ensure cleanup happens even if the action fails
Comments are deleted before tasks, tasks before projects
-}
withTestTaskComment ::
TodoistConfig ->
Text -> -- project name
Text -> -- task content
Text -> -- comment content
(TaskId -> CommentId -> ExceptT TodoistError IO a) ->
IO ()
withTestTaskComment config projectName taskContent commentContent action = do
let createResources = do
liftIO $ putStrLn $ "Creating test project: " <> show projectName
projectId <- liftTodoist config (P.addProject $ runBuilder (P.createProjectBuilder projectName) mempty)
liftIO $ putStrLn $ "Creating test task: " <> show taskContent
let ProjectId {getProjectId = projIdText} = projectId
let taskCreate = buildTestTask taskContent projIdText
newTaskResult <- liftTodoist config (createTask taskCreate)
let NewTask {_id = newTaskIdText} = newTaskResult
let taskId = newTaskIdText
TaskId {getTaskId = taskIdText} = taskId
liftIO $ putStrLn $ "Creating test comment: " <> show commentContent
let commentCreate = buildTestCommentForTask commentContent taskIdText
createdComment <- liftTodoist config (addComment commentCreate)
let commentId = createdComment ^. C.commentId
pure (projectId, taskId, commentId)
let deleteResources (projectId, taskId, commentId) = do
liftIO $ putStrLn $ "Cleaning up test comment: " <> show commentContent
void $ todoist config (deleteComment commentId)
liftIO $ putStrLn $ "Cleaning up test task: " <> show taskContent
void $ todoist config (deleteTask taskId)
liftIO $ putStrLn $ "Cleaning up test project: " <> show projectName
void $ todoist config (P.deleteProject projectId)
let runAction (_, taskId, commentId) = void $ assertSucceeds $ action taskId commentId
bracket (assertSucceeds createResources) deleteResources runAction
{- | Create a test project and multiple comments, run an action with their IDs, then clean up
Ensures all comments are deleted before the project is deleted
-}
withTestProjectComments ::
TodoistConfig ->
Text -> -- project name
[Text] -> -- comment contents
(ProjectId -> [CommentId] -> ExceptT TodoistError IO a) ->
IO ()
withTestProjectComments config projectName commentContents action = do
let createResources = do
liftIO $ putStrLn $ "Creating test project: " <> show projectName
projectId <- liftTodoist config (P.addProject $ runBuilder (P.createProjectBuilder projectName) mempty)
liftIO $ putStrLn $ "Creating " <> show (L.length commentContents) <> " test comments"
let ProjectId {getProjectId = projIdText} = projectId
commentIds <-
mapM
( \content -> do
let commentCreate = buildTestCommentForProject content projIdText
createdComment <- liftTodoist config (addComment commentCreate)
pure $ createdComment ^. C.commentId
)
commentContents
pure (projectId, commentIds)
let deleteResources (projectId, commentIds) = do
liftIO $ putStrLn $ "Cleaning up " <> show (L.length commentIds) <> " test comments"
forM_ commentIds $ \commentId ->
void $ todoist config (deleteComment commentId)
liftIO $ putStrLn $ "Cleaning up test project: " <> show projectName
void $ todoist config (P.deleteProject projectId)
let runAction (projectId, commentIds) = void $ assertSucceeds $ action projectId commentIds
bracket (assertSucceeds createResources) deleteResources runAction
{- | Build a test comment for a project using the Builder pattern
Creates a CommentCreate with content and project_id for testing
-}
buildTestCommentForProject :: Text -> Text -> CommentCreate
buildTestCommentForProject commentContent projectId =
runBuilder
(newCommentBuilder commentContent)
(withProjectId projectId)
{- | Build a test comment for a task using the Builder pattern
Creates a CommentCreate with content and task_id for testing
-}
buildTestCommentForTask :: Text -> Text -> CommentCreate
buildTestCommentForTask commentContent taskId =
runBuilder
(newCommentBuilder commentContent)
(withTaskId taskId)