packages feed

todoist-sdk-0.1.2.1: test/Web/Todoist/Runner/TodoistIOSpec.hs

{-# LANGUAGE DuplicateRecordFields #-}

module Web.Todoist.Runner.TodoistIOSpec (spec) where

import Web.Todoist.Domain.Project
    ( Collaborator
    , Project
    , ProjectCreate
    , ProjectUpdate
    , createdAt
    , updatedAt
    , viewStyle
    , projectUpdateName
    , projectUpdateDescription
    , projectUpdateColor
    , projectUpdateIsFavorite
    , projectUpdateViewStyle
    )
import Web.Todoist.Domain.Types
    ( Description (..)
    , IsFavorite (..)
    , Name (..)
    , ProjectId (..)
    , ViewStyle (..)
    , parseViewStyle
    )
import Web.Todoist.Internal.Types
    ( Action (..)
    , CollaboratorRole (..)
    , ProjectPermissions (..)
    , ProjectResponse (..)
    , RoleActions (..)
    , TodoistReturn (..)
    )
import Web.Todoist.Runner.IO.Interpreters (projectResponseToProject)
import Web.Todoist.TestHelpers
    ( sampleCollaborator
    , sampleCollaboratorsJson
    , samplePartialProjectUpdateJson
    , sampleProject
    , sampleProjectCreate
    , sampleProjectId
    , sampleProjectIdJson
    , sampleProjectPermissionsJson
    , sampleProjectResponse
    , sampleProjectResponseJson
    , sampleProjectUpdate
    , sampleProjectUpdateJson
    , sampleProjectsJson
    )
import Web.Todoist.Lens ((^.))

import Data.Aeson (decode, eitherDecode, encode)
import Data.Bool (Bool (..))
import Data.Either (Either (..), isRight)
import Data.Function (($))
import Data.Functor ((<$>))
import Data.List (head, length, (!!))
import Data.Maybe (Maybe (..), fromJust, isJust)
import Data.String (String)
import Test.Hspec (Spec, describe, it, shouldBe, shouldSatisfy)

spec :: Spec
spec = do
    describe "TodoistProjectM TodoistIO implementations" $ do
        getProjectSpec
        getAllProjectsSpec
        getProjectCollaboratorsSpec
        addProjectSpec
        deleteProjectSpec
        archiveUnarchiveProjectSpec
        getProjectPermissionsSpec
        updateProjectSpec

getProjectSpec :: Spec
getProjectSpec = describe "getProject" $ do
    jsonParsingSpec
    conversionSpec
    viewStyleSpec

jsonParsingSpec :: Spec
jsonParsingSpec = describe "ProjectResponse JSON parsing" $ do
    it "parses valid ProjectResponse JSON" $ do
        let result = eitherDecode sampleProjectResponseJson :: Either String ProjectResponse
        result `shouldSatisfy` isRight

    it "correctly parses all fields from JSON" $ do
        let decoded = decode sampleProjectResponseJson :: Maybe ProjectResponse
        decoded `shouldSatisfy` isJust

        let ProjectResponse
                { p_id = projId
                , p_name = projName
                , p_description = projDescription
                , p_child_order = projChildOrder
                , p_color = projColor
                , p_is_favorite = projIsFavorite
                , p_is_archived = projIsArchived
                , p_is_collapsed = projIsCollapsed
                , p_is_shared = projIsShared
                , p_can_assign_tasks = projCanAssignTasks
                , p_view_style = projViewStyle
                } = fromJust decoded
        projId `shouldBe` "2203306141"
        projName `shouldBe` "Test Project"
        projDescription `shouldBe` "A test project for unit testing"
        projChildOrder `shouldBe` 1
        projColor `shouldBe` "blue"
        projIsFavorite `shouldBe` True
        projIsArchived `shouldBe` False
        projIsCollapsed `shouldBe` False
        projIsShared `shouldBe` False
        projCanAssignTasks `shouldBe` False
        projViewStyle `shouldBe` "list"

conversionSpec :: Spec
conversionSpec = describe "projectResponseToProject" $ do
    it "converts ProjectResponse to Project correctly" $ do
        let project = projectResponseToProject sampleProjectResponse
        project `shouldBe` sampleProject

    it "maps all fields correctly" $ do
        let project :: Project
            project = projectResponseToProject sampleProjectResponse
        project `shouldBe` sampleProject

    it "converts view_style string to ViewStyle type" $ do
        let projectViewStyle = projectResponseToProject sampleProjectResponse ^. viewStyle
        projectViewStyle `shouldBe` List

    it "preserves timestamp fields" $ do
        let projectResponse = projectResponseToProject sampleProjectResponse
            projectCreateAt = projectResponse ^. createdAt
            projectUpdatedAt = projectResponse ^. updatedAt
        projectCreateAt `shouldBe` Just "2023-06-15T10:30:00Z"
        projectUpdatedAt `shouldBe` Just "2023-06-20T14:45:00Z"

viewStyleSpec :: Spec
viewStyleSpec = describe "parseViewStyle" $ do
    it "parses 'list' correctly" $ do
        parseViewStyle "list" `shouldBe` List

    it "parses 'board' correctly" $ do
        parseViewStyle "board" `shouldBe` Board

    it "parses 'calendar' correctly" $ do
        parseViewStyle "calendar" `shouldBe` Calendar

    it "is case-insensitive" $ do
        parseViewStyle "LIST" `shouldBe` List
        parseViewStyle "Board" `shouldBe` Board
        parseViewStyle "CALENDAR" `shouldBe` Calendar

getAllProjectsSpec :: Spec
getAllProjectsSpec = describe "getAllProjects" $ do
    it "parses TodoistReturn [ProjectResponse] JSON" $ do
        let result = eitherDecode sampleProjectsJson :: Either String (TodoistReturn ProjectResponse)
        result `shouldSatisfy` isRight

    it "extracts results from TodoistReturn" $ do
        let decoded = decode sampleProjectsJson :: Maybe (TodoistReturn ProjectResponse)
        decoded `shouldSatisfy` isJust
        let todoistReturn = fromJust decoded
        length (results todoistReturn) `shouldBe` 2

    it "converts all ProjectResponses to Projects" $ do
        let decoded = decode sampleProjectsJson :: Maybe (TodoistReturn ProjectResponse)
        decoded `shouldSatisfy` isJust
        let todoistReturn = fromJust decoded
        let projects = projectResponseToProject <$> results todoistReturn
        length projects `shouldBe` 2
        head projects `shouldBe` sampleProject

    it "correctly parses multiple projects" $ do
        let decoded = decode sampleProjectsJson :: Maybe (TodoistReturn ProjectResponse)
        let todoistReturn = fromJust decoded
        let projects :: [Project]
            projects = projectResponseToProject <$> results todoistReturn
        length projects `shouldBe` 2
        head projects `shouldBe` sampleProject

getProjectCollaboratorsSpec :: Spec
getProjectCollaboratorsSpec = describe "getProjectCollaborators" $ do
    it "parses TodoistReturn [Collaborator] JSON" $ do
        let result = eitherDecode sampleCollaboratorsJson :: Either String (TodoistReturn Collaborator)
        result `shouldSatisfy` isRight

    it "extracts collaborators from TodoistReturn" $ do
        let decoded = decode sampleCollaboratorsJson :: Maybe (TodoistReturn Collaborator)
        decoded `shouldSatisfy` isJust
        let todoistReturn = fromJust decoded
        length (results todoistReturn) `shouldBe` 2

    it "correctly parses Collaborator fields" $ do
        let decoded = decode sampleCollaboratorsJson :: Maybe (TodoistReturn Collaborator)
        let todoistReturn = fromJust decoded
        let collaborators = results todoistReturn
        head collaborators `shouldBe` sampleCollaborator

    it "parses multiple collaborators correctly" $ do
        let decoded = decode sampleCollaboratorsJson :: Maybe (TodoistReturn Collaborator)
        let todoistReturn = fromJust decoded
        let collaborators :: [Collaborator]
            collaborators = results todoistReturn
        length collaborators `shouldBe` 2
        head collaborators `shouldBe` sampleCollaborator

addProjectSpec :: Spec
addProjectSpec = describe "addProject" $ do
    it "serializes ProjectCreate to JSON correctly" $ do
        let encoded = encode sampleProjectCreate
        let decoded = decode encoded :: Maybe ProjectCreate
        decoded `shouldSatisfy` isJust

    it "parses ProjectId response JSON" $ do
        let result = eitherDecode sampleProjectIdJson :: Either String ProjectId
        result `shouldSatisfy` isRight

    it "correctly parses ProjectId fields" $ do
        let decoded = decode sampleProjectIdJson :: Maybe ProjectId
        decoded `shouldSatisfy` isJust
        let projectId :: ProjectId
            projectId = fromJust decoded
        projectId `shouldBe` sampleProjectId

deleteProjectSpec :: Spec
deleteProjectSpec = describe "deleteProject" $ do
    it "returns unit type (no response body to parse)" $ do
        -- deleteProject returns (), which indicates successful deletion
        -- There's no JSON response body to test, but we can verify the type
        let result :: ()
            result = ()
        result `shouldBe` ()

    it "is a void operation (no data returned)" $ do
        -- The Todoist API DELETE endpoint returns no content (204 No Content)
        -- The function signature is: deleteProject :: ProjectId -> TodoistIO ()
        -- This test documents that behavior
        let unitValue :: ()
            unitValue = ()
        unitValue `shouldBe` ()

archiveUnarchiveProjectSpec :: Spec
archiveUnarchiveProjectSpec = describe "archiveProject and unarchiveProject" $ do
    it "both return ProjectId on success (parses same JSON format)" $ do
        let result = eitherDecode sampleProjectIdJson :: Either String ProjectId
        result `shouldSatisfy` isRight
        let projectId :: ProjectId
            projectId = fromJust (decode sampleProjectIdJson :: Maybe ProjectId)
        projectId `shouldBe` sampleProjectId

    it "ProjectId response is consistent across operations" $ do
        let decoded = decode sampleProjectIdJson :: Maybe ProjectId
        decoded `shouldSatisfy` isJust
        decoded `shouldBe` Just sampleProjectId

getProjectPermissionsSpec :: Spec
getProjectPermissionsSpec = describe "getProjectPermissions" $ do
    jsonParsingPermissionsSpec

jsonParsingPermissionsSpec :: Spec
jsonParsingPermissionsSpec = describe "ProjectPermissions JSON parsing" $ do
    it "parses valid ProjectPermissions JSON" $ do
        let result = eitherDecode sampleProjectPermissionsJson :: Either String ProjectPermissions
        result `shouldSatisfy` isRight

    it "correctly parses all fields from JSON" $ do
        let decoded = decode sampleProjectPermissionsJson :: Maybe ProjectPermissions
        decoded `shouldSatisfy` isJust
        let perms = fromJust decoded
        length (p_project_collaborator_actions perms) `shouldBe` 1
        length (p_workspace_collaborator_actions perms) `shouldBe` 1

    it "correctly parses role as Creator" $ do
        let decoded = decode sampleProjectPermissionsJson :: Maybe ProjectPermissions
        decoded `shouldSatisfy` isJust
        let perms = fromJust decoded
        let RoleActions {p_name = roleName} = head (p_project_collaborator_actions perms)
        roleName `shouldBe` Creator

    it "correctly parses action names as Text" $ do
        let decoded = decode sampleProjectPermissionsJson :: Maybe ProjectPermissions
        decoded `shouldSatisfy` isJust
        let perms = fromJust decoded
        let RoleActions {p_actions = actions} = head (p_project_collaborator_actions perms)
        length actions `shouldBe` 2
        let Action {p_name = action1Name} = head actions
        let Action {p_name = action2Name} = actions !! 1
        action1Name `shouldBe` "create_task"
        action2Name `shouldBe` "delete_project"

updateProjectSpec :: Spec
updateProjectSpec = describe "updateProject" $ do
    jsonSerializationSpec
    jsonParsingUpdateSpec
    jsonPartialUpdateSpec

jsonSerializationSpec :: Spec
jsonSerializationSpec = describe "ProjectUpdate JSON serialization" $ do
    it "serializes ProjectUpdate to valid JSON" $ do
        let json = encode sampleProjectUpdate
        let result = eitherDecode json :: Either String ProjectUpdate
        result `shouldSatisfy` isRight

    it "serializes all fields correctly" $ do
        let json = encode sampleProjectUpdate
        let decoded = decode json :: Maybe ProjectUpdate
        decoded `shouldBe` Just sampleProjectUpdate

jsonParsingUpdateSpec :: Spec
jsonParsingUpdateSpec = describe "ProjectUpdate JSON parsing" $ do
    it "parses valid ProjectUpdate JSON" $ do
        let result = eitherDecode sampleProjectUpdateJson :: Either String ProjectUpdate
        result `shouldSatisfy` isRight

    it "correctly parses all fields from JSON" $ do
        let decoded = decode sampleProjectUpdateJson :: Maybe ProjectUpdate
        decoded `shouldSatisfy` isJust
        let projectUpdate = fromJust decoded
            name = projectUpdate ^. projectUpdateName
            desc = projectUpdate ^. projectUpdateDescription
            isFav = projectUpdate ^. projectUpdateIsFavorite
        name `shouldBe` Just (Name "Updated Project Name")
        desc `shouldBe` Just (Description "Updated description")
        isFav `shouldBe` Just (IsFavorite True)

jsonPartialUpdateSpec :: Spec
jsonPartialUpdateSpec = describe "Partial ProjectUpdate" $ do
    it "parses partial update JSON correctly" $ do
        let result = eitherDecode samplePartialProjectUpdateJson :: Either String ProjectUpdate
        result `shouldSatisfy` isRight

    it "handles missing fields as Nothing" $ do
        let decoded = decode samplePartialProjectUpdateJson :: Maybe ProjectUpdate
        decoded `shouldSatisfy` isJust
        let projectUpdate = fromJust decoded
            name = projectUpdate ^. projectUpdateName
            desc = projectUpdate ^. projectUpdateDescription
            color = projectUpdate ^. projectUpdateColor
            isFav = projectUpdate ^. projectUpdateIsFavorite
            viewStyle = projectUpdate ^. projectUpdateViewStyle
        name `shouldBe` Just (Name "New Name")
        desc `shouldBe` Nothing
        color `shouldBe` Nothing
        isFav `shouldBe` Just (IsFavorite True)
        viewStyle `shouldBe` Nothing