Commit d6035f5c authored by GitLab Bot's avatar GitLab Bot

Merge remote-tracking branch 'upstream/master' into ce-to-ee-2018-04-05

# Conflicts:
#	.gitlab-ci.yml
#	app/assets/javascripts/boards/services/board_service.js
#	app/helpers/services_helper.rb

[ci skip]
parents df1ed9c5 942488b8
......@@ -83,6 +83,7 @@ stages:
- mysql:latest
- redis:alpine
<<<<<<< HEAD
# BEGIN EE-only service helpers
.use-pg-9-6: &use-pg-9-6
......@@ -103,6 +104,8 @@ stages:
- docker.elastic.co/elasticsearch/elasticsearch:5.5.2
# END EE-only service helpers
=======
>>>>>>> upstream/master
.rails5-variables: &rails5-variables
script:
- export RAILS5=${RAILS5}
......@@ -199,6 +202,7 @@ stages:
<<: *rspec-metadata-mysql
<<: *rails5
<<<<<<< HEAD
.rspec-metadata-ee: &rspec-metadata-ee
<<: *rspec-metadata
stage: test
......@@ -242,6 +246,8 @@ stages:
<<: *rspec-metadata-pg-geo
<<: *rails5
=======
>>>>>>> upstream/master
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
<<: *except-docs-and-qa
......
......@@ -2,6 +2,14 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.6.3 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.6.2 (2018-03-29)
### Fixed (2 changes, 1 of them is from the community)
......@@ -218,6 +226,14 @@ entry.
- Use host URL to build JIRA remote link icon.
## 10.5.7 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.5.6 (2018-03-16)
### Security (2 changes)
......@@ -485,6 +501,14 @@ entry.
- Adds empty state illustration for pending job.
## 10.4.7 (2018-04-03)
### Security (2 changes)
- Fix XSS on diff view stored on filenames.
- Adds confidential notes channel for Slack/Mattermost.
## 10.4.6 (2018-03-16)
### Security (2 changes)
......
......@@ -20,6 +20,7 @@ export default class BoardService {
static generateIssuePath(boardId, id) {
return `${gon.relative_url_root}/-/boards/${boardId ? `${boardId}` : ''}/issues${id ? `/${id}` : ''}`;
<<<<<<< HEAD
}
allBoards() {
......@@ -50,6 +51,8 @@ export default class BoardService {
deleteBoard({ id }) {
return axios.delete(this.generateBoardsPath(id));
=======
>>>>>>> upstream/master
}
all() {
......
<script>
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
import { mapActions, mapState } from 'vuex';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
export default {
components: {
PanelResizer,
export default {
components: {
PanelResizer,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
props: {
collapsible: {
type: Boolean,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
minSize: {
type: Number,
required: false,
default: 200,
},
side: {
type: String,
required: true,
},
initialWidth: {
type: Number,
required: true,
},
data() {
return {
width: this.initialWidth,
};
minSize: {
type: Number,
required: false,
default: 340,
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
side: {
type: String,
required: true,
},
methods: {
...mapActions([
'setPanelCollapsedStatus',
'setResizingStatus',
]),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
data() {
return {
width: this.initialWidth,
};
},
computed: {
...mapState({
collapsed(state) {
return state[`${this.side}PanelCollapsed`];
},
}),
panelStyle() {
if (!this.collapsed) {
return {
width: `${this.width}px`,
};
}
return {};
},
},
methods: {
...mapActions(['setPanelCollapsedStatus', 'setResizingStatus']),
toggleFullbarCollapsed() {
if (this.collapsed && this.collapsible) {
this.setPanelCollapsedStatus({
side: this.side,
collapsed: !this.collapsed,
});
}
},
maxSize: (window.innerWidth / 2),
};
},
maxSize: window.innerWidth / 2,
};
</script>
<template>
......
......@@ -94,10 +94,10 @@ export default class MilestoneSelect {
if (showMenuAbove) {
$dropdown.data('glDropdown').positionMenuAbove();
}
$(`[data-milestone-id="${selectedMilestone}"] > a`).addClass('is-active');
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`).addClass('is-active');
}),
renderRow: milestone => `
<li data-milestone-id="${milestone.name}">
<li data-milestone-id="${_.escape(milestone.name)}">
<a href='#' class='dropdown-menu-milestone-link'>
${_.escape(milestone.title)}
</a>
......@@ -125,7 +125,6 @@ export default class MilestoneSelect {
return milestone.id;
}
},
isSelected: milestone => milestone.name === selectedMilestone,
hidden: () => {
$selectBox.hide();
// display:block overrides the hide-collapse rule
......@@ -137,7 +136,7 @@ export default class MilestoneSelect {
selectedMilestone = $dropdown[0].dataset.selected || selectedMilestoneDefault;
}
$('a.is-active', $el).removeClass('is-active');
$(`[data-milestone-id="${selectedMilestone}"] > a`, $el).addClass('is-active');
$(`[data-milestone-id="${_.escape(selectedMilestone)}"] > a`, $el).addClass('is-active');
},
vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: (clickEvent) => {
......@@ -158,6 +157,7 @@ export default class MilestoneSelect {
const isMRIndex = (page === page && page === 'projects:merge_requests:index');
const isSelecting = (selected.name !== selectedMilestone);
selectedMilestone = isSelecting ? selected.name : selectedMilestoneDefault;
if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
e.preventDefault();
return;
......
......@@ -46,7 +46,7 @@ class Projects::LfsApiController < Projects::GitHttpClientController
def existing_oids
@existing_oids ||= begin
storage_project.lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
......
module ServicesHelper
<<<<<<< HEAD
prepend EE::ServicesHelper
=======
def service_event_description(event)
case event
when "push", "push_events"
"Event will be triggered by a push to the repository"
when "tag_push", "tag_push_events"
"Event will be triggered when a new tag is pushed to the repository"
when "note", "note_events"
"Event will be triggered when someone adds a comment"
when "confidential_note", "confidential_note_events"
"Event will be triggered when someone adds a comment on a confidential issue"
when "issue", "issue_events"
"Event will be triggered when an issue is created/updated/closed"
when "confidential_issue", "confidential_issues_events"
"Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request", "merge_request_events"
"Event will be triggered when a merge request is created/updated/merged"
when "pipeline", "pipeline_events"
"Event will be triggered when a pipeline status changes"
when "wiki_page", "wiki_page_events"
"Event will be triggered when a wiki page is created/updated"
when "commit", "commit_events"
"Event will be triggered when a commit is created/updated"
end
end
>>>>>>> upstream/master
def service_event_field_name(event)
event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
......
......@@ -10,6 +10,7 @@ class ProjectHook < WebHook
:issue_hooks,
:confidential_issue_hooks,
:note_hooks,
:confidential_note_hooks,
:merge_request_hooks,
:job_hooks,
:pipeline_hooks,
......
......@@ -276,6 +276,10 @@ class Note < ActiveRecord::Base
self.special_role = Note::SpecialRole::FIRST_TIME_CONTRIBUTOR
end
def confidential?
noteable.try(:confidential?)
end
def editable?
!system?
end
......
......@@ -1079,6 +1079,16 @@ class Project < ActiveRecord::Base
end
end
# This will return all `lfs_objects` that are accessible to the project.
# So this might be `self.lfs_objects` if the project is not part of a fork
# network, or it is the base of the fork network.
#
# TODO: refactor this to get the correct lfs objects when implementing
# https://gitlab.com/gitlab-org/gitlab-ce/issues/39769
def all_lfs_objects
lfs_storage_project.lfs_objects
end
def personal?
!group
end
......
......@@ -21,8 +21,16 @@ class ChatNotificationService < Service
end
end
def confidential_issue_channel
properties['confidential_issue_channel'].presence || properties['issue_channel']
end
def confidential_note_channel
properties['confidential_note_channel'].presence || properties['note_channel']
end
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
%w[push issue confidential_issue merge_request note confidential_note tag_push
pipeline wiki_page]
end
......@@ -55,7 +63,9 @@ class ChatNotificationService < Service
return false unless message
channel_name = get_channel_field(object_kind).presence || channel
event_type = data[:event_type] || object_kind
channel_name = get_channel_field(event_type).presence || channel
opts = {}
opts[:channel] = channel_name if channel_name
......
......@@ -46,7 +46,7 @@ class HipchatService < Service
end
def self.supported_events
%w(push issue confidential_issue merge_request note tag_push pipeline)
%w(push issue confidential_issue merge_request note confidential_note tag_push pipeline)
end
def execute(data)
......
......@@ -15,6 +15,7 @@ class Service < ActiveRecord::Base
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :confidential_note_events, true
default_value_for :job_events, true
default_value_for :pipeline_events, true
default_value_for :wiki_page_events, true
......@@ -43,6 +44,7 @@ class Service < ActiveRecord::Base
scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :confidential_note_hooks, -> { where(confidential_note_events: true, active: true) }
scope :job_hooks, -> { where(job_events: true, active: true) }
scope :pipeline_hooks, -> { where(pipeline_events: true, active: true) }
scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
......@@ -169,8 +171,10 @@ class Service < ActiveRecord::Base
def self.prop_accessor(*args)
args.each do |arg|
class_eval %{
def #{arg}
properties['#{arg}']
unless method_defined?(arg)
def #{arg}
properties['#{arg}']
end
end
def #{arg}=(value)
......
......@@ -24,8 +24,10 @@ module Notes
def execute_note_hooks
note_data = hook_data
@note.project.execute_hooks(note_data, :note_hooks)
@note.project.execute_services(note_data, :note_hooks)
hooks_scope = @note.confidential? ? :confidential_note_hooks : :note_hooks
@note.project.execute_hooks(note_data, hooks_scope)
@note.project.execute_services(note_data, hooks_scope)
end
end
end
......@@ -28,7 +28,7 @@ module Projects
end
def save_services
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver].all?(&:save)
[version_saver, avatar_saver, project_tree_saver, uploads_saver, repo_saver, wiki_repo_saver, lfs_saver].all?(&:save)
end
def version_saver
......@@ -55,6 +55,10 @@ module Projects
Gitlab::ImportExport::WikiRepoSaver.new(project: project, shared: @shared)
end
def lfs_saver
Gitlab::ImportExport::LfsSaver.new(project: project, shared: @shared)
end
def cleanup_and_notify_error
Rails.logger.error("Import/Export - Project #{project.name} with ID: #{project.id} export error - #{@shared.errors.join(', ')}")
......
......@@ -21,11 +21,11 @@
%li Project uploads
%li Project configuration including web hooks and services
%li Issues with comments, merge requests with diffs and comments, labels, milestones, snippets, and other project entities
%li LFS objects
%p
The following items will NOT be exported:
%ul
%li Job traces and artifacts
%li LFS objects
%li Container registry images
%li CI variables
%li Any encrypted tokens
......
......@@ -32,6 +32,13 @@
%strong Comments
%p.light
This URL will be triggered when someone adds a comment
%li
= form.check_box :confidential_note_events, class: 'pull-left'
.prepend-left-20
= form.label :confidential_note_events, class: 'list-label' do
%strong Confidential Comments
%p.light
This URL will be triggered when someone adds a comment on a confidential issue
%li
= form.check_box :issues_events, class: 'pull-left'
.prepend-left-20
......
---
title: Fix XSS on diff view stored on filenames
merge_request:
author:
type: security
---
title: Support LFS objects when importing/exporting GitLab project archives
merge_request: 18115
author:
type: added
---
title: Adds confidential notes channel for Slack/Mattermost
merge_request:
author:
type: security
---
title: ListCommitsByOid is executed by Gitaly by default
merge_request:
author:
type: performance
class AddConfidentialNoteEventsToWebHooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :web_hooks, :confidential_note_events, :boolean
end
def down
remove_column :web_hooks, :confidential_note_events
end
end
class AddConfidentialNoteEventsToServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_column :services, :confidential_note_events, :boolean
change_column_default :services, :confidential_note_events, true
end
def down
remove_column :services, :confidential_note_events
end
end
class ScheduleSetConfidentialNoteEventsOnWebhooks < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
INTERVAL = 5.minutes
disable_ddl_transaction!
def up
migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks
migration_name = migration.to_s.demodulize
relation = migration::WebHook.hooks_to_update
queue_background_migration_jobs_by_range_at_intervals(relation,
migration_name,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
end
end
class ScheduleSetConfidentialNoteEventsOnServices < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
BATCH_SIZE = 1_000
INTERVAL = 20.minutes
disable_ddl_transaction!
def up
migration = Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices
migration_name = migration.to_s.demodulize
relation = migration::Service.services_to_update
queue_background_migration_jobs_by_range_at_intervals(relation,
migration_name,
INTERVAL,
batch_size: BATCH_SIZE)
end
def down
end
end
......@@ -2247,6 +2247,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "confidential_issues_events", default: true, null: false
t.boolean "commit_events", default: true, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events", default: true
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
......@@ -2608,6 +2609,7 @@ ActiveRecord::Schema.define(version: 20180327101207) do
t.boolean "confidential_issues_events", default: false, null: false
t.boolean "repository_update_events", default: false, null: false
t.boolean "job_events", default: false, null: false
t.boolean "confidential_note_events"
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
......
......@@ -57,11 +57,11 @@ The following items will be exported:
- Project configuration including web hooks and services
- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
and other project entities
- LFS objects
The following items will NOT be exported:
- Build traces and artifacts
- LFS objects
- Container registry images
- CI variables
- Any encrypted tokens
......
......@@ -72,7 +72,7 @@ module API
class ProjectHook < Hook
expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :pipeline_events, :wiki_page_events
expose :note_events, :confidential_note_events, :pipeline_events, :wiki_page_events
expose :job_events
end
......@@ -822,7 +822,7 @@ module API
expose :id, :title, :created_at, :updated_at, :active
expose :push_events, :issues_events, :confidential_issues_events
expose :merge_requests_events, :tag_push_events, :note_events
expose :pipeline_events, :wiki_page_events
expose :confidential_note_events, :pipeline_events, :wiki_page_events
expose :job_events
# Expose serialized properties
expose :properties do |service, options|
......
......@@ -14,6 +14,7 @@ module API
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
optional :confidential_note_events, type: Boolean, desc: "Trigger hook on confidential note(comment) events"
optional :job_events, type: Boolean, desc: "Trigger hook on job events"
optional :pipeline_events, type: Boolean, desc: "Trigger hook on pipeline events"
optional :wiki_page_events, type: Boolean, desc: "Trigger hook on wiki events"
......
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
# Ensures services which previously recieved all notes events continue
# to recieve confidential ones.
class SetConfidentialNoteEventsOnServices
class Service < ActiveRecord::Base
self.table_name = 'services'
include ::EachBatch
def self.services_to_update
where(confidential_note_events: nil, note_events: true)
end
end
def perform(start_id, stop_id)
Service.services_to_update
.where(id: start_id..stop_id)
.update_all(confidential_note_events: true)
end
end
end
end
# frozen_string_literal: true
# rubocop:disable Style/Documentation
module Gitlab
module BackgroundMigration
# Ensures hooks which previously recieved all notes events continue
# to recieve confidential ones.
class SetConfidentialNoteEventsOnWebhooks
class WebHook < ActiveRecord::Base
self.table_name = 'web_hooks'
include ::EachBatch
def self.hooks_to_update
where(confidential_note_events: nil, note_events: true)
end
end
def perform(start_id, stop_id)
WebHook.hooks_to_update
.where(id: start_id..stop_id)
.update_all(confidential_note_events: true)
end
end
end
end
......@@ -15,8 +15,7 @@ module Gitlab
return false unless new_lfs_pointers.present?
existing_count = @project.lfs_storage_project
.lfs_objects
existing_count = @project.all_lfs_objects
.where(oid: new_lfs_pointers.map(&:lfs_oid))
.count
......
......@@ -9,6 +9,7 @@ module Gitlab
#
# data = {
# object_kind: "note",
# event_type: "confidential_note",
# user: {
# name: String,
# username: String,
......@@ -51,8 +52,11 @@ module Gitlab
end
def build_base_data(project, user, note)
event_type = note.confidential? ? 'confidential_note' : 'note'
base_data = {
object_kind: "note",
event_type: event_type,
user: user.hook_attrs,
project_id: project.id,
project: project.hook_attrs,
......
......@@ -860,7 +860,7 @@ into similar problems in the future (e.g. when new tables are created).
# Each job is scheduled with a `delay_interval` in between.
# If you use a small interval, then some jobs may run at the same time.
#
# model_class - The table being iterated over
# model_class - The table or relation being iterated over
# job_class_name - The background migration job class as a string
# delay_interval - The duration between each job's scheduled time (must respond to `to_f`)
# batch_size - The maximum number of rows per job
......
module Gitlab
module Diff
class InlineDiffMarker < Gitlab::StringRangeMarker
def initialize(line, rich_line = nil)
super(line, rich_line || line)
end
def mark(line_inline_diffs, mode: nil)
mark = super(line_inline_diffs) do |text, left:, right:|
super(line_inline_diffs) do |text, left:, right:|
%{<span class="#{html_class_names(left, right, mode)}">#{text}</span>}
end
mark.html_safe
end
private
......
......@@ -231,7 +231,8 @@ module Gitlab
# relation to each other. The last 10 commits for a branch for example,
# should go through .where
def batch_by_oid(repo, oids)
repo.gitaly_migrate(:list_commits_by_oid) do |is_enabled|
repo.gitaly_migrate(:list_commits_by_oid,
status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT) do |is_enabled|
if is_enabled
repo.gitaly_commit_client.list_commits_by_oid(oids)
else
......
......@@ -11,7 +11,8 @@ module Gitlab
def build(user: nil, changes: {})
hook_data = {
object_kind: issuable.class.name.underscore,
object_kind: object_kind,
event_type: event_type,
user: user.hook_attrs,
project: issuable.project.hook_attrs,
object_attributes: issuable.hook_attrs,
......@@ -36,6 +37,18 @@ module Gitlab
private
def object_kind
issuable.class.name.underscore
end
def event_type
if issuable.try(:confidential?)
"confidential_#{object_kind}"
else
object_kind
end
end
def issuable_builder
case issuable
when Issue
......
......@@ -13,7 +13,7 @@ module Gitlab
end
def execute
if import_file && check_version! && [repo_restorer, wiki_restorer, project_tree, avatar_restorer, uploads_restorer].all?(&:restore)
if import_file && check_version! && restorers.all?(&:restore)
project_tree.restored_project
else
raise Projects::ImportService::Error.new(@shared.errors.join(', '))
......@@ -24,6 +24,11 @@ module Gitlab
private
def restorers
[repo_restorer, wiki_restorer, project_tree, avatar_restorer,
uploads_restorer, lfs_restorer]
end
def import_file
Gitlab::ImportExport::FileImporter.import(archive_file: @archive_file,
shared: @shared)
......@@ -60,6 +65,10 @@ module Gitlab
Gitlab::ImportExport::UploadsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
def lfs_restorer
Gitlab::ImportExport::LfsRestorer.new(project: project_tree.restored_project, shared: @shared)
end
def path_with_namespace
File.join(@project.namespace.full_path, @project.path)
end
......
module Gitlab
module ImportExport
class LfsRestorer
def initialize(project:, shared:)
@project = project
@shared = shared
end
def restore
return true if lfs_file_paths.empty?
lfs_file_paths.each do |file_path|
link_or_create_lfs_object!(file_path)
end
true
rescue => e
@shared.error(e)
false
end
private
def link_or_create_lfs_object!(path)
size = File.size(path)
oid = LfsObject.calculate_oid(path)
lfs_object = LfsObject.find_or_initialize_by(oid: oid, size: size)
lfs_object.file = File.open(path) unless lfs_object.file&.exists?
@project.all_lfs_objects << lfs_object
end
def lfs_file_paths
@lfs_file_paths ||= Dir.glob("#{lfs_storage_path}/*")
end
def lfs_storage_path
File.join(@shared.export_path, 'lfs-objects')
end
end
end
end
module Gitlab
module ImportExport
class LfsSaver
include Gitlab::ImportExport::CommandLineUtil
def initialize(project:, shared:)
@project = project
@shared = shared
end
def save
@project.all_lfs_objects.each do |lfs_object|
save_lfs_object(lfs_object)
end
true
rescue => e
@shared.error(e)
false
end
private
def save_lfs_object(lfs_object)
if lfs_object.local_store?
copy_file_for_lfs_object(lfs_object)
else
download_file_for_lfs_object(lfs_object)
end
end
def download_file_for_lfs_object(lfs_object)
destination = destination_path_for_object(lfs_object)
mkdir_p(File.dirname(destination))
File.open(destination, 'w') do |file|
IO.copy_stream(URI.parse(lfs_object.file.url).open, file)
end
end
def copy_file_for_lfs_object(lfs_object)
copy_files(lfs_object.file.path, destination_path_for_object(lfs_object))
end
def destination_path_for_object(lfs_object)
File.join(lfs_export_path, lfs_object.oid)
end
def lfs_export_path
File.join(@shared.export_path, 'lfs-objects')
end
end
end
end
......@@ -15,6 +15,7 @@ FactoryBot.define do
issues_events true
confidential_issues_events true
note_events true
confidential_note_events true
job_events true
pipeline_events true
wiki_page_events true
......
......@@ -230,6 +230,23 @@ describe 'New/edit issue', :js do
expect(page).to have_selector('.atwho-view')
end
describe 'milestone' do
let!(:milestone) { create(:milestone, title: '">&lt;img src=x onerror=alert(document.domain)&gt;', project: project) }
it 'escapes milestone' do
click_button 'Milestone'
page.within '.issue-milestone' do
click_link milestone.title
end
page.within '.js-milestone-select' do
expect(page).to have_content milestone.title
expect(page).not_to have_selector 'img'
end
end
end
end
context 'edit issue' do
......
......@@ -135,11 +135,37 @@ describe DiffHelper do
it "returns strings with marked inline diffs" do
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>})
expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">&#39;def&#39;</span>})
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>})
expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">&quot;def&quot;</span>})
expect(marked_new_line).to be_html_safe
end
context 'when given HTML' do
it 'sanitizes it' do
old_line = %{test.txt}
new_line = %{<img src=x onerror=alert(document.domain)>}
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{<span class="idiff left right deletion">test.txt</span>})
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{<span class="idiff left right addition">&lt;img src=x onerror=alert(document.domain)&gt;</span>})
expect(marked_new_line).to be_html_safe
end
it 'sanitizes the entire line, not just the changes' do
old_line = %{<img src=x onerror=alert(document.domain)>}
new_line = %{<img src=y onerror=alert(document.domain)>}
marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line)
expect(marked_old_line).to eq(%q{&lt;img src=<span class="idiff left right deletion">x</span> onerror=alert(document.domain)&gt;})
expect(marked_old_line).to be_html_safe
expect(marked_new_line).to eq(%q{&lt;img src=<span class="idiff left right addition">y</span> onerror=alert(document.domain)&gt;})
expect(marked_new_line).to be_html_safe
end
end
end
describe '#parallel_diff_discussions' do
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices, :migration, schema: 20180122154930 do
let(:services) { table(:services) }
describe '#perform' do
it 'migrates services where note_events is true' do
service = services.create(confidential_note_events: nil, note_events: true)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(true)
end
it 'ignores services where note_events is false' do
service = services.create(confidential_note_events: nil, note_events: false)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(nil)
end
it 'ignores services where confidential_note_events has already been set' do
service = services.create(confidential_note_events: false, note_events: true)
subject.perform(service.id, service.id)
expect(service.reload.confidential_note_events).to eq(false)
end
end
end
require 'spec_helper'
describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks, :migration, schema: 20180104131052 do
let(:web_hooks) { table(:web_hooks) }
describe '#perform' do
it 'migrates hooks where note_events is true' do
hook = web_hooks.create(confidential_note_events: nil, note_events: true)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(true)
end
it 'ignores hooks where note_events is false' do
hook = web_hooks.create(confidential_note_events: nil, note_events: false)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(nil)
end
it 'ignores hooks where confidential_note_events has already been set' do
hook = web_hooks.create(confidential_note_events: false, note_events: true)
subject.perform(hook.id, hook.id)
expect(hook.reload.confidential_note_events).to eq(false)
end
end
end
......@@ -55,6 +55,14 @@ describe Gitlab::DataBuilder::Note do
.to be > issue.hook_attrs['updated_at']
end
context 'with confidential issue' do
let(:issue) { create(:issue, project: project, confidential: true) }
it 'sets event_type to confidential_note' do
expect(data[:event_type]).to eq('confidential_note')
end
end
include_examples 'project hook data'
include_examples 'deprecated repository hook data'
end
......
require 'spec_helper'
describe Gitlab::ImportExport::Importer do
let(:test_path) { "#{Dir.tmpdir}/importer_spec" }
let(:shared) { project.import_export_shared }
let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) }
subject(:importer) { described_class.new(project) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path)
FileUtils.mkdir_p(shared.export_path)
FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path)
end
after do
FileUtils.rm_rf(test_path)
end
describe '#execute' do
it 'succeeds' do
importer.execute
expect(shared.errors).to be_empty
end
it 'extracts the archive' do
expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original
importer.execute
end
it 'checks the version' do
expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original
importer.execute
end
context 'all restores are executed' do
[
Gitlab::ImportExport::AvatarRestorer,
Gitlab::ImportExport::RepoRestorer,
Gitlab::ImportExport::WikiRestorer,
Gitlab::ImportExport::UploadsRestorer,
Gitlab::ImportExport::LfsRestorer
].each do |restorer|
it "calls the #{restorer}" do
fake_restorer = double(restorer.to_s)
expect(fake_restorer).to receive(:restore).and_return(true).at_least(1)
expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1)
importer.execute
end
end
it 'restores the ProjectTree' do
expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original
importer.execute
end
end
end
end
require 'spec_helper'
describe Gitlab::ImportExport::LfsRestorer do
include UploadHelpers
let(:export_path) { "#{Dir.tmpdir}/lfs_object_restorer_spec" }
let(:project) { create(:project) }
let(:shared) { project.import_export_shared }
subject(:restorer) { described_class.new(project: project, shared: shared) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
FileUtils.mkdir_p(shared.export_path)
end
after do
FileUtils.rm_rf(shared.export_path)
end
describe '#restore' do
context 'when the archive contains lfs files' do
let(:dummy_lfs_file_path) { File.join(shared.export_path, 'lfs-objects', 'dummy') }
def create_lfs_object_with_content(content)
dummy_lfs_file = Tempfile.new('existing')
File.write(dummy_lfs_file.path, content)
size = dummy_lfs_file.size
oid = LfsObject.calculate_oid(dummy_lfs_file.path)
LfsObject.create!(oid: oid, size: size, file: dummy_lfs_file)
end
before do
FileUtils.mkdir_p(File.dirname(dummy_lfs_file_path))
File.write(dummy_lfs_file_path, 'not very large')
allow(restorer).to receive(:lfs_file_paths).and_return([dummy_lfs_file_path])
end
it 'creates an lfs object for the project' do
expect { restorer.restore }.to change { project.reload.lfs_objects.size }.by(1)
end
it 'assigns the file correctly' do
restorer.restore
expect(project.lfs_objects.first.file.read).to eq('not very large')
end
it 'links an existing LFS object if it existed' do
lfs_object = create_lfs_object_with_content('not very large')
restorer.restore
expect(project.lfs_objects).to include(lfs_object)
end
it 'succeeds' do
expect(restorer.restore).to be_truthy
expect(shared.errors).to be_empty
end
it 'stores the upload' do
expect_any_instance_of(LfsObjectUploader).to receive(:store!)
restorer.restore
end
end
context 'without any LFS-objects' do
it 'succeeds' do
expect(restorer.restore).to be_truthy
expect(shared.errors).to be_empty
end
end
end
end
require 'spec_helper'
describe Gitlab::ImportExport::LfsSaver do
let(:shared) { project.import_export_shared }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:project) { create(:project) }
subject(:saver) { described_class.new(project: project, shared: shared) }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
FileUtils.mkdir_p(shared.export_path)
end
after do
FileUtils.rm_rf(shared.export_path)
end
describe '#save' do
context 'when the project has LFS objects locally stored' do
let(:lfs_object) { create(:lfs_object, :with_file) }
before do
project.lfs_objects << lfs_object
end
it 'does not cause errors' do
saver.save
expect(shared.errors).to be_empty
end
it 'copies the file in the correct location when there is an lfs object' do
saver.save
expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}")
end
end
context 'when the LFS objects are stored in object storage' do
let(:lfs_object) { create(:lfs_object, :object_storage) }
before do
allow(LfsObjectUploader).to receive(:object_store_enabled?).and_return(true)
allow(lfs_object.file).to receive(:url).and_return('http://my-object-storage.local')
project.lfs_objects << lfs_object
end
it 'downloads the file to include in an archive' do
fake_uri = double
exported_file_path = "#{shared.export_path}/lfs-objects/#{lfs_object.oid}"
expect(fake_uri).to receive(:open).and_return(StringIO.new('LFS file content'))
expect(URI).to receive(:parse).with('http://my-object-storage.local').and_return(fake_uri)
saver.save
expect(File.read(exported_file_path)).to eq('LFS file content')
end
end
end
end
......@@ -394,6 +394,7 @@ Service:
- default
- wiki_page_events
- confidential_issues_events
- confidential_note_events
ProjectHook:
- id
- url
......@@ -414,6 +415,7 @@ ProjectHook:
- token
- group_id
- confidential_issues_events
- confidential_note_events
- repository_update_events
ProtectedBranch:
- id
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180122154930_schedule_set_confidential_note_events_on_services.rb')
describe ScheduleSetConfidentialNoteEventsOnServices, :migration, :sidekiq do
let(:services_table) { table(:services) }
let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices }
let(:migration_name) { migration_class.to_s.demodulize }
let!(:service_1) { services_table.create!(confidential_note_events: nil, note_events: true) }
let!(:service_2) { services_table.create!(confidential_note_events: nil, note_events: true) }
let!(:service_migrated) { services_table.create!(confidential_note_events: true, note_events: true) }
let!(:service_skip) { services_table.create!(confidential_note_events: nil, note_events: false) }
let!(:service_new) { services_table.create!(confidential_note_events: false, note_events: true) }
let!(:service_4) { services_table.create!(confidential_note_events: nil, note_events: true) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'schedules background migrations at correct time' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(migration_name).to be_scheduled_delayed_migration(20.minutes, service_1.id, service_1.id)
expect(migration_name).to be_scheduled_delayed_migration(40.minutes, service_2.id, service_2.id)
expect(migration_name).to be_scheduled_delayed_migration(60.minutes, service_4.id, service_4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
it 'correctly processes services' do
Sidekiq::Testing.inline! do
expect(services_table.where(confidential_note_events: nil).count).to eq 4
expect(services_table.where(confidential_note_events: true).count).to eq 1
migrate!
expect(services_table.where(confidential_note_events: nil).count).to eq 1
expect(services_table.where(confidential_note_events: true).count).to eq 4
end
end
end
......@@ -4,8 +4,24 @@ require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_cluste
describe MigrateGcpClustersToNewClustersArchitectures, :migration do
let(:projects) { table(:projects) }
let(:project) { projects.create }
let(:user) { create(:user) }
let(:service) { create(:kubernetes_service, project_id: project.id) }
let(:users) { table(:users) }
let(:user) { users.create! }
let(:service) { GcpMigrationSpec::KubernetesService.create!(project_id: project.id) }
module GcpMigrationSpec
class KubernetesService < ActiveRecord::Base
self.table_name = 'services'
serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize
default_value_for :active, true
default_value_for :type, 'KubernetesService'
default_value_for :properties, {
api_url: 'https://kubernetes.example.com',
token: 'a' * 40
}
end
end
context 'when cluster is being created' do
let(:project_id) { project.id }
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180104131052_schedule_set_confidential_note_events_on_webhooks.rb')
describe ScheduleSetConfidentialNoteEventsOnWebhooks, :migration, :sidekiq do
let(:web_hooks_table) { table(:web_hooks) }
let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks }
let(:migration_name) { migration_class.to_s.demodulize }
let!(:web_hook_1) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
let!(:web_hook_2) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
let!(:web_hook_migrated) { web_hooks_table.create!(confidential_note_events: true, note_events: true) }
let!(:web_hook_skip) { web_hooks_table.create!(confidential_note_events: nil, note_events: false) }
let!(:web_hook_new) { web_hooks_table.create!(confidential_note_events: false, note_events: true) }
let!(:web_hook_4) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) }
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
end
it 'schedules background migrations at correct time' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(migration_name).to be_scheduled_delayed_migration(5.minutes, web_hook_1.id, web_hook_1.id)
expect(migration_name).to be_scheduled_delayed_migration(10.minutes, web_hook_2.id, web_hook_2.id)
expect(migration_name).to be_scheduled_delayed_migration(15.minutes, web_hook_4.id, web_hook_4.id)
expect(BackgroundMigrationWorker.jobs.size).to eq 3
end
end
end
it 'correctly processes web hooks' do
Sidekiq::Testing.inline! do
expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 4
expect(web_hooks_table.where(confidential_note_events: true).count).to eq 1
migrate!
expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 1
expect(web_hooks_table.where(confidential_note_events: true).count).to eq 4
end
end
end
......@@ -191,6 +191,21 @@ describe Note do
end
end
describe "confidential?" do
it "delegates to noteable" do
issue_note = build(:note, :on_issue)
confidential_note = build(:note, noteable: create(:issue, confidential: true))
expect(issue_note.confidential?).to be_falsy
expect(confidential_note.confidential?).to be_truthy
end
it "is falsey when noteable can't be confidential" do
commit_note = build(:note_on_commit)
expect(commit_note.confidential?).to be_falsy
end
end
describe "cross_reference_not_visible_for?" do
let(:private_user) { create(:user) }
let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } }
......
......@@ -266,6 +266,21 @@ describe HipchatService do
"<b>#{title}</b>" \
"<pre>issue <strong>note</strong></pre>")
end
context 'with confidential issue' do
before do
issue.update!(confidential: true)
end
it 'calls Hipchat API with issue comment' do
data = Gitlab::DataBuilder::Note.build(issue_note, user)
hipchat.execute(data)
message = hipchat.send(:create_message, data)
expect(message).to include("<pre>issue <strong>note</strong></pre>")
end
end
end
context 'when snippet comment event triggered' do
......
......@@ -2305,6 +2305,22 @@ describe Project do
expect(forked_project.lfs_storage_project).to eq forked_project
end
end
describe '#all_lfs_objects' do
let(:lfs_object) { create(:lfs_object) }
before do
project.lfs_objects << lfs_object
end
it 'returns the lfs object for a project' do
expect(project.all_lfs_objects).to contain_exactly(lfs_object)
end
it 'returns the lfs object for a fork' do
expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object)
end
end
end
describe '#pushes_since_gc' do
......
......@@ -10,6 +10,22 @@ describe Service do
it { is_expected.to validate_presence_of(:type) }
end
describe 'Scopes' do
describe '.confidential_note_hooks' do
it 'includes services where confidential_note_events is true' do
create(:service, active: true, confidential_note_events: true)
expect(described_class.confidential_note_hooks.count).to eq 1
end
it 'excludes services where confidential_note_events is false' do
create(:service, active: true, confidential_note_events: false)
expect(described_class.confidential_note_hooks.count).to eq 0
end
end
end
describe "Test Button" do
describe '#can_test?' do
let(:service) { create(:service, project: project) }
......
......@@ -33,6 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['confidential_note_events']).to eq(true)
expect(json_response.first['job_events']).to eq(true)
expect(json_response.first['pipeline_events']).to eq(true)
expect(json_response.first['wiki_page_events']).to eq(true)
......@@ -62,6 +63,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
......@@ -104,6 +106,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
expect(json_response['note_events']).to eq(false)
expect(json_response['confidential_note_events']).to eq(nil)
expect(json_response['job_events']).to eq(true)
expect(json_response['pipeline_events']).to eq(false)
expect(json_response['wiki_page_events']).to eq(true)
......@@ -152,6 +155,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
expect(json_response['note_events']).to eq(hook.note_events)
expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events)
expect(json_response['job_events']).to eq(hook.job_events)
expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
......
......@@ -23,5 +23,23 @@ describe Notes::PostProcessService do
described_class.new(@note).execute
end
context 'with a confidential issue' do
let(:issue) { create(:issue, :confidential, project: project) }
it "doesn't call note hooks/services" do
expect(project).not_to receive(:execute_hooks).with(anything, :note_hooks)
expect(project).not_to receive(:execute_services).with(anything, :note_hooks)
described_class.new(@note).execute
end
it "calls confidential-note hooks/services" do
expect(project).to receive(:execute_hooks).with(anything, :confidential_note_hooks)
expect(project).to receive(:execute_services).with(anything, :confidential_note_hooks)
described_class.new(@note).execute
end
end
end
end
......@@ -8,6 +8,49 @@ describe Projects::ImportExport::ExportService do
let(:service) { described_class.new(project, user) }
let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new }
it 'saves the version' do
expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the avatar' do
expect(Gitlab::ImportExport::AvatarSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the models' do
expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the uploads' do
expect(Gitlab::ImportExport::UploadsSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the repo' do
# once for the normal repo, once for the wiki
expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original
service.execute
end
it 'saves the lfs objects' do
expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original
service.execute
end
it 'saves the wiki repo' do
expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original
service.execute
end
context 'when all saver services succeed' do
before do
allow(service).to receive(:save_services).and_return(true)
......
......@@ -4,6 +4,11 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:chat_service) { described_class.new }
let(:webhook_url) { 'https://example.gitlab.com/' }
def execute_with_options(options)
receive(:new).with(webhook_url, options)
.and_return(double(:slack_service).as_null_object)
end
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
......@@ -33,6 +38,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
let(:project) { create(:project, :repository) }
let(:username) { 'slack_username' }
let(:channel) { 'slack_channel' }
let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } }
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
......@@ -48,12 +54,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
WebMock.stub_request(:post, webhook_url)
opts = {
title: 'Awesome issue',
description: 'please fix'
}
issue_service = Issues::CreateService.new(project, user, opts)
issue_service = Issues::CreateService.new(project, user, issue_service_options)
@issue = issue_service.execute
@issues_sample_data = issue_service.hook_data(@issue, 'open')
......@@ -164,6 +165,26 @@ RSpec.shared_examples 'slack or mattermost notifications' do
chat_service.execute(@issues_sample_data)
end
context 'for confidential issues' do
let(:issue_service_options) { { title: 'Secret', confidential: true } }
it "uses confidential issue channel" do
chat_service.update_attributes(confidential_issue_channel: 'confidential')
expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
chat_service.execute(@issues_sample_data)
end
it 'falls back to issue channel' do
chat_service.update_attributes(issue_channel: 'fallback_channel')
expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
chat_service.execute(@issues_sample_data)
end
end
it "uses the right channel for wiki event" do
chat_service.update_attributes(wiki_page_channel: "random")
......@@ -194,6 +215,32 @@ RSpec.shared_examples 'slack or mattermost notifications' do
chat_service.execute(note_data)
end
context 'for confidential notes' do
before do
issue_note.noteable.update!(confidential: true)
end
it "uses confidential channel" do
chat_service.update_attributes(confidential_note_channel: "confidential")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
expect(Slack::Notifier).to execute_with_options(channel: 'confidential')
chat_service.execute(note_data)
end
it 'falls back to note channel' do
chat_service.update_attributes(note_channel: "fallback_channel")
note_data = Gitlab::DataBuilder::Note.build(issue_note, user)
expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel')
chat_service.execute(note_data)
end
end
end
end
end
......@@ -248,8 +295,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do
create(:note_on_issue, project: project, note: "issue note")
end
let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) }
it "calls Slack API for issue comment events" do
data = Gitlab::DataBuilder::Note.build(issue_note, user)
chat_service.execute(data)
expect(WebMock).to have_requested(:post, webhook_url).once
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment