Commit 415d2b14 authored by Patrick Derichs's avatar Patrick Derichs

Add graphql mutation to mark todos as done

Also add specs

Change names of mutations

Remove mark all done mutation

Inherit from BaseMutation

Add policy update_todo

Just support update of one todo at a time

Remove obsolete scope

Refactor spec in this location to just test result
of #resolve

Add requests spec
parent 4d57d18d
# frozen_string_literal: true
module Mutations
module Todos
class Base < ::Mutations::BaseMutation
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def to_global_id(id)
::URI::GID.build(app: GlobalID.app, model_name: Todo.name, model_id: id, params: nil).to_s
end
end
end
end
# frozen_string_literal: true
module Mutations
module Todos
class MarkDone < ::Mutations::Todos::Base
graphql_name 'TodoMarkDone'
authorize :update_todo
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the todo to mark as done'
field :todo, Types::TodoType,
null: false,
description: 'The requested todo'
# rubocop: disable CodeReuse/ActiveRecord
def resolve(id:)
todo = authorized_find!(id: id)
mark_done(Todo.where(id: todo.id)) unless todo.done?
{
todo: todo.reset,
errors: errors_on_object(todo)
}
end
# rubocop: enable CodeReuse/ActiveRecord
private
def mark_done(todo)
TodoService.new.mark_todos_as_done(todo, current_user)
end
end
end
end
......@@ -16,6 +16,7 @@ module Types
mount_mutation Mutations::Notes::Create::ImageDiffNote, calls_gitaly: true
mount_mutation Mutations::Notes::Update
mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Todos::MarkDone
end
end
......
......@@ -160,6 +160,10 @@ class Todo < ApplicationRecord
action == ASSIGNED
end
def done?
state == 'done'
end
def action_name
ACTION_NAMES[action]
end
......
......@@ -7,4 +7,5 @@ class TodoPolicy < BasePolicy
end
rule { own_todo }.enable :read_todo
rule { own_todo }.enable :update_todo
end
---
title: Mark todo done by GraphQL API
merge_request: 18581
author:
type: added
......@@ -3413,6 +3413,7 @@ type Mutation {
mergeRequestSetMilestone(input: MergeRequestSetMilestoneInput!): MergeRequestSetMilestonePayload
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
updateNote(input: UpdateNoteInput!): UpdateNotePayload
......@@ -4826,6 +4827,41 @@ type TodoEdge {
node: Todo
}
"""
Autogenerated input type of TodoMarkDone
"""
input TodoMarkDoneInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the todo to mark as done
"""
id: ID!
}
"""
Autogenerated return type of TodoMarkDone
"""
type TodoMarkDonePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The requested todo
"""
todo: Todo!
}
enum TodoStateEnum {
done
pending
......
......@@ -14557,6 +14557,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todoMarkDone",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TodoMarkDoneInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TodoMarkDonePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "toggleAwardEmoji",
"description": null,
......@@ -16230,6 +16257,112 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "TodoMarkDonePayload",
"description": "Autogenerated return type of TodoMarkDone",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Reasons why the mutation failed.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "todo",
"description": "The requested todo",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "Todo",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "TodoMarkDoneInput",
"description": "Autogenerated input type of TodoMarkDone",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the todo to mark as done",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DesignManagementUploadPayload",
......
......@@ -756,6 +756,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `state` | TodoStateEnum! | State of the todo |
| `createdAt` | Time! | Timestamp this todo was created |
### TodoMarkDonePayload
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `todo` | Todo! | The requested todo |
### ToggleAwardEmojiPayload
| Name | Type | Description |
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Todos::MarkDone do
let_it_be(:current_user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }) }
describe '#resolve' do
it 'marks a single todo as done' do
result = mark_done_mutation(todo1)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = result[:todo]
expect(todo.id).to eq(todo1.id)
expect(todo.state).to eq('done')
end
it 'handles a todo which is already done as expected' do
result = mark_done_mutation(todo2)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = result[:todo]
expect(todo.id).to eq(todo2.id)
expect(todo.state).to eq('done')
end
it 'ignores requests for todos which do not belong to the current user' do
expect { mark_done_mutation(other_user_todo) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
it 'ignores invalid GIDs' do
expect { mutation.resolve(id: 'invalid_gid') }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
def mark_done_mutation(todo)
mutation.resolve(id: global_id_of(todo))
end
def global_id_of(todo)
todo.to_global_id.to_s
end
end
......@@ -150,6 +150,19 @@ describe Todo do
end
end
describe '#done?' do
let_it_be(:todo1) { create(:todo, state: :pending) }
let_it_be(:todo2) { create(:todo, state: :done) }
it 'returns true for todos with done state' do
expect(todo2.done?).to be_truthy
end
it 'returns false for todos with state pending' do
expect(todo1.done?).to be_falsey
end
end
describe '#self_assigned?' do
let(:user_1) { build(:user) }
......
# frozen_string_literal: true
require 'spec_helper'
describe 'Marking todos done' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:author) { create(:user) }
let_it_be(:other_user) { create(:user) }
let_it_be(:todo1) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :done) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :pending) }
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
graphql_mutation(:todo_mark_done, input,
<<-QL.strip_heredoc
clientMutationId
errors
todo {
id
state
}
QL
)
end
def mutation_response
graphql_mutation_response(:todo_mark_done)
end
it 'marks a single todo as done' do
post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo1.to_global_id.to_s)
expect(todo['state']).to eq('done')
end
context 'when todo is already marked done' do
let(:input) { { id: todo2.to_global_id.to_s } }
it 'has the expected response' do
post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo2.to_global_id.to_s)
expect(todo['state']).to eq('done')
end
end
context 'when todo does not belong to requesting user' do
let(:input) { { id: other_user_todo.to_global_id.to_s } }
let(:access_error) { 'The resource that you are attempting to access does not exist or you don\'t have permission to perform this action' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors).not_to be_blank
expect(errors.first['message']).to eq(access_error)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
context 'when using an invalid gid' do
let(:input) { { id: 'invalid_gid' } }
let(:invalid_gid_error) { 'invalid_gid is not a valid GitLab id.' }
it 'contains the expected error' do
post_graphql_mutation(mutation, current_user: current_user)
errors = json_response['errors']
expect(errors).not_to be_blank
expect(errors.first['message']).to eq(invalid_gid_error)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('done')
expect(other_user_todo.reload.state).to eq('pending')
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment