diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 5ff85b568796c3ea719dc0d0b9436bb6a0ceef45..d9a262cc1b776179c872306c951b24bc3ad62f0c 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -6213,6 +6213,30 @@ Input type: `DestroySnippetInput` | `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | | `snippet` | [`Snippet`](#snippet) | Snippet after mutation. | +### `Mutation.devfileValidate` + +{{< details >}} +**Introduced** in GitLab 18.4. +**Status**: Experiment. +{{< /details >}} + +Input type: `DevfileValidateInput` + +#### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `devfileYaml` | [`String!`](#string) | Input devfile. | + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | +| `errors` | [`[String!]!`](#string) | Errors encountered during the mutation. | +| `valid` | [`Boolean`](#boolean) | Status whether devfile is valid or not. | + ### `Mutation.disableDevopsAdoptionNamespace` **Status**: Beta. diff --git a/ee/app/graphql/ee/types/mutation_type.rb b/ee/app/graphql/ee/types/mutation_type.rb index eaf2ff2e2066b2920ecac9cc9845c6b122099337..3b057ca1530acd47c1f96ea4f365270262a71fae 100644 --- a/ee/app/graphql/ee/types/mutation_type.rb +++ b/ee/app/graphql/ee/types/mutation_type.rb @@ -201,6 +201,7 @@ def self.authorization_scopes mount_mutation ::Mutations::Ci::Runners::ExportUsage mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Create mount_mutation ::Mutations::RemoteDevelopment::WorkspaceOperations::Update + mount_mutation ::Mutations::RemoteDevelopment::DevfileOperations::Validate, experiment: { milestone: '18.4' } mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Create mount_mutation ::Mutations::RemoteDevelopment::NamespaceClusterAgentMappingOperations::Delete mount_mutation ::Mutations::RemoteDevelopment::OrganizationClusterAgentMappingOperations::Create, experiment: { diff --git a/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb b/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb new file mode 100644 index 0000000000000000000000000000000000000000..0221524388311d29c285a711f73acb28d34713ab --- /dev/null +++ b/ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module Mutations + module RemoteDevelopment + module DevfileOperations + class Validate < BaseMutation + graphql_name "DevfileValidate" + + argument :devfile_yaml, + GraphQL::Types::String, + required: true, + description: "Input devfile." + + field :valid, + GraphQL::Types::Boolean, + description: "Status whether devfile is valid or not." + + # @param [String] devfile_yaml + # @return [Hash] + def resolve(devfile_yaml) + unless License.feature_available?(:remote_development) + raise_resource_not_available_error!("'remote_development' licensed feature is not available") + end + + yaml_content = devfile_yaml.is_a?(Hash) ? devfile_yaml[:devfile_yaml] : devfile_yaml + + domain_main_class_args = { + devfile_yaml: yaml_content, + user: current_user + } + + response = ::RemoteDevelopment::CommonService.execute( + domain_main_class: ::RemoteDevelopment::DevfileOperations::Main, + domain_main_class_args: domain_main_class_args + ) + + { + valid: response.success?, + errors: response.error? ? response.message.split(", ") : [] + } + end + end + end + end +end diff --git a/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb b/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..6199f6a8ec3a33a5cbe5e0874f8b9c21d78c8307 --- /dev/null +++ b/ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require "spec_helper" + +# noinspection RubyArgCount -- Rubymine detecting wrong types, it thinks some #create are from Minitest, not FactoryBot +RSpec.describe "Validating a devfile", feature_category: :workspaces do + include GraphqlHelpers + include StubFeatureFlags + include_context "with remote development shared fixtures" + + let_it_be(:user) { create(:user) } + let_it_be(:current_user) { user } + let(:devfile_yaml) { " " } + + let(:mutation) do + graphql_mutation(:devfile_validate, mutation_args) + end + + let(:all_mutation_args) do + { + devfile_yaml: devfile_yaml + } + end + + let(:mutation_args) { all_mutation_args } + + let(:expected_service_args) do + { + domain_main_class: ::RemoteDevelopment::DevfileOperations::Main, + domain_main_class_args: { + devfile_yaml: devfile_yaml, + user: current_user + } + } + end + + def mutation_response + graphql_mutation_response(:devfile_validate) + end + + before do + stub_licensed_features(remote_development: true) + end + + context "when devfile is valid" do + let(:stub_service_response) { ServiceResponse.success(message: "Validation Success") } + + before do + allow(RemoteDevelopment::CommonService).to receive(:execute).with(expected_service_args) do + stub_service_response + end + end + + it "returns empty array and valid status" do + post_graphql_mutation(mutation, current_user: user) + + expect_graphql_errors_to_be_empty + expect(mutation_response["valid"]).to be true + end + end + + context "when a devfile is invalid" do + let(:stub_service_response) { ServiceResponse.error(message: "Validation Error", reason: :bad_request) } + + before do + allow(RemoteDevelopment::CommonService).to receive(:execute).with(expected_service_args) do + stub_service_response + end + end + + it_behaves_like "a mutation that returns errors in the response", errors: ["Validation Error"] + + it "asserts valid key is false" do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response["valid"]).to be false + end + end + + context "when devfile argument is missing" do + let(:mutation_args) { all_mutation_args.except(:devfile_yaml) } + + it "returns error about required argument" do + post_graphql_mutation(mutation, current_user: current_user) + + expect_graphql_errors_to_include(/provided invalid value for devfileYaml \(Expected value to not be null\)/) + end + end + + context "when remote_development feature is unlicensed" do + before do + stub_licensed_features(remote_development: false) + end + + it_behaves_like "a mutation that returns top-level errors" do + let(:match_errors) { include(/'remote_development' licensed feature is not available/) } + end + end +end