diff options
44 files changed, 727 insertions, 238 deletions
diff --git a/.rubocop_todo/rake/require.yml b/.rubocop_todo/rake/require.yml deleted file mode 100644 index 0659dd11037..00000000000 --- a/.rubocop_todo/rake/require.yml +++ /dev/null @@ -1,5 +0,0 @@ ---- -Rake/Require: - Details: grace period - Exclude: - - 'qa/tasks/webdrivers.rake' diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4f6ca5a9617..716be080851 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -8,12 +8,10 @@ class CommitStatus < Ci::ApplicationRecord include Presentable include BulkInsertableAssociations include TaggableQueries - include SafelyChangeColumnDefault self.table_name = 'ci_builds' self.primary_key = :id partitionable scope: :pipeline - columns_changing_default :partition_id belongs_to :user belongs_to :project diff --git a/app/models/design_management/git_repository.rb b/app/models/design_management/git_repository.rb new file mode 100644 index 00000000000..92db82f7bd1 --- /dev/null +++ b/app/models/design_management/git_repository.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +module DesignManagement + class GitRepository < ::Repository + extend ::Gitlab::Utils::Override + + # We define static git attributes for the design repository as this + # repository is entirely GitLab-managed rather than user-facing. + # + # Enable all uploaded files to be stored in LFS. + MANAGED_GIT_ATTRIBUTES = <<~GA.freeze + /#{DesignManagement.designs_directory}/* filter=lfs diff=lfs merge=lfs -text + GA + + # Passing the `project` explicitly saves on one query on the `project` table + # in Mutations::DesignManagement::Delete + + def initialize(project) + @project = project + + full_path = @project.full_path + Gitlab::GlRepository::DESIGN.path_suffix + disk_path = @project.disk_path + Gitlab::GlRepository::DESIGN.path_suffix + + # Ideally a DesignManagement::Repository, not a project would be + # the container to this Git repository. + # See https://gitlab.com/gitlab-org/gitlab/-/issues/394816. + + super( + full_path, + @project, + shard: @project.repository_storage, + disk_path: disk_path, + repo_type: Gitlab::GlRepository::DESIGN + ) + end + + # Override of a method called on Repository instances but sent via + # method_missing to Gitlab::Git::Repository where it is defined + def info_attributes + @info_attributes ||= Gitlab::Git::AttributesParser.new(MANAGED_GIT_ATTRIBUTES) + end + + # Override of a method called on Repository instances but sent via + # method_missing to Gitlab::Git::Repository where it is defined + def attributes(path) + info_attributes.attributes(path) + end + + # Override of a method called on Repository instances but sent via + # method_missing to Gitlab::Git::Repository where it is defined + def gitattribute(path, name) + attributes(path)[name] + end + + # Override of a method called on Repository instances but sent via + # method_missing to Gitlab::Git::Repository where it is defined + def attributes_at(_ref = nil) + info_attributes + end + + override :copy_gitattributes + def copy_gitattributes(_ref = nil) + true + end + end +end diff --git a/app/models/design_management/repository.rb b/app/models/design_management/repository.rb index 2b1e6070e6b..e6790bc3253 100644 --- a/app/models/design_management/repository.rb +++ b/app/models/design_management/repository.rb @@ -1,51 +1,24 @@ # frozen_string_literal: true module DesignManagement - class Repository < ::Repository - extend ::Gitlab::Utils::Override + class Repository < ApplicationRecord + include ::Gitlab::Utils::StrongMemoize - # We define static git attributes for the design repository as this - # repository is entirely GitLab-managed rather than user-facing. - # - # Enable all uploaded files to be stored in LFS. - MANAGED_GIT_ATTRIBUTES = <<~GA - /#{DesignManagement.designs_directory}/* filter=lfs diff=lfs merge=lfs -text - GA + belongs_to :project, inverse_of: :design_management_repository + validates :project, presence: true, uniqueness: true - def initialize(project) - full_path = project.full_path + Gitlab::GlRepository::DESIGN.path_suffix - disk_path = project.disk_path + Gitlab::GlRepository::DESIGN.path_suffix + # This is so that git_repo is initialized once `project` has been + # set. If it is not set after intialization and saving the record + # fails for some reason, the first call to `git_repo`` (initiated by + # `delegate_missing_to`) will throw an error because project would + # be missing. + after_initialize :git_repo - super(full_path, project, shard: project.repository_storage, disk_path: disk_path, repo_type: Gitlab::GlRepository::DESIGN) - end - - # Override of a method called on Repository instances but sent via - # method_missing to Gitlab::Git::Repository where it is defined - def info_attributes - @info_attributes ||= Gitlab::Git::AttributesParser.new(MANAGED_GIT_ATTRIBUTES) - end - - # Override of a method called on Repository instances but sent via - # method_missing to Gitlab::Git::Repository where it is defined - def attributes(path) - info_attributes.attributes(path) - end - - # Override of a method called on Repository instances but sent via - # method_missing to Gitlab::Git::Repository where it is defined - def gitattribute(path, name) - attributes(path)[name] - end - - # Override of a method called on Repository instances but sent via - # method_missing to Gitlab::Git::Repository where it is defined - def attributes_at(_ref = nil) - info_attributes - end + delegate_missing_to :git_repo - override :copy_gitattributes - def copy_gitattributes(_ref = nil) - true + def git_repo + GitRepository.new(project) end + strong_memoize_attr :git_repo end end diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index c1511ee1233..ab6c58498ad 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -253,7 +253,7 @@ class MergeRequest < ApplicationRecord Gitlab::Timeless.timeless(merge_request, &block) end - after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition| + after_transition any => [:unchecked, :cannot_be_merged_recheck, :can_be_merged, :cannot_be_merged] do |merge_request, transition| next if merge_request.skip_merge_status_trigger merge_request.run_after_commit do diff --git a/app/models/project.rb b/app/models/project.rb index 03aa131e71b..b2839ad78a3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -223,6 +223,7 @@ class Project < ApplicationRecord has_one :zentao_integration, class_name: 'Integrations::Zentao' has_one :wiki_repository, class_name: 'Projects::WikiRepository', inverse_of: :project + has_one :design_management_repository, class_name: 'DesignManagement::Repository', inverse_of: :project has_one :root_of_fork_network, foreign_key: 'root_project_id', inverse_of: :root_project, diff --git a/app/views/shared/form_elements/_description.html.haml b/app/views/shared/form_elements/_description.html.haml index 2c46b2191c6..12d0af294da 100644 --- a/app/views/shared/form_elements/_description.html.haml +++ b/app/views/shared/form_elements/_description.html.haml @@ -2,6 +2,7 @@ - model = local_assigns.fetch(:model) - form = local_assigns.fetch(:form) - placeholder = model.is_a?(MergeRequest) ? _('Describe the goal of the changes and what reviewers should be aware of.') : _('Write a description or drag your files here…') +- no_issuable_templates = issuable_templates(ref_project, model.to_ability_name).empty? - supports_quick_actions = true - preview_url = preview_markdown_path(project, target_type: model.class.name) @@ -25,3 +26,5 @@ = render 'shared/notes/hints', supports_quick_actions: supports_quick_actions .clearfix .error-alert + - if no_issuable_templates && can?(current_user, :push_code, model.project) + = render 'shared/issuable/form/default_templates' diff --git a/app/views/shared/issuable/form/_default_templates.html.haml b/app/views/shared/issuable/form/_default_templates.html.haml index 50f30e58b35..2dda0049c09 100644 --- a/app/views/shared/issuable/form/_default_templates.html.haml +++ b/app/views/shared/issuable/form/_default_templates.html.haml @@ -1,4 +1,4 @@ -%p.form-text.text-muted +.gl-mt-3.gl-text-secondary - template_link_url = help_page_path('user/project/description_templates') - template_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: template_link_url } = s_('Promotions|Add %{link_start} description templates %{link_end} to help your contributors to communicate effectively!').html_safe % { link_start: template_link_start, link_end: '</a>'.html_safe } diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml index 4d31baee25b..be836f4b8a9 100644 --- a/app/views/shared/issuable/form/_title.html.haml +++ b/app/views/shared/issuable/form/_title.html.haml @@ -1,6 +1,5 @@ - issuable = local_assigns.fetch(:issuable) - form = local_assigns.fetch(:form) -- no_issuable_templates = issuable_templates(ref_project, issuable.to_ability_name).empty? %div{ data: { testid: 'issue-title-input-field' } } = form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true, @@ -13,6 +12,3 @@ = s_('MergeRequests|Mark as draft') = c.help_text do = s_('MergeRequests|Drafts cannot be merged until marked ready.') - - - if no_issuable_templates && can?(current_user, :push_code, issuable.project) - = render 'shared/issuable/form/default_templates' diff --git a/db/docs/design_management_repositories.yml b/db/docs/design_management_repositories.yml new file mode 100644 index 00000000000..61d905c2703 --- /dev/null +++ b/db/docs/design_management_repositories.yml @@ -0,0 +1,10 @@ +--- +table_name: design_management_repositories +classes: +- DesignManagement::Repository +feature_categories: +- design_management +description: Holds information about Design Management Repositories +introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111555' +milestone: '15.11' +gitlab_schema: gitlab_main
\ No newline at end of file diff --git a/db/migrate/20230307000000_create_design_management_repository.rb b/db/migrate/20230307000000_create_design_management_repository.rb new file mode 100644 index 00000000000..da4dee9f320 --- /dev/null +++ b/db/migrate/20230307000000_create_design_management_repository.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class CreateDesignManagementRepository < Gitlab::Database::Migration[2.1] + def change + create_table :design_management_repositories do |t| + t.references :project, index: { unique: true }, null: false, foreign_key: { on_delete: :cascade } + + t.timestamps_with_timezone null: false + end + end +end diff --git a/db/post_migrate/20230321124837_remove_ci_builds_partition_id_default.rb b/db/post_migrate/20230321124837_remove_ci_builds_partition_id_default.rb index 17fd8258e9c..a3a80fc6110 100644 --- a/db/post_migrate/20230321124837_remove_ci_builds_partition_id_default.rb +++ b/db/post_migrate/20230321124837_remove_ci_builds_partition_id_default.rb @@ -4,10 +4,10 @@ class RemoveCiBuildsPartitionIdDefault < Gitlab::Database::Migration[2.1] enable_lock_retries! def up - change_column_default :ci_builds, :partition_id, from: 100, to: nil + # no-op. See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/8588 for details. end def down - change_column_default :ci_builds, :partition_id, from: nil, to: 100 + # no-op. See https://gitlab.com/gitlab-com/gl-infra/production/-/issues/8588 for details. end end diff --git a/db/schema_migrations/20230307000000 b/db/schema_migrations/20230307000000 new file mode 100644 index 00000000000..273a5553777 --- /dev/null +++ b/db/schema_migrations/20230307000000 @@ -0,0 +1 @@ +192e21a20619e8940d1b5db69eeac1a7c1bfe32d821b671096be4eef4b4214f1
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 614f4f8c05d..d30a0d01381 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -13009,7 +13009,7 @@ CREATE TABLE ci_builds ( scheduling_type smallint, id bigint NOT NULL, stage_id bigint, - partition_id bigint NOT NULL, + partition_id bigint DEFAULT 100 NOT NULL, CONSTRAINT check_1e2fbd1b39 CHECK ((lock_version IS NOT NULL)) ); @@ -15338,6 +15338,22 @@ CREATE SEQUENCE design_management_designs_versions_id_seq ALTER SEQUENCE design_management_designs_versions_id_seq OWNED BY design_management_designs_versions.id; +CREATE TABLE design_management_repositories ( + id bigint NOT NULL, + project_id bigint NOT NULL, + created_at timestamp with time zone NOT NULL, + updated_at timestamp with time zone NOT NULL +); + +CREATE SEQUENCE design_management_repositories_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE design_management_repositories_id_seq OWNED BY design_management_repositories.id; + CREATE TABLE design_management_versions ( id bigint NOT NULL, sha bytea NOT NULL, @@ -24703,6 +24719,8 @@ ALTER TABLE ONLY design_management_designs ALTER COLUMN id SET DEFAULT nextval(' ALTER TABLE ONLY design_management_designs_versions ALTER COLUMN id SET DEFAULT nextval('design_management_designs_versions_id_seq'::regclass); +ALTER TABLE ONLY design_management_repositories ALTER COLUMN id SET DEFAULT nextval('design_management_repositories_id_seq'::regclass); + ALTER TABLE ONLY design_management_versions ALTER COLUMN id SET DEFAULT nextval('design_management_versions_id_seq'::regclass); ALTER TABLE ONLY design_user_mentions ALTER COLUMN id SET DEFAULT nextval('design_user_mentions_id_seq'::regclass); @@ -26659,6 +26677,9 @@ ALTER TABLE ONLY design_management_designs ALTER TABLE ONLY design_management_designs_versions ADD CONSTRAINT design_management_designs_versions_pkey PRIMARY KEY (id); +ALTER TABLE ONLY design_management_repositories + ADD CONSTRAINT design_management_repositories_pkey PRIMARY KEY (id); + ALTER TABLE ONLY design_management_versions ADD CONSTRAINT design_management_versions_pkey PRIMARY KEY (id); @@ -30140,6 +30161,8 @@ CREATE INDEX index_design_management_designs_versions_on_event ON design_managem CREATE INDEX index_design_management_designs_versions_on_version_id ON design_management_designs_versions USING btree (version_id); +CREATE UNIQUE INDEX index_design_management_repositories_on_project_id ON design_management_repositories USING btree (project_id); + CREATE INDEX index_design_management_versions_on_author_id ON design_management_versions USING btree (author_id) WHERE (author_id IS NOT NULL); CREATE INDEX index_design_management_versions_on_issue_id ON design_management_versions USING btree (issue_id); @@ -35331,6 +35354,9 @@ ALTER TABLE ONLY dast_site_validations ALTER TABLE ONLY vulnerability_findings_remediations ADD CONSTRAINT fk_rails_28a8d0cf93 FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE; +ALTER TABLE ONLY design_management_repositories + ADD CONSTRAINT fk_rails_2938d8dd8d FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY incident_management_issuable_escalation_statuses ADD CONSTRAINT fk_rails_29abffe3b9 FOREIGN KEY (policy_id) REFERENCES incident_management_escalation_policies(id) ON DELETE SET NULL; diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md index 8c95e68e1f2..200c2609914 100644 --- a/doc/user/project/import/index.md +++ b/doc/user/project/import/index.md @@ -159,3 +159,17 @@ For more information, see: including settings that need checking afterwards and other limitations. For support, customers must enter into a paid engagement with GitLab Professional Services. + +## Security + +Only import projects from sources you trust. If you import a project from an untrusted source, it +may be possible for an attacker to steal your sensitive data. For example, an imported project +with a malicious `.gitlab-ci.yml` file could allow an attacker to exfiltrate group CI/CD variables. + +GitLab self-managed administrators can reduce their attack surface by disabling import sources they don't need: + +1. On the top bar, select **Main menu > Admin**. +1. On the left sidebar, select **Settings > General**. +1. Expand **Visibility and access controls**. +1. Scroll to **Import sources**. +1. Clear checkboxes for importers that are not required. diff --git a/lib/gitlab/gl_repository.rb b/lib/gitlab/gl_repository.rb index d123989ef8e..efdb205b8eb 100644 --- a/lib/gitlab/gl_repository.rb +++ b/lib/gitlab/gl_repository.rb @@ -34,7 +34,7 @@ module Gitlab DESIGN = ::Gitlab::GlRepository::RepoType.new( name: :design, access_checker_class: ::Gitlab::GitAccessDesign, - repository_resolver: -> (project) { ::DesignManagement::Repository.new(project) }, + repository_resolver: -> (project) { ::DesignManagement::Repository.new(project: project) }, suffix: :design ).freeze diff --git a/qa/qa/flow/pipeline.rb b/qa/qa/flow/pipeline.rb index 0765a8758ec..d1cfe8dae09 100644 --- a/qa/qa/flow/pipeline.rb +++ b/qa/qa/flow/pipeline.rb @@ -9,7 +9,7 @@ module QA # canceled, created, failed, manual, passed # pending, running, skipped def visit_latest_pipeline(status: nil, wait: nil, skip_wait: true) - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform do |index| index.has_any_pipeline?(wait: wait) index.wait_for_latest_pipeline(status: status, wait: wait) if status || !skip_wait @@ -18,7 +18,7 @@ module QA end def wait_for_latest_pipeline(status: nil, wait: nil) - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform do |index| index.has_any_pipeline?(wait: wait) index.wait_for_latest_pipeline(status: status, wait: wait) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 451a9c3ee6e..b9bd6b14d7f 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -199,7 +199,8 @@ module QA end def click_diffs_tab - click_element(:diffs_tab) + # Do not wait for spinner due to https://gitlab.com/gitlab-org/gitlab/-/issues/398584 + click_element(:diffs_tab, skip_finished_loading_check: true) end def click_pipeline_link diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index db70d3e1d02..24ed2d766f1 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -15,6 +15,15 @@ module QA include SubMenus::Settings include SubMenus::Packages + if Runtime::Env.super_sidebar_enabled? + include SubMenus::SuperSidebar::Plan + include SubMenus::SuperSidebar::Settings + include SubMenus::SuperSidebar::Repository + include SubMenus::SuperSidebar::CiCd + include SubMenus::SuperSidebar::Compliance + include SubMenus::SuperSidebar::Operations + end + def click_merge_requests within_sidebar do click_element(:sidebar_menu_link, menu_item: 'Merge requests') @@ -22,12 +31,16 @@ module QA end def click_wiki + return go_to_wiki if Runtime::Env.super_sidebar_enabled? + within_sidebar do click_element(:sidebar_menu_link, menu_item: 'Wiki') end end def click_activity + return go_to_activity if Runtime::Env.super_sidebar_enabled? + hover_project_information do within_submenu do click_element(:sidebar_menu_item_link, menu_item: 'Activity') @@ -36,12 +49,16 @@ module QA end def click_snippets + return go_to_snippets if Runtime::Env.super_sidebar_enabled? + within_sidebar do click_element(:sidebar_menu_link, menu_item: 'Snippets') end end def click_members + return go_to_members if Runtime::Env.super_sidebar_enabled? + hover_project_information do within_submenu do click_element(:sidebar_menu_item_link, menu_item: 'Members') diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index 4ae51798e54..3547ea76182 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -15,7 +15,7 @@ module QA end end - def click_ci_cd_pipelines + def go_to_pipelines within_sidebar do click_element(:sidebar_menu_link, menu_item: 'CI/CD') end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/ci_cd.rb b/qa/qa/page/project/sub_menus/super_sidebar/ci_cd.rb new file mode 100644 index 00000000000..032842edc43 --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/ci_cd.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module CiCd + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_pipelines + open_ci_cd_submenu('Pipelines') + end + + def go_to_editor + open_ci_cd_submenu('Editor') + end + + def go_to_jobs + open_ci_cd_submenu('Jobs') + end + + def go_to_schedules + open_ci_cd_submenu('Schedules') + end + + private + + def open_ci_cd_submenu(sub_menu) + open_submenu("CI/CD", "#ci-cd", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/common.rb b/qa/qa/page/project/sub_menus/super_sidebar/common.rb new file mode 100644 index 00000000000..7cb14f4189e --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/common.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Common + private + + def open_submenu(parent_menu_name, parent_section_id, sub_menu) + click_element(:sidebar_menu_link, menu_item: parent_menu_name) + + # TODO: it's not possible to add qa-selectors to sub-menu container + within(parent_section_id) do + click_element(:sidebar_menu_link, menu_item: sub_menu) + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/compliance.rb b/qa/qa/page/project/sub_menus/super_sidebar/compliance.rb new file mode 100644 index 00000000000..98f6a04c5c6 --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/compliance.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Compliance + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_audit_events + open_compliance_submenu('Audit events') + end + + def go_to_security_configuration + open_compliance_submenu('Security configuration') + end + + private + + def open_compliance_submenu(sub_menu) + open_submenu("Security and Compliance", "#security-and-compliance", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/operations.rb b/qa/qa/page/project/sub_menus/super_sidebar/operations.rb new file mode 100644 index 00000000000..f47c113083a --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/operations.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Operations + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_environments + open_operations_submenu('Environments') + end + + def go_to_feature_flags + open_operations_submenu('Feature Flags') + end + + def go_to_releases + open_operations_submenu('Releases') + end + + def go_to_package_registry + open_operations_submenu('Package Registry') + end + + def go_to_infrastructure_registry + open_operations_submenu('Infrastructure Registry') + end + + def go_to_kubernetes_clusters + open_operations_submenu('Kubernetes clusters') + end + + def go_to_terraform + open_operations_submenu('Terraform') + end + + private + + def open_operations_submenu(sub_menu) + open_submenu("Operations", "#operations", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/plan.rb b/qa/qa/page/project/sub_menus/super_sidebar/plan.rb new file mode 100644 index 00000000000..cf90ad9e955 --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/plan.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Plan + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_members + open_plan_submenu("Members") + end + + def go_to_labels + open_plan_submenu("Labels") + end + + def go_to_activity + open_plan_submenu("Activity") + end + + def go_to_boards + open_plan_submenu("Boards") + end + + def go_to_milestones + open_plan_submenu("Milestones") + end + + def go_to_service_desk + open_plan_submenu("Service Desk") + end + + def go_to_wiki + open_plan_submenu("Wiki") + end + + private + + def open_plan_submenu(sub_menu) + open_submenu("Plan", "#plan", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/repository.rb b/qa/qa/page/project/sub_menus/super_sidebar/repository.rb new file mode 100644 index 00000000000..52a9325c0b1 --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/repository.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Repository + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_files + open_repository_submenu("Files") + end + + def go_to_repository_commits + open_repository_submenu("Commits") + end + + def go_to_repository_branches + open_repository_submenu("Branches") + end + + def go_to_repository_tags + open_repository_submenu("Tags") + end + + def go_to_snippets + open_repository_submenu("Snippets") + end + + def go_to_contributor_statistics + open_repository_submenu("Contributor statistics") + end + + def go_to_graph + open_repository_submenu("Graph") + end + + def go_to_compare_revisions + open_repository_submenu("Compare revisions") + end + + private + + def open_repository_submenu(sub_menu) + open_submenu("Repository", "#repository", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/super_sidebar/settings.rb b/qa/qa/page/project/sub_menus/super_sidebar/settings.rb new file mode 100644 index 00000000000..f8371385fd6 --- /dev/null +++ b/qa/qa/page/project/sub_menus/super_sidebar/settings.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module SuperSidebar + module Settings + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + include QA::Page::Project::SubMenus::SuperSidebar::Common + end + end + + def go_to_general_settings + open_settings_submenu('General') + end + + def go_to_integrations_settings + open_settings_submenu('Integrations') + end + + def go_to_access_token_settings + open_settings_submenu('Access Tokens') + end + + def go_to_repository_settings + open_settings_submenu('Repository') + end + + def go_to_merge_request_settings + open_settings_submenu('Merge requests') + end + + def go_to_ci_cd_settings + open_settings_submenu('CI/CD') + end + + def go_to_pages_settings + open_settings_submenu('Pages') + end + + def go_to_monitor_settings + open_settings_submenu('Monitor') + end + + private + + def open_settings_submenu(sub_menu) + open_submenu("Settings", "#settings", sub_menu) + end + end + end + end + end + end +end diff --git a/qa/qa/resource/pipeline.rb b/qa/qa/resource/pipeline.rb index 7d5036c5cf4..e57784ca3b5 100644 --- a/qa/qa/resource/pipeline.rb +++ b/qa/qa/resource/pipeline.rb @@ -28,7 +28,7 @@ module QA def fabricate! project.visit! - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb index 4c1319da0cb..4515353dfc5 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/custom_variable_spec.rb @@ -49,7 +49,7 @@ module QA Flow::Login.sign_in project.visit! - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb index b79f8b5f1f4..b62ae85436f 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb @@ -52,7 +52,7 @@ module QA project.visit! # Navigate to Run Pipeline page - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb index e8ec01577b1..9f43471035f 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb @@ -37,7 +37,7 @@ module QA before do Flow::Login.sign_in project.visit! - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) end it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348011' do diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 1e7d0eab365..0a9f30f0529 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Configure', - only: { subdomain: %i[staging staging-canary] }, product_group: :configure do + only: { pipeline: %i[staging staging-canary canary production] }, product_group: :configure do describe 'Auto DevOps with a Kubernetes Agent' do let!(:app_project) do Resource::Project.fabricate_via_api! do |project| @@ -45,7 +45,7 @@ module QA app_project.visit! - Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Menu.perform(&:go_to_pipelines) Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) diff --git a/qa/tasks/webdrivers.rake b/qa/tasks/webdrivers.rake index f4fa3fab555..cd2a36ddf6b 100644 --- a/qa/tasks/webdrivers.rake +++ b/qa/tasks/webdrivers.rake @@ -1,4 +1,3 @@ # frozen_string_literal: true -require 'webdrivers' load 'webdrivers/Rakefile' diff --git a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb index 2d39e0e5317..a7f3212a6f9 100644 --- a/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb +++ b/spec/controllers/projects/design_management/designs/raw_images_controller_spec.rb @@ -80,8 +80,12 @@ RSpec.describe Projects::DesignManagement::Designs::RawImagesController do let(:oldest_version) { design.versions.ordered.last } shared_examples 'a successful request for sha' do + before do + allow(DesignManagement::GitRepository).to receive(:new).and_call_original + end + it do - expect_next_instance_of(DesignManagement::Repository) do |repository| + expect_next_instance_of(DesignManagement::GitRepository) do |repository| expect(repository).to receive(:blob_at).with(expected_ref, design.full_path).and_call_original end diff --git a/spec/fixtures/lib/gitlab/import_export/complex/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index a0ac70d7d9c..f57f0f3c08c 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -8039,7 +8039,7 @@ "protected_environment_id": 1, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", - "access_level": 40, + "access_level": null, "user_id": 1, "group_id": null } diff --git a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson index 55afaa8bcf6..f87fdd860c7 100644 --- a/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson +++ b/spec/fixtures/lib/gitlab/import_export/complex/tree/project/protected_environments.ndjson @@ -1 +1 @@ -{ "id": 1, "project_id": 9, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "name": "production", "deploy_access_levels": [ { "id": 1, "protected_environment_id": 1, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "access_level": 40, "user_id": 1, "group_id": null } ] } +{ "id": 1, "project_id": 9, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "name": "production", "deploy_access_levels": [ { "id": 1, "protected_environment_id": 1, "created_at": "2017-10-19T15:36:23.466Z", "updated_at": "2017-10-19T15:36:23.466Z", "access_level": null, "user_id": 1, "group_id": null } ] } diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js index 89ac0216f41..0107b27f980 100644 --- a/spec/frontend/notes/components/note_awards_list_spec.js +++ b/spec/frontend/notes/components/note_awards_list_spec.js @@ -1,76 +1,110 @@ import AxiosMockAdapter from 'axios-mock-adapter'; import Vue from 'vue'; +import Vuex from 'vuex'; import { TEST_HOST } from 'helpers/test_constants'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { userDataMock } from 'jest/notes/mock_data'; +import EmojiPicker from '~/emoji/components/picker.vue'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; import awardsNote from '~/notes/components/note_awards_list.vue'; import createStore from '~/notes/stores'; -import { noteableDataMock, notesDataMock } from '../mock_data'; -describe('note_awards_list component', () => { - let store; - let vm; - let awardsMock; - let mock; - - const toggleAwardPath = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`; - - beforeEach(() => { - mock = new AxiosMockAdapter(axios); - - mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, ''); +Vue.use(Vuex); - const Component = Vue.extend(awardsNote); - - store = createStore(); - store.dispatch('setNoteableData', noteableDataMock); - store.dispatch('setNotesData', notesDataMock); - awardsMock = [ - { - name: 'flag_tz', - user: { id: 1, name: 'Administrator', username: 'root' }, - }, - { - name: 'cartwheel_tone3', - user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, - }, - ]; +describe('Note Awards List', () => { + let wrapper; + let mock; - vm = new Component({ + const awardsMock = [ + { + name: 'flag_tz', + user: { id: 1, name: 'Administrator', username: 'root' }, + }, + { + name: 'cartwheel_tone3', + user: { id: 12, name: 'Bobbie Stehr', username: 'erin' }, + }, + ]; + const toggleAwardPathMock = `${TEST_HOST}/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji`; + + const defaultProps = { + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: false, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }; + + const findAddAward = () => wrapper.find('.js-add-award'); + const findAwardButton = () => wrapper.findByTestId('award-button'); + const findAllEmojiAwards = () => wrapper.findAll('gl-emoji'); + const findEmojiPicker = () => wrapper.findComponent(EmojiPicker); + + const createComponent = (props = defaultProps, store = createStore()) => { + wrapper = mountExtended(awardsNote, { store, propsData: { - awards: awardsMock, - noteAuthorId: 2, - noteId: '545', - canAwardEmoji: true, - toggleAwardPath, + ...props, }, - }).$mount(); - }); + }); + }; + + describe('Note Awards functionality', () => { + const toggleAwardRequestSpy = jest.fn(); + const fakeStore = () => { + return new Vuex.Store({ + getters: { + getUserData: () => userDataMock, + }, + actions: { + toggleAwardRequest: toggleAwardRequestSpy, + }, + }); + }; - afterEach(() => { - mock.restore(); - vm.$destroy(); - }); + beforeEach(() => { + mock = new AxiosMockAdapter(axios); + mock.onPost(toggleAwardPathMock).reply(HTTP_STATUS_OK, ''); - it('should render awarded emojis', () => { - expect(vm.$el.querySelector('.js-awards-block button [data-name="flag_tz"]')).toBeDefined(); - expect( - vm.$el.querySelector('.js-awards-block button [data-name="cartwheel_tone3"]'), - ).toBeDefined(); - }); + createComponent( + { + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: true, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }, + fakeStore(), + ); + }); - it('should be possible to remove awarded emoji', () => { - jest.spyOn(vm, 'handleAward'); - jest.spyOn(vm, 'toggleAwardRequest'); - vm.$el.querySelector('.js-awards-block button').click(); + afterEach(() => { + mock.restore(); + }); - expect(vm.handleAward).toHaveBeenCalledWith('flag_tz'); - expect(vm.toggleAwardRequest).toHaveBeenCalled(); - }); + it('should render awarded emojis', () => { + const emojiAwards = findAllEmojiAwards(); + + expect(emojiAwards).toHaveLength(awardsMock.length); + expect(emojiAwards.at(0).attributes('data-name')).toBe('flag_tz'); + expect(emojiAwards.at(1).attributes('data-name')).toBe('cartwheel_tone3'); + }); + + it('should be possible to add new emoji', () => { + expect(findEmojiPicker().exists()).toBe(true); + }); + + it('should be possible to remove awarded emoji', async () => { + await findAwardButton().vm.$emit('click'); - it('should be possible to add new emoji', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeDefined(); + const { toggleAwardPath, noteId } = defaultProps; + expect(toggleAwardRequestSpy).toHaveBeenCalledWith(expect.anything(), { + awardName: awardsMock[0].name, + endpoint: toggleAwardPath, + noteId, + }); + }); }); describe('when the user name contains special HTML characters', () => { @@ -79,85 +113,69 @@ describe('note_awards_list component', () => { user: { id: index, name: `&<>"\`'-${index}`, username: `user-${index}` }, }); - const mountComponent = () => { - const Component = Vue.extend(awardsNote); - vm = new Component({ - store, - propsData: { - awards: awardsMock, - noteAuthorId: 0, - noteId: '545', - canAwardEmoji: true, - toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', - }, - }).$mount(); + const customProps = { + awards: awardsMock, + noteAuthorId: 0, + noteId: '545', + canAwardEmoji: true, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', }; - const findTooltip = () => vm.$el.querySelector('[title]').getAttribute('title'); - - it('should only escape & and " characters', () => { - awardsMock = [...new Array(1)].map(createAwardEmoji); - mountComponent(); - const escapedName = awardsMock[0].user.name.replace(/&/g, '&').replace(/"/g, '"'); - - expect(vm.$el.querySelector('[title]').outerHTML).toContain(escapedName); - }); - it('should not escape special HTML characters twice when only 1 person awarded', () => { - awardsMock = [...new Array(1)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(1)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); - awardsMock.forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); it('should not escape special HTML characters twice when 2 people awarded', () => { - awardsMock = [...new Array(2)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(2)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); - awardsMock.forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); it('should not escape special HTML characters twice when more than 10 people awarded', () => { - awardsMock = [...new Array(11)].map(createAwardEmoji); - mountComponent(); + const awardsCopy = [...new Array(11)].map(createAwardEmoji); + createComponent({ + ...customProps, + awards: awardsCopy, + }); // Testing only the first 10 awards since 11 onward will not be displayed. - awardsMock.slice(0, 10).forEach((award) => { - expect(findTooltip()).toContain(award.user.name); + awardsCopy.slice(0, 10).forEach((award) => { + expect(findAwardButton().attributes('title')).toContain(award.user.name); }); }); }); - describe('when the user cannot award emoji', () => { + describe('when the user cannot award an emoji', () => { beforeEach(() => { - const Component = Vue.extend(awardsNote); - - vm = new Component({ - store, - propsData: { - awards: awardsMock, - noteAuthorId: 2, - noteId: '545', - canAwardEmoji: false, - toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', - }, - }).$mount(); + createComponent({ + awards: awardsMock, + noteAuthorId: 2, + noteId: '545', + canAwardEmoji: false, + toggleAwardPath: '/gitlab-org/gitlab-foss/notes/545/toggle_award_emoji', + }); }); - it('should not be possible to remove awarded emoji', () => { - jest.spyOn(vm, 'toggleAwardRequest'); - - vm.$el.querySelector('.js-awards-block button').click(); - - expect(vm.toggleAwardRequest).not.toHaveBeenCalled(); + it('should display an award emoji button with a disabled class', () => { + expect(findAwardButton().classes()).toContain('disabled'); }); it('should not be possible to add new emoji', () => { - expect(vm.$el.querySelector('.js-add-award')).toBeNull(); + expect(findAddAward().exists()).toBe(false); }); }); }); diff --git a/spec/lib/gitlab/gl_repository/repo_type_spec.rb b/spec/lib/gitlab/gl_repository/repo_type_spec.rb index 0ec94563cbb..40dcbe16688 100644 --- a/spec/lib/gitlab/gl_repository/repo_type_spec.rb +++ b/spec/lib/gitlab/gl_repository/repo_type_spec.rb @@ -136,7 +136,7 @@ RSpec.describe Gitlab::GlRepository::RepoType do let(:expected_identifier) { "design-#{project.id}" } let(:expected_id) { project.id } let(:expected_suffix) { '.design' } - let(:expected_repository) { ::DesignManagement::Repository.new(project) } + let(:expected_repository) { project.design_management_repository } let(:expected_container) { project } end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index bcdbf3d58d0..d481a75667d 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -780,6 +780,7 @@ project: - sbom_occurrences - analytics_dashboards_configuration_project - analytics_dashboards_pointer +- design_management_repository award_emoji: - awardable - user diff --git a/spec/models/design_management/git_repository_spec.rb b/spec/models/design_management/git_repository_spec.rb new file mode 100644 index 00000000000..1b07e337cde --- /dev/null +++ b/spec/models/design_management/git_repository_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe DesignManagement::GitRepository, feature_category: :design_management do + let_it_be(:project) { create(:project) } + let(:git_repository) { described_class.new(project) } + + shared_examples 'returns parsed git attributes that enable LFS for all file types' do + it do + expect(subject.patterns).to be_a_kind_of(Hash) + expect(subject.patterns).to have_key('/designs/*') + expect(subject.patterns['/designs/*']).to eql( + { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false } + ) + end + end + + describe "#info_attributes" do + subject { git_repository.info_attributes } + + include_examples 'returns parsed git attributes that enable LFS for all file types' + end + + describe '#attributes_at' do + subject { git_repository.attributes_at } + + include_examples 'returns parsed git attributes that enable LFS for all file types' + end + + describe '#gitattribute' do + it 'returns a gitattribute when path has gitattributes' do + expect(git_repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs') + end + + it 'returns nil when path has no gitattributes' do + expect(git_repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil + end + end + + describe '#copy_gitattributes' do + it 'always returns regardless of whether given a valid or invalid ref' do + expect(git_repository.copy_gitattributes('master')).to be true + expect(git_repository.copy_gitattributes('invalid')).to be true + end + end + + describe '#attributes' do + it 'confirms that all files are LFS enabled' do + %w[png zip anything].each do |filetype| + path = "/#{DesignManagement.designs_directory}/file.#{filetype}" + attributes = git_repository.attributes(path) + + expect(attributes['filter']).to eq('lfs') + end + end + end +end diff --git a/spec/models/design_management/repository_spec.rb b/spec/models/design_management/repository_spec.rb index 0115e0c139c..67cdba40f82 100644 --- a/spec/models/design_management/repository_spec.rb +++ b/spec/models/design_management/repository_spec.rb @@ -2,57 +2,16 @@ require 'spec_helper' -RSpec.describe DesignManagement::Repository do - let(:project) { create(:project) } - let(:repository) { described_class.new(project) } +RSpec.describe DesignManagement::Repository, feature_category: :design_management do + let_it_be(:project) { create(:project) } + let(:subject) { ::DesignManagement::Repository.new({ project: project }) } - shared_examples 'returns parsed git attributes that enable LFS for all file types' do - it do - expect(subject.patterns).to be_a_kind_of(Hash) - expect(subject.patterns).to have_key('/designs/*') - expect(subject.patterns['/designs/*']).to eql( - { "filter" => "lfs", "diff" => "lfs", "merge" => "lfs", "text" => false } - ) - end + describe 'associations' do + it { is_expected.to belong_to(:project).inverse_of(:design_management_repository) } end - describe "#info_attributes" do - subject { repository.info_attributes } - - include_examples 'returns parsed git attributes that enable LFS for all file types' - end - - describe '#attributes_at' do - subject { repository.attributes_at } - - include_examples 'returns parsed git attributes that enable LFS for all file types' - end - - describe '#gitattribute' do - it 'returns a gitattribute when path has gitattributes' do - expect(repository.gitattribute('/designs/file.txt', 'filter')).to eq('lfs') - end - - it 'returns nil when path has no gitattributes' do - expect(repository.gitattribute('/invalid/file.txt', 'filter')).to be_nil - end - end - - describe '#copy_gitattributes' do - it 'always returns regardless of whether given a valid or invalid ref' do - expect(repository.copy_gitattributes('master')).to be true - expect(repository.copy_gitattributes('invalid')).to be true - end - end - - describe '#attributes' do - it 'confirms that all files are LFS enabled' do - %w(png zip anything).each do |filetype| - path = "/#{DesignManagement.designs_directory}/file.#{filetype}" - attributes = repository.attributes(path) - - expect(attributes['filter']).to eq('lfs') - end - end + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_uniqueness_of(:project) } end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index f9822ff36c6..f0b8eb985c4 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -4486,7 +4486,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev let(:expected_merge_status) { 'checking' } include_examples 'for a valid state transition' - it_behaves_like 'transition triggering mergeRequestMergeStatusUpdated GraphQL subscription' + it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' end context 'when the status is checking' do @@ -4506,7 +4506,7 @@ RSpec.describe MergeRequest, factory_default: :keep, feature_category: :code_rev let(:expected_merge_status) { 'cannot_be_merged_rechecking' } include_examples 'for a valid state transition' - it_behaves_like 'transition triggering mergeRequestMergeStatusUpdated GraphQL subscription' + it_behaves_like 'transition not triggering mergeRequestMergeStatusUpdated GraphQL subscription' end context 'when the status is cannot_be_merged' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0e3c2112691..11adbf161ee 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -42,6 +42,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do it { is_expected.to have_many(:protected_branches) } it { is_expected.to have_many(:exported_protected_branches) } it { is_expected.to have_one(:wiki_repository).class_name('Projects::WikiRepository').inverse_of(:project) } + it { is_expected.to have_one(:design_management_repository).class_name('DesignManagement::Repository').inverse_of(:project) } it { is_expected.to have_one(:slack_integration) } it { is_expected.to have_one(:catalog_resource) } it { is_expected.to have_one(:microsoft_teams_integration) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index b48ad59f7d3..8a02c9c0dd5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -392,11 +392,6 @@ RSpec.configure do |config| ./ee/spec/requests/api/merge_request_approvals_spec.rb ./ee/spec/requests/api/namespaces_spec.rb ./ee/spec/requests/api/notes_spec.rb - ./ee/spec/requests/api/project_aliases_spec.rb - ./ee/spec/requests/api/project_approval_rules_spec.rb - ./ee/spec/requests/api/project_approval_settings_spec.rb - ./ee/spec/requests/api/project_approvals_spec.rb - ./ee/spec/requests/api/projects_spec.rb ./ee/spec/requests/api/settings_spec.rb ./ee/spec/requests/api/users_spec.rb ./ee/spec/support/shared_examples/requests/api/project_approval_rules_api_shared_examples.rb |