diff --git a/app/finders/pages/deployments_finder.rb b/app/finders/pages/deployments_finder.rb index 2726bbfb4c05481259a9768e4873c919a5f14d4d..66e476ed8a76d3d36140d7128ea5f855189a27bd 100644 --- a/app/finders/pages/deployments_finder.rb +++ b/app/finders/pages/deployments_finder.rb @@ -4,14 +4,14 @@ module Pages class DeploymentsFinder attr_reader :params - def initialize(namespace, params = {}) - @namespace = namespace + def initialize(parent, params = {}) + @parent = parent @params = params end def execute deployments = PagesDeployment - deployments = by_namespace(deployments) + deployments = by_parent(deployments) deployments = by_active_status(deployments) deployments = find_versioned(deployments) sort(deployments) @@ -19,8 +19,20 @@ def execute private + def by_parent(deployments) + case @parent + when Namespace then by_namespace(deployments) + when Project then by_project(deployments) + else raise "Pages::DeploymentsFinder only supports Namespace or Projects as parent" + end + end + + def by_project(deployments) + deployments.project_id_in(@parent.id) + end + def by_namespace(deployments) - deployments.project_id_in(@namespace.projects.select(:id)) + deployments.project_id_in(@parent.projects.select(:id)) end def by_active_status(deployments) diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb index e69e29aafebc9458d155a449d3ac02d24b1f90c4..6a11f1fbf3963780c6b01ccb581323cebd4df298 100644 --- a/app/graphql/types/project_type.rb +++ b/app/graphql/types/project_type.rb @@ -731,6 +731,11 @@ class ProjectType < BaseObject description: 'Term by which to search deploy key titles' end + field :pages_deployments, Types::PagesDeploymentType.connection_type, null: true, + resolver: Resolvers::PagesDeploymentsResolver, + connection: true, + description: "List of the project's Pages Deployments." + def protectable_branches ProtectableDropdown.new(project, :branches).protectable_ref_names end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 684b12a920f753d70ce48e2d37cf33f90a9a1b3f..f42880491d17f9f7a92191dbbb8143a366840319 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -28388,6 +28388,24 @@ four standard [pagination arguments](#pagination-arguments): | <a id="projectpackagessort"></a>`sort` | [`PackageSort`](#packagesort) | Sort packages by this criteria. | | <a id="projectpackagesstatus"></a>`status` | [`PackageStatus`](#packagestatus) | Filter a package by status. | +##### `Project.pagesDeployments` + +List of the project's Pages Deployments. + +Returns [`PagesDeploymentConnection`](#pagesdeploymentconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#pagination-arguments): +`before: String`, `after: String`, `first: Int`, and `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="projectpagesdeploymentsactive"></a>`active` | [`Boolean`](#boolean) | Filter by active or inactive state. | +| <a id="projectpagesdeploymentssort"></a>`sort` | [`Sort`](#sort) | Sort results. | +| <a id="projectpagesdeploymentsversioned"></a>`versioned` | [`Boolean`](#boolean) | Filter deployments that are versioned or unversioned. | + ##### `Project.pipeline` Build pipeline of the project. diff --git a/spec/finders/pages/deployments_finder_spec.rb b/spec/finders/pages/deployments_finder_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..3a9a29aee1f8703486e4df4c765a2d1fec385dd8 --- /dev/null +++ b/spec/finders/pages/deployments_finder_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Pages::DeploymentsFinder, feature_category: :pages do + # Most of Pages::DeploymentsFinder is tested with the GraphQL request specs + # so this spec will only test remaining conditions that cannot be + # tested otherwise. + + it 'execute throws an error when passed a parent that\'s not of type Project or Namespace' do + expect { described_class.new("Foo").execute }.to raise_error( + RuntimeError, "Pages::DeploymentsFinder only supports Namespace or Projects as parent" + ) + end +end diff --git a/spec/requests/api/graphql/pages/deployments_query_spec.rb b/spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb similarity index 98% rename from spec/requests/api/graphql/pages/deployments_query_spec.rb rename to spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb index 6af0a7950a41a5d08c21fd69f9f51081bb42c7f0..fa7473fef917392a7bb621495d69f4d25b961bec 100644 --- a/spec/requests/api/graphql/pages/deployments_query_spec.rb +++ b/spec/requests/api/graphql/pages/namespace_deployments_query_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe 'Pages Deployments query', feature_category: :pages do +RSpec.describe 'Namespace Pages Deployments query', feature_category: :pages do include GraphqlHelpers let(:project_maintainer) { create(:user) } diff --git a/spec/requests/api/graphql/pages/project_deployments_query_spec.rb b/spec/requests/api/graphql/pages/project_deployments_query_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..13df0a0e45dd00cdf7ee370d30cdcdd39ef6ec5c --- /dev/null +++ b/spec/requests/api/graphql/pages/project_deployments_query_spec.rb @@ -0,0 +1,179 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project Pages Deployments query', feature_category: :pages do + include GraphqlHelpers + + let_it_be(:project_maintainer) { create(:user) } + let_it_be(:project_developer) { create(:user) } + let_it_be(:project) { create(:project, maintainers: project_maintainer, developers: project_developer) } + let_it_be(:pages_deployments) do + [ + # primary deployment, still active + create(:pages_deployment, project: project), + # primary deployment, inactive + create(:pages_deployment, project: project, deleted_at: Time.now - 2.hours), + # versioned deployment, still active + create(:pages_deployment, project: project, path_prefix: 'foo'), + # versioned deployment, inactive + create(:pages_deployment, project: project, path_prefix: 'foo', deleted_at: Time.now - 2.hours) + ] + end + + let(:query) do + <<~GRAPHQL + query GetProjectWithPagesDeployments { + project(fullPath: "#{project.full_path}") { + pagesDeployments(#{pages_deployments_arguments}) { + count + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + edges { + cursor + node { + #{all_graphql_fields_for('PagesDeployment', max_depth: 1)} + } + } + } + } + } + GRAPHQL + end + + let(:pages_deployments_arguments) { 'sort: CREATED_ASC, first: 10' } + let(:deployments_response) do + graphql_data_at(:project, :pages_deployments, :edges) + end + + def fields_for_deployment(deployment) + { + 'id' => deployment.to_global_id.to_s, + 'active' => deployment.active?, + 'ciBuildId' => anything, + 'createdAt' => deployment.created_at.iso8601, + 'deletedAt' => deployment.deleted_at&.iso8601, + 'fileCount' => deployment.file_count, + 'pathPrefix' => deployment.path_prefix, + 'rootDirectory' => deployment.root_directory, + 'size' => deployment.size, + 'updatedAt' => deployment.updated_at.iso8601, + 'url' => deployment.url + } + end + + def match_deployment_node_for(deployment) + expect(deployments_response.first).to match({ + 'cursor' => instance_of(String), + 'node' => fields_for_deployment(deployment) + }) + end + + before do + post_graphql(query, current_user: current_user) + end + + describe 'user is authorized' do + let(:current_user) { project_maintainer } + let(:expected_deployment) { pages_deployments[0] } + + describe 'response' do + it 'returns a deployment with all of the expected fields' do + match_deployment_node_for(expected_deployment) + end + end + + describe 'default connection fields' do + let(:pages_deployments_arguments) { 'first: 3' } + + it 'has all expected connection pagination fields' do + expect( + graphql_data_at(:project, :pages_deployments) + ).to match(hash_including({ + 'count' => 4, + 'pageInfo' => { + 'endCursor' => instance_of(String), + 'hasNextPage' => true, + 'hasPreviousPage' => false, + 'startCursor' => instance_of(String) + } + })) + end + end + + describe 'sorting' do + let(:pages_deployments_arguments) { 'sort: CREATED_DESC' } + let(:expected_deployment) { pages_deployments[pages_deployments.length - 1] } + + it 'returns the expected deployment' do + match_deployment_node_for(expected_deployment) + end + end + + describe 'filtering' do + describe 'active deployments' do + let(:pages_deployments_arguments) { 'active: true' } + + it 'only returns active deployments' do + expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({ + 'cursor' => instance_of(String), + 'node' => hash_including({ + 'active' => true + }) + })) + end + end + + describe 'only inactive deployments' do + let(:pages_deployments_arguments) { 'active: false' } + + it 'only returns inactive deployments' do + expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({ + 'cursor' => instance_of(String), + 'node' => hash_including({ + 'active' => false + }) + })) + end + end + + describe 'versioned deployments' do + let(:pages_deployments_arguments) { 'versioned: true' } + + it 'only returns versioned deployments' do + expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({ + 'cursor' => instance_of(String), + 'node' => hash_including({ + 'pathPrefix' => 'foo' + }) + })) + end + end + + describe 'unversioned deployments' do + let(:pages_deployments_arguments) { 'versioned: false' } + + it 'only returns unversioned deployments' do + expect(graphql_data_at(:project, :pages_deployments, :edges)).to all(match({ + 'cursor' => instance_of(String), + 'node' => hash_including({ + 'pathPrefix' => nil + }) + })) + end + end + end + + describe 'user is unauthorized to view pages deployments' do + let(:current_user) { project_developer } + + it 'returns an empty result' do + expect(deployments_response).to match([]) + end + end + end +end