Commit 48f11f06 authored by Felipe Artur's avatar Felipe Artur

Allow to mark a requirement as satisfied

Allow to mark requirement as satisfied with GraphQL
parent 9cc1bd95
...@@ -14254,6 +14254,11 @@ type Requirement { ...@@ -14254,6 +14254,11 @@ type Requirement {
""" """
iid: ID! iid: ID!
"""
Latest requirement test report state
"""
lastTestReportState: TestReportState
""" """
Project to which the requirement belongs Project to which the requirement belongs
""" """
...@@ -17314,6 +17319,11 @@ input UpdateRequirementInput { ...@@ -17314,6 +17319,11 @@ input UpdateRequirementInput {
""" """
iid: String! iid: String!
"""
Creates a test report for the requirement with the given state
"""
lastTestReportState: TestReportState
""" """
The project full path the requirement is associated with The project full path the requirement is associated with
""" """
......
...@@ -41613,6 +41613,20 @@ ...@@ -41613,6 +41613,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "lastTestReportState",
"description": "Latest requirement test report state",
"args": [
],
"type": {
"kind": "ENUM",
"name": "TestReportState",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "project", "name": "project",
"description": "Project to which the requirement belongs", "description": "Project to which the requirement belongs",
...@@ -50832,6 +50846,16 @@ ...@@ -50832,6 +50846,16 @@
}, },
"defaultValue": null "defaultValue": null
}, },
{
"name": "lastTestReportState",
"description": "Creates a test report for the requirement with the given state",
"type": {
"kind": "ENUM",
"name": "TestReportState",
"ofType": null
},
"defaultValue": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
...@@ -1998,6 +1998,7 @@ Represents a requirement ...@@ -1998,6 +1998,7 @@ Represents a requirement
| `createdAt` | Time! | Timestamp of when the requirement was created | | `createdAt` | Time! | Timestamp of when the requirement was created |
| `id` | ID! | ID of the requirement | | `id` | ID! | ID of the requirement |
| `iid` | ID! | Internal ID of the requirement | | `iid` | ID! | Internal ID of the requirement |
| `lastTestReportState` | TestReportState | Latest requirement test report state |
| `project` | Project! | Project to which the requirement belongs | | `project` | Project! | Project to which the requirement belongs |
| `state` | RequirementState! | State of the requirement | | `state` | RequirementState! | State of the requirement |
| `title` | String | Title of the requirement | | `title` | String | Title of the requirement |
......
...@@ -29,10 +29,14 @@ module Mutations ...@@ -29,10 +29,14 @@ module Mutations
required: true, required: true,
description: 'The project full path the requirement is associated with' description: 'The project full path the requirement is associated with'
argument :last_test_report_state, Types::RequirementsManagement::TestReportStateEnum,
required: false,
description: 'Creates a test report for the requirement with the given state'
def ready?(**args) def ready?(**args)
if args.values_at(:title, :state).compact.blank? if args.values_at(:title, :state, :last_test_report_state).compact.blank?
raise Gitlab::Graphql::Errors::ArgumentError, raise Gitlab::Graphql::Errors::ArgumentError,
'title or state argument is required' 'title, state or last_test_report_state argument is required'
end end
super super
......
...@@ -18,7 +18,8 @@ module Types ...@@ -18,7 +18,8 @@ module Types
description: 'Title of the requirement' description: 'Title of the requirement'
field :state, RequirementsManagement::RequirementStateEnum, null: false, field :state, RequirementsManagement::RequirementStateEnum, null: false,
description: 'State of the requirement' description: 'State of the requirement'
field :last_test_report_state, RequirementsManagement::TestReportStateEnum, null: true, complexity: 5,
description: 'Latest requirement test report state'
field :project, ProjectType, null: false, field :project, ProjectType, null: false,
description: 'Project to which the requirement belongs', description: 'Project to which the requirement belongs',
resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.project_id).find } resolve: -> (obj, _args, _ctx) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, obj.project_id).find }
......
...@@ -58,5 +58,13 @@ module RequirementsManagement ...@@ -58,5 +58,13 @@ module RequirementsManagement
def resource_parent def resource_parent
project project
end end
def last_test_report_state
test_reports.last&.state
end
def last_test_report_manually_created?
test_reports.last&.build.nil?
end
end end
end end
...@@ -31,6 +31,16 @@ module RequirementsManagement ...@@ -31,6 +31,16 @@ module RequirementsManagement
bulk_insert!(reports) bulk_insert!(reports)
end end
def build_report(author: nil, state:, requirement:, build: nil, timestamp: Time.current)
new(
requirement_id: requirement.id,
build_id: build&.id,
author_id: build&.user_id || author&.id,
created_at: timestamp,
state: state
)
end
private private
def passed_reports_for_all_requirements(build, timestamp) def passed_reports_for_all_requirements(build, timestamp)
...@@ -55,16 +65,6 @@ module RequirementsManagement ...@@ -55,16 +65,6 @@ module RequirementsManagement
end end
end end
end end
def build_report(state:, requirement:, build:, timestamp:)
new(
requirement_id: requirement.id,
build_id: build.id,
author_id: build.user_id,
created_at: timestamp,
state: state
)
end
end end
end end
end end
...@@ -8,11 +8,23 @@ module RequirementsManagement ...@@ -8,11 +8,23 @@ module RequirementsManagement
attrs = whitelisted_requirement_params attrs = whitelisted_requirement_params
requirement.update(attrs) requirement.update(attrs)
create_test_report_for(requirement) if manually_create_test_report?
requirement requirement
end end
private private
def manually_create_test_report?
params[:last_test_report_state].present?
end
def create_test_report_for(requirement)
return unless can?(current_user, :create_requirement_test_report, project)
TestReport.build_report(requirement: requirement, state: params[:last_test_report_state], author: current_user).save!
end
def whitelisted_requirement_params def whitelisted_requirement_params
params.slice(:title, :state) params.slice(:title, :state)
end end
......
---
title: Allow requirement status to be updated with GraphQL
merge_request: 39371
author:
type: added
...@@ -21,13 +21,14 @@ RSpec.describe Mutations::RequirementsManagement::UpdateRequirement do ...@@ -21,13 +21,14 @@ RSpec.describe Mutations::RequirementsManagement::UpdateRequirement do
project_path: project.full_path, project_path: project.full_path,
iid: requirement.iid.to_s, iid: requirement.iid.to_s,
title: 'foo', title: 'foo',
state: 'archived' state: 'archived',
last_test_report_state: 'passed'
) )
end end
it_behaves_like 'requirements not available' it_behaves_like 'requirements not available'
context 'when the user can update the epic' do context 'when the user can update the requirement' do
before do before do
project.add_developer(user) project.add_developer(user)
end end
...@@ -40,7 +41,8 @@ RSpec.describe Mutations::RequirementsManagement::UpdateRequirement do ...@@ -40,7 +41,8 @@ RSpec.describe Mutations::RequirementsManagement::UpdateRequirement do
it 'updates new requirement', :aggregate_failures do it 'updates new requirement', :aggregate_failures do
expect(subject[:requirement]).to have_attributes( expect(subject[:requirement]).to have_attributes(
title: 'foo', title: 'foo',
state: 'archived' state: 'archived',
last_test_report_state: 'passed'
) )
expect(subject[:errors]).to be_empty expect(subject[:errors]).to be_empty
end end
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['Requirement'] do RSpec.describe GitlabSchema.types['Requirement'] do
fields = %i[id iid title state project author created_at updated_at user_permissions test_reports] fields = %i[id iid title state last_test_report_state project author created_at updated_at user_permissions test_reports]
it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Requirement) } it { expect(described_class).to expose_permissions_using(Types::PermissionTypes::Requirement) }
......
...@@ -77,4 +77,50 @@ RSpec.describe RequirementsManagement::Requirement do ...@@ -77,4 +77,50 @@ RSpec.describe RequirementsManagement::Requirement do
end end
end end
end end
describe '#last_test_report_state' do
let_it_be(:requirement) { create(:requirement) }
context 'when latest test report is passing' do
it 'returns passing' do
create(:test_report, requirement: requirement, state: :passed, build: nil)
expect(requirement.last_test_report_state).to eq('passed')
end
end
context 'when latest test report is failing' do
it 'returns failing' do
create(:test_report, requirement: requirement, state: :failed, build: nil)
expect(requirement.last_test_report_state).to eq('failed')
end
end
context 'when there are no test reports' do
it 'returns nil' do
expect(requirement.last_test_report_state).to eq(nil)
end
end
end
describe '#status_manually_updated' do
let_it_be(:requirement) { create(:requirement) }
context 'when latest test report has a build' do
it 'returns false' do
create(:test_report, requirement: requirement, state: :passed)
expect(requirement.last_test_report_manually_created?).to eq(false)
end
end
context 'when latest test report does not have a build' do
it 'returns true' do
create(:test_report, requirement: requirement, state: :passed, build: nil)
expect(requirement.last_test_report_manually_created?).to eq(true)
end
end
end
end end
...@@ -96,4 +96,36 @@ RSpec.describe RequirementsManagement::TestReport do ...@@ -96,4 +96,36 @@ RSpec.describe RequirementsManagement::TestReport do
end end
end end
end end
describe '.build_report' do
let_it_be(:user) { create(:user) }
let_it_be(:build_author) { create(:user) }
let_it_be(:build) { create(:ci_build, author: build_author) }
let_it_be(:requirement) { create(:requirement, state: :opened) }
let(:now) { Time.current }
context 'when build is passed as argument' do
it 'builds test report with correct attributes' do
test_report = described_class.build_report(requirement: requirement, author: user, state: 'failed', build: build, timestamp: now)
expect(test_report.author).to eq(build.author)
expect(test_report.build).to eq(build)
expect(test_report.requirement).to eq(requirement)
expect(test_report.state).to eq('failed')
expect(test_report.created_at).to eq(now)
end
end
context 'when build is not passed as argument' do
it 'builds test report with correct attributes' do
test_report = described_class.build_report(requirement: requirement, author: user, state: 'passed', timestamp: now)
expect(test_report.author).to eq(user)
expect(test_report.build).to eq(nil)
expect(test_report.requirement).to eq(requirement)
expect(test_report.state).to eq('passed')
expect(test_report.created_at).to eq(now)
end
end
end
end end
...@@ -81,7 +81,7 @@ RSpec.describe 'Updating a Requirement' do ...@@ -81,7 +81,7 @@ RSpec.describe 'Updating a Requirement' do
let(:attributes) { {} } let(:attributes) { {} }
it_behaves_like 'a mutation that returns top-level errors', it_behaves_like 'a mutation that returns top-level errors',
errors: ['title or state argument is required'] errors: ['title, state or last_test_report_state argument is required']
end end
context 'when requirements_management flag is disabled' do context 'when requirements_management flag is disabled' do
......
...@@ -38,6 +38,46 @@ RSpec.describe RequirementsManagement::UpdateRequirementService do ...@@ -38,6 +38,46 @@ RSpec.describe RequirementsManagement::UpdateRequirementService do
author_id: params[:author_id] author_id: params[:author_id]
) )
end end
context 'when updating last test report state' do
context 'as passing' do
it 'creates passing test report with null build_id' do
service = described_class.new(project, user, { last_test_report_state: 'passed' })
expect { service.execute(requirement) }.to change { RequirementsManagement::TestReport.count }.from(0).to(1)
test_report = requirement.test_reports.last
expect(requirement.last_test_report_state).to eq('passed')
expect(requirement.last_test_report_manually_created?).to eq(true)
expect(test_report.state).to eq('passed')
expect(test_report.build).to eq(nil)
expect(test_report.author).to eq(user)
end
end
context 'as failed' do
it 'creates failing test report with null build_id' do
service = described_class.new(project, user, { last_test_report_state: 'failed' })
expect { service.execute(requirement) }.to change { RequirementsManagement::TestReport.count }.from(0).to(1)
test_report = requirement.test_reports.last
expect(requirement.last_test_report_state).to eq('failed')
expect(requirement.last_test_report_manually_created?).to eq(true)
expect(test_report.state).to eq('failed')
expect(test_report.build).to eq(nil)
expect(test_report.author).to eq(user)
end
end
context 'when user cannot create test reports' do
it 'does not create test report' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?).with(user, :create_requirement_test_report, project).and_return(false)
service = described_class.new(project, user, { last_test_report_state: 'failed' })
expect { service.execute(requirement) }.not_to change { RequirementsManagement::TestReport.count }
end
end
end
end end
context 'when user is not allowed to update requirements' do context 'when user is not allowed to update requirements' do
......
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