Commit a031673c authored by Patrick Derichs's avatar Patrick Derichs

Add GraphQL mutation to restore a Todo

Also add specs
parent 34968253
# frozen_string_literal: true
module Mutations
module Todos
class Restore < ::Mutations::Todos::Base
graphql_name 'TodoRestore'
authorize :update_todo
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'The global id of the todo to restore'
field :todo, Types::TodoType,
null: false,
description: 'The requested todo'
def resolve(id:)
todo = authorized_find!(id: id)
restore(todo.id) if todo.done?
{
todo: todo.reset,
errors: errors_on_object(todo)
}
end
private
def restore(id)
TodoService.new.mark_todos_as_pending_by_ids([id], current_user)
end
end
end
end
...@@ -21,6 +21,7 @@ module Types ...@@ -21,6 +21,7 @@ module Types
mount_mutation Mutations::Notes::Update mount_mutation Mutations::Notes::Update
mount_mutation Mutations::Notes::Destroy mount_mutation Mutations::Notes::Destroy
mount_mutation Mutations::Todos::MarkDone mount_mutation Mutations::Todos::MarkDone
mount_mutation Mutations::Todos::Restore
end end
end end
......
---
title: Add GraphQL mutation to restore a Todo
merge_request: 20261
author:
type: added
...@@ -3519,6 +3519,7 @@ type Mutation { ...@@ -3519,6 +3519,7 @@ type Mutation {
mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload mergeRequestSetWip(input: MergeRequestSetWipInput!): MergeRequestSetWipPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload toggleAwardEmoji(input: ToggleAwardEmojiInput!): ToggleAwardEmojiPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
updateNote(input: UpdateNoteInput!): UpdateNotePayload updateNote(input: UpdateNoteInput!): UpdateNotePayload
...@@ -4992,6 +4993,41 @@ type TodoMarkDonePayload { ...@@ -4992,6 +4993,41 @@ type TodoMarkDonePayload {
todo: Todo! todo: Todo!
} }
"""
Autogenerated input type of TodoRestore
"""
input TodoRestoreInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The global id of the todo to restore
"""
id: ID!
}
"""
Autogenerated return type of TodoRestore
"""
type TodoRestorePayload {
"""
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 { enum TodoStateEnum {
done done
pending pending
......
...@@ -14328,6 +14328,33 @@ ...@@ -14328,6 +14328,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "todoRestore",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "TodoRestoreInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "TodoRestorePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "toggleAwardEmoji", "name": "toggleAwardEmoji",
"description": null, "description": null,
...@@ -16692,6 +16719,112 @@ ...@@ -16692,6 +16719,112 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "OBJECT",
"name": "TodoRestorePayload",
"description": "Autogenerated return type of TodoRestore",
"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": "TodoRestoreInput",
"description": "Autogenerated input type of TodoRestore",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global id of the todo to restore",
"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", "kind": "OBJECT",
"name": "DesignManagementUploadPayload", "name": "DesignManagementUploadPayload",
......
...@@ -777,6 +777,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph ...@@ -777,6 +777,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `todo` | Todo! | The requested todo | | `todo` | Todo! | The requested todo |
### TodoRestorePayload
| 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 ### ToggleAwardEmojiPayload
| Name | Type | Description | | Name | Type | Description |
......
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Todos::Restore 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: :done) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) }
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }) }
describe '#resolve' do
it 'restores a single todo' do
result = restore_mutation(todo1)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
todo = result[:todo]
expect(todo.id).to eq(todo1.id)
expect(todo.state).to eq('pending')
end
it 'handles a todo which is already pending as expected' do
result = restore_mutation(todo2)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
todo = result[:todo]
expect(todo.id).to eq(todo2.id)
expect(todo.state).to eq('pending')
end
it 'ignores requests for todos which do not belong to the current user' do
expect { restore_mutation(other_user_todo) }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
expect(todo1.reload.state).to eq('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
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('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
end
end
def restore_mutation(todo)
mutation.resolve(id: global_id_of(todo))
end
def global_id_of(todo)
todo.to_global_id.to_s
end
end
# frozen_string_literal: true
require 'spec_helper'
describe 'Restoring Todos' 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: :done) }
let_it_be(:todo2) { create(:todo, user: current_user, author: author, state: :pending) }
let_it_be(:other_user_todo) { create(:todo, user: other_user, author: author, state: :done) }
let(:input) { { id: todo1.to_global_id.to_s } }
let(:mutation) do
graphql_mutation(:todo_restore, input,
<<-QL.strip_heredoc
clientMutationId
errors
todo {
id
state
}
QL
)
end
def mutation_response
graphql_mutation_response(:todo_restore)
end
it 'restores a single todo' do
post_graphql_mutation(mutation, current_user: current_user)
expect(todo1.reload.state).to eq('pending')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo1.to_global_id.to_s)
expect(todo['state']).to eq('pending')
end
context 'when todo is already marked pending' 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('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
todo = mutation_response['todo']
expect(todo['id']).to eq(todo2.to_global_id.to_s)
expect(todo['state']).to eq('pending')
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('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
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('done')
expect(todo2.reload.state).to eq('pending')
expect(other_user_todo.reload.state).to eq('done')
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