Commit cfcd84cb authored by Max Woolf's avatar Max Woolf

Add ability to update compliance frameworks via GraphQL

Adds a new UpdateService and updateComplianceFramework
mutation
parent 5559a68e
......@@ -14593,6 +14593,7 @@ type Mutation {
updateBoard(input: UpdateBoardInput!): UpdateBoardPayload
updateBoardEpicUserPreferences(input: UpdateBoardEpicUserPreferencesInput!): UpdateBoardEpicUserPreferencesPayload
updateBoardList(input: UpdateBoardListInput!): UpdateBoardListPayload
updateComplianceFramework(input: UpdateComplianceFrameworkInput!): UpdateComplianceFrameworkPayload
updateContainerExpirationPolicy(input: UpdateContainerExpirationPolicyInput!): UpdateContainerExpirationPolicyPayload
updateDevopsAdoptionSegment(input: UpdateDevopsAdoptionSegmentInput!): UpdateDevopsAdoptionSegmentPayload
updateEpic(input: UpdateEpicInput!): UpdateEpicPayload
......@@ -23359,6 +23360,56 @@ type UpdateBoardPayload {
errors: [String!]!
}
"""
Autogenerated input type of UpdateComplianceFramework
"""
input UpdateComplianceFrameworkInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
New color representation of the compliance framework in hex format. e.g. #FCA121
"""
color: String
"""
New description for the compliance framework
"""
description: String
"""
The global ID of the compliance framework to update
"""
id: ComplianceManagementFrameworkID!
"""
New name for the compliance framework
"""
name: String
}
"""
Autogenerated return type of UpdateComplianceFramework
"""
type UpdateComplianceFrameworkPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The compliance framework after mutation
"""
complianceFramework: ComplianceFramework
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
"""
Autogenerated input type of UpdateContainerExpirationPolicy
"""
......
......@@ -43261,6 +43261,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateComplianceFramework",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "UpdateComplianceFrameworkInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "UpdateComplianceFrameworkPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateContainerExpirationPolicy",
"description": null,
......@@ -68342,6 +68369,138 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateComplianceFrameworkInput",
"description": "Autogenerated input type of UpdateComplianceFramework",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "The global ID of the compliance framework to update",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ComplianceManagementFrameworkID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "name",
"description": "New name for the compliance framework",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "description",
"description": "New description for the compliance framework",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "color",
"description": "New color representation of the compliance framework in hex format. e.g. #FCA121",
"type": {
"kind": "SCALAR",
"name": "String",
"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": "UpdateComplianceFrameworkPayload",
"description": "Autogenerated return type of UpdateComplianceFramework",
"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": "complianceFramework",
"description": "The compliance framework after mutation",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFramework",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"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
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "UpdateContainerExpirationPolicyInput",
......@@ -3554,6 +3554,16 @@ Autogenerated return type of UpdateBoard.
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateComplianceFrameworkPayload
Autogenerated return type of UpdateComplianceFramework.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `complianceFramework` | ComplianceFramework | The compliance framework after mutation |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### UpdateContainerExpirationPolicyPayload
Autogenerated return type of UpdateContainerExpirationPolicy.
......
......@@ -11,6 +11,7 @@ module EE
mount_mutation ::Mutations::Clusters::AgentTokens::Create
mount_mutation ::Mutations::Clusters::AgentTokens::Delete
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Destroy
mount_mutation ::Mutations::ComplianceManagement::Frameworks::Update
mount_mutation ::Mutations::Issues::SetIteration
mount_mutation ::Mutations::Issues::SetWeight
mount_mutation ::Mutations::Issues::SetEpic
......
# frozen_string_literal: true
module Mutations
module ComplianceManagement
module Frameworks
class Update < ::Mutations::BaseMutation
graphql_name 'UpdateComplianceFramework'
authorize :manage_compliance_framework
argument :id,
::Types::GlobalIDType[::ComplianceManagement::Framework],
required: true,
description: 'The global ID of the compliance framework to update'
argument :name,
GraphQL::STRING_TYPE,
required: false,
description: 'New name for the compliance framework'
argument :description,
GraphQL::STRING_TYPE,
required: false,
description: 'New description for the compliance framework'
argument :color,
GraphQL::STRING_TYPE,
required: false,
description: 'New color representation of the compliance framework in hex format. e.g. #FCA121'
field :compliance_framework,
Types::ComplianceManagement::ComplianceFrameworkType,
null: true,
description: "The compliance framework after mutation"
def resolve(id:, **args)
framework = authorized_find!(id: id)
::ComplianceManagement::Frameworks::UpdateService.new(framework: framework,
current_user: current_user,
params: args).execute
{ compliance_framework: framework, errors: errors_on_object(framework) }
end
private
def find_object(id:)
GitlabSchema.object_from_id(id, expected_type: ::ComplianceManagement::Framework)
end
end
end
end
end
# frozen_string_literal: true
module ComplianceManagement
module Frameworks
class UpdateService < BaseService
attr_reader :framework, :current_user, :params
def initialize(framework:, current_user:, params:)
@framework = framework
@current_user = current_user
@params = params
end
def execute
return error unless permitted?
framework.update(params) ? success : error
end
def success
ServiceResponse.success(payload: { framework: framework })
end
def error
ServiceResponse.error(message: _('Failed to update framework'), payload: framework.errors )
end
private
def permitted?
can? current_user, :manage_compliance_framework, framework
end
end
end
end
---
title: Add ability to update compliance frameworks via GraphQL
merge_request: 49157
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::ComplianceManagement::Frameworks::Update do
include GraphqlHelpers
let_it_be(:framework) { create(:compliance_framework) }
let(:user) { framework.namespace.owner }
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
let(:params) do
{
name: 'New Name',
description: 'New Description',
color: '#AAAAA1'
}
end
subject { mutation.resolve(id: global_id_of(framework), **params) }
context 'feature is enabled and licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'parameters are valid' do
it 'returns the new object' do
response = subject[:compliance_framework]
expect(response.name).to eq('New Name')
expect(response.description).to eq('New Description')
expect(response.color).to eq('#AAAAA1')
end
it 'returns no errors' do
expect(subject[:errors]).to be_empty
end
context 'current_user is not authorized to update framework' do
let_it_be(:user) { create(:user) }
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
context 'parameters are invalid' do
let(:params) do
{
name: '',
description: '',
color: 'AAAAA1'
}
end
it 'does not change the framework attributes' do
expect { subject }.not_to change { framework.name }
expect { subject }.not_to change { framework.description }
expect { subject }.not_to change { framework.color }
end
it 'returns validation errors' do
expect(subject[:errors]).to contain_exactly("Name can't be blank", "Description can't be blank", "Color must be a valid color code")
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update a compliance framework' do
include GraphqlHelpers
let_it_be(:framework) { create(:compliance_framework) }
let(:mutation) { graphql_mutation(:update_compliance_framework, { id: global_id_of(framework), **params }) }
let(:current_user) { framework.namespace.owner }
let(:params) do
{
name: 'New Name',
description: 'New Description',
color: '#AAC112'
}
end
subject { post_graphql_mutation(mutation, current_user: current_user) }
def mutation_response
graphql_mutation_response(:update_compliance_framework)
end
context 'feature is unlicensed' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
context 'feature is licensed but disabled' do
before do
stub_licensed_features(custom_compliance_frameworks: false)
end
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
context 'feature is licensed and enabled' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'with valid params' do
it 'returns an empty array of errors' do
subject
expect(mutation_response['errors']).to be_empty
end
it 'returns the updated framework' do
subject
expect(mutation_response['complianceFramework']['name']).to eq 'New Name'
expect(mutation_response['complianceFramework']['description']).to eq 'New Description'
expect(mutation_response['complianceFramework']['color']).to eq '#AAC112'
end
context 'current_user is not permitted to update framework' do
let_it_be(:current_user) { create(:user) }
it_behaves_like 'a mutation that returns top-level errors',
errors: ["The resource that you are attempting to access does not exist or you don't have permission to perform this action"]
end
end
context 'with invalid params' do
let(:params) do
{
name: '',
description: '',
color: 'NOTACOLOR'
}
end
it 'returns an array of errors' do
subject
expect(mutation_response['errors']).to contain_exactly "Color must be a valid color code", "Description can't be blank", "Name can't be blank"
end
it 'does not update the framework' do
expect { subject }.not_to change { framework.name }
expect { subject }.not_to change { framework.description }
expect { subject }.not_to change { framework.color }
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe ComplianceManagement::Frameworks::UpdateService do
let_it_be(:namespace) { create(:namespace) }
let_it_be(:framework) { create(:compliance_framework, namespace: namespace) }
let(:current_user) { namespace.owner }
let(:params) { { color: '#000001', description: 'New Description', name: 'New Name' } }
subject { described_class.new(framework: framework, current_user: current_user, params: params) }
shared_examples 'a failed update request' do
it 'does not update the compliance framework' do
expect { subject.execute }.not_to change { framework.name }
expect { subject.execute }.not_to change { framework.description }
expect { subject.execute }.not_to change { framework.color }
end
it 'is unsuccessful' do
expect(subject.execute.success?).to be false
end
end
context 'feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it_behaves_like 'a failed update request'
end
context 'feature is licensed but disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
stub_licensed_features(custom_compliance_frameworks: true)
end
it_behaves_like 'a failed update request'
end
context 'current_user is not the namespace owner' do
let(:current_user) { create(:user) }
it_behaves_like 'a failed update request'
end
context 'when feature is enabled and licensed' do
before do
stub_licensed_features(custom_compliance_frameworks: true)
stub_feature_flags(ff_custom_compliance_frameworks: true)
end
context 'with an invalid param passed' do
let(:params) { { color: '0001', description: '', name: 'New Name' } }
it 'is unsuccessful' do
expect(subject.execute.success?).to be false
end
it 'has appropriate errors' do
expect(subject.execute.payload.full_messages).to contain_exactly 'Color must be a valid color code', "Description can't be blank"
end
end
context 'with valid params passed' do
it 'updates the compliance framework with valid params' do
subject.execute
expect(framework.name).to eq('New Name')
expect(framework.color).to eq('#000001')
expect(framework.description).to eq('New Description')
end
it 'is successful' do
expect(subject.execute.success?).to be true
end
end
end
end
......@@ -11679,6 +11679,9 @@ msgstr ""
msgid "Failed to update environment!"
msgstr ""
msgid "Failed to update framework"
msgstr ""
msgid "Failed to update issue status"
msgstr ""
......
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