From 8c0900fb3e4ca1bfcc231919df614daa95acc9bf Mon Sep 17 00:00:00 2001 From: anarinesingh Date: Thu, 20 Nov 2025 12:01:41 -0500 Subject: [PATCH 1/5] Create finder, resolver and types for graphql Implements graphql functionality for the newly made table security_project_tracked_contexts, aka SecurityProjectRefs --- app/graphql/types/security/ref_input_type.rb | 16 +++++++ doc/api/graphql/reference/_index.md | 41 ++++++++++++++++++ .../finders/security/tracked_refs_finder.rb | 24 +++++++++++ ee/app/graphql/ee/types/project_type.rb | 7 ++++ .../security/tracked_refs_resolver.rb | 17 ++++++++ .../types/security/tracked_ref_type.rb | 36 ++++++++++++++++ .../security/tracked_refs_finder_spec.rb | 42 +++++++++++++++++++ 7 files changed, 183 insertions(+) create mode 100644 app/graphql/types/security/ref_input_type.rb create mode 100644 ee/app/finders/security/tracked_refs_finder.rb create mode 100644 ee/app/graphql/resolvers/security/tracked_refs_resolver.rb create mode 100644 ee/app/graphql/types/security/tracked_ref_type.rb create mode 100644 ee/spec/finders/security/tracked_refs_finder_spec.rb diff --git a/app/graphql/types/security/ref_input_type.rb b/app/graphql/types/security/ref_input_type.rb new file mode 100644 index 00000000000000..df6aaf507c3095 --- /dev/null +++ b/app/graphql/types/security/ref_input_type.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module Types + module Security + class RefInputType < BaseInputObject + graphql_name 'Ref' + description 'Input for specifying a Git reference' + + argument :name, GraphQL::Types::String, required: true, + description: 'Name of the ref.' + + argument :ref_type, Types::RefTypeEnum, required: true, + description: 'Type of the ref (heads or tags).' + end + end +end diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 26ed284ec1e8a1..414522c7b40f1d 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -23083,6 +23083,29 @@ The edge type for [`SecurityPolicyType`](#securitypolicytype). | `cursor` | [`String!`](#string) | A cursor for use in pagination. | | `node` | [`SecurityPolicyType`](#securitypolicytype) | The item at the end of the edge. | +#### `SecurityTrackedRefConnection` + +The connection type for [`SecurityTrackedRef`](#securitytrackedref). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `edges` | [`[SecurityTrackedRefEdge]`](#securitytrackedrefedge) | A list of edges. | +| `nodes` | [`[SecurityTrackedRef]`](#securitytrackedref) | A list of nodes. | +| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `SecurityTrackedRefEdge` + +The edge type for [`SecurityTrackedRef`](#securitytrackedref). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `cursor` | [`String!`](#string) | A cursor for use in pagination. | +| `node` | [`SecurityTrackedRef`](#securitytrackedref) | The item at the end of the edge. | + #### `SentryErrorConnection` The connection type for [`SentryError`](#sentryerror). @@ -42040,6 +42063,7 @@ Project-level settings for product analytics provider. | `securityPolicyProjectLinkedProjects` | [`ProjectConnection`](#projectconnection) | Projects linked to the project, when used as Security Policy Project. (see [Connections](#connections)) | | `securityScanProfiles` {{< icon name="warning-solid" >}} | [`[ScanProfileType!]`](#scanprofiletype) | **Introduced** in GitLab 18.7. **Status**: Experiment. Security scan profiles attached to the project. | | `securityScanners` | [`SecurityScanners`](#securityscanners) | Information about security analyzers used in the project. | +| `securityTrackedRefs` {{< icon name="warning-solid" >}} | [`SecurityTrackedRefConnection`](#securitytrackedrefconnection) | **Introduced** in GitLab 18.7. **Status**: Experiment. Refs tracked for security vulnerabilities. | | `sentryErrors` | [`SentryErrorCollection`](#sentryerrorcollection) | Paginated collection of Sentry errors on the project. | | `serviceDeskAddress` | [`String`](#string) | E-mail address of the Service Desk. | | `serviceDeskEnabled` | [`Boolean`](#boolean) | Indicates if the project has Service Desk enabled. | @@ -46397,6 +46421,23 @@ Represents a list of security scanners. | `enabled` | [`[SecurityScannerType!]`](#securityscannertype) | List of analyzers which are enabled for the project. | | `pipelineRun` | [`[SecurityScannerType!]`](#securityscannertype) | List of analyzers which ran successfully in the latest pipeline. | +### `SecurityTrackedRef` + +Represents a ref (branch or tag) tracked for security vulnerabilities. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `commit` | [`Commit`](#commit) | Latest commit on the ref. | +| `id` | [`ID!`](#id) | Global ID of the tracked ref. | +| `isDefault` | [`Boolean!`](#boolean) | Whether the is the default branch. | +| `isProtected` | [`Boolean!`](#boolean) | Whether the ref is protected. | +| `name` | [`String!`](#string) | Name of the ref (branch or tag name). | +| `refType` | [`RefType!`](#reftype) | Type of the ref (branch or tag). | +| `trackedAt` | [`Time!`](#time) | When tracking was enabled for the ref. | +| `vulnerabilitiesCount` | [`Int!`](#int) | Count of open vulnerabilities on the ref. | + ### `SecurityTrainingUrl` Represents a URL related to a security training. diff --git a/ee/app/finders/security/tracked_refs_finder.rb b/ee/app/finders/security/tracked_refs_finder.rb new file mode 100644 index 00000000000000..3dcf61bc19cf4f --- /dev/null +++ b/ee/app/finders/security/tracked_refs_finder.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module Security + class TrackedRefsFinder + def initialize(project, current_user = nil) + @project = project + @current_user = current_user + end + + def execute + return Security::ProjectTrackedContext.none unless can_read_security_refs? + + @project.security_project_tracked_contexts + end + + private + + attr_reader :project, :current_user + + def can_read_security_refs? + Ability.allowed?(current_user, :read_security_resource, project) + end + end +end diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb index b120e9c664b2e2..a3c3efb1e12e64 100644 --- a/ee/app/graphql/ee/types/project_type.rb +++ b/ee/app/graphql/ee/types/project_type.rb @@ -716,6 +716,13 @@ module ProjectType description: 'Security scan profiles attached to the project.', authorize: :read_security_scan_profiles, experiment: { milestone: '18.7' } + + field :security_tracked_refs, + ::Types::Security::TrackedRefType.connection_type, + null: true, + description: 'Refs tracked for security vulnerabilities.', + resolver: ::Resolvers::Security::TrackedRefsResolver, + experiment: { milestone: '18.7' } end def tracking_key diff --git a/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb b/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb new file mode 100644 index 00000000000000..de331ace2b701e --- /dev/null +++ b/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Resolvers + module Security + class TrackedRefsResolver < BaseResolver + type Types::Security::TrackedRefType.connection_type, null: true + + authorize :read_security_resource + + description 'Security tracked refs for vulnerability tracking' + + def resolve + Security::TrackedRefsFinder.new(object, current_user).execute + end + end + end +end diff --git a/ee/app/graphql/types/security/tracked_ref_type.rb b/ee/app/graphql/types/security/tracked_ref_type.rb new file mode 100644 index 00000000000000..fcabe3aeff78b4 --- /dev/null +++ b/ee/app/graphql/types/security/tracked_ref_type.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Types + module Security + class TrackedRefType < BaseObject + graphql_name 'SecurityTrackedRef' + description 'Represents a ref (branch or tag) tracked for security vulnerabilities' + + authorize :read_security_resource + + field :id, GraphQL::Types::ID, null: false, + description: 'Global ID of the tracked ref.' + + field :name, GraphQL::Types::String, null: false, + description: 'Name of the ref (branch or tag name).' + + field :ref_type, Types::RefTypeEnum, null: false, + description: 'Type of the ref (branch or tag).' + + field :is_default, GraphQL::Types::Boolean, null: false, + description: 'Whether this is the default branch.' + + field :is_protected, GraphQL::Types::Boolean, null: false, + description: 'Whether the ref is protected.' + + field :commit, Types::Repositories::CommitType, null: true, + description: 'Latest commit on the ref.' + + field :vulnerabilities_count, GraphQL::Types::Int, null: false, + description: 'Count of open vulnerabilities on the ref.' + + field :tracked_at, Types::TimeType, null: false, + description: 'When tracking was enabled for the ref.' + end + end +end diff --git a/ee/spec/finders/security/tracked_refs_finder_spec.rb b/ee/spec/finders/security/tracked_refs_finder_spec.rb new file mode 100644 index 00000000000000..4fa9aa899b2b51 --- /dev/null +++ b/ee/spec/finders/security/tracked_refs_finder_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Security::TrackedRefsFinder, feature_category: :vulnerability_management do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:main_branch) { create(:security_project_tracked_context, :tracked, :default, project: project) } + let_it_be(:tag_ref) do + create(:security_project_tracked_context, :tracked, :tag, project: project, context_name: 'v1.0') + end + + subject(:finder) { described_class.new(project, user) } + + before do + stub_licensed_features(security_dashboard: true) + end + + describe '#execute' do + context 'when user has developer access' do + before_all { project.add_developer(user) } + + it 'returns all tracked refs for the project' do + expect(finder.execute).to contain_exactly(main_branch, tag_ref) + end + end + + context 'when user lacks permission' do + it 'returns empty relation' do + expect(finder.execute).to be_empty + end + end + + context 'when user is nil' do + let(:finder) { described_class.new(project, nil) } + + it 'returns empty relation' do + expect(finder.execute).to be_empty + end + end + end +end -- GitLab From e6e3b93de133f090ae7e39972013020a8b2a7cc8 Mon Sep 17 00:00:00 2001 From: anarinesingh Date: Thu, 20 Nov 2025 13:29:14 -0500 Subject: [PATCH 2/5] Add remaining specs Add specs for resolver and types. Also fix namespace bugs. --- .../security/tracked_refs_resolver.rb | 2 +- .../security/tracked_refs_resolver_spec.rb | 77 ++++++++++++++++ ee/spec/graphql/types/project_type_spec.rb | 2 +- .../types/security/ref_input_type_spec.rb | 30 ++++++ .../types/security/tracked_ref_type_spec.rb | 92 +++++++++++++++++++ 5 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb create mode 100644 ee/spec/graphql/types/security/ref_input_type_spec.rb create mode 100644 ee/spec/graphql/types/security/tracked_ref_type_spec.rb diff --git a/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb b/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb index de331ace2b701e..50b973e96c8d03 100644 --- a/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb +++ b/ee/app/graphql/resolvers/security/tracked_refs_resolver.rb @@ -10,7 +10,7 @@ class TrackedRefsResolver < BaseResolver description 'Security tracked refs for vulnerability tracking' def resolve - Security::TrackedRefsFinder.new(object, current_user).execute + ::Security::TrackedRefsFinder.new(object, current_user).execute end end end diff --git a/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb b/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb new file mode 100644 index 00000000000000..f4186beb14966c --- /dev/null +++ b/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Security::TrackedRefsResolver, feature_category: :vulnerability_management do + include GraphqlHelpers + + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:main_branch) { create(:security_project_tracked_context, :tracked, :default, project: project) } + let_it_be(:tag_ref) { create(:security_project_tracked_context, :tracked, :tag, project: project) } + let_it_be(:untracked_ref) { create(:security_project_tracked_context, project: project) } + + subject(:resolver) { described_class } + + specify { expect(resolver.type).to eq(Types::Security::TrackedRefType.connection_type) } + specify { expect(resolver.null).to be_truthy } + specify { expect(resolver.required_permissions).to include(:read_security_resource) } + + describe '#resolve' do + before do + stub_licensed_features(security_dashboard: true) + end + + context 'when user has permission' do + before_all do + project.add_developer(user) + end + + it 'returns tracked refs for the project' do + result = resolve(resolver, obj: project, ctx: { current_user: user }) + + expect(result).to contain_exactly(main_branch, tag_ref) + expect(result).not_to include(untracked_ref) + end + + it 'supports pagination' do + result = resolve(resolver, obj: project, ctx: { current_user: user }, args: { first: 1 }) + + expect(result).to be_a(Gitlab::Graphql::Pagination::Keyset::Connection) + expect(result.nodes.size).to be <= 1 + end + end + + context 'when user lacks permission' do + it 'returns empty result' do + result = resolve(resolver, obj: project, ctx: { current_user: user }) + + expect(result).to be_empty + end + end + + context 'when user is nil' do + it 'returns empty result' do + result = resolve(resolver, obj: project, ctx: { current_user: nil }) + + expect(result).to be_empty + end + end + + context 'when security dashboard is not licensed' do + before_all do + project.add_developer(user) + end + + before do + stub_licensed_features(security_dashboard: false) + end + + it 'returns empty result' do + result = resolve(resolver, obj: project, ctx: { current_user: user }) + + expect(result).to be_empty + end + end + end +end diff --git a/ee/spec/graphql/types/project_type_spec.rb b/ee/spec/graphql/types/project_type_spec.rb index 6b87d331325215..dc4cd21a8048cf 100644 --- a/ee/spec/graphql/types/project_type_spec.rb +++ b/ee/spec/graphql/types/project_type_spec.rb @@ -39,7 +39,7 @@ compliance_standards_adherence target_branch_rules duo_workflow_status_check component_usages vulnerability_archives component_versions vulnerability_statistic analyzer_statuses compliance_requirement_statuses duo_agentic_chat_available container_scanning_for_registry_enabled duo_workflow_workflows - duo_workflow_events security_metrics ai_catalog_item security_scan_profiles + duo_workflow_events security_metrics ai_catalog_item security_scan_profiles security_tracked_refs ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/ee/spec/graphql/types/security/ref_input_type_spec.rb b/ee/spec/graphql/types/security/ref_input_type_spec.rb new file mode 100644 index 00000000000000..62eabdaf7f8df5 --- /dev/null +++ b/ee/spec/graphql/types/security/ref_input_type_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Security::RefInputType, feature_category: :vulnerability_management do + specify { expect(described_class.graphql_name).to eq('Ref') } + specify { expect(described_class.description).to eq('Input for specifying a Git reference') } + + it 'has the expected arguments' do + expect(described_class.arguments.keys).to contain_exactly('name', 'refType') + end + + describe 'name argument' do + let(:argument) { described_class.arguments['name'] } + + it 'has correct properties' do + expect(argument.type).to eq(GraphQL::Types::String.to_non_null_type) + expect(argument.description).to eq('Name of the ref.') + end + end + + describe 'refType argument' do + let(:argument) { described_class.arguments['refType'] } + + it 'has correct properties' do + expect(argument.type).to eq(GraphQL::Types::String.to_non_null_type) + expect(argument.description).to eq('Type of the ref (heads or tags).') + end + end +end diff --git a/ee/spec/graphql/types/security/tracked_ref_type_spec.rb b/ee/spec/graphql/types/security/tracked_ref_type_spec.rb new file mode 100644 index 00000000000000..53d59b362a05b0 --- /dev/null +++ b/ee/spec/graphql/types/security/tracked_ref_type_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Security::TrackedRefType, feature_category: :vulnerability_management do + let_it_be(:project) { create(:project, :repository) } + let_it_be(:tracked_ref) { create(:security_project_tracked_context, :tracked, :default, project: project) } + + specify { expect(described_class.graphql_name).to eq('SecurityTrackedRef') } + + specify do + expect(described_class.description).to eq('Represents a ref (branch or tag) tracked for security vulnerabilities') + end + + it 'requires read_security_resource permission' do + expect(described_class.required_permissions).to include(:read_security_resource) + end + + it 'has the expected fields' do + expected_fields = %w[ + id name refType isDefault isProtected commit vulnerabilitiesCount trackedAt + ] + + expect(described_class.fields.keys).to match_array(expected_fields) + end + + describe 'field definitions' do + using RSpec::Parameterized::TableSyntax + + where(:field_name, :type_class, :null, :description_snippet) do + 'id' | 'ID' | false | 'Global ID' + 'name' | 'String' | false | 'Name of the ref' + 'refType' | nil | false | 'Type of the ref' + 'isDefault' | 'Boolean' | false | 'Whether the is the default branch' + 'isProtected' | 'Boolean' | false | 'Whether the ref is protected' + 'commit' | nil | true | 'Latest commit' + 'vulnerabilitiesCount' | 'Int' | false | 'Count of open vulnerabilities' + 'trackedAt' | nil | false | 'When tracking was enabled' + end + + with_them do + let(:field) { described_class.fields[field_name] } + + it 'has correct null setting' do + expect(field.null).to eq(null) + end + + it 'has correct description' do + expect(field.description).to include(description_snippet) + end + + it 'has correct type' do + next unless type_class + + expect(field.type.to_s).to include(type_class) + end + end + end + + describe 'field resolvers' do + let(:user) { create(:user) } + let(:context) { { current_user: user } } + + before_all do + project.add_developer(user) + end + + before do + stub_licensed_features(security_dashboard: true) + end + + it 'resolves id field' do + expect(resolve_field(:id, tracked_ref, context)).to eq(tracked_ref.to_global_id.to_s) + end + + it 'resolves name field' do + expect(resolve_field(:name, tracked_ref, context)).to eq(tracked_ref.context_name) + end + + it 'resolves isDefault field' do + expect(resolve_field(:is_default, tracked_ref, context)).to eq(tracked_ref.is_default) + end + + it 'resolves trackedAt field' do + expect(resolve_field(:tracked_at, tracked_ref, context)).to eq(tracked_ref.created_at) + end + end + + def resolve_field(field_name, object, context) + described_class.fields[field_name.to_s.camelize(:lower)].resolve(object, {}, context) + end +end -- GitLab From dfd1a3838222d66abb983485ed3291ac8fc15a24 Mon Sep 17 00:00:00 2001 From: anarinesingh Date: Thu, 20 Nov 2025 13:49:37 -0500 Subject: [PATCH 3/5] Create security policy for Security::ProjectTrackedContext --- ee/app/policies/security/project_tracked_context_policy.rb | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 ee/app/policies/security/project_tracked_context_policy.rb diff --git a/ee/app/policies/security/project_tracked_context_policy.rb b/ee/app/policies/security/project_tracked_context_policy.rb new file mode 100644 index 00000000000000..5e2e393065f6f7 --- /dev/null +++ b/ee/app/policies/security/project_tracked_context_policy.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +module Security + class ProjectTrackedContextPolicy < BasePolicy + delegate { @subject.project } + end +end -- GitLab From 2d8e0ab233dc760fc727aab7866d1456d896ed89 Mon Sep 17 00:00:00 2001 From: anarinesingh Date: Mon, 8 Dec 2025 06:59:34 -0800 Subject: [PATCH 4/5] Ensure finder returns only tracked scopes --- doc/api/graphql/reference/_index.md | 2 +- .../finders/security/tracked_refs_finder.rb | 2 +- .../types/security/tracked_ref_type.rb | 2 +- .../security/tracked_refs_resolver_spec.rb | 22 ++++++++++++++++--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 414522c7b40f1d..636d0f69ce915d 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -46431,7 +46431,7 @@ Represents a ref (branch or tag) tracked for security vulnerabilities. | ---- | ---- | ----------- | | `commit` | [`Commit`](#commit) | Latest commit on the ref. | | `id` | [`ID!`](#id) | Global ID of the tracked ref. | -| `isDefault` | [`Boolean!`](#boolean) | Whether the is the default branch. | +| `isDefault` | [`Boolean!`](#boolean) | Whether the ref is the default branch. | | `isProtected` | [`Boolean!`](#boolean) | Whether the ref is protected. | | `name` | [`String!`](#string) | Name of the ref (branch or tag name). | | `refType` | [`RefType!`](#reftype) | Type of the ref (branch or tag). | diff --git a/ee/app/finders/security/tracked_refs_finder.rb b/ee/app/finders/security/tracked_refs_finder.rb index 3dcf61bc19cf4f..ed694e784b7dc0 100644 --- a/ee/app/finders/security/tracked_refs_finder.rb +++ b/ee/app/finders/security/tracked_refs_finder.rb @@ -10,7 +10,7 @@ def initialize(project, current_user = nil) def execute return Security::ProjectTrackedContext.none unless can_read_security_refs? - @project.security_project_tracked_contexts + @project.security_project_tracked_contexts.tracked end private diff --git a/ee/app/graphql/types/security/tracked_ref_type.rb b/ee/app/graphql/types/security/tracked_ref_type.rb index fcabe3aeff78b4..c5c6e1048d37e6 100644 --- a/ee/app/graphql/types/security/tracked_ref_type.rb +++ b/ee/app/graphql/types/security/tracked_ref_type.rb @@ -18,7 +18,7 @@ class TrackedRefType < BaseObject description: 'Type of the ref (branch or tag).' field :is_default, GraphQL::Types::Boolean, null: false, - description: 'Whether this is the default branch.' + description: 'Whether the ref is the default branch.' field :is_protected, GraphQL::Types::Boolean, null: false, description: 'Whether the ref is protected.' diff --git a/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb b/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb index f4186beb14966c..94c2096462bbbf 100644 --- a/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb +++ b/ee/spec/graphql/resolvers/security/tracked_refs_resolver_spec.rb @@ -5,11 +5,27 @@ RSpec.describe Resolvers::Security::TrackedRefsResolver, feature_category: :vulnerability_management do include GraphqlHelpers + before_all do + Security::ProjectTrackedContext.delete_all + end + let_it_be(:project) { create(:project, :repository) } let_it_be(:user) { create(:user) } - let_it_be(:main_branch) { create(:security_project_tracked_context, :tracked, :default, project: project) } - let_it_be(:tag_ref) { create(:security_project_tracked_context, :tracked, :tag, project: project) } - let_it_be(:untracked_ref) { create(:security_project_tracked_context, project: project) } + + let_it_be(:main_branch) do + create(:security_project_tracked_context, :tracked, :default, + project: project, context_name: 'main', context_type: :branch) + end + + let_it_be(:tag_ref) do + create(:security_project_tracked_context, :tracked, + project: project, context_name: 'v1.1.0', context_type: :tag) + end + + let_it_be(:untracked_ref) do + create(:security_project_tracked_context, :untracked, + project: project, context_name: 'untracked-branch', context_type: :branch) + end subject(:resolver) { described_class } -- GitLab From 5d5d0601fab8be66a860f221bceebd69305e2a14 Mon Sep 17 00:00:00 2001 From: anarinesingh Date: Wed, 10 Dec 2025 06:18:37 -0800 Subject: [PATCH 5/5] Update spec and milestone --- doc/api/graphql/reference/_index.md | 2 +- ee/app/graphql/ee/types/project_type.rb | 2 +- .../types/security/ref_input_type_spec.rb | 2 +- .../types/security/tracked_ref_type_spec.rb | 106 +++++------------- 4 files changed, 32 insertions(+), 80 deletions(-) diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 636d0f69ce915d..b44aee9ad15a05 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -42063,7 +42063,7 @@ Project-level settings for product analytics provider. | `securityPolicyProjectLinkedProjects` | [`ProjectConnection`](#projectconnection) | Projects linked to the project, when used as Security Policy Project. (see [Connections](#connections)) | | `securityScanProfiles` {{< icon name="warning-solid" >}} | [`[ScanProfileType!]`](#scanprofiletype) | **Introduced** in GitLab 18.7. **Status**: Experiment. Security scan profiles attached to the project. | | `securityScanners` | [`SecurityScanners`](#securityscanners) | Information about security analyzers used in the project. | -| `securityTrackedRefs` {{< icon name="warning-solid" >}} | [`SecurityTrackedRefConnection`](#securitytrackedrefconnection) | **Introduced** in GitLab 18.7. **Status**: Experiment. Refs tracked for security vulnerabilities. | +| `securityTrackedRefs` {{< icon name="warning-solid" >}} | [`SecurityTrackedRefConnection`](#securitytrackedrefconnection) | **Introduced** in GitLab 18.8. **Status**: Experiment. Refs tracked for security vulnerabilities. | | `sentryErrors` | [`SentryErrorCollection`](#sentryerrorcollection) | Paginated collection of Sentry errors on the project. | | `serviceDeskAddress` | [`String`](#string) | E-mail address of the Service Desk. | | `serviceDeskEnabled` | [`Boolean`](#boolean) | Indicates if the project has Service Desk enabled. | diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb index a3c3efb1e12e64..203dfe6dc40a96 100644 --- a/ee/app/graphql/ee/types/project_type.rb +++ b/ee/app/graphql/ee/types/project_type.rb @@ -722,7 +722,7 @@ module ProjectType null: true, description: 'Refs tracked for security vulnerabilities.', resolver: ::Resolvers::Security::TrackedRefsResolver, - experiment: { milestone: '18.7' } + experiment: { milestone: '18.8' } end def tracking_key diff --git a/ee/spec/graphql/types/security/ref_input_type_spec.rb b/ee/spec/graphql/types/security/ref_input_type_spec.rb index 62eabdaf7f8df5..4a9c44066f1acd 100644 --- a/ee/spec/graphql/types/security/ref_input_type_spec.rb +++ b/ee/spec/graphql/types/security/ref_input_type_spec.rb @@ -23,7 +23,7 @@ let(:argument) { described_class.arguments['refType'] } it 'has correct properties' do - expect(argument.type).to eq(GraphQL::Types::String.to_non_null_type) + expect(argument.type).to eq(Types::RefTypeEnum.to_non_null_type) expect(argument.description).to eq('Type of the ref (heads or tags).') end end diff --git a/ee/spec/graphql/types/security/tracked_ref_type_spec.rb b/ee/spec/graphql/types/security/tracked_ref_type_spec.rb index 53d59b362a05b0..b4a746339e4c5e 100644 --- a/ee/spec/graphql/types/security/tracked_ref_type_spec.rb +++ b/ee/spec/graphql/types/security/tracked_ref_type_spec.rb @@ -3,90 +3,42 @@ require 'spec_helper' RSpec.describe Types::Security::TrackedRefType, feature_category: :vulnerability_management do - let_it_be(:project) { create(:project, :repository) } - let_it_be(:tracked_ref) { create(:security_project_tracked_context, :tracked, :default, project: project) } - - specify { expect(described_class.graphql_name).to eq('SecurityTrackedRef') } + include GraphqlHelpers - specify do - expect(described_class.description).to eq('Represents a ref (branch or tag) tracked for security vulnerabilities') + let_it_be(:project) { create(:project, :repository) } + let_it_be(:user) { create(:user) } + let_it_be(:tracked_context) do + create(:security_project_tracked_context, :tracked, :default, + project: project, context_name: 'main', context_type: :branch) end - it 'requires read_security_resource permission' do - expect(described_class.required_permissions).to include(:read_security_resource) + before_all do + project.add_developer(user) end - it 'has the expected fields' do - expected_fields = %w[ - id name refType isDefault isProtected commit vulnerabilitiesCount trackedAt - ] - - expect(described_class.fields.keys).to match_array(expected_fields) - end + specify { expect(described_class).to require_graphql_authorizations(:read_security_resource) } describe 'field definitions' do - using RSpec::Parameterized::TableSyntax - - where(:field_name, :type_class, :null, :description_snippet) do - 'id' | 'ID' | false | 'Global ID' - 'name' | 'String' | false | 'Name of the ref' - 'refType' | nil | false | 'Type of the ref' - 'isDefault' | 'Boolean' | false | 'Whether the is the default branch' - 'isProtected' | 'Boolean' | false | 'Whether the ref is protected' - 'commit' | nil | true | 'Latest commit' - 'vulnerabilitiesCount' | 'Int' | false | 'Count of open vulnerabilities' - 'trackedAt' | nil | false | 'When tracking was enabled' - end - - with_them do - let(:field) { described_class.fields[field_name] } - - it 'has correct null setting' do - expect(field.null).to eq(null) - end - - it 'has correct description' do - expect(field.description).to include(description_snippet) - end - - it 'has correct type' do - next unless type_class - - expect(field.type.to_s).to include(type_class) - end - end - end - - describe 'field resolvers' do - let(:user) { create(:user) } - let(:context) { { current_user: user } } - - before_all do - project.add_developer(user) - end - - before do - stub_licensed_features(security_dashboard: true) - end - - it 'resolves id field' do - expect(resolve_field(:id, tracked_ref, context)).to eq(tracked_ref.to_global_id.to_s) - end - - it 'resolves name field' do - expect(resolve_field(:name, tracked_ref, context)).to eq(tracked_ref.context_name) - end - - it 'resolves isDefault field' do - expect(resolve_field(:is_default, tracked_ref, context)).to eq(tracked_ref.is_default) + subject { described_class } + + it { is_expected.to have_graphql_field(:id) } + it { is_expected.to have_graphql_field(:name) } + it { is_expected.to have_graphql_field(:ref_type) } + it { is_expected.to have_graphql_field(:is_default) } + it { is_expected.to have_graphql_field(:is_protected) } + it { is_expected.to have_graphql_field(:commit) } + it { is_expected.to have_graphql_field(:vulnerabilities_count) } + it { is_expected.to have_graphql_field(:tracked_at) } + + it 'has correct field descriptions' do + expect(described_class.fields['id'].description).to include('Global ID') + expect(described_class.fields['name'].description).to include('Name of the ref') + expect(described_class.fields['refType'].description).to include('Type of the ref') + expect(described_class.fields['isDefault'].description).to include('Whether the ref is the default branch') + expect(described_class.fields['isProtected'].description).to include('Whether the ref is protected') + expect(described_class.fields['commit'].description).to include('Latest commit') + expect(described_class.fields['vulnerabilitiesCount'].description).to include('Count of open vulnerabilities') + expect(described_class.fields['trackedAt'].description).to include('When tracking was enabled') end - - it 'resolves trackedAt field' do - expect(resolve_field(:tracked_at, tracked_ref, context)).to eq(tracked_ref.created_at) - end - end - - def resolve_field(field_name, object, context) - described_class.fields[field_name.to_s.camelize(:lower)].resolve(object, {}, context) end end -- GitLab