Commit e79b6cc1 authored by Max Woolf's avatar Max Woolf

Add compliance frameworks to namespace and project in GraphQL API

Adds the ability to traverse to a namespace's
compliance frameworks using the GraphQL API.

Additionally adds other attributes to the
compliance framework type, namely:
id, description and color.
parent 8ee3a28e
...@@ -3139,6 +3139,21 @@ enum CommitEncoding { ...@@ -3139,6 +3139,21 @@ enum CommitEncoding {
Represents a ComplianceFramework associated with a Project Represents a ComplianceFramework associated with a Project
""" """
type ComplianceFramework { type ComplianceFramework {
"""
Hexadecimal representation of compliance framework's label color
"""
color: String!
"""
Description of the compliance framework
"""
description: String!
"""
Compliance framework ID
"""
id: ID!
""" """
Name of the compliance framework Name of the compliance framework
""" """
...@@ -8818,6 +8833,32 @@ type Group { ...@@ -8818,6 +8833,32 @@ type Group {
startDate: Date! startDate: Date!
): CodeCoverageActivityConnection ): CodeCoverageActivityConnection
"""
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
""" """
Container repositories of the group Container repositories of the group
""" """
...@@ -14182,6 +14223,32 @@ type Namespace { ...@@ -14182,6 +14223,32 @@ type Namespace {
""" """
additionalPurchasedStorageSize: Float additionalPurchasedStorageSize: Float
"""
Compliance frameworks available to projects in this namespace. Available only
when feature flag `ff_custom_compliance_frameworks` is enabled
"""
complianceFrameworks(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
): ComplianceFrameworkConnection
""" """
Includes at least one project where the repository size exceeds the limit Includes at least one project where the repository size exceeds the limit
""" """
......
...@@ -8529,6 +8529,60 @@ ...@@ -8529,6 +8529,60 @@
"name": "ComplianceFramework", "name": "ComplianceFramework",
"description": "Represents a ComplianceFramework associated with a Project", "description": "Represents a ComplianceFramework associated with a Project",
"fields": [ "fields": [
{
"name": "color",
"description": "Hexadecimal representation of compliance framework's label color",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "description",
"description": "Description of the compliance framework",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "id",
"description": "Compliance framework ID",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "name", "name": "name",
"description": "Name of the compliance framework", "description": "Name of the compliance framework",
...@@ -24512,6 +24566,59 @@ ...@@ -24512,6 +24566,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "containerRepositories", "name": "containerRepositories",
"description": "Container repositories of the group", "description": "Container repositories of the group",
...@@ -42200,6 +42307,59 @@ ...@@ -42200,6 +42307,59 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "complianceFrameworks",
"description": "Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled",
"args": [
{
"name": "after",
"description": "Returns the elements in the list that come after the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "before",
"description": "Returns the elements in the list that come before the specified cursor.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
},
{
"name": "first",
"description": "Returns the first _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
},
{
"name": "last",
"description": "Returns the last _n_ elements from the list.",
"type": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "ComplianceFrameworkConnection",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "containsLockedProjects", "name": "containsLockedProjects",
"description": "Includes at least one project where the repository size exceeds the limit", "description": "Includes at least one project where the repository size exceeds the limit",
...@@ -507,6 +507,9 @@ Represents a ComplianceFramework associated with a Project. ...@@ -507,6 +507,9 @@ Represents a ComplianceFramework associated with a Project.
| Field | Type | Description | | Field | Type | Description |
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `color` | String! | Hexadecimal representation of compliance framework's label color |
| `description` | String! | Description of the compliance framework |
| `id` | ID! | Compliance framework ID |
| `name` | String! | Name of the compliance framework | | `name` | String! | Name of the compliance framework |
### ConfigureSastPayload ### ConfigureSastPayload
...@@ -1459,6 +1462,7 @@ Autogenerated return type of EpicTreeReorder. ...@@ -1459,6 +1462,7 @@ Autogenerated return type of EpicTreeReorder.
| `board` | Board | A single board of the group | | `board` | Board | A single board of the group |
| `boards` | BoardConnection | Boards of the group | | `boards` | BoardConnection | Boards of the group |
| `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group | | `codeCoverageActivities` | CodeCoverageActivityConnection | Represents the code coverage activity for this group |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled |
| `containerRepositories` | ContainerRepositoryConnection | Container repositories of the group | | `containerRepositories` | ContainerRepositoryConnection | Container repositories of the group |
| `containerRepositoriesCount` | Int! | Number of container repositories in the group | | `containerRepositoriesCount` | Int! | Number of container repositories in the group |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit | | `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
...@@ -2183,6 +2187,7 @@ Contains statistics about a milestone. ...@@ -2183,6 +2187,7 @@ Contains statistics about a milestone.
| ----- | ---- | ----------- | | ----- | ---- | ----------- |
| `actualRepositorySizeLimit` | Float | Size limit for repositories in the namespace in bytes | | `actualRepositorySizeLimit` | Float | Size limit for repositories in the namespace in bytes |
| `additionalPurchasedStorageSize` | Float | Additional storage purchased for the root namespace in bytes | | `additionalPurchasedStorageSize` | Float | Additional storage purchased for the root namespace in bytes |
| `complianceFrameworks` | ComplianceFrameworkConnection | Compliance frameworks available to projects in this namespace. Available only when feature flag `ff_custom_compliance_frameworks` is enabled |
| `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit | | `containsLockedProjects` | Boolean! | Includes at least one project where the repository size exceeds the limit |
| `description` | String | Description of the namespace | | `description` | String | Description of the namespace |
| `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` | | `descriptionHtml` | String | The GitLab Flavored Markdown rendering of `description` |
......
...@@ -54,6 +54,12 @@ module EE ...@@ -54,6 +54,12 @@ module EE
null: true, null: true,
description: 'Date until the temporary storage increase is active' description: 'Date until the temporary storage increase is active'
field :compliance_frameworks,
::Types::ComplianceManagement::ComplianceFrameworkType.connection_type,
null: true,
description: 'Compliance frameworks available to projects in this namespace',
feature_flag: :ff_custom_compliance_frameworks
def additional_purchased_storage_size def additional_purchased_storage_size
object.additional_purchased_storage_size.megabytes object.additional_purchased_storage_size.megabytes
end end
...@@ -61,6 +67,16 @@ module EE ...@@ -61,6 +67,16 @@ module EE
def storage_size_limit def storage_size_limit
object.root_storage_size.limit object.root_storage_size.limit
end end
def compliance_frameworks
BatchLoader::GraphQL.for(object.id).batch(default_value: []) do |namespace_ids, loader|
results = ::ComplianceManagement::Framework.with_namespaces(namespace_ids)
results.each do |framework|
loader.call(framework.namespace.id) { |xs| xs << framework }
end
end
end
end end
end end
end end
......
...@@ -55,7 +55,6 @@ module EE ...@@ -55,7 +55,6 @@ module EE
field :compliance_frameworks, ::Types::ComplianceManagement::ComplianceFrameworkType.connection_type, field :compliance_frameworks, ::Types::ComplianceManagement::ComplianceFrameworkType.connection_type,
description: 'Compliance frameworks associated with the project', description: 'Compliance frameworks associated with the project',
resolver: ::Resolvers::ComplianceFrameworksResolver,
null: true null: true
field :security_dashboard_path, GraphQL::STRING_TYPE, field :security_dashboard_path, GraphQL::STRING_TYPE,
...@@ -141,6 +140,18 @@ module EE ...@@ -141,6 +140,18 @@ module EE
def security_dashboard_path def security_dashboard_path
Rails.application.routes.url_helpers.project_security_dashboard_index_path(object) Rails.application.routes.url_helpers.project_security_dashboard_index_path(object)
end end
def compliance_frameworks
BatchLoader::GraphQL.for(object.id).batch(default_value: []) do |project_ids, loader|
results = ::ComplianceManagement::Framework.with_projects(project_ids)
results.each do |framework|
framework.project_ids.each do |project_id|
loader.call(project_id) { |xs| xs << framework }
end
end
end
end
end end
end end
end end
......
# frozen_string_literal: true
module Resolvers
class ComplianceFrameworksResolver < BaseResolver
type Types::ComplianceManagement::ComplianceFrameworkType, null: true
alias_method :project, :object
def resolve(**args)
Array.wrap(project.compliance_framework_setting)
end
end
end
...@@ -7,13 +7,21 @@ module Types ...@@ -7,13 +7,21 @@ module Types
graphql_name 'ComplianceFramework' graphql_name 'ComplianceFramework'
description 'Represents a ComplianceFramework associated with a Project' description 'Represents a ComplianceFramework associated with a Project'
field :id, GraphQL::ID_TYPE,
null: false,
description: 'Compliance framework ID'
field :name, GraphQL::STRING_TYPE, field :name, GraphQL::STRING_TYPE,
null: false, null: false,
description: 'Name of the compliance framework' description: 'Name of the compliance framework'
def name field :description, GraphQL::STRING_TYPE,
object.compliance_management_framework.name null: false,
end description: 'Description of the compliance framework'
field :color, GraphQL::STRING_TYPE,
null: false,
description: 'Hexadecimal representation of compliance framework\'s label color'
end end
end end
end end
...@@ -59,6 +59,8 @@ module ComplianceManagement ...@@ -59,6 +59,8 @@ module ComplianceManagement
strip_attributes :name, :color strip_attributes :name, :color
belongs_to :namespace belongs_to :namespace
has_many :project_settings, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings'
has_many :projects, through: :project_settings
validates :namespace, presence: true validates :namespace, presence: true
validates :name, presence: true, length: { maximum: 255 } validates :name, presence: true, length: { maximum: 255 }
...@@ -67,6 +69,9 @@ module ComplianceManagement ...@@ -67,6 +69,9 @@ module ComplianceManagement
validates :regulated, presence: true validates :regulated, presence: true
validates :namespace_id, uniqueness: { scope: :name } validates :namespace_id, uniqueness: { scope: :name }
scope :with_projects, ->(project_ids) { includes(:projects).where(projects: { id: project_ids }) }
scope :with_namespaces, ->(namespace_ids) { includes(:namespace).where(namespaces: { id: namespace_ids })}
def default_framework_definition def default_framework_definition
strong_memoize(:default_framework_definition) do strong_memoize(:default_framework_definition) do
DEFAULT_FRAMEWORKS.find { |framework| framework.name.eql?(name) } DEFAULT_FRAMEWORKS.find { |framework| framework.name.eql?(name) }
......
...@@ -43,6 +43,7 @@ module EE ...@@ -43,6 +43,7 @@ module EE
has_one :status_page_setting, inverse_of: :project, class_name: 'StatusPage::ProjectSetting' has_one :status_page_setting, inverse_of: :project, class_name: 'StatusPage::ProjectSetting'
has_one :compliance_framework_setting, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings', inverse_of: :project has_one :compliance_framework_setting, class_name: 'ComplianceManagement::ComplianceFramework::ProjectSettings', inverse_of: :project
has_many :compliance_management_frameworks, through: :compliance_framework_setting, source: 'compliance_management_framework'
has_one :security_setting, class_name: 'ProjectSecuritySetting' has_one :security_setting, class_name: 'ProjectSecuritySetting'
has_one :vulnerability_statistic, class_name: 'Vulnerabilities::Statistic' has_one :vulnerability_statistic, class_name: 'Vulnerabilities::Statistic'
......
---
title: Add compliance frameworks to namespaces in GraphQL API
merge_request: 47779
author:
type: added
---
name: ff_custom_compliance_frameworks
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/47779
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/287779
milestone: '13.7'
type: development
group: group::compliance
default_enabled: false
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::ComplianceFrameworksResolver do
include GraphqlHelpers
let(:project) { create(:project) }
describe '#resolve' do
subject { resolve_compliance_frameworks(project) }
context 'when a project has a compliance framework set' do
before do
project.update!(compliance_framework_setting: create(:compliance_framework_project_setting, :sox))
end
it 'includes the name of the compliance frameworks' do
expect(subject.size).to eq(1)
framework = subject.first.compliance_management_framework
expect(framework.name).to eq('SOX')
end
end
context 'when a project has no compliance framework set' do
it 'is an empty array' do
expect(subject).to be_empty
end
end
end
def resolve_compliance_frameworks(project)
resolve(described_class, obj: project)
end
end
...@@ -3,5 +3,16 @@ ...@@ -3,5 +3,16 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['ComplianceFramework'] do RSpec.describe GitlabSchema.types['ComplianceFramework'] do
it { expect(described_class).to have_graphql_field(:name) } subject { described_class }
fields = %w[
id
name
description
color
]
it 'has the correct fields' do
is_expected.to have_graphql_fields(fields)
end
end end
...@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do ...@@ -14,6 +14,7 @@ RSpec.describe GitlabSchema.types['Namespace'] do
storage_size_limit storage_size_limit
is_temporary_storage_increase_enabled is_temporary_storage_increase_enabled
temporary_storage_increase_ends_on temporary_storage_increase_ends_on
compliance_frameworks
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe GitlabSchema.types['Project'] do RSpec.describe GitlabSchema.types['Project'] do
include GraphqlHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:vulnerability) { create(:vulnerability, project: project, severity: :high) } let_it_be(:vulnerability) { create(:vulnerability, project: project, severity: :high) }
...@@ -356,4 +358,29 @@ RSpec.describe GitlabSchema.types['Project'] do ...@@ -356,4 +358,29 @@ RSpec.describe GitlabSchema.types['Project'] do
it { is_expected.to have_graphql_type(Types::Ci::CodeCoverageSummaryType) } it { is_expected.to have_graphql_type(Types::Ci::CodeCoverageSummaryType) }
end end
describe 'compliance_frameworks' do
it 'queries in batches' do
projects = create_list(:project, 2, :with_compliance_framework)
projects.each { |p| p.add_maintainer(user) }
results = batch_sync(max_queries: 1) do
projects.flat_map do |p|
resolve_field(:compliance_frameworks, p)
end
end
frameworks = results.flat_map(&:items)
expect(frameworks).to match_array(projects.flat_map(&:compliance_management_frameworks))
end
end
private
def query_for_project(project)
graphql_query_for(
:projects, { ids: [global_id_of(project)] }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting a list of compliance frameworks for a root namespace' do
include GraphqlHelpers
let_it_be(:namespace) { create(:namespace) }
let_it_be(:compliance_framework_1) { create(:compliance_framework, namespace: namespace, name: 'Test1') }
let_it_be(:compliance_framework_2) { create(:compliance_framework, namespace: namespace, name: 'Test2') }
let(:path) { %i[namespace compliance_frameworks nodes] }
let!(:query) do
graphql_query_for(
:namespace, { full_path: namespace.full_path }, query_nodes(:compliance_frameworks)
)
end
context 'when authenticated as the namespace owner' do
let(:current_user) { namespace.owner }
it 'returns the groups compliance frameworks' do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path)).to contain_exactly(
a_hash_including('id' => global_id_of(compliance_framework_1)),
a_hash_including('id' => global_id_of(compliance_framework_2))
)
end
context 'when querying multiple namespaces' do
let(:group) { create(:group) }
let(:multiple_namespace_query) do
<<~QUERY
query {
a: namespace(fullPath: "#{namespace.full_path}") {
complianceFrameworks { nodes { id name } }
}
b: namespace(fullPath: "#{group.full_path}") {
complianceFrameworks { nodes { id name } }
}
}
QUERY
end
before do
create(:compliance_framework, namespace: group)
group.add_owner(current_user)
end
it 'avoids N+1 queries' do
query_count = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }.count
expect { post_graphql(multiple_namespace_query, current_user: current_user) }.not_to exceed_query_limit(query_count + 4)
end
it 'responds with the expected list of compliance frameworks' do
post_graphql(multiple_namespace_query, current_user: current_user)
expect(graphql_data_at(:a, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('Test1', 'Test2')
expect(graphql_data_at(:b, :complianceFrameworks, :nodes).map { |f| f['name'] }).to contain_exactly('GDPR')
end
end
context 'feature is disabled' do
before do
stub_feature_flags(ff_custom_compliance_frameworks: false)
end
it 'responds with error when querying a compliance framework' do
post_graphql(query, current_user: current_user)
expect(graphql_errors).to contain_exactly(include('message' => "Field 'complianceFrameworks' doesn't exist on type 'Namespace'"))
end
end
end
context 'when authenticated as a different user' do
let(:current_user) { build(:user) }
it "does not return the namespaces compliance frameworks" do
post_graphql(query, current_user: current_user)
expect(graphql_data_at(*path)).to be_nil
end
end
context 'when not authenticated' do
it "does not return the namespace's compliance frameworks" do
post_graphql(query)
expect(graphql_data_at(*path)).to be_nil
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'getting compliance frameworks for a collection of projects' do
include GraphqlHelpers
let_it_be(:current_user) { create(:user) }
let_it_be(:project_members) { create_list(:project_member, 2, :maintainer, user: current_user) }
let_it_be(:project_ids) { project_members.map { |p| global_id_of(p.source) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before_all do
project_members.map(&:project).each do |project|
project.compliance_framework_setting = create(:compliance_framework_project_setting)
end
end
context 'querying a single project' do
let(:single_project_query) do
graphql_query_for(
:projects, { ids: [project_ids.first] }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
it 'avoids N+1 queries', :use_sql_query_cache do
query_count = ActiveRecord::QueryRecorder.new(skip_cached: false) { post_graphql(query, current_user: current_user) }.count
expect { post_graphql(single_project_query, current_user: current_user) }.not_to exceed_all_query_limit(query_count)
end
it 'contains the expected compliance framework' do
post_graphql(single_project_query, current_user: current_user)
expect(graphql_data_at(:projects, :nodes, 0, :complianceFrameworks, :nodes, 0, :name)).to eq 'GDPR'
end
end
context 'projects can have a compliance framework' do
let_it_be(:compliance_projects) { create_list(:project, 2, :with_compliance_framework) }
let_it_be(:non_compliance_project) { create(:project) }
let(:projects) { compliance_projects + [non_compliance_project] }
let(:project_ids) { projects.map { |p| global_id_of(p) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before do
projects.each { |p| create(:project_member, :maintainer, source: p, user: current_user)}
post_graphql(query, current_user: current_user)
end
subject { graphql_data_at(:projects, :nodes).map { |p| p.dig('complianceFrameworks', 'nodes') } }
it 'contains the correct number of compliance frameworks' do
expect(subject[0].size).to eq 0
expect(subject[1].size).to eq 1
expect(subject[2].size).to eq 1
end
end
context 'projects that share the same compliance framework' do
let_it_be(:framework) { create(:compliance_framework) }
let_it_be(:project_1) { create(:project, compliance_framework_setting: create(:compliance_framework_project_setting, compliance_management_framework: framework )) }
let_it_be(:project_2) { create(:project, compliance_framework_setting: create(:compliance_framework_project_setting, compliance_management_framework: framework )) }
let(:projects) { [project_1, project_2] }
let(:project_ids) { projects.map { |p| global_id_of(p) } }
let(:query) do
graphql_query_for(
:projects, { ids: project_ids }, "nodes { #{query_nodes(:compliance_frameworks)} }"
)
end
before do
projects.each { |p| create(:project_member, :maintainer, source: p, user: current_user)}
post_graphql(query, current_user: current_user)
end
subject { graphql_data_at(:projects, :nodes).map { |p| p.dig('complianceFrameworks', 'nodes', 0, 'id') } }
it 'shares the same compliance framework id' do
expect(subject[0]).to eq(subject[1])
end
end
end
...@@ -542,6 +542,7 @@ project: ...@@ -542,6 +542,7 @@ project:
- daily_build_group_report_results - daily_build_group_report_results
- jira_imports - jira_imports
- compliance_framework_setting - compliance_framework_setting
- compliance_management_frameworks
- metrics_users_starred_dashboards - metrics_users_starred_dashboards
- alert_management_alerts - alert_management_alerts
- repository_storage_moves - repository_storage_moves
......
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