Add latest changes from gitlab-org/gitlab@master
|
@ -99,10 +99,10 @@ export default {
|
|||
</gl-sprintf>
|
||||
</div>
|
||||
<div class="gl-ml-auto">
|
||||
<gl-button data-testid="clear-btn" variant="default" @click="onClearChecked">{{
|
||||
<gl-button variant="default" @click="onClearChecked">{{
|
||||
s__('Runners|Clear selection')
|
||||
}}</gl-button>
|
||||
<gl-button data-testid="delete-btn" variant="danger" @click="onClickDelete">{{
|
||||
<gl-button variant="danger" @click="onClickDelete">{{
|
||||
s__('Runners|Delete selected')
|
||||
}}</gl-button>
|
||||
</div>
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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'
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
f1d4faf4d32a3271a97b389d53c9d3accbfa3fa2bd47d63257fe589efa4bb665
|
|
@ -0,0 +1 @@
|
|||
bf12037cb99a399302610f948dad48589eca4e631d82d9f26b04bae882b10020
|
|
@ -0,0 +1 @@
|
|||
047147acc972ab8681f097d5060998a47e44612fde7f2137714683bd61350c2d
|
|
@ -0,0 +1 @@
|
|||
2cdf4c4fe218a5fb7061bf65643868c7b592cd3ef0d7611949e8fd86bc635c24
|
|
@ -0,0 +1 @@
|
|||
07488e8c6ea0f3dc92e1370efb0190facf520b850e170fcd8f3ce0e2a15c096a
|
|
@ -0,0 +1 @@
|
|||
bab4f4d3aaedd698400fcbd5991797530450fe845a8034b03b1bf525a61e628a
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
|||
<th>Language Versions</th>
|
||||
<th>Package Manager</th>
|
||||
<th>Supported files</th>
|
||||
<th>Analyzer</th>
|
||||
<th><a href="#how-multiple-files-are-processed">Processes multiple files?</a></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Ruby</td>
|
||||
<td>Not applicable</td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://bundler.io/">Bundler</a></td>
|
||||
<td>
|
||||
<ul>
|
||||
|
@ -163,23 +164,20 @@ table.supported-languages ul {
|
|||
<li><code>gems.locked</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PHP</td>
|
||||
<td>Not applicable</td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://getcomposer.org/">Composer</a></td>
|
||||
<td><code>composer.lock</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>C</td>
|
||||
<td rowspan="2">Not applicable</td>
|
||||
<td rowspan="2">All versions</td>
|
||||
<td rowspan="2"><a href="https://conan.io/">Conan</a></td>
|
||||
<td rowspan="2"><a href="https://docs.conan.io/en/latest/versioning/lockfiles.html"><code>conan.lock</code></a></td>
|
||||
<td rowspan="2"><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td rowspan="2">Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -187,10 +185,9 @@ table.supported-languages ul {
|
|||
</tr>
|
||||
<tr>
|
||||
<td>Go</td>
|
||||
<td>Not applicable</td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://go.dev/">Go</a></td>
|
||||
<td><code>go.sum</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -211,41 +208,36 @@ table.supported-languages ul {
|
|||
<li><code>build.gradle.kts</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://maven.apache.org/">Maven</a></td>
|
||||
<td><code>pom.xml</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td rowspan="2">JavaScript</td>
|
||||
<td>Not applicable</td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://www.npmjs.com/">npm</a></td>
|
||||
<td>
|
||||
<ul>
|
||||
<li><code>package-lock.json</code></li>
|
||||
<li><code>package-lock.json</code><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-3">3</a></b></sup></li>
|
||||
<li><code>npm-shrinkwrap.json</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Not applicable</td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://classic.yarnpkg.com/en/">yarn</a></td>
|
||||
<td><code>yarn.lock</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>.NET</td>
|
||||
<td rowspan="2">Not applicable</td>
|
||||
<td rowspan="2">All versions</td>
|
||||
<td rowspan="2"><a href="https://www.nuget.org/">NuGet</a></td>
|
||||
<td rowspan="2"><a href="https://docs.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#enabling-lock-file"><code>packages.lock.json</code></a></td>
|
||||
<td rowspan="2"><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td rowspan="2">Y</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -256,7 +248,6 @@ table.supported-languages ul {
|
|||
<td rowspan="4">3.9</td>
|
||||
<td><a href="https://setuptools.readthedocs.io/en/latest/">setuptools</a></td>
|
||||
<td><code>setup.py</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -268,7 +259,6 @@ table.supported-languages ul {
|
|||
<li><code>requires.txt</code></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -276,24 +266,21 @@ table.supported-languages ul {
|
|||
<td>
|
||||
<ul>
|
||||
<li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile</code></a></li>
|
||||
<li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile.lock</code></a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-3">3</a></b></sup></li>
|
||||
<li><a href="https://pipenv.pypa.io/en/latest/basics/#example-pipfile-pipfile-lock"><code>Pipfile.lock</code></a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-4">4</a></b></sup></li>
|
||||
</ul>
|
||||
</td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a href="https://python-poetry.org/">Poetry</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-5">5</a></b></sup></td>
|
||||
<td><code>poetry.lock</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Scala</td>
|
||||
<td>Not applicable</td>
|
||||
<td><a href="https://www.scala-sbt.org/">sbt</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-4">4</a></b></sup></td>
|
||||
<td>All versions</td>
|
||||
<td><a href="https://www.scala-sbt.org/">sbt</a><sup><b><a href="#notes-regarding-supported-languages-and-package-managers-6">6</a></b></sup></td>
|
||||
<td><code>build.sbt</code></td>
|
||||
<td><a href="https://gitlab.com/gitlab-org/security-products/analyzers/gemnasium">Gemnasium</a></td>
|
||||
<td>N</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -316,6 +303,12 @@ table.supported-languages ul {
|
|||
</li>
|
||||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-3"></a>
|
||||
<p>
|
||||
npm is only supported when `lockfileVersion = 1` or `lockfileVersion = 2`. Work to add support for `lockfileVersion = 3` is being tracked in issue <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/365176">GitLab#365176</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-4"></a>
|
||||
<p>
|
||||
The presence of a <code>Pipfile.lock</code> file alone will <i>not</i> trigger the analyzer; the presence of a <code>Pipfile</code> is
|
||||
still required in order for the analyzer to be executed. However, if a <code>Pipfile.lock</code> file is found, it will be used by
|
||||
|
@ -327,12 +320,6 @@ table.supported-languages ul {
|
|||
installing project dependencies</a>.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-4"></a>
|
||||
<p>
|
||||
Support for <a href="https://www.scala-sbt.org/">sbt</a> 1.3 and above was added in GitLab 13.9.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-5"></a>
|
||||
<p>
|
||||
|
@ -341,6 +328,12 @@ table.supported-languages ul {
|
|||
<a href="https://gitlab.com/gitlab-org/gitlab/-/issues/32774">Poetry's pyproject.toml support for dependency scanning.</a>
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<a id="notes-regarding-supported-languages-and-package-managers-6"></a>
|
||||
<p>
|
||||
Support for <a href="https://www.scala-sbt.org/">sbt</a> 1.3 and above was added in GitLab 13.9.
|
||||
</p>
|
||||
</li>
|
||||
</ol>
|
||||
<!-- markdownlint-enable MD044 -->
|
||||
|
||||
|
|
|
@ -68,14 +68,22 @@ fingerprint.
|
|||
|
||||
## Okta
|
||||
|
||||
Basic SAML app configuration:
|
||||
Basic SAML app configuration for GitLab.com groups:
|
||||
|
||||
![Okta basic SAML](img/Okta-SAMLsetup.png)
|
||||
![Okta basic SAML](img/Okta-GroupSAML.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)
|
||||
|
|
After Width: | Height: | Size: 7.6 KiB |
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 54 KiB |
Before Width: | Height: | Size: 48 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 21 KiB |
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
|
|
|
@ -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: `<div>foo bar</div>`,
|
||||
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -627,6 +627,7 @@ project:
|
|||
- security_trainings
|
||||
- vulnerability_reads
|
||||
- build_artifacts_size_refresh
|
||||
- project_callouts
|
||||
award_emoji:
|
||||
- awardable
|
||||
- user
|
||||
|
|
|
@ -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) }
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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__
|
||||
.
|
||||
<p><strong>bold</strong></p>
|
||||
````````````````````````````````
|
||||
|
||||
```````````````````````````````` 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__
|
||||
.
|
||||
<p><strong>bold with more text</strong></p>
|
||||
|
@ -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
|
||||
<strong>
|
||||
bold
|
||||
|
|
|
@ -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
|