todoist-sdk-0.1.2.1: test/Web/Todoist/Runner/TodoistIO/TaskSpec.hs
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Web.Todoist.Runner.TodoistIO.TaskSpec (spec) where
import Web.Todoist.Domain.Section (SectionId (..))
import Web.Todoist.Domain.Task
( Deadline (..)
, Due (..)
, Duration (..)
, DurationUnit (..)
, NewTask (..)
, Task (..)
)
import Web.Todoist.Domain.Types
( Content (..)
, Description (..)
, Order (..)
, ParentId (..)
, ProjectId (..)
, TaskId (..)
, Uid (..)
)
import Web.Todoist.Internal.Types
( DeadlineResponse (..)
, DueResponse (..)
, DurationResponse (..)
, NewTaskResponse (..)
, TaskResponse (..)
, TodoistReturn (..)
)
import qualified Web.Todoist.Internal.Types as IT
import Web.Todoist.Runner.IO.Interpreters ()
import Web.Todoist.TestHelpers
( sampleDeadline
, sampleDeadlineResponse
, sampleDue
, sampleDueResponse
, sampleDuration
, sampleDurationResponse
, sampleNewTask
, sampleNewTaskResponse
, sampleNewTaskResponseJson
, sampleTask
, sampleTaskResponse
, sampleTaskResponseJson
, sampleTasksJson
)
import Data.Aeson (decode, eitherDecode, encode)
import Data.Bool (Bool (..))
import Data.Either (Either (..), isRight)
import Data.Function (($))
import Data.Int (Int)
import Data.List (head, length)
import Data.Maybe (Maybe (..), fromJust, isJust)
import Data.String (String)
import Data.Text (Text)
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)
spec :: Spec
spec = do
describe "TodoistTaskM TodoistIO implementations" $ do
parseDurationUnitSpec
durationConversionSpec
deadlineConversionSpec
dueConversionSpec
taskResponseParsingSpec
taskConversionSpec
newTaskResponseParsingSpec
newTaskConversionSpec
todoistReturnTasksSpec
parseDurationUnitSpec :: Spec
parseDurationUnitSpec = describe "parseDurationUnit" $ do
it "parses 'minute' correctly" $ do
let durationResp = IT.DurationResponse {IT.p_amount = 30, IT.p_unit = "minute"}
let decoded = decode (encode durationResp) :: Maybe DurationResponse
decoded `shouldSatisfy` isJust
it "parses 'day' correctly" $ do
let durationResp = IT.DurationResponse {IT.p_amount = 5, IT.p_unit = "day"}
let decoded = decode (encode durationResp) :: Maybe DurationResponse
decoded `shouldSatisfy` isJust
it "handles case variations" $ do
let durationResp1 = IT.DurationResponse {IT.p_amount = 30, IT.p_unit = "MINUTE"}
let durationResp2 = IT.DurationResponse {IT.p_amount = 5, IT.p_unit = "Day"}
decode (encode durationResp1) `shouldSatisfy` (isJust :: Maybe DurationResponse -> Bool)
decode (encode durationResp2) `shouldSatisfy` (isJust :: Maybe DurationResponse -> Bool)
durationConversionSpec :: Spec
durationConversionSpec = describe "DurationResponse JSON parsing" $ do
it "parses valid DurationResponse JSON" $ do
let json = "{\"amount\":30,\"unit\":\"minute\"}"
let result = eitherDecode json :: Either String DurationResponse
result `shouldSatisfy` isRight
it "correctly parses all fields from JSON" $ do
let json = "{\"amount\":30,\"unit\":\"minute\"}"
let decoded = decode json :: Maybe DurationResponse
decoded `shouldSatisfy` isJust
let response :: DurationResponse = fromJust decoded
response.p_amount `shouldBe` (30 :: Int)
response.p_unit `shouldBe` ("minute" :: Text)
it "matches sample data" $ do
let encoded = encode sampleDurationResponse
let decoded = decode encoded :: Maybe DurationResponse
decoded `shouldSatisfy` isJust
fromJust decoded `shouldBe` sampleDurationResponse
deadlineConversionSpec :: Spec
deadlineConversionSpec = describe "DeadlineResponse JSON parsing" $ do
it "parses valid DeadlineResponse JSON" $ do
let json = "{\"date\":\"2025-12-31\",\"lang\":\"en\"}"
let result = eitherDecode json :: Either String DeadlineResponse
result `shouldSatisfy` isRight
it "correctly parses all fields from JSON" $ do
let json = "{\"date\":\"2025-12-31\",\"lang\":\"en\"}"
let decoded = decode json :: Maybe DeadlineResponse
decoded `shouldSatisfy` isJust
let response :: DeadlineResponse = fromJust decoded
response.p_date `shouldBe` ("2025-12-31" :: Text)
response.p_lang `shouldBe` ("en" :: Text)
it "matches sample data" $ do
let encoded = encode sampleDeadlineResponse
let decoded = decode encoded :: Maybe DeadlineResponse
decoded `shouldSatisfy` isJust
fromJust decoded `shouldBe` sampleDeadlineResponse
dueConversionSpec :: Spec
dueConversionSpec = describe "DueResponse JSON parsing" $ do
it "parses valid DueResponse JSON" $ do
let json =
"{\"date\":\"2025-11-15\",\"string\":\"Nov 15\",\"lang\":\"en\",\"is_recurring\":false,\"timezone\":\"America/New_York\"}"
let result = eitherDecode json :: Either String DueResponse
result `shouldSatisfy` isRight
it "correctly parses all fields from JSON" $ do
let json =
"{\"date\":\"2025-11-15\",\"string\":\"Nov 15\",\"lang\":\"en\",\"is_recurring\":false,\"timezone\":\"America/New_York\"}"
let decoded = decode json :: Maybe DueResponse
decoded `shouldSatisfy` isJust
let response :: DueResponse = fromJust decoded
response.p_date `shouldBe` ("2025-11-15" :: Text)
response.p_string `shouldBe` ("Nov 15" :: Text)
response.p_lang `shouldBe` ("en" :: Text)
response.p_is_recurring `shouldBe` False
response.p_timezone `shouldBe` Just ("America/New_York" :: Text)
it "handles optional timezone field" $ do
let json =
"{\"date\":\"2025-11-15\",\"string\":\"Nov 15\",\"lang\":\"en\",\"is_recurring\":false,\"timezone\":null}"
let decoded = decode json :: Maybe DueResponse
decoded `shouldSatisfy` isJust
let response :: DueResponse = fromJust decoded
response.p_timezone `shouldBe` (Nothing :: Maybe Text)
it "matches sample data" $ do
let encoded = encode sampleDueResponse
let decoded = decode encoded :: Maybe DueResponse
decoded `shouldSatisfy` isJust
fromJust decoded `shouldBe` sampleDueResponse
taskResponseParsingSpec :: Spec
taskResponseParsingSpec = describe "TaskResponse" $ do
describe "JSON parsing" $ do
it "parses valid TaskResponse JSON" $ do
let result = eitherDecode sampleTaskResponseJson :: Either String TaskResponse
result `shouldSatisfy` isRight
it "correctly parses all fields from JSON" $ do
let decoded = decode sampleTaskResponseJson :: Maybe TaskResponse
decoded `shouldSatisfy` isJust
let response :: TaskResponse = fromJust decoded
response.p_user_id `shouldBe` ("56092663" :: Text)
response.p_id `shouldBe` ("7654321098" :: Text)
response.p_project_id `shouldBe` ("2203306141" :: Text)
response.p_section_id `shouldBe` Just ("section123" :: Text)
response.p_parent_id `shouldBe` Just ("parent456" :: Text)
response.p_labels `shouldBe` (["urgent", "work"] :: [Text])
response.p_priority `shouldBe` (3 :: Int)
response.p_content `shouldBe` ("Test Task Content" :: Text)
response.p_description `shouldBe` ("This is a test task description" :: Text)
it "handles nested optional fields (due, deadline, duration)" $ do
let decoded = decode sampleTaskResponseJson :: Maybe TaskResponse
decoded `shouldSatisfy` isJust
let response :: TaskResponse = fromJust decoded
response.p_due `shouldSatisfy` isJust
response.p_deadline `shouldSatisfy` isJust
response.p_duration `shouldSatisfy` isJust
it "matches sample data" $ do
let encoded = encode sampleTaskResponse
let decoded = decode encoded :: Maybe TaskResponse
decoded `shouldSatisfy` isJust
fromJust decoded `shouldBe` sampleTaskResponse
taskConversionSpec :: Spec
taskConversionSpec = describe "taskResponseToTask conversion" $ do
it "verifies sample Task domain model structure" $ do
-- We can't directly test the conversion function since it's not exported
-- But we can verify our sample data is correctly structured
let task :: Task = sampleTask
task._id `shouldBe` TaskId "7654321098"
task._content `shouldBe` Content "Test Task Content"
task._description `shouldBe` Description "This is a test task description"
task._project_id `shouldBe` ProjectId "2203306141"
task._section_id `shouldBe` Just (SectionId "section123")
task._parent_id `shouldBe` Just (ParentId "parent456")
task._labels `shouldBe` (["urgent", "work"] :: [Text])
task._priority `shouldBe` (3 :: Int)
task._order `shouldBe` Order 1
task._creator_id `shouldBe` Uid "56092663"
task._created_at `shouldBe` ("2025-11-01T10:00:00Z" :: Text)
task._updated_at `shouldBe` ("2025-11-03T14:30:00Z" :: Text)
it "verifies nested optional fields in domain model" $ do
let task :: Task = sampleTask
task._due `shouldSatisfy` isJust
task._deadline `shouldSatisfy` isJust
task._duration `shouldSatisfy` isJust
task._assignee_id `shouldBe` Just (Uid "assignee789")
task._assigner_id `shouldBe` Just (Uid "56092663")
it "verifies Due conversion" $ do
let due :: Due = sampleDue
due._date `shouldBe` ("2025-11-15" :: Text)
due._string `shouldBe` ("Nov 15" :: Text)
due._lang `shouldBe` ("en" :: Text)
due._is_recurring `shouldBe` False
due._timezone `shouldBe` Just ("America/New_York" :: Text)
it "verifies Deadline conversion" $ do
let deadline :: Deadline = sampleDeadline
deadline._date `shouldBe` ("2025-12-31" :: Text)
deadline._lang `shouldBe` ("en" :: Text)
it "verifies Duration conversion" $ do
let Duration {_amount, _unit} = sampleDuration
_amount `shouldBe` (30 :: Int)
_unit `shouldBe` Minute
newTaskResponseParsingSpec :: Spec
newTaskResponseParsingSpec = describe "NewTaskResponse" $ do
describe "JSON parsing" $ do
it "parses valid NewTaskResponse JSON" $ do
let result = eitherDecode sampleNewTaskResponseJson :: Either String NewTaskResponse
result `shouldSatisfy` isRight
it "correctly parses all fields from JSON" $ do
let decoded = decode sampleNewTaskResponseJson :: Maybe NewTaskResponse
decoded `shouldSatisfy` isJust
let response :: NewTaskResponse = fromJust decoded
response.p_user_id `shouldBe` ("56092663" :: Text)
response.p_id `shouldBe` ("9876543210" :: Text)
response.p_project_id `shouldBe` ("2203306141" :: Text)
response.p_section_id `shouldBe` (Nothing :: Maybe Text)
response.p_parent_id `shouldBe` (Nothing :: Maybe Text)
response.p_labels `shouldBe` (["new"] :: [Text])
response.p_priority `shouldBe` (1 :: Int)
response.p_content `shouldBe` ("New Task" :: Text)
response.p_description `shouldBe` ("A newly created task" :: Text)
response.p_checked `shouldBe` False
response.p_is_deleted `shouldBe` False
it "matches sample data structure" $ do
let encoded = encode sampleNewTaskResponse
let decoded = decode encoded :: Maybe NewTaskResponse
decoded `shouldSatisfy` isJust
let response :: NewTaskResponse = fromJust decoded
response.p_id `shouldBe` ("9876543210" :: Text)
response.p_content `shouldBe` ("New Task" :: Text)
response.p_priority `shouldBe` (1 :: Int)
newTaskConversionSpec :: Spec
newTaskConversionSpec = describe "newTaskResponseToNewTask conversion" $ do
it "verifies sample NewTask domain model structure" $ do
let newTask :: NewTask = sampleNewTask
newTask._user_id `shouldBe` ("56092663" :: Text)
newTask._id `shouldBe` TaskId "9876543210"
newTask._project_id `shouldBe` ProjectId "2203306141"
newTask._section_id `shouldBe` (Nothing :: Maybe SectionId)
newTask._parent_id `shouldBe` (Nothing :: Maybe ParentId)
newTask._labels `shouldBe` (["new"] :: [Text])
newTask._priority `shouldBe` (1 :: Int)
newTask._content `shouldBe` Content "New Task"
newTask._description `shouldBe` Description "A newly created task"
newTask._checked `shouldBe` False
newTask._is_deleted `shouldBe` False
newTask._child_order `shouldBe` Order 0
it "handles optional fields correctly" $ do
let newTask :: NewTask = sampleNewTask
newTask._added_at `shouldBe` Just ("2025-11-04T09:00:00Z" :: Text)
newTask._completed_at `shouldBe` (Nothing :: Maybe Text)
newTask._updated_at `shouldBe` Just ("2025-11-04T09:00:00Z" :: Text)
todoistReturnTasksSpec :: Spec
todoistReturnTasksSpec = describe "TodoistReturn [TaskResponse]" $ do
it "parses getTasks response JSON" $ do
let result = eitherDecode sampleTasksJson :: Either String (TodoistReturn TaskResponse)
result `shouldSatisfy` isRight
it "extracts results from TodoistReturn" $ do
let decoded = decode sampleTasksJson :: Maybe (TodoistReturn TaskResponse)
decoded `shouldSatisfy` isJust
let TodoistReturn {results} = fromJust decoded
length results `shouldBe` (1 :: Int)
it "correctly parses TaskResponse within TodoistReturn" $ do
let decoded = decode sampleTasksJson :: Maybe (TodoistReturn TaskResponse)
decoded `shouldSatisfy` isJust
let TodoistReturn {results} = fromJust decoded
let taskResp :: TaskResponse = head results
taskResp.p_id `shouldBe` ("7654321098" :: Text)
taskResp.p_content `shouldBe` ("Test Task Content" :: Text)
taskResp.p_priority `shouldBe` (3 :: Int)