diff --git a/app/graphql/types/current_user_todos.rb b/app/graphql/types/current_user_todos.rb new file mode 100644 index 0000000000000000000000000000000000000000..e610286c1a9d9b77adf2d1d475b08022aa965970 --- /dev/null +++ b/app/graphql/types/current_user_todos.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Interface to expose todos for the current_user on the `object` +module Types + module CurrentUserTodos + include BaseInterface + + field_class Types::BaseField + + field :current_user_todos, Types::TodoType.connection_type, + description: 'Todos for the current user', + null: false do + argument :state, Types::TodoStateEnum, + description: 'State of the todos', + required: false + end + + def current_user_todos(state: nil) + state ||= %i(done pending) # TodosFinder treats a `nil` state param as `pending` + + TodosFinder.new(current_user, state: state, type: object.class.name, target_id: object.id).execute + end + end +end diff --git a/app/graphql/types/design_management/design_type.rb b/app/graphql/types/design_management/design_type.rb index 3c84dc151bda168a80bbf7e1e6653e366f84ffc7..4e11a7aaf094bde231708ab1d5d6091085b9e3b2 100644 --- a/app/graphql/types/design_management/design_type.rb +++ b/app/graphql/types/design_management/design_type.rb @@ -12,6 +12,7 @@ class DesignType < BaseObject implements(Types::Notes::NoteableType) implements(Types::DesignManagement::DesignFields) + implements(Types::CurrentUserTodos) field :versions, Types::DesignManagement::VersionType.connection_type, diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb index 37af317996dca9eadb810a03e7d7cb181b01203d..811323c6345beb1ef260a856c97653d7d11a328f 100644 --- a/app/graphql/types/issue_type.rb +++ b/app/graphql/types/issue_type.rb @@ -7,6 +7,7 @@ class IssueType < BaseObject connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_issue diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb index 01b02b7976f9548fb863e86f986193b098628f6f..805ae111ff71be7bf3f8326a2e2222fb95f03840 100644 --- a/app/graphql/types/merge_request_type.rb +++ b/app/graphql/types/merge_request_type.rb @@ -7,6 +7,7 @@ class MergeRequestType < BaseObject connection_type_class(Types::CountableConnectionType) implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) authorize :read_merge_request diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb index 08e7fabeb74ec8a816eae41271ba46132153ab80..4f21da3d8979f18207433fb75846bd9baf192b22 100644 --- a/app/graphql/types/todo_type.rb +++ b/app/graphql/types/todo_type.rb @@ -26,7 +26,7 @@ class TodoType < BaseObject resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find } field :author, Types::UserType, - description: 'The owner of this todo', + description: 'The author of this todo', null: false, resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find } diff --git a/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml new file mode 100644 index 0000000000000000000000000000000000000000..221279e8b19b6cfd64de1ebb763aff8761fc49fb --- /dev/null +++ b/changelogs/unreleased/198439-expose-pending-todo-in-graphql.yml @@ -0,0 +1,5 @@ +--- +title: Expose the todos of the current user on relevant objects in GraphQL +merge_request: 40555 +author: +type: added diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index 492bcfe952b4040a25dd44d7666cc3872e1e8209..04935a0526f2024f70a99e7fb321da6c4627be2a 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2939,6 +2939,38 @@ type CreateSnippetPayload { snippet: Snippet } +interface CurrentUserTodos { + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! +} + """ Autogenerated input type of DastOnDemandScanCreate """ @@ -3496,7 +3528,37 @@ type DeleteJobsResponse { """ A single design """ -type Design implements DesignFields & Noteable { +type Design implements CurrentUserTodos & DesignFields & Noteable { + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ The diff refs for this design """ @@ -4891,7 +4953,7 @@ type EnvironmentEdge { """ Represents an epic. """ -type Epic implements Noteable { +type Epic implements CurrentUserTodos & Noteable { """ Author of the epic """ @@ -4994,6 +5056,36 @@ type Epic implements Noteable { """ createdAt: Time + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Number of open and closed descendant epics and issues """ @@ -5433,7 +5525,7 @@ type EpicHealthStatus { """ Relationship between an epic and an issue """ -type EpicIssue implements Noteable { +type EpicIssue implements CurrentUserTodos & Noteable { """ Alert associated to this issue """ @@ -5489,6 +5581,36 @@ type EpicIssue implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Description of the issue """ @@ -7313,7 +7435,7 @@ enum IssuableState { opened } -type Issue implements Noteable { +type Issue implements CurrentUserTodos & Noteable { """ Alert associated to this issue """ @@ -7369,6 +7491,36 @@ type Issue implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Description of the issue """ @@ -8904,7 +9056,7 @@ type MemberInterfaceEdge { node: MemberInterface } -type MergeRequest implements Noteable { +type MergeRequest implements CurrentUserTodos & Noteable { """ Indicates if members of the target project can push to the fork """ @@ -8980,6 +9132,36 @@ type MergeRequest implements Noteable { """ createdAt: Time! + """ + Todos for the current user + """ + currentUserTodos( + """ + Returns the elements in the list that come after the specified cursor. + """ + after: String + + """ + Returns the elements in the list that come before the specified cursor. + """ + before: String + + """ + Returns the first _n_ elements from the list. + """ + first: Int + + """ + Returns the last _n_ elements from the list. + """ + last: Int + + """ + State of the todos + """ + state: TodoStateEnum + ): TodoConnection! + """ Default merge commit message of the merge request """ @@ -16113,7 +16295,7 @@ type Todo { action: TodoActionEnum! """ - The owner of this todo + The author of this todo """ author: User! diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index ff30c1455e4f5fa05da8e9c260f84d6213cfc8c3..035f9f65cef9c6d705315410fe813d1ddde25022 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -7967,6 +7967,110 @@ "enumValues": null, "possibleTypes": null }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "description": null, + "fields": [ + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Design", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Epic", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "EpicIssue", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "Issue", + "ofType": null + }, + { + "kind": "OBJECT", + "name": "MergeRequest", + "ofType": null + } + ] + }, { "kind": "INPUT_OBJECT", "name": "DastOnDemandScanCreateInput", @@ -9542,6 +9646,73 @@ "name": "Design", "description": "A single design", "fields": [ + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "diffRefs", "description": "The diff refs for this design", @@ -9921,6 +10092,11 @@ "kind": "INTERFACE", "name": "DesignFields", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -13980,6 +14156,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "descendantCounts", "description": "Number of open and closed descendant epics and issues", @@ -14759,6 +15002,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -15357,6 +15605,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "description", "description": "Description of the issue", @@ -16137,6 +16452,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -20333,6 +20653,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "description", "description": "Description of the issue", @@ -21085,6 +21472,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -24962,6 +25354,73 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "currentUserTodos", + "description": "Todos for the current user", + "args": [ + { + "name": "after", + "description": "Returns the elements in the list that come after the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "before", + "description": "Returns the elements in the list that come before the specified cursor.", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "first", + "description": "Returns the first _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "last", + "description": "Returns the last _n_ elements from the list.", + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "defaultValue": null + }, + { + "name": "state", + "description": "State of the todos", + "type": { + "kind": "ENUM", + "name": "TodoStateEnum", + "ofType": null + }, + "defaultValue": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "TodoConnection", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "defaultMergeCommitMessage", "description": "Default merge commit message of the merge request", @@ -26120,6 +26579,11 @@ "kind": "INTERFACE", "name": "Noteable", "ofType": null + }, + { + "kind": "INTERFACE", + "name": "CurrentUserTodos", + "ofType": null } ], "enumValues": null, @@ -47417,7 +47881,7 @@ }, { "name": "author", - "description": "The owner of this todo", + "description": "The author of this todo", "args": [ ], diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 2262e1b6a3eb7dfcfb666756cc1da55412061acb..db1f204ccf1b8788ffa81b36bec38cefd54df886 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -2362,7 +2362,7 @@ Representing a todo entry | Name | Type | Description | | --- | ---- | ---------- | | `action` | TodoActionEnum! | Action of the todo | -| `author` | User! | The owner of this todo | +| `author` | User! | The author of this todo | | `body` | String! | Body of the todo | | `createdAt` | Time! | Timestamp this todo was created | | `group` | Group | Group this todo is associated with | diff --git a/ee/app/graphql/types/epic_type.rb b/ee/app/graphql/types/epic_type.rb index 12b866f4b607791ef46c162e6d995584e5e65ddb..998863ee747ac262158d81c634b0b389f42d16af 100644 --- a/ee/app/graphql/types/epic_type.rb +++ b/ee/app/graphql/types/epic_type.rb @@ -14,6 +14,7 @@ class EpicType < BaseObject present_using EpicPresenter implements(Types::Notes::NoteableType) + implements(Types::CurrentUserTodos) field :id, GraphQL::ID_TYPE, null: false, description: 'ID of the epic' diff --git a/ee/spec/graphql/types/epic_type_spec.rb b/ee/spec/graphql/types/epic_type_spec.rb index 22b5ef82ae0ec136ab55777bf2f16b60032212c2..62318adbac518f1cee071a76cff186fd54f87b54 100644 --- a/ee/spec/graphql/types/epic_type_spec.rb +++ b/ee/spec/graphql/types/epic_type_spec.rb @@ -12,9 +12,12 @@ web_path web_url relation_path reference issues user_permissions notes discussions relative_position subscribed participants descendant_counts descendant_weight_sum upvotes downvotes health_status + current_user_todos ] end + it { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Epic) } it { expect(described_class.graphql_name).to eq('Epic') } diff --git a/spec/graphql/types/current_user_todos_type_spec.rb b/spec/graphql/types/current_user_todos_type_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..a0015e967886bc98735b93d53a9ccdda1721ebc3 --- /dev/null +++ b/spec/graphql/types/current_user_todos_type_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CurrentUserTodos'] do + specify { expect(described_class.graphql_name).to eq('CurrentUserTodos') } + + specify { expect(described_class).to have_graphql_fields(:current_user_todos).only } +end diff --git a/spec/graphql/types/design_management/design_type_spec.rb b/spec/graphql/types/design_management/design_type_spec.rb index 7a38b397965c19ed9d97eb7ced474692c0166a28..cae98a013e11ca2b1f626356cca195428183b02f 100644 --- a/spec/graphql/types/design_management/design_type_spec.rb +++ b/spec/graphql/types/design_management/design_type_spec.rb @@ -3,8 +3,10 @@ require 'spec_helper' RSpec.describe GitlabSchema.types['Design'] do + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it_behaves_like 'a GraphQL type with design fields' do - let(:extra_design_fields) { %i[notes discussions versions] } + let(:extra_design_fields) { %i[notes current_user_todos discussions versions] } let_it_be(:design) { create(:design, :with_versions) } let(:object_id) { GitlabSchema.id_from_object(design) } let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) } diff --git a/spec/graphql/types/issue_type_spec.rb b/spec/graphql/types/issue_type_spec.rb index db2a1751be07ba86b0704df0c9a39188bfceb37b..c55e624dd1116eb9e2e583e593d245ef98db8835 100644 --- a/spec/graphql/types/issue_type_spec.rb +++ b/spec/graphql/types/issue_type_spec.rb @@ -11,11 +11,13 @@ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) } + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it 'has specific fields' do fields = %i[id iid title description state reference author assignees participants labels milestone due_date confidential discussion_locked upvotes downvotes user_notes_count web_path web_url relative_position subscribed time_estimate total_time_spent closed_at created_at updated_at task_completion_status - designs design_collection alert_management_alert severity] + designs design_collection alert_management_alert severity current_user_todos] fields.each do |field_name| expect(described_class).to have_graphql_field(field_name) diff --git a/spec/graphql/types/merge_request_type_spec.rb b/spec/graphql/types/merge_request_type_spec.rb index a9a74114dda3287b655186d4e2887a3718061389..1279f01f104c733c71c7d57ed1ed896e8dfc071f 100644 --- a/spec/graphql/types/merge_request_type_spec.rb +++ b/spec/graphql/types/merge_request_type_spec.rb @@ -9,6 +9,8 @@ specify { expect(described_class.interfaces).to include(Types::Notes::NoteableType) } + specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) } + it 'has the expected fields' do expected_fields = %w[ notes discussions user_permissions id iid title title_html description @@ -24,7 +26,7 @@ source_branch_exists target_branch_exists upvotes downvotes head_pipeline pipelines task_completion_status milestone assignees participants subscribed labels discussion_locked time_estimate - total_time_spent reference author merged_at commit_count + total_time_spent reference author merged_at commit_count current_user_todos ] if Gitlab.ee? diff --git a/spec/requests/api/graphql/current_user_todos_spec.rb b/spec/requests/api/graphql/current_user_todos_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..b657f15d0e9849205f2e7839ce5eee5af8d2cd03 --- /dev/null +++ b/spec/requests/api/graphql/current_user_todos_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'A Todoable that implements the CurrentUserTodos interface' do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + let_it_be(:todoable) { create(:issue, project: project) } + let_it_be(:done_todo) { create(:todo, state: :done, target: todoable, user: current_user) } + let_it_be(:pending_todo) { create(:todo, state: :pending, target: todoable, user: current_user) } + let(:state) { 'null' } + + let(:todoable_response) do + graphql_data_at(:project, :issue, :currentUserTodos, :nodes) + end + + let(:query) do + <<~GQL + { + project(fullPath: "#{project.full_path}") { + issue(iid: "#{todoable.iid}") { + currentUserTodos(state: #{state}) { + nodes { + #{all_graphql_fields_for('Todo', max_depth: 1)} + } + } + } + } + } + GQL + end + + it 'returns todos of the current user' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)), + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + + it 'does not return todos of another user', :aggregate_failures do + post_graphql(query, current_user: create(:user)) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + it 'does not error when there is no logged in user', :aggregate_failures do + post_graphql(query) + + expect(response).to have_gitlab_http_status(:success) + expect(todoable_response).to be_empty + end + + context 'when `state` argument is `pending`' do + let(:state) { 'pending' } + + it 'returns just the pending todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(pending_todo)) + ) + end + end + + context 'when `state` argument is `done`' do + let(:state) { 'done' } + + it 'returns just the done todo' do + post_graphql(query, current_user: current_user) + + expect(todoable_response).to contain_exactly( + a_hash_including('id' => global_id_of(done_todo)) + ) + end + end +end