Commit 925d5cb8 authored by Jan Provaznik's avatar Jan Provaznik

Merge branch '290813-graphql-mutation-export-requirements' into 'master'

Add GraphQL mutation to export Requirements

See merge request gitlab-org/gitlab!50546
parents afd7b679 d4d854b3
......@@ -9442,6 +9442,56 @@ enum EpicWildcardId {
NONE
}
"""
Autogenerated input type of ExportRequirements
"""
input ExportRequirementsInput {
"""
Filter requirements by author username.
"""
authorUsername: [String!]
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Full project path the requirements are associated with.
"""
projectPath: ID!
"""
Search query for requirement title.
"""
search: String
"""
List requirements by sort order.
"""
sort: Sort
"""
Filter requirements by state.
"""
state: RequirementState
}
"""
Autogenerated return type of ExportRequirements
"""
type ExportRequirementsPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Represents an external issue
"""
......@@ -15382,6 +15432,7 @@ type Mutation {
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
exportRequirements(input: ExportRequirementsInput!): ExportRequirementsPayload
httpIntegrationCreate(input: HttpIntegrationCreateInput!): HttpIntegrationCreatePayload
httpIntegrationDestroy(input: HttpIntegrationDestroyInput!): HttpIntegrationDestroyPayload
httpIntegrationResetToken(input: HttpIntegrationResetTokenInput!): HttpIntegrationResetTokenPayload
......@@ -18465,7 +18516,7 @@ type Project {
"""
requirement(
"""
Filter requirements by author username
Filter requirements by author username.
"""
authorUsername: [String!]
......@@ -18480,17 +18531,17 @@ type Project {
iids: [ID!]
"""
Search query for requirement title
Search query for requirement title.
"""
search: String
"""
List requirements by sort order
List requirements by sort order.
"""
sort: Sort
"""
Filter requirements by state
Filter requirements by state.
"""
state: RequirementState
): Requirement
......@@ -18510,7 +18561,7 @@ type Project {
after: String
"""
Filter requirements by author username
Filter requirements by author username.
"""
authorUsername: [String!]
......@@ -18540,17 +18591,17 @@ type Project {
last: Int
"""
Search query for requirement title
Search query for requirement title.
"""
search: String
"""
List requirements by sort order
List requirements by sort order.
"""
sort: Sort
"""
Filter requirements by state
Filter requirements by state.
"""
state: RequirementState
): RequirementConnection
......
......@@ -1530,6 +1530,15 @@ Autogenerated return type of EpicTreeReorder.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### ExportRequirementsPayload
Autogenerated return type of ExportRequirements.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### ExternalIssue
Represents an external issue.
......
......@@ -25,6 +25,7 @@ module EE
mount_mutation ::Mutations::Iterations::Create
mount_mutation ::Mutations::Iterations::Update
mount_mutation ::Mutations::RequirementsManagement::CreateRequirement
mount_mutation ::Mutations::RequirementsManagement::ExportRequirements
mount_mutation ::Mutations::RequirementsManagement::UpdateRequirement
mount_mutation ::Mutations::Vulnerabilities::Dismiss
mount_mutation ::Mutations::Vulnerabilities::Resolve
......
# frozen_string_literal: true
module Mutations
module RequirementsManagement
class ExportRequirements < BaseMutation
include ResolvesProject
include CommonRequirementArguments
graphql_name 'ExportRequirements'
authorize :export_requirements
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'Full project path the requirements are associated with.'
def resolve(args)
project_path = args.delete(:project_path)
project = authorized_find!(full_path: project_path)
IssuableExportCsvWorker.perform_async(:requirement, current_user.id, project.id, args)
{
errors: []
}
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
# frozen_string_literal: true
module CommonRequirementArguments
extend ActiveSupport::Concern
included do
argument :sort, Types::SortEnum,
required: false,
description: 'List requirements by sort order.'
argument :state, Types::RequirementsManagement::RequirementStateEnum,
required: false,
description: 'Filter requirements by state.'
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Search query for requirement title.'
argument :author_username, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter requirements by author username.'
end
end
......@@ -4,6 +4,7 @@ module Resolvers
module RequirementsManagement
class RequirementsResolver < BaseResolver
include LooksAhead
include CommonRequirementArguments
type ::Types::RequirementsManagement::RequirementType.connection_type, null: true
......@@ -15,22 +16,6 @@ module Resolvers
required: false,
description: 'List of IIDs of requirements, e.g., [1, 2]'
argument :sort, Types::SortEnum,
required: false,
description: 'List requirements by sort order'
argument :state, Types::RequirementsManagement::RequirementStateEnum,
required: false,
description: 'Filter requirements by state'
argument :search, GraphQL::STRING_TYPE,
required: false,
description: 'Search query for requirement title'
argument :author_username, [GraphQL::STRING_TYPE],
required: false,
description: 'Filter requirements by author username'
def resolve_with_lookahead(**args)
# The project could have been loaded in batch by `BatchLoader`.
# At this point we need the `id` of the project to query for issues, so
......
......@@ -364,6 +364,7 @@ module EE
enable :admin_requirement
enable :update_requirement
enable :import_requirements
enable :export_requirements
end
rule { requirements_available & owner }.enable :destroy_requirement
......
---
title: Add GraphQL mutation to export Requirements
merge_request: 50546
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::RequirementsManagement::ExportRequirements do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:requirement) { create(:requirement, project: project) }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
shared_examples 'requirements not available' do
it 'raises a not accessible error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
subject do
mutation.resolve(
project_path: project.full_path,
author_username: user.username,
state: 'OPENED',
search: 'foo'
)
end
it_behaves_like 'requirements not available'
context 'when the user can update the requirement' do
before do
project.add_developer(user)
end
context 'when requirements feature is available' do
before do
stub_licensed_features(requirements: true)
end
it 'export requirements' do
args = { author_username: user.username, state: 'OPENED', search: 'foo' }
expect(IssuableExportCsvWorker).to receive(:perform_async)
.with(:requirement, user.id, project.id, args)
subject
end
end
context 'when requirements feature is disabled' do
before do
stub_licensed_features(requirements: false)
end
it_behaves_like 'requirements not available'
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Exporting Requirements' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:requirement) { create(:requirement, project: project) }
let(:attributes) { { state: 'OPENED', author_username: [current_user.username], sort: :CREATED_ASC, search: requirement.title } }
let(:mutation) do
params = { project_path: project.full_path }.merge(attributes)
graphql_mutation(:export_requirements, params)
end
def mutation_response
graphql_mutation_response(:export_requirements)
end
shared_examples 'requirements export fails' do
it_behaves_like 'a mutation that returns a top-level access error'
it 'does not schedule export job' do
expect(IssuableExportCsvWorker).not_to receive(:perform_async)
end
end
context 'when the user does not have permission' do
before do
stub_licensed_features(requirements: true)
end
it_behaves_like 'requirements export fails'
end
context 'when the user has permission' do
before do
project.add_reporter(current_user)
end
context 'when requirements are disabled' do
before do
stub_licensed_features(requirements: false)
end
it_behaves_like 'requirements export fails'
end
context 'when requirements are enabled' do
before do
stub_licensed_features(requirements: true)
end
it 'schedules job to export requirements', :aggregate_failures do
args = {
author_username: [current_user.username],
search: requirement.title,
sort: :created_asc,
state: 'opened'
}
expect(IssuableExportCsvWorker)
.to receive(:perform_async).with(:requirement, current_user.id, project.id, args)
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to be_empty
end
end
end
end
......@@ -6,7 +6,7 @@ RSpec.shared_examples 'resource with requirement permissions' do
let(:all_permissions) do
[:read_requirement, :create_requirement, :admin_requirement,
:update_requirement, :destroy_requirement,
:create_requirement_test_report]
:create_requirement_test_report, :export_requirements]
end
let(:manage_permissions) { all_permissions - [:destroy_requirement] }
......
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