From a1c408c2e1ede3c5b3477e4fc96d30217b57c491 Mon Sep 17 00:00:00 2001 From: Omar Nasser Date: Fri, 18 Jul 2025 07:53:35 +0300 Subject: [PATCH] Introduce a new graphql mutation to validate devfile --- doc/api/graphql/reference/_index.md | 24 +++++ ee/app/graphql/ee/types/mutation_type.rb | 1 + .../devfile_operations/validate.rb | 45 +++++++++ .../devfile_operations/validate_spec.rb | 99 +++++++++++++++++++ 4 files changed, 169 insertions(+) create mode 100644 ee/app/graphql/mutations/remote_development/devfile_operations/validate.rb create mode 100644 ee/spec/requests/api/graphql/mutations/remote_development/devfile_operations/validate_spec.rb diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 5ff85b568796c3..d9a262cc1b7761 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 eaf2ff2e2066b2..3b057ca1530acd 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 00000000000000..0221524388311d --- /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 00000000000000..6199f6a8ec3a33 --- /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 -- GitLab