summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-05-16 09:08:02 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2023-05-16 09:08:02 +0000
commit613868af23d7c0e09210857518895edd6678f5e9 (patch)
tree90b5ba583bbec4cb2a1eef3b34b2df1bb13de50f
parentb78b8c1103e1e9542891a1c333c8abcd4d7e10ab (diff)
downloadgitlab-ce-613868af23d7c0e09210857518895edd6678f5e9.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar.vue19
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_button.vue14
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js1
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js23
-rw-r--r--app/assets/stylesheets/page_bundles/editor.scss17
-rw-r--r--app/controllers/profiles/webauthn_registrations_controller.rb3
-rw-r--r--app/mailers/emails/issues.rb100
-rw-r--r--app/services/webauthn/destroy_service.rb30
-rw-r--r--app/views/projects/blob/_editor.html.haml7
-rw-r--r--config/feature_flags/development/product_intelligence_database_event_tracking.yml2
-rw-r--r--config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml2
-rw-r--r--db/post_migrate/20230511132140_create_component_id_index.rb17
-rw-r--r--db/schema_migrations/202305111321401
-rw-r--r--db/structure.sql2
-rw-r--r--doc/administration/logs/index.md12
-rw-r--r--doc/administration/sidekiq/sidekiq_troubleshooting.md35
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/architecture/blueprints/remote_development/index.md58
-rw-r--r--doc/development/ai_features.md9
-rw-r--r--doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.pngbin0 -> 12025 bytes
-rw-r--r--doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.pngbin0 -> 16411 bytes
-rw-r--r--doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.pngbin0 -> 11205 bytes
-rw-r--r--doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.pngbin0 -> 16485 bytes
-rw-r--r--doc/tutorials/boards_for_teams/index.md25
-rw-r--r--doc/tutorials/plan_and_track.md1
-rw-r--r--doc/user/profile/notifications.md1
-rw-r--r--doc/user/project/web_ide/index.md17
-rw-r--r--qa/Gemfile2
-rw-r--r--qa/Gemfile.lock4
-rw-r--r--qa/qa/page/file/edit.rb8
-rw-r--r--spec/controllers/profiles/webauthn_registrations_controller_spec.rb24
-rw-r--r--spec/factories/users.rb2
-rw-r--r--spec/features/projects/blobs/edit_spec.rb47
-rw-r--r--spec/frontend/editor/components/source_editor_toolbar_button_spec.js11
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js10
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js4
-rw-r--r--spec/services/webauthn/destroy_service_spec.rb80
37 files changed, 471 insertions, 118 deletions
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
index 0afee7bebe0..f2550d753d6 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
@@ -1,6 +1,5 @@
<script>
import { isEmpty } from 'lodash';
-import { GlButtonGroup } from '@gitlab/ui';
import getToolbarItemsQuery from '~/editor/graphql/get_items.query.graphql';
import { EDITOR_TOOLBAR_BUTTON_GROUPS } from '~/editor/constants';
import SourceEditorToolbarButton from './source_editor_toolbar_button.vue';
@@ -9,7 +8,6 @@ export default {
name: 'SourceEditorToolbar',
components: {
SourceEditorToolbarButton,
- GlButtonGroup,
},
data() {
return {
@@ -52,31 +50,34 @@ export default {
<section
v-if="isVisible"
id="se-toolbar"
- class="gl-py-3 gl-px-5 gl-bg-white gl-border-b gl-display-flex gl-align-items-center"
+ class="file-buttons gl-display-flex gl-align-items-center gl-justify-content-end"
>
- <gl-button-group v-if="hasGroupItems($options.groups.file)">
+ <div v-if="hasGroupItems($options.groups.file)">
<source-editor-toolbar-button
v-for="item in getGroupItems($options.groups.file)"
:key="item.id"
:button="item"
@click="$emit('click', item)"
/>
- </gl-button-group>
- <gl-button-group v-if="hasGroupItems($options.groups.edit)">
+ </div>
+ <div
+ v-if="hasGroupItems($options.groups.edit)"
+ class="md-header-toolbar gl-display-flex gl-flex-wrap gl-gap-3 gl-ml-auto"
+ >
<source-editor-toolbar-button
v-for="item in getGroupItems($options.groups.edit)"
:key="item.id"
:button="item"
@click="$emit('click', item)"
/>
- </gl-button-group>
- <gl-button-group v-if="hasGroupItems($options.groups.settings)" class="gl-ml-auto">
+ </div>
+ <div v-if="hasGroupItems($options.groups.settings)" class="gl-align-self-start">
<source-editor-toolbar-button
v-for="item in getGroupItems($options.groups.settings)"
:key="item.id"
:button="item"
@click="$emit('click', item)"
/>
- </gl-button-group>
+ </div>
</section>
</template>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
index 38f586f0773..996ecea04e5 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
@@ -30,6 +30,15 @@ export default {
showButton() {
return Object.entries(this.button).length > 0;
},
+ showLabel() {
+ if (this.button.category === 'tertiary' && this.button.icon) {
+ return false;
+ }
+ return true;
+ },
+ isSelected() {
+ return this.button.category === 'tertiary' && this.button.selected;
+ },
},
mounted() {
if (this.button.data) {
@@ -55,11 +64,12 @@ export default {
:category="button.category"
:variant="button.variant"
type="button"
- :selected="button.selected"
+ :selected="isSelected"
:icon="icon"
:title="label"
:aria-label="label"
:class="button.class"
@click="clickHandler($event)"
- />
+ ><template v-if="showLabel">{{ label }}</template></gl-button
+ >
</template>
diff --git a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 8ec83e4df1c..905126cae52 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -151,6 +151,7 @@ export class SourceEditorExtension {
instance.toolbar.updateItem(EXTENSION_SOFTWRAP_ID, {
selected: !isSoftWrapped,
});
+ document.querySelector('.soft-wrap-toggle')?.blur();
}
},
};
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index 9ec1a97ba1a..60aa00da861 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -14,7 +14,6 @@ import {
EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY,
EXTENSION_MARKDOWN_PREVIEW_LABEL,
EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
- EDITOR_TOOLBAR_BUTTON_GROUPS,
} from '../constants';
const fetchPreview = (text, previewMarkdownPath) => {
@@ -58,9 +57,6 @@ export class EditorMarkdownPreviewExtension {
this.toolbarButtons = [];
this.setupPreviewAction(instance);
- if (instance.toolbar) {
- this.setupToolbar(instance);
- }
const debouncedResizeHandler = debounce((entries) => {
for (const entry of entries) {
@@ -104,25 +100,6 @@ export class EditorMarkdownPreviewExtension {
instance.layout({ width, height });
}
- setupToolbar(instance) {
- this.toolbarButtons = [
- {
- id: EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- label: EXTENSION_MARKDOWN_PREVIEW_LABEL,
- icon: 'live-preview',
- selected: false,
- group: EDITOR_TOOLBAR_BUTTON_GROUPS.settings,
- category: 'primary',
- selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
- onClick: () => instance.togglePreview(),
- data: {
- qaSelector: 'editor_toolbar_button',
- },
- },
- ];
- instance.toolbar.addItems(this.toolbarButtons);
- }
-
togglePreviewLayout(instance) {
const { width } = instance.getLayoutInfo();
let newWidth;
diff --git a/app/assets/stylesheets/page_bundles/editor.scss b/app/assets/stylesheets/page_bundles/editor.scss
index 9e9723d2e5a..0c1979424b1 100644
--- a/app/assets/stylesheets/page_bundles/editor.scss
+++ b/app/assets/stylesheets/page_bundles/editor.scss
@@ -110,11 +110,13 @@
.file-buttons {
display: flex;
- flex-direction: column;
+ flex-direction: row;
+ justify-content: space-between;
width: 100%;
+ padding: $gl-padding-8 0 0;
.md-header-toolbar {
- margin: $gl-padding 0;
+ margin-left: 0;
}
.soft-wrap-toggle {
@@ -129,6 +131,17 @@
}
}
+@include media-breakpoint-down(sm) {
+ .file-editor .file-buttons {
+ flex-direction: column;
+ padding: 0;
+
+ .md-header-toolbar {
+ margin: $gl-padding-8 0;
+ }
+ }
+}
+
.blob-new-page-title,
.blob-edit-page-title {
margin: 19px 0 21px;
diff --git a/app/controllers/profiles/webauthn_registrations_controller.rb b/app/controllers/profiles/webauthn_registrations_controller.rb
index 345d7bdbca8..ef3144f6f8c 100644
--- a/app/controllers/profiles/webauthn_registrations_controller.rb
+++ b/app/controllers/profiles/webauthn_registrations_controller.rb
@@ -4,8 +4,7 @@ class Profiles::WebauthnRegistrationsController < Profiles::ApplicationControlle
feature_category :system_access
def destroy
- webauthn_registration = current_user.webauthn_registrations.find(params[:id])
- webauthn_registration.destroy
+ Webauthn::DestroyService.new(current_user, current_user, params[:id]).execute
redirect_to profile_two_factor_auth_path, status: :found, notice: _("Successfully deleted WebAuthn device.")
end
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index e053fc0453c..0328d262dc7 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -5,18 +5,32 @@ module Emails
def new_issue_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
- mail_new_thread(@issue, issue_thread_options(@issue.author_id, reason))
+ mail_new_thread(
+ @issue,
+ issue_thread_options(
+ @issue.author_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def issue_due_email(recipient_id, issue_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
- mail_answer_thread(@issue, issue_thread_options(@issue.author_id, reason))
+ mail_answer_thread(@issue, issue_thread_options(@issue.author_id, reason, confidentiality: @issue.confidential?))
end
def new_mention_in_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -26,7 +40,14 @@ module Emails
@previous_assignees = []
@previous_assignees = User.where(id: previous_assignee_ids) if previous_assignee_ids.any?
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
# rubocop: enable CodeReuse/ActiveRecord
@@ -35,7 +56,14 @@ module Emails
@updated_by = User.find(updated_by_user_id)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def relabeled_issue_email(recipient_id, issue_id, label_names, updated_by_user_id, reason = nil)
@@ -43,13 +71,27 @@ module Emails
@label_names = label_names
@labels_url = project_labels_url(@project)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def removed_milestone_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
setup_issue_mail(issue_id, recipient_id)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def changed_milestone_issue_email(recipient_id, issue_id, milestone, updated_by_user_id, reason = nil)
@@ -57,9 +99,14 @@ module Emails
@milestone = milestone
@milestone_url = milestone_url(@milestone)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason).merge({
- template_name: 'changed_milestone_email'
- }))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ ).merge({ template_name: 'changed_milestone_email' })
+ )
end
def issue_status_changed_email(recipient_id, issue_id, status, updated_by_user_id, reason = nil)
@@ -67,7 +114,14 @@ module Emails
@issue_status = status
@updated_by = User.find(updated_by_user_id)
- mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, reason))
+ mail_answer_thread(
+ @issue,
+ issue_thread_options(
+ updated_by_user_id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def issue_moved_email(recipient, issue, new_issue, updated_by_user, reason = nil)
@@ -76,7 +130,14 @@ module Emails
@new_issue = new_issue
@new_project = new_issue.project
@can_access_project = recipient.can?(:read_project, @new_project)
- mail_answer_thread(issue, issue_thread_options(updated_by_user.id, reason))
+ mail_answer_thread(
+ issue,
+ issue_thread_options(
+ updated_by_user.id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def issue_cloned_email(recipient, issue, new_issue, updated_by_user, reason = nil)
@@ -86,7 +147,14 @@ module Emails
@issue = issue
@new_issue = new_issue
@can_access_project = recipient.can?(:read_project, @new_issue.project)
- mail_answer_thread(issue, issue_thread_options(updated_by_user.id, reason))
+ mail_answer_thread(
+ issue,
+ issue_thread_options(
+ updated_by_user.id,
+ reason,
+ confidentiality: @issue.confidential?
+ )
+ )
end
def import_issues_csv_email(user_id, project_id, results)
@@ -115,12 +183,14 @@ module Emails
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
- def issue_thread_options(sender_id, reason)
+ def issue_thread_options(sender_id, reason, confidentiality: false)
+ confidentiality = false if confidentiality.nil?
{
from: sender(sender_id),
to: @recipient.notification_email_for(@project.group),
subject: subject("#{@issue.title} (##{@issue.iid})"),
- 'X-GitLab-NotificationReason' => reason
+ 'X-GitLab-NotificationReason' => reason,
+ 'X-GitLab-ConfidentialIssue' => confidentiality
}
end
end
diff --git a/app/services/webauthn/destroy_service.rb b/app/services/webauthn/destroy_service.rb
new file mode 100644
index 00000000000..afad2680d42
--- /dev/null
+++ b/app/services/webauthn/destroy_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Webauthn
+ class DestroyService < BaseService
+ attr_reader :webauthn_registration, :user, :current_user
+
+ def initialize(current_user, user, webauthn_registrations_id)
+ @current_user = current_user
+ @user = user
+ @webauthn_registration = user.webauthn_registrations.find(webauthn_registrations_id)
+ end
+
+ def execute
+ return error(_('You are not authorized to perform this action')) unless authorized?
+
+ webauthn_registration.destroy
+ user.reset_backup_codes! if last_two_factor_registration?
+ end
+
+ private
+
+ def last_two_factor_registration?
+ user.webauthn_registrations.empty? && !user.otp_required_for_login?
+ end
+
+ def authorized?
+ current_user.can?(:disable_two_factor, user)
+ end
+ end
+end
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 621cd251bdf..68520d36858 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -26,7 +26,10 @@
dismiss_key: @project.id,
human_access: human_access } }
- - unless Feature.enabled?(:source_editor_toolbar, current_user)
+ - if Feature.enabled?(:source_editor_toolbar, current_user)
+ #editor-toolbar
+
+ - else
.file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- if is_markdown
.md-header.gl-display-flex.gl-px-2.gl-rounded-base.gl-mx-2.gl-mt-2
@@ -40,8 +43,6 @@
= _("Soft wrap")
.file-editor.code
- - if Feature.enabled?(:source_editor_toolbar, current_user)
- #editor-toolbar
.js-edit-mode-pane#editor{ data: { 'editor-loading': true, qa_selector: 'source_editor_preview_container' } }<
%pre.editor-loading-content= params[:content] || local_assigns[:blob_data]
- if local_assigns[:path]
diff --git a/config/feature_flags/development/product_intelligence_database_event_tracking.yml b/config/feature_flags/development/product_intelligence_database_event_tracking.yml
index 545cdc47f2b..63b53996eea 100644
--- a/config/feature_flags/development/product_intelligence_database_event_tracking.yml
+++ b/config/feature_flags/development/product_intelligence_database_event_tracking.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/368976
milestone: '15.3'
type: development
group: group::product intelligence
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml b/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml
index 2e15d86004b..825f684ed8c 100644
--- a/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml
+++ b/config/feature_flags/development/product_intelligence_database_event_tracking_batch2.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/403041
milestone: '16.0'
type: development
group: group::analytics instrumentation
-default_enabled: false
+default_enabled: true
diff --git a/db/post_migrate/20230511132140_create_component_id_index.rb b/db/post_migrate/20230511132140_create_component_id_index.rb
new file mode 100644
index 00000000000..3b466010f7c
--- /dev/null
+++ b/db/post_migrate/20230511132140_create_component_id_index.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class CreateComponentIdIndex < Gitlab::Database::Migration[2.1]
+ disable_ddl_transaction!
+
+ INDEX_NAME = 'index_sbom_occurrences_on_project_id_component_id'
+
+ def up
+ return if index_exists_by_name?(:sbom_occurrences, INDEX_NAME)
+
+ add_concurrent_index :sbom_occurrences, [:project_id, :component_id], name: INDEX_NAME
+ end
+
+ def down
+ remove_concurrent_index_by_name :sbom_sources, INDEX_NAME
+ end
+end
diff --git a/db/schema_migrations/20230511132140 b/db/schema_migrations/20230511132140
new file mode 100644
index 00000000000..e90a58e2e28
--- /dev/null
+++ b/db/schema_migrations/20230511132140
@@ -0,0 +1 @@
+a9fc2734da81d15b8c6bdb3812c22e7c5a24d6729b98bf9118e6c8a9c85a6712 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8dea1331b6e..9e64191eb74 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -32339,6 +32339,8 @@ CREATE INDEX index_sbom_occurrences_on_project_id ON sbom_occurrences USING btre
CREATE INDEX index_sbom_occurrences_on_project_id_and_id ON sbom_occurrences USING btree (project_id, id);
+CREATE INDEX index_sbom_occurrences_on_project_id_component_id ON sbom_occurrences USING btree (project_id, component_id);
+
CREATE INDEX index_sbom_occurrences_on_source_id ON sbom_occurrences USING btree (source_id);
CREATE UNIQUE INDEX index_sbom_occurrences_on_uuid ON sbom_occurrences USING btree (uuid);
diff --git a/doc/administration/logs/index.md b/doc/administration/logs/index.md
index 7fab97f76da..3533237f946 100644
--- a/doc/administration/logs/index.md
+++ b/doc/administration/logs/index.md
@@ -1034,6 +1034,18 @@ can be used.
}
```
+## `llm.log` **(ULTIMATE SAAS)**
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/120506) in GitLab 16.0.
+
+The `llm.log` file logs information related to
+[AI features](../../user/ai_features.md).
+
+Depending on your installation method, this file is located at:
+
+- Omnibus GitLab: `/var/log/gitlab/gitlab-rails/llm.log`
+- Installations from source: `/home/git/gitlab/log/llm.log`
+
## Registry logs
For Omnibus GitLab installations, Container Registry logs are in `/var/log/gitlab/registry/current`.
diff --git a/doc/administration/sidekiq/sidekiq_troubleshooting.md b/doc/administration/sidekiq/sidekiq_troubleshooting.md
index 8b95a9f6f0a..cce9420f455 100644
--- a/doc/administration/sidekiq/sidekiq_troubleshooting.md
+++ b/doc/administration/sidekiq/sidekiq_troubleshooting.md
@@ -494,6 +494,41 @@ has number of drawbacks, as mentioned in [Why Ruby's Timeout is dangerous (and T
>
> Nobody writes code to defend against an exception being raised on literally any line. That's not even possible. So Thread.raise is basically like a sneak attack on your code that could result in almost anything. It would probably be okay if it were pure-functional code that did not modify any state. But this is Ruby, so that's unlikely :)
+## Manually trigger a cron job
+
+By visiting `/admin/background_jobs`, you can look into what jobs are scheduled/running/pending on your instance.
+
+You can trigger a cron job from the UI by selecting the "Enqueue Now" button. To trigger a cron job programmatically first open a [Rails console](../operations/rails_console.md).
+
+To find the cron job you want to test:
+
+```irb
+job = Sidekiq::Cron::Job.find('job-name')
+
+# get status of job:
+job.status
+
+# enqueue job right now!
+job.enque!
+```
+
+For example, to trigger the `update_all_mirrors_worker` cron job that updates the repository mirrors:
+
+```irb
+irb(main):001:0> job = Sidekiq::Cron::Job.find('update_all_mirrors_worker')
+=>
+#<Sidekiq::Cron::Job:0x00007f147f84a1d0
+...
+irb(main):002:0> job.status
+=> "enabled"
+irb(main):003:0> job.enque!
+=> 257
+```
+
+The list of available jobs can be found in the [workers](https://gitlab.com/gitlab-org/gitlab/-/tree/master/app/workers) directory.
+
+For more information about Sidekiq jobs, see the [Sidekiq-cron](https://github.com/sidekiq-cron/sidekiq-cron#work-with-job) documentation.
+
## Omnibus GitLab 14.0 and later: remove the `sidekiq-cluster` service
Omnibus GitLab instances that were configured to run `sidekiq-cluster` prior to GitLab 14.0
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index c52d14f59fe..9a7724932eb 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -19474,6 +19474,7 @@ four standard [pagination arguments](#connection-pagination-arguments):
| Name | Type | Description |
| ---- | ---- | ----------- |
+| <a id="projectdependenciescomponentnames"></a>`componentNames` | [`[String!]`](#string) | Filter dependencies by component names. |
| <a id="projectdependenciespackagemanagers"></a>`packageManagers` | [`[PackageManager!]`](#packagemanager) | Filter dependencies by package managers. |
| <a id="projectdependenciessort"></a>`sort` | [`DependencySort`](#dependencysort) | Sort dependencies by given criteria. |
diff --git a/doc/architecture/blueprints/remote_development/index.md b/doc/architecture/blueprints/remote_development/index.md
index f214ef56967..e2647551a95 100644
--- a/doc/architecture/blueprints/remote_development/index.md
+++ b/doc/architecture/blueprints/remote_development/index.md
@@ -42,6 +42,64 @@ As a [new Software Developer to a team such as Sasha](https://about.gitlab.com/h
![User Flow](img/remote_dev_15_7.png)
+## Architecture
+
+```plantuml
+@startuml
+node "Kubernetes" {
+ [Ingress Controller] --> [GitLab Workspaces Proxy] : Decrypt Traffic
+
+ note right of "Ingress Controller"
+ Customers can choose
+ an ingress controller
+ of their choice
+ end note
+
+ note top of "GitLab Workspaces Proxy"
+ Authenticate and
+ authorize user traffic
+ end note
+
+ [GitLab Workspaces Proxy] ..> [Workspace n] : Forward traffic\nfor workspace n
+ [GitLab Workspaces Proxy] ..> [Workspace 2] : Forward traffic\nfor workspace 2
+ [GitLab Workspaces Proxy] --> [Workspace 1] : Forward traffic\nfor workspace 1
+
+ [Agentk] .up.> [Workspace n] : Applies kubernetes resources\nfor workspace n
+ [Agentk] .up.> [Workspace 2] : Applies kubernetes resources\nfor workspace 2
+ [Agentk] .up.> [Workspace 1] : Applies kubernetes resources\nfor workspace 1
+
+ [Agentk] --> [Kubernetes API Server] : Interact and get/apply\nKubernetes resources
+}
+
+node "GitLab" {
+ [Nginx] --> [GitLab Rails] : Forward
+ [GitLab Rails] --> [Postgres] : Access database
+ [GitLab Rails] --> [Gitaly] : Fetch files
+ [KAS] -up-> [GitLab Rails] : Proxy
+}
+
+[Agentk] -up-> [KAS] : Initiate reconciliation loop
+"Load Balancer IP" --> [Ingress Controller]
+[Browser] --> [Nginx] : Browse GitLab
+[Browser] -right-> "Domain IP" : Browse workspace URL
+"Domain IP" .right.> "Load Balancer IP"
+[GitLab Workspaces Proxy] ..> [GitLab Rails] : Authenticate and authorize\nthe user accessing the workspace.
+
+note top of "Domain IP"
+ For local development, workspace URL
+ is [workspace-name].workspaces.localdev.me
+ which resolves to localhost (127.0.0.1)
+end note
+
+note top of "Load Balancer IP"
+ For local development,
+ it includes all local loopback interfaces
+ e.g. 127.0.0.1, 172.16.123.1, 192.168.0.1, etc.
+end note
+
+@enduml
+```
+
## Terminology
We use the following terms to describe components and properties of the Remote Development architecture.
diff --git a/doc/development/ai_features.md b/doc/development/ai_features.md
index 6ed1d59c3e0..25da99addc1 100644
--- a/doc/development/ai_features.md
+++ b/doc/development/ai_features.md
@@ -89,6 +89,15 @@ To populate the embedding database for GitLab chat:
1. Open a rails console
1. Run [this script](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/10588#note_1373586079) to populate the embedding database
+### Debugging
+
+To gather more insights about the full request, use the `Gitlab::Llm::Logger` file to debug logs.
+To follow the debugging messages related to the AI requests on the abstraction layer, you can use:
+
+```shell
+tail -f log/llm.log
+```
+
### Configure GCP Vertex access
In order to obtain a GCP service key for local development, please follow the steps below:
diff --git a/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png b/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png
new file mode 100644
index 00000000000..6521da25754
--- /dev/null
+++ b/doc/tutorials/boards_for_teams/img/frontend_board_empty_v16_0.png
Binary files differ
diff --git a/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png b/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png
new file mode 100644
index 00000000000..58f39fdfb10
--- /dev/null
+++ b/doc/tutorials/boards_for_teams/img/frontend_board_filled_v16_0.png
Binary files differ
diff --git a/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png b/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png
new file mode 100644
index 00000000000..cfce3bf0743
--- /dev/null
+++ b/doc/tutorials/boards_for_teams/img/ux_board_empty_v16_0.png
Binary files differ
diff --git a/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png b/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png
new file mode 100644
index 00000000000..c7c32fed841
--- /dev/null
+++ b/doc/tutorials/boards_for_teams/img/ux_board_filled_v16_0.png
Binary files differ
diff --git a/doc/tutorials/boards_for_teams/index.md b/doc/tutorials/boards_for_teams/index.md
index e37bf5a2d31..9b34f580e05 100644
--- a/doc/tutorials/boards_for_teams/index.md
+++ b/doc/tutorials/boards_for_teams/index.md
@@ -31,12 +31,9 @@ After you set up everything, the two teams will be able to hand off issues from
1. A product designer on the UX team:
1. Checks the `Workflow::Ready for design` list on the **UX workflow** board and decides to work on the profile page redesign.
- <!-- Image: UX board with lists:
- ~Workflow::Ready for design,
- ~Workflow::Design
- ~Workflow::Ready for development -->
+ ![Issue board called "UX workflow" with three columns and three issues](img/ux_board_filled_v16_0.png)
- 1. Assigns themselves to the issue.
+ 1. Assigns themselves to the **Redesign user profile page** issue.
1. Drags the issue card to the `Workflow::Design` list. The previous workflow label is automatically removed.
1. Creates the ✨new designs✨.
1. [Adds the designs to the issue](../../user/project/issues/design_management.md).
@@ -45,12 +42,9 @@ After you set up everything, the two teams will be able to hand off issues from
1. A developer on the Frontend team:
1. Checks the `Workflow::Ready for development` list on the **Frontend workflow** board and chooses an issue to work on.
- <!-- Image: Frontend board, scoped to ~Frontend, with lists:
- ~Workflow::Ready for development
- ~Workflow::In development
- ~Workflow::Complete -->
+ ![Issue board called "Frontend workflow" with three columns and three issues](img/frontend_board_filled_v16_0.png)
- 1. Assigns themselves to the issue.
+ 1. Assigns themselves to the **Redesign user profile page** issue.
1. Drags the issue card to the `Workflow::In development` list. The previous workflow label is automatically removed.
1. Adds the frontend code in a [merge request](../../user/project/merge_requests/index.md).
1. Adds the `Workflow::Complete` label.
@@ -140,15 +134,14 @@ To create the **UX workflow** issue board:
1. In the **Title field**, enter `UX workflow`.
1. Clear the **Show the Open list** and **Show the Closed list** checkboxes.
1. Select **Create board**. You should see an empty board.
-
- <!-- Image: empty UX workflow board -->
-
1. Create a list for the `Workflow::Ready for design` label:
1. In the upper-left corner of the issue board page, select **Create list**.
1. In the column that appears, from the **Value** dropdown list, select the `Workflow::Ready for design` label.
1. Select **Add to board**.
1. Repeat the previous step for labels `Workflow::Design` and `Workflow::Ready for development`.
+![Issue board called "UX workflow" with three columns and no issues](img/ux_board_empty_v16_0.png)
+
To create the **Frontend workflow** board:
1. In the upper-left corner of the issue board page, select the dropdown list with the current board name.
@@ -164,6 +157,8 @@ To create the **Frontend workflow** board:
1. Select **Add to board**.
1. Repeat the previous step for labels `Workflow::In development` and `Workflow::Complete`.
+![Issue board called "Frontend workflow" with three columns and no issues](img/frontend_board_empty_v16_0.png)
+
For now, lists in both your boards should be empty. Next, you'll populate them with some issues.
## Create issues for features
@@ -193,9 +188,9 @@ Repeat these steps to create a few more issues with the same labels.
You should now see at least one issue there, ready for your product designers to start working on!
-<!-- Image: UX workflow board with at least one issue in the `Workflow::Ready for design` list -->
-
Congratulations! Now your teams can start collaborating on amazing software.
+As a next step, you can try out [the goal workflow](#the-goal-workflow) for yourself using these boards,
+simulating the two teams interacting.
## Learn more about project management in GitLab
diff --git a/doc/tutorials/plan_and_track.md b/doc/tutorials/plan_and_track.md
index 35b552cdaa5..0206c462cb2 100644
--- a/doc/tutorials/plan_and_track.md
+++ b/doc/tutorials/plan_and_track.md
@@ -15,4 +15,5 @@ issues, epics, and more.
| [Create a project from a template](https://gitlab.com/projects/new#create_from_template) | Choose a project template and create a project with files to get you started. | |
| [Migrate to GitLab](../user/project/import/index.md) | If you are coming to GitLab from another platform, you can import or convert your projects. | |
| [Run an agile iteration](agile_sprint/index.md) | Use group, projects, and iterations to run an agile development iteration. |
+| [Set up issue boards for team hand-off](boards_for_teams/index.md) | Use issue boards and scoped labels to set up collaboration across many teams. | **{star}** |
| <i class="fa fa-youtube-play youtube" aria-hidden="true"></i> [Epics and Issue Boards](https://www.youtube.com/watch?v=I1bFIAQBHB8) | Find out how to use epics and issue boards for project management. | |
diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md
index 89366c73b16..b60ff8471bf 100644
--- a/doc/user/profile/notifications.md
+++ b/doc/user/profile/notifications.md
@@ -362,6 +362,7 @@ The following table lists all GitLab-specific email headers:
| ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `List-Id` | The path of the project in an RFC 2919 mailing list identifier. You can use it for email organization with filters. |
| `X-GitLab-(Resource)-ID` | The ID of the resource the notification is for. The resource, for example, can be `Issue`, `MergeRequest`, `Commit`, or another such resource. |
+| `X-GitLab-ConfidentialIssue` | The boolean value indicating issue confidentiality for notifications. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/222908) in GitLab 16.0. |
| `X-GitLab-Discussion-ID` | The ID of the thread the comment belongs to, in notification emails for comments. |
| `X-GitLab-Group-Id` | The group's ID. Only present on notification emails for [epics](../group/epics/index.md). |
| `X-GitLab-Group-Path` | The group's path. Only present on notification emails for [epics](../group/epics/index.md) |
diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md
index bb1609a74e5..18721c0582d 100644
--- a/doc/user/project/web_ide/index.md
+++ b/doc/user/project/web_ide/index.md
@@ -131,6 +131,16 @@ To commit changes in the Web IDE:
1. Select **Commit & Push**.
1. Commit to the current branch, or create a new branch.
+## Create a merge request
+
+To create a merge request:
+
+1. [Commit the changes](index.md#commit-changes).
+1. In the pop-up notification in the lower-right corner, select `Create Merge Request`.
+This action opens a new window with the [merge request creation form](../merge_requests/index.md).
+
+To access missed notifications, see the [IDE notification tips](index.md#access-missed-ide-notifications).
+
## Use the command palette
In the Web IDE, you can access many commands through the command palette.
@@ -203,3 +213,10 @@ You cannot use interactive web terminals to interact with a runner.
However, you can use a terminal to install dependencies and compile and debug code.
For more information about configuring a workspace that supports interactive web terminals, see [remote development](../remote_development/index.md).
+
+## Access missed IDE notifications
+
+Actions that you take in the Web IDE trigger notifications that pop up in the lower-right corner. If you missed the pop-up:
+
+1. In the lower-right corner, select the bell icon (**{notifications}**). This opens a list of notifications.
+1. Select the notification you want to access.
diff --git a/qa/Gemfile b/qa/Gemfile
index 6379f65db3d..e8d3a435766 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -11,7 +11,7 @@ gem 'capybara-screenshot', '~> 1.0.26'
gem 'rake', '~> 13', '>= 13.0.6'
gem 'rspec', '~> 3.12'
# 4.9.1 drops Ruby 2.7 support. We can upgrade further after we drop Ruby 2.7 support.
-gem 'selenium-webdriver', '= 4.9.1'
+gem 'selenium-webdriver', '= 4.9.0'
gem 'airborne', '~> 0.3.7', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
gem 'rest-client', '~> 2.1.0'
gem 'rspec-retry', '~> 0.6.2', require: 'rspec/retry'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index b8f4880a436..9b2b8acfb87 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -269,7 +269,7 @@ GEM
sawyer (0.9.2)
addressable (>= 2.3.5)
faraday (>= 0.17.3, < 3)
- selenium-webdriver (4.9.1)
+ selenium-webdriver (4.9.0)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
@@ -345,7 +345,7 @@ DEPENDENCIES
rspec-retry (~> 0.6.2)
rspec_junit_formatter (~> 0.6.0)
ruby-debug-ide (~> 0.7.3)
- selenium-webdriver (= 4.9.1)
+ selenium-webdriver (= 4.9.0)
slack-notifier (~> 2.4)
terminal-table (~> 3.0.2)
warning (~> 1.3)
diff --git a/qa/qa/page/file/edit.rb b/qa/qa/page/file/edit.rb
index e66019279ce..ccf163cd1b4 100644
--- a/qa/qa/page/file/edit.rb
+++ b/qa/qa/page/file/edit.rb
@@ -8,10 +8,6 @@ module QA
include Shared::CommitButton
include Shared::Editor
- view 'app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js' do
- element :editor_toolbar_button, "qaSelector: 'editor_toolbar_button'" # rubocop:disable QA/ElementWithPattern
- end
-
def has_markdown_preview?(component, content)
within_element(:source_editor_preview_container) do
has_css?(component, exact_text: content)
@@ -23,10 +19,6 @@ module QA
raise ElementNotFound, %("Couldn't find #{component} element with content '#{content}')
end
-
- def click_editor_toolbar
- click_element(:editor_toolbar_button)
- end
end
end
end
diff --git a/spec/controllers/profiles/webauthn_registrations_controller_spec.rb b/spec/controllers/profiles/webauthn_registrations_controller_spec.rb
index 0c475039963..949de9d0b90 100644
--- a/spec/controllers/profiles/webauthn_registrations_controller_spec.rb
+++ b/spec/controllers/profiles/webauthn_registrations_controller_spec.rb
@@ -10,11 +10,27 @@ RSpec.describe Profiles::WebauthnRegistrationsController do
end
describe '#destroy' do
- it 'deletes the given webauthn registration' do
- registration_to_delete = user.webauthn_registrations.first
+ let(:webauthn_id) { user.webauthn_registrations.first.id }
- expect { delete :destroy, params: { id: registration_to_delete.id } }.to change { user.webauthn_registrations.count }.by(-1)
- expect(response).to be_redirect
+ subject { delete :destroy, params: { id: webauthn_id } }
+
+ it 'redirects to the profile two factor authentication page' do
+ subject
+
+ expect(response).to redirect_to profile_two_factor_auth_path
+ end
+
+ it 'destroys the webauthn registration' do
+ expect { subject }.to change { user.webauthn_registrations.count }.by(-1)
+ end
+
+ it 'calls the Webauthn::DestroyService' do
+ service = double
+
+ expect(Webauthn::DestroyService).to receive(:new).with(user, user, webauthn_id.to_s).and_return(service)
+ expect(service).to receive(:execute)
+
+ subject
end
end
end
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 351583b7ef6..448d937f30e 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -124,6 +124,8 @@ FactoryBot.define do
transient { registrations_count { 5 } }
after(:create) do |user, evaluator|
+ user.generate_otp_backup_codes!
+
create_list(:webauthn_registration, evaluator.registrations_count, user: user)
end
end
diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb
index 6e335871ed1..7474f416146 100644
--- a/spec/features/projects/blobs/edit_spec.rb
+++ b/spec/features/projects/blobs/edit_spec.rb
@@ -83,29 +83,20 @@ RSpec.describe 'Editing file blob', :js, feature_category: :projects do
end
context 'blob edit toolbar' do
- toolbar_buttons = [
- "Add bold text",
- "Add italic text",
- "Add strikethrough text",
- "Insert a quote",
- "Insert code",
- "Add a link",
- "Add a bullet list",
- "Add a numbered list",
- "Add a checklist",
- "Add a collapsible section",
- "Add a table"
- ]
-
- it "does not have any buttons" do
- stub_feature_flags(source_editor_toolbar: true)
- visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
- buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]')
- expect(buttons.length).to eq(0)
- end
-
- it "has defined set of toolbar buttons when the flag is off" do
- stub_feature_flags(source_editor_toolbar: false)
+ def has_toolbar_buttons
+ toolbar_buttons = [
+ "Add bold text",
+ "Add italic text",
+ "Add strikethrough text",
+ "Insert a quote",
+ "Insert code",
+ "Add a link",
+ "Add a bullet list",
+ "Add a numbered list",
+ "Add a checklist",
+ "Add a collapsible section",
+ "Add a table"
+ ]
visit project_edit_blob_path(project, tree_join(branch, readme_file_path))
buttons = page.all('.file-buttons .md-header-toolbar button[type="button"]')
expect(buttons.length).to eq(toolbar_buttons.length)
@@ -113,6 +104,16 @@ RSpec.describe 'Editing file blob', :js, feature_category: :projects do
expect(buttons[i]['title']).to include(button_title)
end
end
+
+ it "has defined set of toolbar buttons when the flag is on" do
+ stub_feature_flags(source_editor_toolbar: true)
+ has_toolbar_buttons
+ end
+
+ it "has defined set of toolbar buttons when the flag is off" do
+ stub_feature_flags(source_editor_toolbar: false)
+ has_toolbar_buttons
+ end
end
context 'from blob file path' do
diff --git a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
index b5944a52af7..1e592f435e4 100644
--- a/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
+++ b/spec/frontend/editor/components/source_editor_toolbar_button_spec.js
@@ -7,6 +7,7 @@ import { buildButton } from './helpers';
describe('Source Editor Toolbar button', () => {
let wrapper;
const defaultBtn = buildButton();
+ const tertiaryBtnWithIcon = buildButton({ category: 'tertiary' });
const findButton = () => wrapper.findComponent(GlButton);
@@ -41,6 +42,16 @@ describe('Source Editor Toolbar button', () => {
const btn = findButton();
expect(btn.exists()).toBe(true);
expect(btn.props()).toMatchObject(defaultProps);
+ expect(btn.text()).toBe('Foo Bar Button');
+ });
+
+ it('does not render button for tertiary button with icon', () => {
+ createComponent({
+ button: {
+ tertiaryBtnWithIcon,
+ },
+ });
+ expect(findButton().text()).toBe('');
});
it('renders a button based on the props passed', () => {
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index b1b8173188c..70bc1dee0ee 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -19,12 +19,12 @@ describe('The basis for an Source Editor extension', () => {
const findLine = (num) => {
return document.querySelector(`.${EXTENSION_BASE_LINE_NUMBERS_CLASS}:nth-child(${num})`);
};
- const generateLines = () => {
+ const generateFixture = () => {
let res = '';
for (let line = 1, lines = 5; line <= lines; line += 1) {
res += `<div class="${EXTENSION_BASE_LINE_NUMBERS_CLASS}">${line}</div>`;
}
- return res;
+ return `<span class="soft-wrap-toggle"></span>${res}`;
};
const generateEventMock = ({ line = defaultLine, el = null } = {}) => {
return {
@@ -51,7 +51,7 @@ describe('The basis for an Source Editor extension', () => {
};
beforeEach(() => {
- setHTMLFixture(generateLines());
+ setHTMLFixture(generateFixture());
event = generateEventMock();
});
@@ -156,12 +156,13 @@ describe('The basis for an Source Editor extension', () => {
describe('toggleSoftwrap', () => {
let instance;
-
beforeEach(() => {
instance = createInstance();
instance.toolbar = toolbar;
instance.use({ definition: SourceEditorExtension });
+
+ jest.spyOn(document.querySelector('.soft-wrap-toggle'), 'blur');
});
it.each`
@@ -183,6 +184,7 @@ describe('The basis for an Source Editor extension', () => {
expect(instance.toolbar.updateItem).toHaveBeenCalledWith(EXTENSION_SOFTWRAP_ID, {
selected: expectSelected,
});
+ expect(document.querySelector('.soft-wrap-toggle').blur).toHaveBeenCalled();
},
);
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index fb5fce92482..512b298bbbd 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -206,9 +206,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
it('removes the registered buttons from the toolbar', () => {
expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
instance.unuse(extension);
- expect(instance.toolbar.removeItems).toHaveBeenCalledWith([
- EXTENSION_MARKDOWN_PREVIEW_ACTION_ID,
- ]);
+ expect(instance.toolbar.removeItems).toHaveBeenCalledWith([]);
});
it('disposes the modelChange listener and does not fetch preview on content changes', () => {
diff --git a/spec/services/webauthn/destroy_service_spec.rb b/spec/services/webauthn/destroy_service_spec.rb
new file mode 100644
index 00000000000..dd04601ccf0
--- /dev/null
+++ b/spec/services/webauthn/destroy_service_spec.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Webauthn::DestroyService, feature_category: :system_access do
+ let(:user) { create(:user, :two_factor_via_webauthn, registrations_count: 1) }
+ let(:current_user) { user }
+
+ describe '#execute' do
+ let(:webauthn_id) { user.webauthn_registrations.first.id }
+
+ subject { described_class.new(current_user, user, webauthn_id).execute }
+
+ context 'with only one webauthn method enabled' do
+ context 'when another user is calling the service' do
+ context 'for a user without permissions' do
+ let(:current_user) { create(:user) }
+
+ it 'does not destry the webauthn registration' do
+ expect { subject }.not_to change { user.webauthn_registrations.count }
+ end
+
+ it 'does not remove the user backup codes' do
+ expect { subject }.not_to change { user.otp_backup_codes }
+ end
+
+ it 'returns error' do
+ expect(subject[:status]).to eq(:error)
+ end
+ end
+
+ context 'for an admin' do
+ it 'destroys the webauthn registration' do
+ expect { subject }.to change { user.webauthn_registrations.count }.by(-1)
+ end
+
+ it 'removes the user backup codes' do
+ subject
+
+ expect(user.otp_backup_codes).to be_nil
+ end
+ end
+ end
+
+ context 'when current user is calling the service' do
+ context 'when there is also OTP enabled' do
+ before do
+ user.otp_required_for_login = true
+ user.otp_secret = User.generate_otp_secret(32)
+ user.otp_grace_period_started_at = Time.current
+ user.generate_otp_backup_codes!
+ user.save!
+ end
+
+ it 'removes the webauth registrations' do
+ expect { subject }.to change { user.webauthn_registrations.count }.by(-1)
+ end
+
+ it 'does not remove the user backup codes' do
+ expect { subject }.not_to change { user.otp_backup_codes }
+ end
+ end
+ end
+ end
+
+ context 'with multiple webauthn methods enabled' do
+ before do
+ create(:webauthn_registration, user: user)
+ end
+
+ it 'destroys the webauthn registration' do
+ expect { subject }.to change { user.webauthn_registrations.count }.by(-1)
+ end
+
+ it 'does not remove the user backup codes' do
+ expect { subject }.not_to change { user.otp_backup_codes }
+ end
+ end
+ end
+end