diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 8c9c69d51d7bcb73ca4cce55b0772541a3b46f80..596eccd05865871ab4861d653bfe448270f33f8b 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -6497,6 +6497,30 @@ Input type: `ProjectSetLockedInput` | <a id="mutationprojectsetlockederrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationprojectsetlockedproject"></a>`project` | [`Project`](#project) | Project after mutation. | +### `Mutation.projectSettingsUpdate` + +NOTE: +**Introduced** in 16.9. +**Status**: Experiment. + +Input type: `ProjectSettingsUpdateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsettingsupdateduofeaturesenabled"></a>`duoFeaturesEnabled` | [`Boolean!`](#boolean) | Indicates whether GitLab Duo features are enabled for the project. | +| <a id="mutationprojectsettingsupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Full Path of the project the settings belong to. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="mutationprojectsettingsupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| <a id="mutationprojectsettingsupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | +| <a id="mutationprojectsettingsupdateprojectsettings"></a>`projectSettings` | [`ProjectSetting!`](#projectsetting) | Project settings after mutation. | + ### `Mutation.projectSubscriptionCreate` Input type: `ProjectSubscriptionCreateInput` @@ -24600,6 +24624,7 @@ Represents vulnerability finding of a security report on the pipeline. | <a id="projectdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. | | <a id="projectdetailedimportstatus"></a>`detailedImportStatus` | [`DetailedImportStatus`](#detailedimportstatus) | Detailed import status of the project. | | <a id="projectdora"></a>`dora` | [`Dora`](#dora) | Project's DORA metrics. | +| <a id="projectduofeaturesenabled"></a>`duoFeaturesEnabled` **{warning-solid}** | [`Boolean`](#boolean) | **Introduced** in 16.9. **Status**: Experiment. Indicates whether GitLab Duo features are enabled for the project. | | <a id="projectflowmetrics"></a>`flowMetrics` **{warning-solid}** | [`ProjectValueStreamAnalyticsFlowMetrics`](#projectvaluestreamanalyticsflowmetrics) | **Introduced** in 15.10. **Status**: Experiment. Flow metrics for value stream analytics. | | <a id="projectforkingaccesslevel"></a>`forkingAccessLevel` | [`ProjectFeatureAccess`](#projectfeatureaccess) | Access level required for forking access. | | <a id="projectforkscount"></a>`forksCount` | [`Int!`](#int) | Number of times the project has been forked. | @@ -26384,6 +26409,15 @@ Represents the source of a security policy belonging to a project. | <a id="projectsecuritytrainingname"></a>`name` | [`String!`](#string) | Name of the training provider. | | <a id="projectsecuritytrainingurl"></a>`url` | [`String!`](#string) | URL of the provider. | +### `ProjectSetting` + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectsettingduofeaturesenabled"></a>`duoFeaturesEnabled` | [`Boolean`](#boolean) | Indicates whether GitLab Duo features are enabled for the project. | +| <a id="projectsettingproject"></a>`project` | [`Project`](#project) | Project the settings belong to. | + ### `ProjectStatistics` #### Fields diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index 98bc8e1134e05fd8f1d6dbbb40cfcff479477371..bfdccf4856122401ebffe129c2ce9ffe9fb6a275 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -97,6 +97,7 @@ module MutationType mount_mutation ::Mutations::IncidentManagement::IssuableResourceLink::Destroy mount_mutation ::Mutations::AppSec::Fuzzing::Coverage::Corpus::Create mount_mutation ::Mutations::Projects::SetComplianceFramework + mount_mutation ::Mutations::Projects::ProjectSettingsUpdate, alpha: { milestone: '16.9' } mount_mutation ::Mutations::Projects::InitializeProductAnalytics mount_mutation ::Mutations::SecurityPolicy::CommitScanExecutionPolicy mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject diff --git a/ee/app/graphql/ee/types/project_type.rb b/ee/app/graphql/ee/types/project_type.rb index 2be55d9b9223776fe5e097dcbe721cc3788f61b5..3ae375fe2fb4db10448a8ac05a79d533ccce0f43 100644 --- a/ee/app/graphql/ee/types/project_type.rb +++ b/ee/app/graphql/ee/types/project_type.rb @@ -245,6 +245,11 @@ module ProjectType description: 'Indicates that merges of merge requests should be blocked ' \ 'unless all status checks have passed.' + field :duo_features_enabled, GraphQL::Types::Boolean, + null: true, + alpha: { milestone: '16.9' }, + description: 'Indicates whether GitLab Duo features are enabled for the project.' + field :gitlab_subscriptions_preview_billable_user_change, ::Types::GitlabSubscriptions::PreviewBillableUserChangeType, null: true, diff --git a/ee/app/graphql/mutations/projects/project_settings_update.rb b/ee/app/graphql/mutations/projects/project_settings_update.rb new file mode 100644 index 0000000000000000000000000000000000000000..0462d9728bb2ba4e17d7d04c65645f049a7b8d84 --- /dev/null +++ b/ee/app/graphql/mutations/projects/project_settings_update.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +module Mutations + module Projects + class ProjectSettingsUpdate < BaseMutation + graphql_name 'ProjectSettingsUpdate' + + include FindsProject + include Gitlab::Utils::StrongMemoize + + authorize :admin_project + + argument :full_path, + GraphQL::Types::ID, + required: true, + description: 'Full Path of the project the settings belong to.' + + argument :duo_features_enabled, + GraphQL::Types::Boolean, + required: true, + description: 'Indicates whether GitLab Duo features are enabled for the project.' + + field :project_settings, + Types::Projects::SettingType, + null: false, + description: 'Project settings after mutation.' + + def resolve(full_path:, **args) + raise raise_resource_not_available_error! unless allowed? + + settings = authorized_find!(full_path).project_setting + settings.update(args) + + { + project_settings: settings, + errors: errors_on_object(settings) + } + end + + private + + def allowed? + # TODO clean up via https://gitlab.com/gitlab-org/gitlab/-/issues/440546 + return true if ::Gitlab::Saas.feature_available?(:duo_chat_on_saas) + return false unless ::License.feature_available?(:code_suggestions) + + if ::CodeSuggestions::SelfManaged::SERVICE_START_DATE.past? + ::GitlabSubscriptions::AddOnPurchase + .for_code_suggestions + .any? + else # Before service start date + # TODO: Remove this else branch after the service start date + ::Gitlab::CurrentSettings.instance_level_code_suggestions_enabled + end + end + end + end +end diff --git a/ee/app/graphql/types/projects/setting_type.rb b/ee/app/graphql/types/projects/setting_type.rb new file mode 100644 index 0000000000000000000000000000000000000000..4ee8045c150310d9d84a2ba986e910b3f8285c67 --- /dev/null +++ b/ee/app/graphql/types/projects/setting_type.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Types + module Projects + # rubocop: disable Graphql/AuthorizeTypes -- parent handles auth + class SettingType < BaseObject + graphql_name 'ProjectSetting' + + field :duo_features_enabled, + GraphQL::Types::Boolean, + null: true, + description: 'Indicates whether GitLab Duo features are enabled for the project.' + + field :project, + Types::ProjectType, + null: true, + description: 'Project the settings belong to.' + end + # rubocop: enable Graphql/AuthorizeTypes + end +end diff --git a/ee/spec/graphql/mutations/projects/project_settings_update_spec.rb b/ee/spec/graphql/mutations/projects/project_settings_update_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..d619216030254956cc2037326ad52c8f9ec650ef --- /dev/null +++ b/ee/spec/graphql/mutations/projects/project_settings_update_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Mutations::Projects::ProjectSettingsUpdate, feature_category: :code_suggestions do + subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) } + + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create(:group) } + let_it_be(:add_on) { create(:gitlab_subscription_add_on, :code_suggestions) } + let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on) } + let_it_be(:project) { create(:project, namespace: namespace) } + + let_it_be(:project_without_addon) { create(:project) } + + describe '#resolve' do + subject(:resolve) do + mutation.resolve( + full_path: project.full_path, + duo_features_enabled: duo_features_enabled) + end + + let(:duo_features_enabled) { true } + + it 'raises an error if the resource is not accessible to the user' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + + context 'when the user can update duo features enabled' do + before_all do + project.add_owner(user) + end + + context 'when duo features are not available' do + before do + stub_licensed_features(code_suggestions: false) + end + + it 'raises an error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when duo addon is not available' do + before do + stub_licensed_features(code_suggestions: true) + stub_const("::CodeSuggestions::SelfManaged::SERVICE_START_DATE", Time.zone.parse('2000-02-15T00:00:00Z')) + end + + it 'raises an error' do + expect do + mutation.resolve(full_path: project_without_addon.full_path, + duo_features_enabled: duo_features_enabled) + end.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when instance has it disabled' do + before do + stub_licensed_features(code_suggestions: true) + stub_const("::CodeSuggestions::SelfManaged::SERVICE_START_DATE", Time.zone.parse('3000-02-15T00:00:00Z')) + stub_application_setting(instance_level_code_suggestions_enabled: false) + end + + it 'raises an error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + context 'when duo chat is enabled on saas' do + before do + stub_licensed_features(code_suggestions: false) + stub_saas_features(duo_chat_on_saas: true) + end + + it 'updates the setting' do + expect(resolve[:project_settings]).to have_attributes(duo_features_enabled: duo_features_enabled) + end + end + + context 'when disabling duo features' do + let(:duo_features_enabled) { false } + + before do + stub_saas_features(duo_chat_on_saas: true) + end + + it 'updates the setting' do + expect(resolve[:project_settings]).to have_attributes(duo_features_enabled: duo_features_enabled) + end + end + end + + context 'when user cannot update duo features enabled' do + before_all do + project.add_developer(user) + end + + it 'will raise an error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + 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 34d64b5b362092f9a6ca19008478bd3d084c0f44..8b62b1dc3a8aca4ce5bc8e5a6ce2054433d5e58e 100644 --- a/ee/spec/graphql/types/project_type_spec.rb +++ b/ee/spec/graphql/types/project_type_spec.rb @@ -29,7 +29,7 @@ security_policy_project security_training_urls vulnerability_images only_allow_merge_if_all_status_checks_passed security_policy_project_linked_projects security_policy_project_linked_namespaces dependencies merge_requests_disable_committers_approval has_jira_vulnerability_issue_creation_enabled - ci_subscriptions_projects ci_subscribed_projects ai_agents + ci_subscriptions_projects ci_subscribed_projects ai_agents duo_features_enabled ] expect(described_class).to include_graphql_fields(*expected_fields) diff --git a/ee/spec/requests/api/graphql/mutations/projects/project_settings_update_spec.rb b/ee/spec/requests/api/graphql/mutations/projects/project_settings_update_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..91041d2dbb32535212b3d86afdbbae540778f606 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/projects/project_settings_update_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe "Project settings update", feature_category: :code_suggestions do + include GraphqlHelpers + include ProjectForksHelper + include ExclusiveLeaseHelpers + + let_it_be(:user) { create(:user) } + let_it_be(:namespace) { create(:group) } + let_it_be(:add_on) { create(:gitlab_subscription_add_on, :code_suggestions) } + let_it_be(:add_on_purchase) { create(:gitlab_subscription_add_on_purchase, namespace: namespace, add_on: add_on) } + let_it_be(:project) { create(:project, namespace: namespace) } + let_it_be(:duo_features_enabled) { true } + + let(:mutation) do + params = { full_path: project.full_path, duo_features_enabled: duo_features_enabled } + + graphql_mutation(:project_settings_update, params) do + <<-QL.strip_heredoc + projectSettings { + duoFeaturesEnabled + } + errors + QL + end + end + + context 'when updating settings' do + before_all do + project.add_maintainer(user) + end + + before do + stub_saas_features(duo_chat_on_saas: true) + end + + it 'will update the settings' do + post_graphql_mutation(mutation, current_user: user) + expect(graphql_mutation_response('projectSettingsUpdate')['projectSettings']) + .to eq({ 'duoFeaturesEnabled' => duo_features_enabled }) + end + end +end diff --git a/ee/spec/requests/api/graphql/projects/duo_features_enabled_spec.rb b/ee/spec/requests/api/graphql/projects/duo_features_enabled_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..7db8c1be0eed807f199c2f1112b34a42ed42819f --- /dev/null +++ b/ee/spec/requests/api/graphql/projects/duo_features_enabled_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'querying duoFeaturesEnabled', feature_category: :code_suggestions do + include GraphqlHelpers + + let_it_be(:current_user) { create(:user) } + let_it_be(:project) { create(:project) } + + describe 'duoFeaturesEnabled' do + before_all do + project.add_maintainer(current_user) + end + + it 'is available to query' do + result = GitlabSchema.execute(%( + query { + project(fullPath: "#{project.full_path}") { + duoFeaturesEnabled + } + } + ), context: { current_user: current_user }).as_json + + expect(result.dig('data', 'project', 'duoFeaturesEnabled')).to eq(project.duo_features_enabled) + end + end +end