From 8754d20bbb9e573d48e80d7f6aed1ded40a40263 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 11 Aug 2022 03:09:21 +0000 Subject: Add latest changes from gitlab-org/gitlab@master --- .../runner/components/runner_bulk_delete.vue | 4 +- .../users/project_callouts_controller.rb | 17 +++++ app/models/project.rb | 2 + app/models/user.rb | 12 ++++ app/models/users/project_callout.rb | 21 ++++++ .../users/dismiss_project_callout_service.rb | 11 +++ config/routes/user.rb | 1 + db/docs/user_project_callouts.yml | 9 +++ .../20220803145637_create_user_project_callout.rb | 19 +++++ ...add_project_id_fkey_for_user_project_callout.rb | 15 ++++ ...58_add_user_id_fkey_for_user_project_callout.rb | 15 ++++ ...8133824_add_timestamps_to_project_statistics.rb | 7 ++ ...4_update_index_vulnerabilities_project_id_id.rb | 22 ++++++ ...ly_usages_additional_amount_available_column.rb | 11 +++ db/schema_migrations/20220614185644 | 1 + db/schema_migrations/20220803145637 | 1 + db/schema_migrations/20220803154543 | 1 + db/schema_migrations/20220803154758 | 1 + db/schema_migrations/20220808131659 | 1 + db/schema_migrations/20220808133824 | 1 + db/structure.sql | 41 +++++++++-- doc/administration/repository_checks.md | 60 ++++++++-------- .../dependency_scanning/index.md | 57 +++++++-------- doc/user/group/saml_sso/example_saml_config.md | 16 +++-- .../group/saml_sso/img/Okta-GroupAttribute.png | Bin 0 -> 7744 bytes doc/user/group/saml_sso/img/Okta-GroupSAML.png | Bin 0 -> 23681 bytes doc/user/group/saml_sso/img/Okta-SAMLsetup.png | Bin 18920 -> 0 bytes doc/user/group/saml_sso/img/Okta-SM.png | Bin 0 -> 23386 bytes .../group/saml_sso/img/Okta-advancedsettings.png | Bin 15614 -> 18912 bytes doc/user/group/saml_sso/img/Okta-attributes.png | Bin 5540 -> 13687 bytes doc/user/group/saml_sso/img/Okta-linkscert.png | Bin 58832 -> 55321 bytes .../group/saml_sso/img/okta_admin_panel_v13_9.png | Bin 49319 -> 0 bytes doc/user/group/saml_sso/img/okta_saml_settings.png | Bin 25470 -> 21280 bytes lib/gitlab/database/gitlab_schemas.yml | 1 + spec/factories/users/project_user_callouts.rb | 10 +++ spec/frontend/__helpers__/vue_test_utils_helper.js | 19 ++++- .../__helpers__/vue_test_utils_helper_spec.js | 57 +++++++++++++-- .../runner/components/runner_bulk_delete_spec.js | 5 +- spec/lib/gitlab/import_export/all_models.yml | 1 + spec/models/project_spec.rb | 1 + spec/models/user_spec.rb | 79 +++++++++++++++++++++ spec/models/users/project_callout_spec.rb | 23 ++++++ spec/requests/users/project_callouts_spec.rb | 58 +++++++++++++++ .../lib/glfm/update_example_snapshots_spec.rb | 12 +++- .../users/dismiss_project_callout_service_spec.rb | 25 +++++++ 45 files changed, 553 insertions(+), 84 deletions(-) create mode 100644 app/controllers/users/project_callouts_controller.rb create mode 100644 app/models/users/project_callout.rb create mode 100644 app/services/users/dismiss_project_callout_service.rb create mode 100644 db/docs/user_project_callouts.yml create mode 100644 db/migrate/20220803145637_create_user_project_callout.rb create mode 100644 db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb create mode 100644 db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb create mode 100644 db/migrate/20220808133824_add_timestamps_to_project_statistics.rb create mode 100644 db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb create mode 100644 db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb create mode 100644 db/schema_migrations/20220614185644 create mode 100644 db/schema_migrations/20220803145637 create mode 100644 db/schema_migrations/20220803154543 create mode 100644 db/schema_migrations/20220803154758 create mode 100644 db/schema_migrations/20220808131659 create mode 100644 db/schema_migrations/20220808133824 create mode 100644 doc/user/group/saml_sso/img/Okta-GroupAttribute.png create mode 100644 doc/user/group/saml_sso/img/Okta-GroupSAML.png delete mode 100644 doc/user/group/saml_sso/img/Okta-SAMLsetup.png create mode 100644 doc/user/group/saml_sso/img/Okta-SM.png delete mode 100644 doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png create mode 100644 spec/factories/users/project_user_callouts.rb create mode 100644 spec/models/users/project_callout_spec.rb create mode 100644 spec/requests/users/project_callouts_spec.rb create mode 100644 spec/services/users/dismiss_project_callout_service_spec.rb diff --git a/app/assets/javascripts/runner/components/runner_bulk_delete.vue b/app/assets/javascripts/runner/components/runner_bulk_delete.vue index 50791de0bda..6f065c38b25 100644 --- a/app/assets/javascripts/runner/components/runner_bulk_delete.vue +++ b/app/assets/javascripts/runner/components/runner_bulk_delete.vue @@ -99,10 +99,10 @@ export default {
- {{ + {{ s__('Runners|Clear selection') }} - {{ + {{ s__('Runners|Delete selected') }}
diff --git a/app/controllers/users/project_callouts_controller.rb b/app/controllers/users/project_callouts_controller.rb new file mode 100644 index 00000000000..64d89630021 --- /dev/null +++ b/app/controllers/users/project_callouts_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Users + class ProjectCalloutsController < Users::CalloutsController + private + + def callout + Users::DismissProjectCalloutService.new( + container: nil, current_user: current_user, params: callout_params + ).execute + end + + def callout_params + params.permit(:project_id).merge(feature_name: feature_name) + end + end +end diff --git a/app/models/project.rb b/app/models/project.rb index b0eb273e526..28dd51fc67d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -291,6 +291,8 @@ class Project < ApplicationRecord has_many :project_members, -> { where(requested_at: nil) }, as: :source, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent + has_many :project_callouts, class_name: 'Users::ProjectCallout', foreign_key: :project_id + alias_method :members, :project_members has_many :users, through: :project_members diff --git a/app/models/user.rb b/app/models/user.rb index 6f6dde236ba..5503f28a4fe 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -222,6 +222,7 @@ class User < ApplicationRecord has_many :custom_attributes, class_name: 'UserCustomAttribute' has_many :callouts, class_name: 'Users::Callout' has_many :group_callouts, class_name: 'Users::GroupCallout' + has_many :project_callouts, class_name: 'Users::ProjectCallout' has_many :namespace_callouts, class_name: 'Users::NamespaceCallout' has_many :term_agreements belongs_to :accepted_term, class_name: 'ApplicationSetting::Term' @@ -2087,6 +2088,12 @@ class User < ApplicationRecord callout_dismissed?(callout, ignore_dismissal_earlier_than) end + def dismissed_callout_for_project?(feature_name:, project:, ignore_dismissal_earlier_than: nil) + callout = project_callouts.find_by(feature_name: feature_name, project: project) + + callout_dismissed?(callout, ignore_dismissal_earlier_than) + end + # Load the current highest access by looking directly at the user's memberships def current_highest_access_level members.non_request.maximum(:access_level) @@ -2118,6 +2125,11 @@ class User < ApplicationRecord .find_or_initialize_by(feature_name: ::Users::NamespaceCallout.feature_names[feature_name], namespace_id: namespace_id) end + def find_or_initialize_project_callout(feature_name, project_id) + project_callouts + .find_or_initialize_by(feature_name: ::Users::ProjectCallout.feature_names[feature_name], project_id: project_id) + end + def can_trigger_notifications? confirmed? && !blocked? && !ghost? end diff --git a/app/models/users/project_callout.rb b/app/models/users/project_callout.rb new file mode 100644 index 00000000000..ddc5f8fb4de --- /dev/null +++ b/app/models/users/project_callout.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module Users + class ProjectCallout < ApplicationRecord + include Users::Calloutable + + self.table_name = 'user_project_callouts' + + belongs_to :project + + enum feature_name: { + awaiting_members_banner: 1 # EE-only + } + + validates :project, presence: true + validates :feature_name, + presence: true, + uniqueness: { scope: [:user_id, :project_id] }, + inclusion: { in: ProjectCallout.feature_names.keys } + end +end diff --git a/app/services/users/dismiss_project_callout_service.rb b/app/services/users/dismiss_project_callout_service.rb new file mode 100644 index 00000000000..23549b3b194 --- /dev/null +++ b/app/services/users/dismiss_project_callout_service.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module Users + class DismissProjectCalloutService < DismissCalloutService + private + + def callout + current_user.find_or_initialize_project_callout(params[:feature_name], params[:project_id]) + end + end +end diff --git a/config/routes/user.rb b/config/routes/user.rb index 96e8c850da4..0c1bc1956a9 100644 --- a/config/routes/user.rb +++ b/config/routes/user.rb @@ -65,6 +65,7 @@ scope '-/users', module: :users do resources :callouts, only: [:create] resources :group_callouts, only: [:create] + resources :project_callouts, only: [:create] end scope(constraints: { username: Gitlab::PathRegex.root_namespace_route_regex }) do diff --git a/db/docs/user_project_callouts.yml b/db/docs/user_project_callouts.yml new file mode 100644 index 00000000000..308c3048aa7 --- /dev/null +++ b/db/docs/user_project_callouts.yml @@ -0,0 +1,9 @@ +--- +table_name: user_project_callouts +classes: +- Users::ProjectCallout +feature_categories: +- navigation +description: Adds the ability to track a user callout being dismissed by project +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94144 +milestone: '15.3' diff --git a/db/migrate/20220803145637_create_user_project_callout.rb b/db/migrate/20220803145637_create_user_project_callout.rb new file mode 100644 index 00000000000..1d0baf741a9 --- /dev/null +++ b/db/migrate/20220803145637_create_user_project_callout.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class CreateUserProjectCallout < Gitlab::Database::Migration[2.0] + def up + create_table :user_project_callouts do |t| + t.bigint :user_id, null: false + t.bigint :project_id, null: false + t.integer :feature_name, limit: 2, null: false + t.datetime_with_timezone :dismissed_at + + t.index :project_id + t.index [:user_id, :feature_name, :project_id], unique: true, name: 'index_project_user_callouts_feature' + end + end + + def down + drop_table :user_project_callouts + end +end diff --git a/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb b/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb new file mode 100644 index 00000000000..3cea53dc127 --- /dev/null +++ b/db/migrate/20220803154543_add_project_id_fkey_for_user_project_callout.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddProjectIdFkeyForUserProjectCallout < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :user_project_callouts, :projects, column: :project_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :user_project_callouts, column: :project_id + end + end +end diff --git a/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb b/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb new file mode 100644 index 00000000000..01a05f816da --- /dev/null +++ b/db/migrate/20220803154758_add_user_id_fkey_for_user_project_callout.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddUserIdFkeyForUserProjectCallout < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + def up + add_concurrent_foreign_key :user_project_callouts, :users, column: :user_id, on_delete: :cascade + end + + def down + with_lock_retries do + remove_foreign_key :user_project_callouts, column: :user_id + end + end +end diff --git a/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb b/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb new file mode 100644 index 00000000000..c5125ebc8ff --- /dev/null +++ b/db/migrate/20220808133824_add_timestamps_to_project_statistics.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddTimestampsToProjectStatistics < Gitlab::Database::Migration[2.0] + def change + add_timestamps_with_timezone(:project_statistics, null: false, default: -> { 'NOW()' }) + end +end diff --git a/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb b/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb new file mode 100644 index 00000000000..142cac09a01 --- /dev/null +++ b/db/post_migrate/20220614185644_update_index_vulnerabilities_project_id_id.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +class UpdateIndexVulnerabilitiesProjectIdId < Gitlab::Database::Migration[2.0] + disable_ddl_transaction! + + NEW_INDEX_NAME = 'index_vulnerabilities_project_id_and_id_on_default_branch' + OLD_INDEX_NAME = 'index_vulnerabilities_on_project_id_and_id' + + def up + add_concurrent_index :vulnerabilities, [:project_id, :id], + where: 'present_on_default_branch IS TRUE', + name: NEW_INDEX_NAME + + remove_concurrent_index_by_name(:vulnerabilities, OLD_INDEX_NAME) + end + + def down + add_concurrent_index :vulnerabilities, [:project_id, :id], name: OLD_INDEX_NAME + + remove_concurrent_index_by_name(:vulnerabilities, NEW_INDEX_NAME) + end +end diff --git a/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb b/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb new file mode 100644 index 00000000000..70a1e425e0d --- /dev/null +++ b/db/post_migrate/20220808131659_remove_ci_namespace_monthly_usages_additional_amount_available_column.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +class RemoveCiNamespaceMonthlyUsagesAdditionalAmountAvailableColumn < Gitlab::Database::Migration[2.0] + def up + remove_column :ci_namespace_monthly_usages, :additional_amount_available + end + + def down + add_column :ci_namespace_monthly_usages, :additional_amount_available, :integer, default: 0, null: false + end +end diff --git a/db/schema_migrations/20220614185644 b/db/schema_migrations/20220614185644 new file mode 100644 index 00000000000..dcc2d926276 --- /dev/null +++ b/db/schema_migrations/20220614185644 @@ -0,0 +1 @@ +f1d4faf4d32a3271a97b389d53c9d3accbfa3fa2bd47d63257fe589efa4bb665 \ No newline at end of file diff --git a/db/schema_migrations/20220803145637 b/db/schema_migrations/20220803145637 new file mode 100644 index 00000000000..36688f97184 --- /dev/null +++ b/db/schema_migrations/20220803145637 @@ -0,0 +1 @@ +bf12037cb99a399302610f948dad48589eca4e631d82d9f26b04bae882b10020 \ No newline at end of file diff --git a/db/schema_migrations/20220803154543 b/db/schema_migrations/20220803154543 new file mode 100644 index 00000000000..f5cfb3d91ba --- /dev/null +++ b/db/schema_migrations/20220803154543 @@ -0,0 +1 @@ +047147acc972ab8681f097d5060998a47e44612fde7f2137714683bd61350c2d \ No newline at end of file diff --git a/db/schema_migrations/20220803154758 b/db/schema_migrations/20220803154758 new file mode 100644 index 00000000000..71ac8b4f301 --- /dev/null +++ b/db/schema_migrations/20220803154758 @@ -0,0 +1 @@ +2cdf4c4fe218a5fb7061bf65643868c7b592cd3ef0d7611949e8fd86bc635c24 \ No newline at end of file diff --git a/db/schema_migrations/20220808131659 b/db/schema_migrations/20220808131659 new file mode 100644 index 00000000000..65a08ad1b7a --- /dev/null +++ b/db/schema_migrations/20220808131659 @@ -0,0 +1 @@ +07488e8c6ea0f3dc92e1370efb0190facf520b850e170fcd8f3ce0e2a15c096a \ No newline at end of file diff --git a/db/schema_migrations/20220808133824 b/db/schema_migrations/20220808133824 new file mode 100644 index 00000000000..bf7755d3163 --- /dev/null +++ b/db/schema_migrations/20220808133824 @@ -0,0 +1 @@ +bab4f4d3aaedd698400fcbd5991797530450fe845a8034b03b1bf525a61e628a \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3636c609091..73b83a51bb4 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -12856,7 +12856,6 @@ CREATE TABLE ci_namespace_monthly_usages ( id bigint NOT NULL, namespace_id bigint NOT NULL, date date NOT NULL, - additional_amount_available integer DEFAULT 0 NOT NULL, amount_used numeric(18,2) DEFAULT 0.0 NOT NULL, notification_level smallint DEFAULT 100 NOT NULL, shared_runners_duration integer DEFAULT 0 NOT NULL, @@ -19839,7 +19838,9 @@ CREATE TABLE project_statistics ( snippets_size bigint, pipeline_artifacts_size bigint DEFAULT 0 NOT NULL, uploads_size bigint DEFAULT 0 NOT NULL, - container_registry_size bigint DEFAULT 0 NOT NULL + container_registry_size bigint DEFAULT 0 NOT NULL, + created_at timestamp with time zone DEFAULT now() NOT NULL, + updated_at timestamp with time zone DEFAULT now() NOT NULL ); CREATE SEQUENCE project_statistics_id_seq @@ -21906,6 +21907,23 @@ CREATE SEQUENCE user_preferences_id_seq ALTER SEQUENCE user_preferences_id_seq OWNED BY user_preferences.id; +CREATE TABLE user_project_callouts ( + id bigint NOT NULL, + user_id bigint NOT NULL, + project_id bigint NOT NULL, + feature_name smallint NOT NULL, + dismissed_at timestamp with time zone +); + +CREATE SEQUENCE user_project_callouts_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE user_project_callouts_id_seq OWNED BY user_project_callouts.id; + CREATE TABLE user_statuses ( user_id integer NOT NULL, cached_markdown_version integer, @@ -23788,6 +23806,8 @@ ALTER TABLE ONLY user_permission_export_uploads ALTER COLUMN id SET DEFAULT next ALTER TABLE ONLY user_preferences ALTER COLUMN id SET DEFAULT nextval('user_preferences_id_seq'::regclass); +ALTER TABLE ONLY user_project_callouts ALTER COLUMN id SET DEFAULT nextval('user_project_callouts_id_seq'::regclass); + ALTER TABLE ONLY user_statuses ALTER COLUMN user_id SET DEFAULT nextval('user_statuses_user_id_seq'::regclass); ALTER TABLE ONLY user_synced_attributes_metadata ALTER COLUMN id SET DEFAULT nextval('user_synced_attributes_metadata_id_seq'::regclass); @@ -26076,6 +26096,9 @@ ALTER TABLE ONLY user_permission_export_uploads ALTER TABLE ONLY user_preferences ADD CONSTRAINT user_preferences_pkey PRIMARY KEY (id); +ALTER TABLE ONLY user_project_callouts + ADD CONSTRAINT user_project_callouts_pkey PRIMARY KEY (id); + ALTER TABLE ONLY user_statuses ADD CONSTRAINT user_statuses_pkey PRIMARY KEY (user_id); @@ -29484,6 +29507,8 @@ CREATE UNIQUE INDEX index_project_topics_on_project_id_and_topic_id ON project_t CREATE INDEX index_project_topics_on_topic_id ON project_topics USING btree (topic_id); +CREATE UNIQUE INDEX index_project_user_callouts_feature ON user_project_callouts USING btree (user_id, feature_name, project_id); + CREATE INDEX index_projects_aimed_for_deletion ON projects USING btree (marked_for_deletion_at) WHERE ((marked_for_deletion_at IS NOT NULL) AND (pending_delete = false)); CREATE INDEX index_projects_api_created_at_id_desc ON projects USING btree (created_at, id DESC); @@ -30122,6 +30147,8 @@ CREATE INDEX index_user_preferences_on_gitpod_enabled ON user_preferences USING CREATE UNIQUE INDEX index_user_preferences_on_user_id ON user_preferences USING btree (user_id); +CREATE INDEX index_user_project_callouts_on_project_id ON user_project_callouts USING btree (project_id); + CREATE INDEX index_user_statuses_on_clear_status_at_not_null ON user_statuses USING btree (clear_status_at) WHERE (clear_status_at IS NOT NULL); CREATE INDEX index_user_statuses_on_user_id ON user_statuses USING btree (user_id); @@ -30218,8 +30245,6 @@ CREATE INDEX index_vulnerabilities_on_last_edited_by_id ON vulnerabilities USING CREATE INDEX index_vulnerabilities_on_milestone_id ON vulnerabilities USING btree (milestone_id); -CREATE INDEX index_vulnerabilities_on_project_id_and_id ON vulnerabilities USING btree (project_id, id); - CREATE INDEX index_vulnerabilities_on_project_id_and_id_active_cis ON vulnerabilities USING btree (project_id, id) WHERE ((report_type = 7) AND (state = ANY (ARRAY[1, 4]))); CREATE INDEX index_vulnerabilities_on_project_id_and_state_and_severity ON vulnerabilities USING btree (project_id, state, severity); @@ -30234,6 +30259,8 @@ CREATE INDEX index_vulnerabilities_on_state_case_id_desc ON vulnerabilities USIN CREATE INDEX index_vulnerabilities_on_updated_by_id ON vulnerabilities USING btree (updated_by_id); +CREATE INDEX index_vulnerabilities_project_id_and_id_on_default_branch ON vulnerabilities USING btree (project_id, id) WHERE (present_on_default_branch IS TRUE); + CREATE INDEX index_vulnerabilities_project_id_state_severity_default_branch ON vulnerabilities USING btree (project_id, state, severity, present_on_default_branch); CREATE INDEX index_vulnerability_exports_on_author_id ON vulnerability_exports USING btree (author_id); @@ -32043,6 +32070,9 @@ ALTER TABLE ONLY namespaces ALTER TABLE ONLY issue_tracker_data ADD CONSTRAINT fk_33921c0ee1 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_project_callouts + ADD CONSTRAINT fk_33b4814f6b FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE; + ALTER TABLE ONLY namespaces ADD CONSTRAINT fk_3448c97865 FOREIGN KEY (push_rule_id) REFERENCES push_rules(id) ON DELETE SET NULL; @@ -32739,6 +32769,9 @@ ALTER TABLE ONLY analytics_devops_adoption_segments ALTER TABLE ONLY boards_epic_list_user_preferences ADD CONSTRAINT fk_f5f2fe5c1f FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; +ALTER TABLE ONLY user_project_callouts + ADD CONSTRAINT fk_f62dd11a33 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; + ALTER TABLE ONLY cluster_agents ADD CONSTRAINT fk_f7d43dee13 FOREIGN KEY (created_by_user_id) REFERENCES users(id) ON DELETE SET NULL; diff --git a/doc/administration/repository_checks.md b/doc/administration/repository_checks.md index 7c9dc9f2c03..a97c8611239 100644 --- a/doc/administration/repository_checks.md +++ b/doc/administration/repository_checks.md @@ -7,8 +7,18 @@ info: To determine the technical writer assigned to the Stage/Group associated w # Repository checks **(FREE SELF)** You can use [`git fsck`](https://git-scm.com/docs/git-fsck) to verify the integrity of all data -committed to a repository. GitLab administrators can trigger this check for a project using the -GitLab UI: +committed to a repository. GitLab administrators can: + +- Manually trigger this check for a project, using the GitLab UI. +- Schedule this check to run automatically for all projects. +- Run this check from the command line. +- Run a [Rake task](raketasks/check.md#repository-integrity) for checking Git repositories, which can be used to run + `git fsck` against all repositories and generate repository checksums, as a way to compare repositories on different + servers. + +## Check a project's repository using GitLab UI + +To check a project's repository using GitLab UI: 1. On the top bar, select **Menu > Admin**. 1. On the left sidebar, select **Overview > Projects**. @@ -18,9 +28,7 @@ GitLab UI: The checks run asynchronously so it may take a few minutes before the check result is visible on the project page in the Admin Area. If the checks fail, see [what to do](#what-to-do-if-a-check-failed). -This setting is off by default, because it can cause many false alarms. - -## Enable periodic checks +## Enable repository checks for all projects Instead of checking repositories manually, GitLab can be configured to run the checks periodically: @@ -45,10 +53,27 @@ the start of Sunday. Repositories with known check failures can be found at `/admin/projects?last_repository_check_failed=1`. +## Run a check using the command line + +You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories on +[Gitaly servers](gitaly/index.md). To locate the repositories: + +1. Go to the storage location for repositories: + - For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory + by default. + - For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the + Gitaly pod by default. +1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path) + that you need to check. +1. Run the check. For example: + + ```shell + sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck + ``` + ## What to do if a check failed -If a repository check fails, locate the error in the [`repocheck.log` file](logs/index.md#repochecklog) on -disk at: +If a repository check fails, locate the error in the [`repocheck.log` file](logs/index.md#repochecklog) on disk at: - `/var/log/gitlab/gitlab-rails` for Omnibus GitLab installations. - `/home/git/gitlab/log` for installations from source. @@ -60,24 +85,3 @@ If periodic repository checks cause false alarms, you can clear all repository c 1. On the left sidebar, select **Settings > Repository** (`/admin/application_settings/repository`). 1. Expand the **Repository maintenance** section. 1. Select **Clear all repository checks**. - -## Run a check using the command line - -You can run [`git fsck`](https://git-scm.com/docs/git-fsck) using the command line on repositories -on [Gitaly servers](gitaly/index.md). To locate the repositories: - -1. Go to the storage location for repositories: - - For Omnibus GitLab installations, repositories are stored in the `/var/opt/gitlab/git-data/repositories` directory by default. - - For GitLab Helm chart installations, repositories are stored in the `/home/git/repositories` directory inside the Gitaly pod by default. -1. [Identify the subdirectory that contains the repository](repository_storage_types.md#from-project-name-to-hashed-path) - that you need to check. - -To run a check (for example): - -```shell -sudo /opt/gitlab/embedded/bin/git -C /var/opt/gitlab/git-data/repositories/@hashed/0b/91/0b91...f9.git fsck -``` - -You can also run [Rake tasks](raketasks/check.md#repository-integrity) for checking Git -repositories, which can be used to run `git fsck` against all repositories and generate repository -checksums, as a way to compare repositories on different servers. diff --git a/doc/user/application_security/dependency_scanning/index.md b/doc/user/application_security/dependency_scanning/index.md index c4672e61c99..521bb6adbf0 100644 --- a/doc/user/application_security/dependency_scanning/index.md +++ b/doc/user/application_security/dependency_scanning/index.md @@ -109,6 +109,8 @@ maximum of two directory levels from the repository's root. For example, the `gemnasium-dependency_scanning` job is enabled if a repository contains either `Gemfile`, `api/Gemfile`, or `api/client/Gemfile`, but not if the only supported dependency file is `api/v1/client/Gemfile`. +For Java and Python, when a supported depedency file is detected, Dependency Scanning attempts to build the project and execute some Java or Python commands to get the list of dependencies. For all other projects, the lock file is parsed to obtain the list of dependencies without needing to build the project first. + When a supported dependency file is detected, all dependencies, including transitive dependencies are analyzed. There is no limit to the depth of nested or transitive dependencies that are analyzed. The following languages and dependency managers are supported: @@ -148,14 +150,13 @@ table.supported-languages ul { Language Versions Package Manager Supported files - Analyzer Processes multiple files? Ruby - Not applicable + All versions Bundler - Gemnasium Y PHP - Not applicable + All versions Composer composer.lock - Gemnasium Y C - Not applicable + All versions Conan conan.lock - Gemnasium Y @@ -187,10 +185,9 @@ table.supported-languages ul { Go - Not applicable + All versions Go go.sum - Gemnasium Y @@ -211,41 +208,36 @@ table.supported-languages ul {
  • build.gradle.kts
  • - Gemnasium N Maven pom.xml - Gemnasium N JavaScript - Not applicable + All versions npm - Gemnasium Y - Not applicable + All versions yarn yarn.lock - Gemnasium Y .NET - Not applicable + All versions NuGet packages.lock.json - Gemnasium Y @@ -256,7 +248,6 @@ table.supported-languages ul { 3.9 setuptools setup.py - Gemnasium N @@ -268,7 +259,6 @@ table.supported-languages ul {
  • requires.txt
  • - Gemnasium N @@ -276,24 +266,21 @@ table.supported-languages ul { - Gemnasium N Poetry5 poetry.lock - Gemnasium N Scala - Not applicable - sbt4 + All versions + sbt6 build.sbt - Gemnasium N @@ -316,6 +303,12 @@ table.supported-languages ul {
  • +

    + npm is only supported when `lockfileVersion = 1` or `lockfileVersion = 2`. Work to add support for `lockfileVersion = 3` is being tracked in issue GitLab#365176. +

    +
  • +
  • +

    The presence of a Pipfile.lock file alone will not trigger the analyzer; the presence of a Pipfile is still required in order for the analyzer to be executed. However, if a Pipfile.lock file is found, it will be used by @@ -327,12 +320,6 @@ table.supported-languages ul { installing project dependencies.

  • -
  • - -

    - Support for sbt 1.3 and above was added in GitLab 13.9. -

    -
  • @@ -341,6 +328,12 @@ table.supported-languages ul { Poetry's pyproject.toml support for dependency scanning.

  • +
  • + +

    + Support for sbt 1.3 and above was added in GitLab 13.9. +

    +
  • diff --git a/doc/user/group/saml_sso/example_saml_config.md b/doc/user/group/saml_sso/example_saml_config.md index fc3d582a291..97e8f9c54a3 100644 --- a/doc/user/group/saml_sso/example_saml_config.md +++ b/doc/user/group/saml_sso/example_saml_config.md @@ -68,14 +68,22 @@ fingerprint. ## Okta -Basic SAML app configuration: +Basic SAML app configuration for GitLab.com groups: + +![Okta basic SAML](img/Okta-GroupSAML.png) -![Okta basic SAML](img/Okta-SAMLsetup.png) +Basic SAML app configuration for GitLab self-managed: + +![Okta admin panel view](img/Okta-SM.png) User claims and attributes: ![Okta Attributes](img/Okta-attributes.png) +Groups attribute: + +![Okta Group attribute](img/Okta-GroupAttribute.png) + Advanced SAML app settings (defaults): ![Okta Advanced Settings](img/Okta-advancedsettings.png) @@ -88,10 +96,6 @@ Sign on settings: ![Okta SAML settings](img/okta_saml_settings.png) -Self-managed instance example: - -![Okta admin panel view](img/okta_admin_panel_v13_9.png) - Setting the username for the newly provisioned users when assigning them the SCIM app: ![Assigning SCIM app to users on Okta](img/okta_setting_username.png) diff --git a/doc/user/group/saml_sso/img/Okta-GroupAttribute.png b/doc/user/group/saml_sso/img/Okta-GroupAttribute.png new file mode 100644 index 00000000000..54c69053754 Binary files /dev/null and b/doc/user/group/saml_sso/img/Okta-GroupAttribute.png differ diff --git a/doc/user/group/saml_sso/img/Okta-GroupSAML.png b/doc/user/group/saml_sso/img/Okta-GroupSAML.png new file mode 100644 index 00000000000..7871e4ff82b Binary files /dev/null and b/doc/user/group/saml_sso/img/Okta-GroupSAML.png differ diff --git a/doc/user/group/saml_sso/img/Okta-SAMLsetup.png b/doc/user/group/saml_sso/img/Okta-SAMLsetup.png deleted file mode 100644 index 1bd9bf4d7e9..00000000000 Binary files a/doc/user/group/saml_sso/img/Okta-SAMLsetup.png and /dev/null differ diff --git a/doc/user/group/saml_sso/img/Okta-SM.png b/doc/user/group/saml_sso/img/Okta-SM.png new file mode 100644 index 00000000000..b335009fed9 Binary files /dev/null and b/doc/user/group/saml_sso/img/Okta-SM.png differ diff --git a/doc/user/group/saml_sso/img/Okta-advancedsettings.png b/doc/user/group/saml_sso/img/Okta-advancedsettings.png index 45e378d1d12..1478dc58ccd 100644 Binary files a/doc/user/group/saml_sso/img/Okta-advancedsettings.png and b/doc/user/group/saml_sso/img/Okta-advancedsettings.png differ diff --git a/doc/user/group/saml_sso/img/Okta-attributes.png b/doc/user/group/saml_sso/img/Okta-attributes.png index a3405e4de9b..38af72474d8 100644 Binary files a/doc/user/group/saml_sso/img/Okta-attributes.png and b/doc/user/group/saml_sso/img/Okta-attributes.png differ diff --git a/doc/user/group/saml_sso/img/Okta-linkscert.png b/doc/user/group/saml_sso/img/Okta-linkscert.png index 38cae415f7e..62db5d2b7e3 100644 Binary files a/doc/user/group/saml_sso/img/Okta-linkscert.png and b/doc/user/group/saml_sso/img/Okta-linkscert.png differ diff --git a/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png b/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png deleted file mode 100644 index 2ebb1f0112c..00000000000 Binary files a/doc/user/group/saml_sso/img/okta_admin_panel_v13_9.png and /dev/null differ diff --git a/doc/user/group/saml_sso/img/okta_saml_settings.png b/doc/user/group/saml_sso/img/okta_saml_settings.png index ee275ece369..9c774b72d66 100644 Binary files a/doc/user/group/saml_sso/img/okta_saml_settings.png and b/doc/user/group/saml_sso/img/okta_saml_settings.png differ diff --git a/lib/gitlab/database/gitlab_schemas.yml b/lib/gitlab/database/gitlab_schemas.yml index a99f6585d9c..06287126978 100644 --- a/lib/gitlab/database/gitlab_schemas.yml +++ b/lib/gitlab/database/gitlab_schemas.yml @@ -531,6 +531,7 @@ user_custom_attributes: :gitlab_main user_details: :gitlab_main user_follow_users: :gitlab_main user_group_callouts: :gitlab_main +user_project_callouts: :gitlab_main user_highest_roles: :gitlab_main user_interacted_projects: :gitlab_main user_permission_export_uploads: :gitlab_main diff --git a/spec/factories/users/project_user_callouts.rb b/spec/factories/users/project_user_callouts.rb new file mode 100644 index 00000000000..50e85315bb9 --- /dev/null +++ b/spec/factories/users/project_user_callouts.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :project_callout, class: 'Users::ProjectCallout' do + feature_name { :awaiting_members_banner } + + user + project + end +end diff --git a/spec/frontend/__helpers__/vue_test_utils_helper.js b/spec/frontend/__helpers__/vue_test_utils_helper.js index 2aae91f8a39..75bd5df8cbf 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper.js @@ -6,6 +6,20 @@ const vNodeContainsText = (vnode, text) => (vnode.text && vnode.text.includes(text)) || (vnode.children && vnode.children.filter((child) => vNodeContainsText(child, text)).length); +/** + * Create a VTU wrapper from an element. + * + * If a Vue instance manages the element, the wrapper is created + * with that Vue instance. + * + * @param {HTMLElement} element + * @param {Object} options + * @returns VTU wrapper + */ +const createWrapperFromElement = (element, options) => + // eslint-disable-next-line no-underscore-dangle + createWrapper(element.__vue__ || element, options || {}); + /** * Determines whether a `shallowMount` Wrapper contains text * within one of it's slots. This will also work on Wrappers @@ -85,8 +99,7 @@ export const extendedWrapper = (wrapper) => { if (!elements.length) { return new ErrorWrapper(query); } - - return createWrapper(elements[0], this.options || {}); + return createWrapperFromElement(elements[0], this.options); }, }, }; @@ -104,7 +117,7 @@ export const extendedWrapper = (wrapper) => { ); const wrappers = elements.map((element) => { - const elementWrapper = createWrapper(element, this.options || {}); + const elementWrapper = createWrapperFromElement(element, this.options); elementWrapper.selector = text; return elementWrapper; diff --git a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js index 3bb228f94b8..ae180c3b49d 100644 --- a/spec/frontend/__helpers__/vue_test_utils_helper_spec.js +++ b/spec/frontend/__helpers__/vue_test_utils_helper_spec.js @@ -6,6 +6,7 @@ import { WrapperArray as VTUWrapperArray, ErrorWrapper as VTUErrorWrapper, } from '@vue/test-utils'; +import Vue from 'vue'; import { extendedWrapper, shallowMountExtended, @@ -139,9 +140,12 @@ describe('Vue test utils helpers', () => { const text = 'foo bar'; const options = { selector: 'div' }; const mockDiv = document.createElement('div'); + const mockVm = new Vue({ render: (h) => h('div') }).$mount(); let wrapper; beforeEach(() => { + jest.spyOn(vtu, 'createWrapper'); + wrapper = extendedWrapper( shallowMount({ template: `
    foo bar
    `, @@ -164,7 +168,6 @@ describe('Vue test utils helpers', () => { describe('when element is found', () => { beforeEach(() => { jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns a VTU wrapper', () => { @@ -172,14 +175,27 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); }); }); + describe('when a Vue instance element is found', () => { + beforeEach(() => { + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockVm.$el]); + }); + + it('returns a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); + }); + }); describe('when multiple elements are found', () => { beforeEach(() => { const mockSpan = document.createElement('span'); jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => [mockDiv, mockSpan]); - jest.spyOn(vtu, 'createWrapper'); }); it('returns the first element as a VTU wrapper', () => { @@ -187,6 +203,24 @@ describe('Vue test utils helpers', () => { expect(vtu.createWrapper).toHaveBeenCalledWith(mockDiv, wrapper.options); expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeUndefined(); + }); + }); + + describe('when multiple Vue instances are found', () => { + beforeEach(() => { + const mockVm2 = new Vue({ render: (h) => h('span') }).$mount(); + jest + .spyOn(testingLibrary, expectedQuery) + .mockImplementation(() => [mockVm.$el, mockVm2.$el]); + }); + + it('returns the first element as a VTU wrapper', () => { + const result = wrapper[findMethod](text, options); + + expect(vtu.createWrapper).toHaveBeenCalledWith(mockVm, wrapper.options); + expect(result).toBeInstanceOf(VTUWrapper); + expect(result.vm).toBeInstanceOf(Vue); }); }); @@ -211,12 +245,17 @@ describe('Vue test utils helpers', () => { ${'findAllByAltText'} | ${'queryAllByAltText'} `('$findMethod', ({ findMethod, expectedQuery }) => { const text = 'foo bar'; - const options = { selector: 'div' }; + const options = { selector: 'li' }; const mockElements = [ document.createElement('li'), document.createElement('li'), document.createElement('li'), ]; + const mockVms = [ + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + new Vue({ render: (h) => h('li') }).$mount(), + ]; let wrapper; beforeEach(() => { @@ -245,9 +284,13 @@ describe('Vue test utils helpers', () => { ); }); - describe('when elements are found', () => { + describe.each` + case | mockResult | isVueInstance + ${'HTMLElements'} | ${mockElements} | ${false} + ${'Vue instance elements'} | ${mockVms} | ${true} + `('when $case are found', ({ mockResult, isVueInstance }) => { beforeEach(() => { - jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockElements); + jest.spyOn(testingLibrary, expectedQuery).mockImplementation(() => mockResult); }); it('returns a VTU wrapper array', () => { @@ -257,7 +300,9 @@ describe('Vue test utils helpers', () => { expect( result.wrappers.every( (resultWrapper) => - resultWrapper instanceof VTUWrapper && resultWrapper.options === wrapper.options, + resultWrapper instanceof VTUWrapper && + resultWrapper.vm instanceof Vue === isVueInstance && + resultWrapper.options === wrapper.options, ), ).toBe(true); expect(result.length).toBe(3); diff --git a/spec/frontend/runner/components/runner_bulk_delete_spec.js b/spec/frontend/runner/components/runner_bulk_delete_spec.js index f5b56396cf1..cc679a52b34 100644 --- a/spec/frontend/runner/components/runner_bulk_delete_spec.js +++ b/spec/frontend/runner/components/runner_bulk_delete_spec.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import { GlSprintf } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import RunnerBulkDelete from '~/runner/components/runner_bulk_delete.vue'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -17,8 +18,8 @@ describe('RunnerBulkDelete', () => { let mockState; let mockCheckedRunnerIds; - const findClearBtn = () => wrapper.findByTestId('clear-btn'); - const findDeleteBtn = () => wrapper.findByTestId('delete-btn'); + const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection')); + const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected')); const createComponent = () => { const { cacheConfig, localMutations } = mockState; diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index a324fd70424..4ffde842819 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -627,6 +627,7 @@ project: - security_trainings - vulnerability_reads - build_artifacts_size_refresh +- project_callouts award_emoji: - awardable - user diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index afb321c0777..2920278679f 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -147,6 +147,7 @@ RSpec.describe Project, factory_default: :keep do it { is_expected.to have_many(:build_trace_chunks).through(:builds).dependent(:restrict_with_error) } it { is_expected.to have_many(:secure_files).class_name('Ci::SecureFile').dependent(:restrict_with_error) } it { is_expected.to have_one(:build_artifacts_size_refresh).class_name('Projects::BuildArtifactsSizeRefresh') } + it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout').with_foreign_key(:project_id) } # GitLab Pages it { is_expected.to have_many(:pages_domains) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 12029b4151d..01b6b36db77 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -137,6 +137,7 @@ RSpec.describe User do it { is_expected.to have_many(:callouts).class_name('Users::Callout') } it { is_expected.to have_many(:group_callouts).class_name('Users::GroupCallout') } it { is_expected.to have_many(:namespace_callouts).class_name('Users::NamespaceCallout') } + it { is_expected.to have_many(:project_callouts).class_name('Users::ProjectCallout') } describe '#user_detail' do it 'does not persist `user_detail` by default' do @@ -6671,6 +6672,40 @@ RSpec.describe User do end end + describe '#dismissed_callout_for_project?' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + context 'when no callout dismissal record exists' do + it 'returns false when no ignore_dismissal_earlier_than provided' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq false + end + end + + context 'when dismissed callout exists' do + before_all do + create(:project_callout, + user: user, + project_id: project.id, + feature_name: feature_name, + dismissed_at: 4.months.ago) + end + + it 'returns true when no ignore_dismissal_earlier_than provided' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project)).to eq true + end + + it 'returns true when ignore_dismissal_earlier_than is earlier than dismissed_at' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 6.months.ago)).to eq true + end + + it 'returns false when ignore_dismissal_earlier_than is later than dismissed_at' do + expect(user.dismissed_callout_for_project?(feature_name: feature_name, project: project, ignore_dismissal_earlier_than: 3.months.ago)).to eq false + end + end + end + describe '#find_or_initialize_group_callout' do let_it_be(:user, refind: true) { create(:user) } let_it_be(:group) { create(:group) } @@ -6715,6 +6750,50 @@ RSpec.describe User do end end + describe '#find_or_initialize_project_callout' do + let_it_be(:user, refind: true) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + subject(:callout_with_source) do + user.find_or_initialize_project_callout(feature_name, project.id) + end + + context 'when callout exists' do + let!(:callout) do + create(:project_callout, user: user, feature_name: feature_name, project_id: project.id) + end + + it 'returns existing callout' do + expect(callout_with_source).to eq(callout) + end + end + + context 'when callout does not exist' do + context 'when feature name is valid' do + it 'initializes a new callout' do + expect(callout_with_source).to be_a_new(Users::ProjectCallout) + end + + it 'is valid' do + expect(callout_with_source).to be_valid + end + end + + context 'when feature name is not valid' do + let(:feature_name) { 'notvalid' } + + it 'initializes a new callout' do + expect(callout_with_source).to be_a_new(Users::ProjectCallout) + end + + it 'is not valid' do + expect(callout_with_source).not_to be_valid + end + end + end + end + describe '#hook_attrs' do let(:user) { create(:user) } let(:user_attributes) do diff --git a/spec/models/users/project_callout_spec.rb b/spec/models/users/project_callout_spec.rb new file mode 100644 index 00000000000..214568b4de3 --- /dev/null +++ b/spec/models/users/project_callout_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::ProjectCallout do + let_it_be(:user) { create_default(:user) } + let_it_be(:project) { create_default(:project) } + let_it_be(:callout) { create(:project_callout) } + + it_behaves_like 'having unique enum values' + + describe 'relationships' do + it { is_expected.to belong_to(:project) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:feature_name) } + it { + is_expected.to validate_uniqueness_of(:feature_name).scoped_to(:user_id, :project_id).ignoring_case_sensitivity + } + end +end diff --git a/spec/requests/users/project_callouts_spec.rb b/spec/requests/users/project_callouts_spec.rb new file mode 100644 index 00000000000..98c00fef052 --- /dev/null +++ b/spec/requests/users/project_callouts_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'Project callouts' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + before do + sign_in(user) + end + + describe 'POST /-/users/project_callouts' do + let(:params) { { feature_name: feature_name, project_id: project.id } } + + subject { post project_callouts_path, params: params, headers: { 'ACCEPT' => 'application/json' } } + + context 'with valid feature name and project' do + let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + context 'when callout entry does not exist' do + it 'creates a callout entry with dismissed state' do + expect { subject }.to change { Users::ProjectCallout.count }.by(1) + end + + it 'returns success' do + subject + + expect(response).to have_gitlab_http_status(:ok) + end + end + + context 'when callout entry already exists' do + let!(:callout) do + create(:project_callout, + feature_name: Users::ProjectCallout.feature_names.each_key.first, + user: user, + project: project) + end + + it 'returns success', :aggregate_failures do + expect { subject }.not_to change { Users::ProjectCallout.count } + expect(response).to have_gitlab_http_status(:ok) + end + end + end + + context 'with invalid feature name' do + let(:feature_name) { 'bogus_feature_name' } + + it 'returns bad request' do + subject + + expect(response).to have_gitlab_http_status(:bad_request) + end + end + end +end diff --git a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb index 149a384d31e..fe815aa6f1e 100644 --- a/spec/scripts/lib/glfm/update_example_snapshots_spec.rb +++ b/spec/scripts/lib/glfm/update_example_snapshots_spec.rb @@ -65,13 +65,19 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do ## Strong + This example doesn't have an extension after the `example` keyword, so its + `source_specification` will be `commonmark`. + ```````````````````````````````` example __bold__ .

    bold

    ```````````````````````````````` - ```````````````````````````````` example strong + This example has an extension after the `example` keyword, so its + `source_specification` will be `github`. + + ```````````````````````````````` example some_extension_name __bold with more text__ .

    bold with more text

    @@ -132,6 +138,10 @@ RSpec.describe Glfm::UpdateExampleSnapshots, '#process' do ## Strong but with HTML + This example has the `gitlab` keyword after the `example` keyword, so its + `source_specification` will be `gitlab`. + + ```````````````````````````````` example gitlab strong bold diff --git a/spec/services/users/dismiss_project_callout_service_spec.rb b/spec/services/users/dismiss_project_callout_service_spec.rb new file mode 100644 index 00000000000..73e50a4c37d --- /dev/null +++ b/spec/services/users/dismiss_project_callout_service_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DismissProjectCalloutService do + describe '#execute' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:params) { { feature_name: feature_name, project_id: project.id } } + let(:feature_name) { Users::ProjectCallout.feature_names.each_key.first } + + subject(:execute) do + described_class.new( + container: nil, current_user: user, params: params + ).execute + end + + it_behaves_like 'dismissing user callout', Users::ProjectCallout + + it 'sets the project_id' do + expect(execute.project_id).to eq(project.id) + end + end +end -- cgit v1.2.3