Commit f61c6cdd authored by Robert Speicher's avatar Robert Speicher

Merge branch 'master' into ce-to-ee-2018-03-06

parents 856900d2 2ed6b0cd
......@@ -7,6 +7,7 @@ export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
super({
page: 'boards',
isGroup: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysIssues,
stateFiltersSelector: '.issues-state-filters',
});
......
......@@ -125,6 +125,16 @@ export default class FilteredSearchDropdownManager {
endpoint = `${endpoint}?only_group_labels=true`;
}
// EE-only
if (this.groupAncestor) {
endpoint = `${endpoint}&include_ancestor_groups=true`;
}
// EE-only
if (this.isGroupDecendent) {
endpoint = `${endpoint}&include_descendant_groups=true`;
}
return endpoint;
}
......
......@@ -109,6 +109,7 @@ export default class FilteredSearchManager {
page: this.page,
isGroup: this.isGroup,
isGroupAncestor: this.isGroupAncestor,
isGroupDecendent: this.isGroupDecendent,
filteredSearchTokenKeys: this.filteredSearchTokenKeys,
});
......
......@@ -8,4 +8,5 @@ export default {
OK: 200,
MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
NOT_FOUND: 404,
};
......@@ -88,7 +88,7 @@ export default {
</script>
<template>
<div class="block labels">
<div class="block labels js-labels-block">
<dropdown-value-collapsed
v-if="showCreate"
:labels="context.labels"
......@@ -104,7 +104,7 @@ export default {
</dropdown-value>
<div
v-if="canEdit"
class="selectbox"
class="selectbox js-selectbox"
style="display: none;"
>
<dropdown-hidden-input
......
......@@ -35,7 +35,7 @@ export default {
</script>
<template>
<div class="hide-collapsed value issuable-show-labels">
<div class="hide-collapsed value issuable-show-labels js-value">
<span
v-if="isEmpty"
class="text-secondary"
......
module Boards
class IssuesController < Boards::ApplicationController
prepend EE::BoardsResponses
prepend EE::Boards::IssuesController
include BoardsResponses
include ControllerWithCrossProjectAccessCheck
requires_cross_project_access if: -> { board&.group_board? }
before_action :whitelist_query_limiting, only: [:index, :update]
before_action :authorize_read_issue, only: [:index]
......@@ -66,11 +67,19 @@ module Boards
end
def issues_finder
IssuesFinder.new(current_user, project_id: board_parent.id)
if board.group_board?
IssuesFinder.new(current_user, group_id: board_parent.id)
else
IssuesFinder.new(current_user, project_id: board_parent.id)
end
end
def project
board_parent
@project ||= if board.group_board?
Project.find(issue_params[:project_id])
else
board_parent
end
end
def move_params
......
module Boards
class ListsController < Boards::ApplicationController
prepend EE::BoardsResponses
include BoardsResponses
before_action :authorize_admin_list, only: [:create, :update, :destroy, :generate]
......
module BoardsResponses
include Gitlab::Utils::StrongMemoize
def board_params
params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: [])
end
def parent
strong_memoize(:parent) do
group? ? group : project
end
end
def boards_path
if group?
group_boards_path(parent)
else
project_boards_path(parent)
end
end
def board_path(board)
if group?
group_board_path(parent, board)
else
project_board_path(parent, board)
end
end
def group?
instance_variable_defined?(:@group)
end
def authorize_read_list
authorize_action_for!(board.parent, :read_list)
ability = board.group_board? ? :read_group : :read_list
authorize_action_for!(board.parent, ability)
end
def authorize_read_issue
authorize_action_for!(board.parent, :read_issue)
ability = board.group_board? ? :read_group : :read_issue
authorize_action_for!(board.parent, ability)
end
def authorize_update_issue
......@@ -31,6 +67,10 @@ module BoardsResponses
respond_with(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
def respond_with(resource)
respond_to do |format|
format.html
......
class Groups::BoardsController < Groups::ApplicationController
prepend EE::Boards::BoardsController
prepend EE::BoardsResponses
include BoardsResponses
before_action :check_group_issue_boards_available!
before_action :assign_endpoint_vars
def index
......@@ -23,4 +21,8 @@ class Groups::BoardsController < Groups::ApplicationController
@namespace_path = group.to_param
@labels_endpoint = group_labels_url(group)
end
def serialize_as_json(resource)
resource.as_json(only: [:id])
end
end
class Projects::BoardsController < Projects::ApplicationController
prepend EE::Boards::BoardsController
prepend EE::BoardsResponses
include BoardsResponses
include IssuableCollections
......
......@@ -19,23 +19,35 @@ module BoardsHelper
end
def build_issue_link_base
project_issues_path(@project)
if board.group_board?
"#{group_path(@board.group)}/:project_path/issues"
else
project_issues_path(@project)
end
end
def board_base_url
project_boards_path(@project)
if board.group_board?
group_boards_url(@group)
else
project_boards_path(@project)
end
end
def multiple_boards_available?
current_board_parent.multiple_issue_boards_available?(current_user)
current_board_parent.multiple_issue_boards_available?
end
def current_board_path(board)
@current_board_path ||= project_board_path(current_board_parent, board)
@current_board_path ||= if board.group_board?
group_board_path(current_board_parent, board)
else
project_board_path(current_board_parent, board)
end
end
def current_board_parent
@current_board_parent ||= @project
@current_board_parent ||= @group || @project
end
def can_admin_issue?
......@@ -49,7 +61,8 @@ module BoardsHelper
labels: labels_filter_path(true),
labels_endpoint: @labels_endpoint,
namespace_path: @namespace_path,
project_path: @project&.try(:path)
project_path: @project&.path,
group_path: @group&.path
}
end
......@@ -61,7 +74,8 @@ module BoardsHelper
field_name: 'issue[assignee_ids][]',
first_user: current_user&.username,
current_user: 'true',
project_id: @project&.try(:id),
project_id: @project&.id,
group_id: @group&.id,
null_user: 'true',
multi_select: 'true',
'dropdown-header': dropdown_options[:data][:'dropdown-header'],
......
......@@ -30,7 +30,7 @@ module FormHelper
null_user: true,
current_user: true,
project_id: @project&.id,
field_name: "issue[assignee_ids][]",
field_name: 'issue[assignee_ids][]',
default_label: 'Unassigned',
'max-select': 1,
'dropdown-header': 'Assignee',
......
......@@ -137,7 +137,7 @@ module GroupsHelper
links = [:overview, :group_members]
if can?(current_user, :read_cross_project)
links += [:activity, :issues, :labels, :milestones, :merge_requests]
links += [:activity, :issues, :boards, :labels, :milestones, :merge_requests]
end
if can?(current_user, :admin_group, @group)
......
......@@ -38,5 +38,13 @@ module Emails
reply_to: @message.reply_to,
subject: @message.subject)
end
def mirror_was_hard_failed_email(project_id, user_id)
@project = Project.find(project_id)
user = User.find(user_id)
mail(to: user.notification_email,
subject: subject('Repository mirroring paused'))
end
end
end
class Board < ActiveRecord::Base
prepend EE::Board
belongs_to :group
belongs_to :project
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all # rubocop:disable Cop/ActiveRecordDependent
validates :project, presence: true, if: :project_needed?
validates :group, presence: true, unless: :project
def project_needed?
true
!group
end
def parent
project
@parent ||= group || project
end
def group_board?
false
group_id.present?
end
def backlog_list
......
......@@ -36,6 +36,8 @@ class Group < Namespace
has_many :hooks, dependent: :destroy, class_name: 'GroupHook' # rubocop:disable Cop/ActiveRecordDependent
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :boards
# We cannot simply set `has_many :audit_events, as: :entity, dependent: :destroy`
# here since Group inherits from Namespace, the entity_type would be set to `Namespace`.
has_many :audit_events, -> { where(entity_type: Group) }, foreign_key: 'entity_id'
......
class Label < ActiveRecord::Base
# EE specific
prepend EE::Label
include CacheMarkdownField
include Referable
include Subscribable
......@@ -38,6 +35,7 @@ class Label < ActiveRecord::Base
scope :templates, -> { where(template: true) }
scope :with_title, ->(title) { where(title: title) }
scope :with_lists_and_board, -> { joins(lists: :board).merge(List.movable) }
scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
scope :on_project_boards, ->(project_id) { with_lists_and_board.where(boards: { project_id: project_id }) }
def self.prioritized(project)
......
......@@ -232,9 +232,9 @@ class Namespace < ActiveRecord::Base
has_parent?
end
## EE only
def multiple_issue_boards_available?(user = nil)
feature_available?(:multiple_issue_boards)
# Overridden on EE module
def multiple_issue_boards_available?
false
end
def full_path_was
......
......@@ -1693,8 +1693,9 @@ class Project < ActiveRecord::Base
end
end
def multiple_issue_boards_available?(user)
feature_available?(:multiple_issue_boards, user)
# Overridden on EE module
def multiple_issue_boards_available?
false
end
def full_path_was
......
......@@ -51,7 +51,12 @@ class GroupPolicy < BasePolicy
rule { has_access }.enable :read_namespace
rule { developer }.enable :admin_milestones
rule { reporter }.enable :admin_label
rule { reporter }.policy do
enable :admin_label
enable :admin_list
enable :admin_issue
end
rule { master }.policy do
enable :create_projects
......
......@@ -41,7 +41,11 @@ module Boards
end
def set_parent
params[:project_id] = parent.id
if parent.is_a?(Group)
params[:group_id] = parent.id
else
params[:project_id] = parent.id
end
end
def set_state
......
module Boards
module Issues
class MoveService < Boards::BaseService
prepend EE::Boards::Issues::MoveService
def execute(issue)
return false unless can?(current_user, :update_issue, issue)
return false if issue_params.empty?
......@@ -62,8 +60,10 @@ module Boards
label_ids =
if moving_to_list.movable?
moving_from_list.label_id
elsif board.group_board?
::Label.on_group_boards(parent.id).pluck(:label_id)
else
Label.on_project_boards(parent.id).pluck(:label_id)
::Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
......
module Boards
module Lists
class CreateService < Boards::BaseService
prepend EE::Boards::Lists::CreateService
def execute(board)
List.transaction do
label = available_labels_for(board).find(params[:label_id])
......@@ -14,7 +12,11 @@ module Boards
private
def available_labels_for(board)
LabelsFinder.new(current_user, project_id: parent.id).execute
if board.group_board?
parent.labels
else
LabelsFinder.new(current_user, project_id: parent.id).execute
end
end
def next_position(board)
......
module Ci
class CreateTraceArtifactService < BaseService
def execute(job)
return if job.job_artifacts_trace
job.trace.read do |stream|
break unless stream.file?
clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_job_trace!(job, clone_path)
FileUtils.rm(stream.path)
end
end
end
private
def create_job_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
def clone_file!(src_path, temp_dir)
FileUtils.mkdir_p(temp_dir)
Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.copy(src_path, temp_path)
yield(temp_path)
end
end
end
end
- page_title 'Labels'
- issuables = ['issues', 'merge requests'] + (@group&.feature_available?(:epics) ? ['epics'] : [])
.top-area.adjust
.nav-text
Labels can be applied to issues and merge requests. Group labels are available for any project within the group.
= _("Labels can be applied to %{features}. Group labels are available for any project within the group.") % { features: issuables.to_sentence }
.nav-controls
- if can?(current_user, :admin_label, @group)
......@@ -16,4 +18,4 @@
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
No labels created yet.
= _("No labels created yet.")
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
- if @group.feature_available?(:group_issue_boards)
- issues_sub_menu_items.push('boards#index', 'boards#show')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
......@@ -62,6 +59,7 @@
%strong.fly-out-top-item-name
#{ _('Issues') }
%span.badge.count.issue_counter.fly-out-badge= number_with_delimiter(issues_count)
%li.divider.fly-out-top-item
= nav_link(path: 'groups#issues', html_options: { class: 'home' }) do
= link_to issues_group_path(@group), title: 'List' do
......@@ -70,9 +68,9 @@
- if group_sidebar_link?(:boards)
= nav_link(path: ['boards#index', 'boards#show']) do
= link_to group_boards_path(@group), title: 'Boards' do
= link_to group_boards_path(@group), title: boards_link_text do
%span
Boards
= boards_link_text
- if group_sidebar_link?(:labels)
= nav_link(path: 'labels#index') do
......
......@@ -2,6 +2,7 @@
- status = label_subscription_status(label, @project).inquiry if current_user
- subject = local_assigns[:subject]
- toggle_subscription_path = toggle_subscription_label_path(label, @project) if current_user
- show_label_epics_link = @group&.feature_available?(:epics)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
......@@ -14,6 +15,10 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
- if show_label_epics_link
%li
= link_to group_epics_path(@group, label_name:[label.name]) do
View epics
- if show_label_merge_requests_link
%li
= link_to_label(label, subject: subject, type: :merge_request) do
......
- subject = local_assigns[:subject]
- show_label_epics_link = @group&.feature_available?(:epics)
- show_label_issues_link = show_label_issuables_link?(label, :issues, project: @project)
- show_label_merge_requests_link = show_label_issuables_link?(label, :merge_requests, project: @project)
......@@ -23,6 +24,9 @@
.description-text
= markdown_field(label, :description)
.hidden-xs.hidden-sm
- if show_label_epics_link
= link_to 'Epics', group_epics_path(@group, label_name:[label.name])
&middot;
- if show_label_issues_link
= link_to_label(label, subject: subject) { 'Issues' }
- if show_label_merge_requests_link
......
......@@ -43,9 +43,9 @@
- pipeline_cache:expire_pipeline_cache
- pipeline_creation:create_pipeline
- pipeline_creation:run_pipeline_schedule
- pipeline_background:archive_trace
- pipeline_default:build_coverage
- pipeline_default:build_trace_sections
- pipeline_default:create_trace_artifact
- pipeline_default:pipeline_metrics
- pipeline_default:pipeline_notification
- pipeline_default:update_head_pipeline_for_merge_request
......
class ArchiveTraceWorker
include ApplicationWorker
include PipelineBackgroundQueue
def perform(job_id)
Ci::Build.find_by(id: job_id).try do |job|
job.trace.archive!
end
end
end
......@@ -14,7 +14,7 @@ class BuildFinishedWorker
# We execute that async as this are two indepentent operations that can be executed after TraceSections and Coverage
BuildHooksWorker.perform_async(build.id)
CreateTraceArtifactWorker.perform_async(build.id)
ArchiveTraceWorker.perform_async(build.id)
end
end
end
##
# Concern for setting Sidekiq settings for the low priority CI pipeline workers.
#
module PipelineBackgroundQueue
extend ActiveSupport::Concern
included do
queue_namespace :pipeline_background
end
end
class CreateTraceArtifactWorker
include ApplicationWorker
include PipelineQueue
def perform(job_id)
Ci::Build.preload(:project, :user).find_by(id: job_id).try do |job|
Ci::CreateTraceArtifactService.new(job.project, job.user).execute(job)
end
end
end
---
title: Add archive feature to trace
merge_request: 17314
author:
type: added
......@@ -80,7 +80,6 @@ constraints(GroupUrlConstrainer.new) do
end
resources :billings, only: [:index]
resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :epics do
member do
get :realtime_changes
......@@ -89,6 +88,9 @@ constraints(GroupUrlConstrainer.new) do
resources :epic_issues, only: [:index, :create, :destroy, :update], as: 'issues', path: 'issues'
end
# On CE only index and show are needed
resources :boards, only: [:index, :show, :create, :update, :destroy]
legacy_ee_group_boards_redirect = redirect do |params, request|
path = "/groups/#{params[:group_id]}/-/boards"
path << "/#{params[:extra_params]}" if params[:extra_params].present?
......
......@@ -422,6 +422,7 @@ constraints(ProjectUrlConstrainer.new) do
get 'noteable/:target_type/:target_id/notes' => 'notes#index', as: 'noteable_notes'
# On CE only index and show are needed
resources :boards, only: [:index, :show, :create, :update, :destroy]
resources :todos, only: [:create]
......
......@@ -69,6 +69,7 @@
- [storage_migrator, 1]
- [pages_domain_verification, 1]
- [plugin, 1]
- [pipeline_background, 1]
# EE-specific queues
- [ldap_group_sync, 2]
......
class MigrateCreateTraceArtifactSidekiqQueue < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
def up
sidekiq_queue_migrate 'pipeline_default:create_trace_artifact', to: 'pipeline_background:archive_trace'
end
def down
sidekiq_queue_migrate 'pipeline_background:archive_trace', to: 'pipeline_default:create_trace_artifact'
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180305144721) do
ActiveRecord::Schema.define(version: 20180306074045) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -88,7 +88,7 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [Discussions](user/discussions/index.md): Threads, comments, and resolvable discussions in issues, commits, and merge requests.
- [Issues](user/project/issues/index.md)
- [Project issue Board](user/project/issue_board.md)
- **(Premium)** [Group Issue Boards](user/project/issue_board.md#group-issue-boards)
- [Group Issue Boards](user/project/issue_board.md#group-issue-boards)
- **(Starter/Premium)** [Related Issues](user/project/issues/related_issues.md): create a relationship between issues
- [Issues and merge requests templates](user/project/description_templates.md): Create templates for submitting new issues and merge requests.
- [Labels](user/project/labels.md): Categorize your issues or merge requests based on descriptive titles.
......
......@@ -31,7 +31,7 @@ following locations:
- [Group Members](members.md)
- [Issues](issues.md)
- [Issue Boards](boards.md)
- **(Premium)** [Group Issue Boards] (group_boards.md)
- [Group Issue Boards](group_boards.md)
- [Jobs](jobs.md)
- [Keys](keys.md)
- [Labels](labels.md)
......
......@@ -210,4 +210,3 @@ DELETE /groups/:id/-/epics/:epic_iid
```bash
curl --header DELETE "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/1/-/epics/5?title=New%20Title
```
......@@ -96,12 +96,24 @@ Example response:
}
```
## Delete a Geo node
Removes the Geo node.
```
DELETE /geo_nodes/:id
```
| Attribute | Type | Required | Description |
|-----------|---------|----------|-------------------------|
| `id` | integer | yes | The ID of the Geo node. |
## Repair a Geo node
To repair the OAuth authentication of a Geo node.
```
PUT /geo_nodes/:id/repair
POST /geo_nodes/:id/repair
```
Example response:
......@@ -177,6 +189,10 @@ Example response:
GET /geo_nodes/:id/status
```
| Attribute | Type | Required | Description |
| --------- | ------- | -------- | ----------- |
| `refresh` | boolean | no | Attempt to fetch the latest status from the Geo node directly, ignoring the cache |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/geo_nodes/2/status
```
......
......@@ -1756,4 +1756,5 @@ CI with various languages.
[ce-7447]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7447
[ce-3442]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3442
[schedules]: ../../user/project/pipelines/schedules.md
[ee]: https://about.gitlab.com/gitlab-ee/
[gitlab-versions]: https://about.gitlab.com/products/
......@@ -103,6 +103,99 @@ Notes:
- You can use [`git rerere`](https://git-scm.com/blog/2010/03/08/rerere.html)
to avoid resolving the same conflicts multiple times.
### Cherry-picking from CE to EE
For avoiding merge conflicts, we use a method of creating equivalent branches
for CE and EE. If the `ee-compat-check` job fails, this process is required.
This method only requires that you have cloned both CE and EE into your computer.
If you don't have them yet, please go ahead and clone them:
- Clone CE repo: `git clone git@gitlab.com:gitlab-org/gitlab-ce.git`
- Clone EE repo: `git clone git@gitlab.com:gitlab-org/gitlab-ee.git`
And the only additional setup we need is to add CE as remote of EE and vice-versa:
- Open two terminal windows, one in CE, and another one in EE:
- In EE: `git remote add ce git@gitlab.com:gitlab-org/gitlab-ce.git`
- In CE: `git remote add ee git@gitlab.com:gitlab-org/gitlab-ee.git`
That's all setup we need, so that we can cherry-pick a commit from CE to EE, and
from EE to CE.
Now, every time you create an MR for CE and EE:
1. Open two terminal windows, one in CE, and another one in EE
1. In the CE terminal:
1. Create the CE branch, e.g., `branch-example`
1. Make your changes and push a commit (commit A)
1. Create the CE merge request in GitLab
1. In the EE terminal:
1. Create the EE-equivalent branch ending with `-ee`, e.g.,
`git checkout -b branch-example-ee`
1. Fetch the CE branch: `git fetch ce branch-example`
1. Cherry-pick the commit A: `git cherry-pick commit-A-SHA`
1. If Git prompts you to fix the conflicts, do a `git status`
to check which files contain conflicts, fix them, save the files
1. Add the changes with `git add .` but **DO NOT commit** them
1. Continue cherry-picking: `git cherry-pick --continue`
1. Push to EE: `git push origin branch-example-ee`
1. Create the EE-equivalent MR and link to the CE MR from the
description "Ports [CE-MR-LINK] to EE"
1. Once all the jobs are passing in both CE and EE, you've addressed the
feedback from your own team, and got them approved, the merge requests can be merged.
1. When both MRs are ready, the EE merge request will be merged first, and the
CE-equivalent will be merged next.
**Important notes:**
- The commit SHA can be easily found from the GitLab UI. From a merge request,
open the tab **Commits** and click the copy icon to copy the commit SHA.
- To cherry-pick a **commit range**, such as [A > B > C > D] use:
```shell
git cherry-pick "oldest-commit-SHA^..newest-commit-SHA"
```
For example, suppose the commit A is the oldest, and its SHA is `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
and the commit D is the newest, and its SHA is `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
The cherry-pick command will be:
```shell
git cherry-pick "4f5e4018c09ed797fdf446b3752f82e46f5af502^..80e1c9e56783bd57bd7129828ec20b252ebc0538"
```
- To cherry-pick a **merge commit**, use the flag `-m 1`. For example, suppose that the
merge commit SHA is `138f5e2f20289bb376caffa0303adb0cac859ce1`:
```shell
git cherry-pick -m 1 138f5e2f20289bb376caffa0303adb0cac859ce1
```
- To cherry-pick multiple commits, such as B and D in a range [A > B > C > D], use:
```shell
git cherry-pick commmit-B-SHA commit-D-SHA
```
For example, suppose commit B SHA = `4f5e4018c09ed797fdf446b3752f82e46f5af502`,
and the commit D SHA = `80e1c9e56783bd57bd7129828ec20b252ebc0538`.
The cherry-pick command will be:
```shell
git cherry-pick 4f5e4018c09ed797fdf446b3752f82e46f5af502 80e1c9e56783bd57bd7129828ec20b252ebc0538
```
This case is particularly useful when you have a merge commit in a sequence of
commits and you want to cherry-pick all but the merge commit.
- If you push more commits to the CE branch, you can safely repeat the procedure
to cherry-pick them to the EE-equivalent branch. You can do that as many times as
necessary, using the same CE and EE branches.
- If you submitted the merge request to the CE repo and the `ee-compat-check` job passed,
you are not required to submit the EE-equivalent MR, but it's still recommended. If the
job failed, you are required to submit the EE MR so that you can fix the conflicts in EE
before merging your changes into CE.
---
[Return to Development documentation](README.md)
......@@ -19,7 +19,7 @@ The one responsible for writing the first piece of documentation is the develope
wrote the code. It's the job of the Product Manager to ensure all features are
shipped with its docs, whether is a small or big change. At the pace GitLab evolves,
this is the only way to keep the docs up-to-date. If you have any questions about it,
please ask a Technical Writer. Otherwise, when your content is ready, assign one of
ask a Technical Writer. Otherwise, when your content is ready, assign one of
them to review it for you.
We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
......@@ -27,6 +27,8 @@ is documented.
Whenever you submit a merge request for the documentation, use the documentation MR description template.
Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started.
### Documentation directory structure
The documentation is structured based on the GitLab UI structure itself,
......@@ -40,7 +42,7 @@ all docs should be linked. Every new document should be cross-linked to its rela
The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
been deprecated and the majority their docs have been moved to their correct location
in small iterations. Please don't create new docs in these folders.
in small iterations. Don't create new docs in these folders.
To move a document from its location to another directory, read the section
[changing document location](doc_styleguide.md#changing-document-location) of the doc style guide.
......@@ -116,6 +118,49 @@ choices:
If your branch name matches any of the above, it will run only the docs
tests. If it doesn't, the whole test suite will run (including docs).
### Merge requests for GitLab documentation
Before getting started, make sure you read the introductory section
"[contributing to docs](#contributing-to-docs)" above and the
[tech writing workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/)
for GitLab Team members.
- Use the current [merge request description template](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab/merge_request_templates/Documentation.md)
- Use the correct [branch name](#branch-naming)
- Label the MR `Documentation`
- Assign the correct milestone (see note below)
NOTE: **Note:**
If the release version you want to add the documentation to has already been
frozen or released, use the label `Pick into X.Y` to get it merged into
the correct release. Avoid picking into a past release as much as you can, as
it increases the work of the release managers.
#### Cherry-picking from CE to EE
As we have the `master` branch of CE merged into EE once a day, it's common to
run into merge conflicts. To avoid them, we [test for merge conflicts against EE](#testing)
with the `ee-compat-check` job, and use the following method of creating equivalent
branches for CE and EE.
Follow this [method for cherry-picking from CE to EE](automatic_ce_ee_merge.md#cherry-picking-from-ce-to-ee), with a few adjustments:
- Create the [CE branch](#branch-naming) starting with `docs-`,
e.g.: `git checkout -b docs-example`
- Create the EE-equivalent branch ending with `-ee`, e.g.,
`git checkout -b docs-example-ee`
- Once all the jobs are passing in CE and EE, and you've addressed the
feedback from your own team, assign the CE MR to a technical writer for review
- When both MRs are ready, the EE merge request will be merged first, and the
CE-equivalent will be merged next.
- Note that the review will occur only in the CE MR, as the EE MR
contains the same commits as the CE MR.
- If you have a few more changes that apply to the EE-version only, you can submit
a couple more commits to the EE branch, but ask the reviewer to review the EE merge request
additionally to the CE MR. If there are many EE-only changes though, start a new MR
to EE only.
### Previewing the changes live
If you want to preview the doc changes of your merge request live, you can use
......
......@@ -272,13 +272,14 @@ to another list the label changes and a system not is recorded.
> Introduced in [GitLab Enterprise Edition 8.13](https://about.gitlab.com/2016/10/22/gitlab-8-13-released/#multiple-issue-boards-ee).
Multiple Issue Boards, as the name suggests, allow for more than one Issue Board
for a given project. This is great for large projects with more than one team
for a given project or group. This is great for large projects with more than one team
or in situations where a repository is used to host the code of multiple
products.
Clicking on the current board name in the upper left corner will reveal a
menu from where you can create another Issue Board and rename or delete the
existing one.
Multiple issue boards feature is available for **projects in GitLab Starter Edition** and for **groups in GitLab Premium Edition**.
![Multiple Issue Boards](img/issue_boards_multiple.png)
......
......@@ -3,8 +3,9 @@
import issuableApp from '~/issue_show/components/app.vue';
import relatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import issuableAppEventHub from '~/issue_show/event_hub';
import epicHeader from './epic_header.vue';
import epicSidebar from '../../sidebar/components/sidebar_app.vue';
import SidebarContext from '../sidebar_context';
import epicHeader from './epic_header.vue';
export default {
name: 'EpicShowApp',
......@@ -85,6 +86,27 @@
type: String,
required: false,
},
labels: {
type: Array,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
},
data() {
return {
......@@ -94,6 +116,9 @@
projectNamespace: '',
};
},
mounted() {
this.sidebarContext = new SidebarContext();
},
methods: {
deleteEpic() {
issuableAppEventHub.$emit('delete.issuable');
......@@ -137,6 +162,12 @@
:editable="canUpdate"
:initial-start-date="startDate"
:initial-end-date="endDate"
:initial-labels="labels"
:namespace="namespace"
:update-path="updateEndpoint"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:epics-web-url="epicsWebUrl"
/>
<related-issues-root
:endpoint="issueLinksEndpoint"
......
import Vue from 'vue';
import '~/vue_shared/models/label';
import EpicShowApp from './components/epic_show_app.vue';
export default () => {
......@@ -6,7 +7,7 @@ export default () => {
const metaData = JSON.parse(el.dataset.meta);
const initialData = JSON.parse(el.dataset.initial);
const props = Object.assign({}, initialData, metaData);
const props = Object.assign({}, initialData, metaData, el.dataset);
// Convert backend casing to match frontend style guide
props.startDate = props.start_date;
......
import Mousetrap from 'mousetrap';
export default class SidebarContext {
constructor() {
const $issuableSidebar = $('.js-issuable-update');
Mousetrap.bind('l', () => SidebarContext.openSidebarDropdown($issuableSidebar.find('.js-labels-block')));
$issuableSidebar
.off('click', '.js-sidebar-dropdown-toggle')
.on('click', '.js-sidebar-dropdown-toggle', function onClickEdit(e) {
e.preventDefault();
const $block = $(this).parents('.js-labels-block');
const $selectbox = $block.find('.js-selectbox');
// We use `:visible` to detect element visibility
// since labels dropdown itself is handled by
// labels_select.js which internally uses
// $.hide() & $.show() to toggle elements
// which requires us to use `display: none;`
// in `labels_select/base.vue` as well.
// see: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4773#note_61844731
if ($selectbox.is(':visible')) {
$selectbox.hide();
$block.find('.js-value').show();
} else {
$selectbox.show();
$block.find('.js-value').hide();
}
if ($selectbox.is(':visible')) {
setTimeout(() => $block.find('.js-label-select').trigger('click'), 0);
}
});
}
static openSidebarDropdown($block) {
$block.find('.js-sidebar-dropdown-toggle').trigger('click');
}
}
<script>
/* global ListLabel */
/* eslint-disable vue/require-default-prop */
import Cookies from 'js-cookie';
import Flash from '~/flash';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import sidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
import SidebarCollapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
import SidebarLabelsSelect from '~/vue_shared/components/sidebar/labels_select/base.vue';
import SidebarService from '../services/sidebar_service';
import Store from '../stores/sidebar_store';
export default {
name: 'EpicSidebar',
components: {
sidebarDatePicker,
sidebarCollapsedGroupedDatePicker,
SidebarDatePicker,
SidebarCollapsedGroupedDatePicker,
SidebarLabelsSelect,
},
props: {
endpoint: {
......@@ -32,6 +35,31 @@
type: String,
required: false,
},
initialLabels: {
type: Array,
required: true,
},
namespace: {
type: String,
required: false,
default: '#',
},
updatePath: {
type: String,
required: true,
},
labelsPath: {
type: String,
required: true,
},
labelsWebUrl: {
type: String,
required: true,
},
epicsWebUrl: {
type: String,
required: true,
},
},
data() {
const store = new Store({
......@@ -46,13 +74,16 @@
savingStartDate: false,
savingEndDate: false,
service: new SidebarService(this.endpoint),
epicContext: {
labels: this.initialLabels,
},
};
},
methods: {
toggleSidebar() {
this.collapsed = !this.collapsed;
const contentContainer = this.$el.closest('.page-with-sidebar');
const contentContainer = this.$el.closest('.page-with-contextual-sidebar');
contentContainer.classList.toggle('right-sidebar-expanded');
contentContainer.classList.toggle('right-sidebar-collapsed');
......@@ -82,6 +113,24 @@
saveEndDate(date) {
return this.saveDate('end', date);
},
handleLabelClick(label) {
if (label.isAny) {
this.epicContext.labels = [];
} else {
const labelIndex = this.epicContext.labels.findIndex(l => l.id === label.id);
if (labelIndex === -1) {
this.epicContext.labels.push(new ListLabel({
id: label.id,
title: label.title,
color: label.color[0],
textColor: label.text_color,
}));
} else {
this.epicContext.labels.splice(labelIndex, 1);
}
}
},
},
};
</script>
......@@ -91,9 +140,10 @@
class="right-sidebar"
:class="{ 'right-sidebar-expanded' : !collapsed, 'right-sidebar-collapsed': collapsed }"
>
<div class="issuable-sidebar">
<div class="issuable-sidebar js-issuable-update">
<sidebar-date-picker
v-if="!collapsed"
block-class="start-date"
:collapsed="collapsed"
:is-loading="savingStartDate"
:editable="editable"
......@@ -106,6 +156,7 @@
/>
<sidebar-date-picker
v-if="!collapsed"
block-class="end-date"
:collapsed="collapsed"
:is-loading="savingEndDate"
:editable="editable"
......@@ -123,6 +174,20 @@
:show-toggle-sidebar="true"
@toggleCollapse="toggleSidebar"
/>
<sidebar-labels-select
ability-name="epic"
:context="epicContext"
:namespace="namespace"
:update-path="updatePath"
:labels-path="labelsPath"
:labels-web-url="labelsWebUrl"
:label-filter-base-path="epicsWebUrl"
:can-edit="editable"
:show-create="true"
@onLabelClick="handleLabelClick"
>
{{ __('None') }}
</sidebar-labels-select>
</div>
</aside>
</template>
......@@ -5,6 +5,13 @@ const tokenKeys = [{
symbol: '@',
icon: 'pencil',
tag: '@author',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
icon: 'tag',
tag: '~label',
}];
const alternativeTokenKeys = [{
......
<script>
import { s__ } from '~/locale';
import Flash from '~/flash';
import statusCodes from '~/lib/utils/http_status';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import modal from '~/vue_shared/components/modal.vue';
import SmartInterval from '~/smart_interval';
import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
import geoNodesList from './geo_nodes_list.vue';
export default {
components: {
loadingIcon,
modal,
geoNodesList,
},
props: {
......@@ -33,6 +40,12 @@
return {
isLoading: true,
hasError: false,
showModal: false,
targetNode: null,
targetNodeActionType: '',
modalKind: 'warning',
modalMessage: '',
modalActionLabel: '',
errorMessage: '',
};
},
......@@ -43,17 +56,34 @@
},
created() {
eventHub.$on('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$on('showNodeActionModal', this.showNodeActionModal);
eventHub.$on('repairNode', this.repairNode);
},
mounted() {
this.fetchGeoNodes();
},
beforeDestroy() {
eventHub.$off('pollNodeDetails', this.initNodeDetailsPolling);
eventHub.$off('showNodeActionModal', this.showNodeActionModal);
eventHub.$off('repairNode', this.repairNode);
if (this.nodePollingInterval) {
this.nodePollingInterval.stopTimer();
}
},
methods: {
setNodeActionStatus(node, status) {
Object.assign(node, { nodeActionActive: status });
},
initNodeDetailsPolling(node) {
this.nodePollingInterval = new SmartInterval({
callback: this.fetchNodeDetails.bind(this, node),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
},
fetchGeoNodes() {
this.hasError = false;
this.service.getGeoNodes()
......@@ -67,8 +97,9 @@
this.errorMessage = err;
});
},
fetchNodeDetails(nodeId) {
return this.service.getGeoNodeDetails(nodeId)
fetchNodeDetails(node) {
const nodeId = node.id;
return this.service.getGeoNodeDetails(node)
.then(res => res.data)
.then((nodeDetails) => {
const primaryNodeVersion = this.store.getPrimaryNodeVersion();
......@@ -80,18 +111,81 @@
eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId));
})
.catch((err) => {
eventHub.$emit('nodeDetailsLoadFailed', nodeId, err);
if (err.response && err.response.status === statusCodes.NOT_FOUND) {
this.store.setNodeDetails(nodeId, {
geo_node_id: nodeId,
health: err.message,
health_status: 'Unknown',
missing_oauth_application: false,
sync_status_unavailable: true,
storage_shards_match: null,
});
eventHub.$emit('nodeDetailsLoaded', this.store.getNodeDetails(nodeId));
} else {
eventHub.$emit('nodeDetailsLoadFailed', nodeId, err);
}
});
},
initNodeDetailsPolling(nodeId) {
this.nodePollingInterval = new SmartInterval({
callback: this.fetchNodeDetails.bind(this, nodeId),
startingInterval: 30000,
maxInterval: 120000,
hiddenInterval: 240000,
incrementByFactorOf: 15000,
immediateExecution: true,
});
repairNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.repairNode(targetNode)
.then(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Node Authentication was successfully repaired.'), 'notice');
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while repairing node'));
});
},
toggleNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.toggleNode(targetNode)
.then(res => res.data)
.then((node) => {
Object.assign(targetNode, { enabled: node.enabled, nodeActionActive: false });
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while changing node status'));
});
},
removeNode(targetNode) {
this.setNodeActionStatus(targetNode, true);
this.service.removeNode(targetNode)
.then(() => {
this.store.removeNode(targetNode);
Flash(s__('GeoNodes|Node was successfully removed.'), 'notice');
})
.catch(() => {
this.setNodeActionStatus(targetNode, false);
Flash(s__('GeoNodes|Something went wrong while removing node'));
});
},
handleNodeAction() {
this.showModal = false;
if (this.targetNodeActionType === NODE_ACTIONS.TOGGLE) {
this.toggleNode(this.targetNode);
} else if (this.targetNodeActionType === NODE_ACTIONS.REMOVE) {
this.removeNode(this.targetNode);
}
},
showNodeActionModal({ actionType, node, modalKind = 'warning', modalMessage, modalActionLabel }) {
this.targetNode = node;
this.targetNodeActionType = actionType;
this.modalKind = modalKind;
this.modalMessage = modalMessage;
this.modalActionLabel = modalActionLabel;
if (actionType === NODE_ACTIONS.TOGGLE && !node.enabled) {
this.toggleNode(this.targetNode);
} else {
this.showModal = true;
}
},
hideNodeActionModal() {
this.showModal = false;
},
},
};
......@@ -120,5 +214,14 @@
>
{{ errorMessage }}
</p>
<modal
v-show="showModal"
:title="__('Are you sure?')"
:kind="modalKind"
:text="modalMessage"
:primary-button-label="modalActionLabel"
@cancel="hideNodeActionModal"
@submit="handleNodeAction"
/>
</div>
</template>
......@@ -2,7 +2,9 @@
import { __, s__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import { NODE_ACTION_BASE_PATH, NODE_ACTIONS } from '../constants';
import eventHub from '../event_hub';
import { NODE_ACTIONS } from '../constants';
export default {
components: {
......@@ -22,11 +24,6 @@
required: true,
},
},
data() {
return {
isNodeToggleInProgress: false,
};
},
computed: {
isToggleAllowed() {
return !this.node.primary && this.nodeEditAllowed;
......@@ -34,20 +31,27 @@
nodeToggleLabel() {
return this.node.enabled ? __('Disable') : __('Enable');
},
nodeDisableMessage() {
return this.node.enabled ? s__('GeoNodes|Disabling a node stops the sync process. Are you sure?') : '';
},
nodePath() {
return `${NODE_ACTION_BASE_PATH}${this.node.id}`;
},
nodeRepairAuthPath() {
return `${this.nodePath}${NODE_ACTIONS.REPAIR}`;
},
methods: {
onToggleNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.TOGGLE,
node: this.node,
modalMessage: s__('GeoNodes|Disabling a node stops the sync process. Are you sure?'),
modalActionLabel: this.nodeToggleLabel,
});
},
nodeTogglePath() {
return `${this.nodePath}${NODE_ACTIONS.TOGGLE}`;
onRemoveNode() {
eventHub.$emit('showNodeActionModal', {
actionType: NODE_ACTIONS.REMOVE,
node: this.node,
modalKind: 'danger',
modalMessage: s__('GeoNodes|Removing a node stops the sync process. Are you sure?'),
modalActionLabel: __('Remove'),
});
},
nodeEditPath() {
return `${this.nodePath}${NODE_ACTIONS.EDIT}`;
onRepairNode() {
eventHub.$emit('repairNode', this.node);
},
},
};
......@@ -59,30 +63,29 @@
v-if="nodeMissingOauth"
class="node-action-container"
>
<a
<button
type="button"
class="btn btn-default btn-sm btn-node-action"
data-method="post"
:href="nodeRepairAuthPath"
@click="onRepairNode"
>
{{ s__('Repair authentication') }}
</a>
</button>
</div>
<div
v-if="isToggleAllowed"
class="node-action-container"
>
<a
<button
type="button"
class="btn btn-sm btn-node-action"
data-method="post"
:href="nodeTogglePath"
:data-confirm="nodeDisableMessage"
:class="{
'btn-warning': node.enabled,
'btn-success': !node.enabled
}"
@click="onToggleNode"
>
{{ nodeToggleLabel }}
</a>
</button>
</div>
<div
v-if="nodeEditAllowed"
......@@ -90,19 +93,19 @@
>
<a
class="btn btn-sm btn-node-action"
:href="nodeEditPath"
:href="node.editPath"
>
{{ __('Edit') }}
</a>
</div>
<div class="node-action-container">
<a
<button
type="button"
class="btn btn-sm btn-node-action btn-danger"
data-method="delete"
:href="nodePath"
@click="onRemoveNode"
>
{{ __('Remove') }}
</a>
</button>
</div>
</div>
</template>
......@@ -106,6 +106,7 @@
/>
<geo-node-sync-settings
v-else-if="isCustomTypeSync"
:sync-status-unavailable="itemValue.syncStatusUnavailable"
:selective-sync-type="itemValue.selectiveSyncType"
:last-event="itemValue.lastEvent"
:cursor-last-event="itemValue.cursorLastEvent"
......
......@@ -97,6 +97,10 @@
return this.showAdvanceItems ? 'angle-up' : 'angle-down';
},
nodeVersion() {
if (this.nodeDetails.version == null &&
this.nodeDetails.revision == null) {
return __('Unknown');
}
return `${this.nodeDetails.version} (${this.nodeDetails.revision})`;
},
replicationSlotWAL() {
......@@ -113,7 +117,8 @@
return stringifyTime(parsedTime);
}
return 'Unknown';
return __('Unknown');
},
lastEventStatus() {
return {
......@@ -150,6 +155,7 @@
},
syncSettings() {
return {
syncStatusUnavailable: this.nodeDetails.syncStatusUnavailable,
selectiveSyncType: this.nodeDetails.selectiveSyncType,
lastEvent: this.nodeDetails.lastEvent,
cursorLastEvent: this.nodeDetails.cursorLastEvent,
......
......@@ -112,14 +112,16 @@ export default {
}
},
handleMounted() {
eventHub.$emit('pollNodeDetails', this.node.id);
eventHub.$emit('pollNodeDetails', this.node);
},
},
};
</script>
<template>
<li>
<li
:class="{ 'node-action-active': node.nodeActionActive }"
>
<div class="row">
<div class="col-md-8">
<div class="row">
......@@ -128,7 +130,7 @@ export default {
{{ node.url }}
</strong>
<loading-icon
v-if="isNodeDetailsLoading"
v-if="isNodeDetailsLoading || node.nodeActionActive"
class="node-details-loading prepend-left-10 pull-left inline"
size="1"
/>
......
......@@ -14,6 +14,11 @@
icon,
},
props: {
syncStatusUnavailable: {
type: Boolean,
required: false,
default: false,
},
selectiveSyncType: {
type: String,
required: false,
......@@ -105,6 +110,13 @@
class="node-detail-value"
>
<span
v-if="syncStatusUnavailable"
class="node-detail-value-bold"
>
{{ __('Unknown') }}
</span>
<span
v-else
v-tooltip
class="node-sync-settings inline"
data-placement="bottom"
......
export const NODE_ACTION_BASE_PATH = '/admin/geo_nodes/';
export const NODE_ACTIONS = {
TOGGLE: '/toggle',
EDIT: '/edit',
REPAIR: '/repair',
TOGGLE: 'toggle',
REMOVE: 'remove',
};
export const VALUE_TYPE = {
......
......@@ -14,11 +14,10 @@ export default () => {
const el = document.getElementById('js-geo-nodes');
if (!el) {
return;
return false;
}
// eslint-disable-next-line no-new
new Vue({
return new Vue({
el,
components: {
geoNodesApp,
......@@ -28,7 +27,7 @@ export default () => {
const nodeActionsAllowed = convertPermissionToBoolean(dataset.nodeActionsAllowed);
const nodeEditAllowed = convertPermissionToBoolean(dataset.nodeEditAllowed);
const store = new GeoNodesStore(dataset.primaryVersion, dataset.primaryRevision);
const service = new GeoNodesService(dataset.nodeDetailsPath);
const service = new GeoNodesService();
return {
store,
......
......@@ -3,8 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
export default class GeoNodesService {
constructor(nodeDetailsBasePath) {
this.geoNodeDetailsBasePath = nodeDetailsBasePath;
constructor() {
this.geoNodesPath = Api.buildUrl(Api.geoNodesPath);
}
......@@ -12,8 +11,29 @@ export default class GeoNodesService {
return axios.get(this.geoNodesPath);
}
getGeoNodeDetails(nodeId) {
const geoNodeDetailsPath = `${this.geoNodeDetailsBasePath}/${nodeId}/status.json`;
return axios.get(geoNodeDetailsPath);
// eslint-disable-next-line class-methods-use-this
getGeoNodeDetails(node) {
return axios.get(node.statusPath, {
params: {
refresh: true,
},
});
}
// eslint-disable-next-line class-methods-use-this
toggleNode(node) {
return axios.put(node.basePath, {
enabled: !node.enabled, // toggle from existing status
});
}
// eslint-disable-next-line class-methods-use-this
repairNode(node) {
return axios.post(node.repairPath);
}
// eslint-disable-next-line class-methods-use-this
removeNode(node) {
return axios.delete(node.basePath);
}
}
......@@ -8,7 +8,9 @@ export default class GeoNodesStore {
}
setNodes(nodes) {
this.state.nodes = nodes;
this.state.nodes = nodes.map(
node => GeoNodesStore.formatNode(node),
);
}
getNodes() {
......@@ -19,6 +21,16 @@ export default class GeoNodesStore {
this.state.nodeDetails[nodeId] = GeoNodesStore.formatNodeDetails(nodeDetails);
}
removeNode(node) {
const indexOfRemovedNode = this.state.nodes.indexOf(node);
if (indexOfRemovedNode > -1) {
this.state.nodes.splice(indexOfRemovedNode, 1);
if (this.state.nodeDetails[node.id]) {
delete this.state.nodeDetails[node.id];
}
}
}
getPrimaryNodeVersion() {
return {
version: this.state.primaryVersion,
......@@ -30,6 +42,22 @@ export default class GeoNodesStore {
return this.state.nodeDetails[nodeId];
}
static formatNode(rawNode) {
const { id, url, primary, current, enabled } = rawNode;
return {
id,
url,
primary,
current,
enabled,
nodeActionActive: false,
basePath: rawNode._links.self,
repairPath: rawNode._links.repair,
editPath: rawNode.web_edit_url,
statusPath: rawNode._links.status,
};
}
static formatNodeDetails(rawNodeDetails) {
return {
id: rawNodeDetails.geo_node_id,
......@@ -41,8 +69,9 @@ export default class GeoNodesStore {
primaryVersion: rawNodeDetails.primaryVersion,
primaryRevision: rawNodeDetails.primaryRevision,
replicationSlotWAL: rawNodeDetails.replication_slots_max_retained_wal_bytes,
missingOAuthApplication: rawNodeDetails.missing_oauth_application,
missingOAuthApplication: rawNodeDetails.missing_oauth_application || false,
storageShardsMatch: rawNodeDetails.storage_shards_match,
syncStatusUnavailable: rawNodeDetails.sync_status_unavailable || false,
replicationSlots: {
totalCount: rawNodeDetails.replication_slots_count || 0,
successCount: rawNodeDetails.replication_slots_used_count || 0,
......
......@@ -83,12 +83,12 @@ export default {
this.editor.attachModel(this.model);
this.model.onChange((model) => {
const { file } = this.model;
const { file } = model;
if (file.active) {
this.changeFileContent({
file,
content: model.getValue(),
content: model.getModel().getValue(),
});
}
});
......
......@@ -61,14 +61,14 @@ export default class Model {
this.events.set(
this.path,
this.disposable.add(
this.model.onDidChangeContent(e => cb(this.model, e)),
this.model.onDidChangeContent(e => cb(this, e)),
),
);
}
updateContent(content) {
this.getModel().setValue(content);
this.getOriginalModel().setValue(content);
this.getModel().setValue(content);
}
dispose() {
......
......@@ -89,12 +89,17 @@ export const updateFilesAfterCommit = (
lastCommit,
}, { root: true });
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.content);
commit(rootTypes.SET_FILE_RAW_DATA, {
file: entry,
raw: entry.content,
}, { root: true });
eventHub.$emit(`editor.update.model.content.${entry.path}`, entry.raw);
commit(rootTypes.TOGGLE_FILE_CHANGED, {
file: entry,
changed: false,
}, { root: true });
});
commit(rootTypes.REMOVE_ALL_CHANGES_FILES, null, { root: true });
......
......@@ -39,6 +39,7 @@ export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
export const ADD_FILE_TO_CHANGED = 'ADD_FILE_TO_CHANGED';
export const REMOVE_FILE_FROM_CHANGED = 'REMOVE_FILE_FROM_CHANGED';
export const TOGGLE_FILE_CHANGED = 'TOGGLE_FILE_CHANGED';
// Viewer mutation types
export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
......
......@@ -79,4 +79,9 @@ export default {
state.changedFiles.splice(indexOfChangedFile, 1);
},
[types.TOGGLE_FILE_CHANGED](state, { file, changed }) {
Object.assign(file, {
changed,
});
},
};
......@@ -5,6 +5,9 @@ import initNewEpic from 'ee/epics/new_epic/new_epic_bundle';
document.addEventListener('DOMContentLoaded', () => {
initFilteredSearch({
page: 'epics',
isGroup: true,
isGroupAncestor: true,
isGroupDecendent: true,
filteredSearchTokenKeys: FilteredSearchTokenKeysEpics,
stateFiltersSelector: '.epics-state-filters',
});
......
......@@ -13,7 +13,7 @@
}
.health-message {
padding: 4px 8px 1px;
padding: 2px 8px;
background-color: $red-100;
color: $red-500;
border-radius: $border-radius-default;
......@@ -29,9 +29,9 @@
background: $white-light;
}
&.node-disabled,
&.node-disabled:hover {
background-color: $gray-lightest;
&.node-action-active {
pointer-events: none;
opacity: 0.5;
}
}
}
......
......@@ -37,50 +37,6 @@ class Admin::GeoNodesController < Admin::ApplicationController
end
end
def destroy
@node.destroy
redirect_to admin_geo_nodes_path, status: 302, notice: 'Node was successfully removed.'
end
def repair
if !@node.missing_oauth_application?
flash[:notice] = "This node doesn't need to be repaired."
elsif @node.repair
flash[:notice] = 'Node Authentication was successfully repaired.'
else
flash[:alert] = 'There was a problem repairing Node Authentication.'
end
redirect_to admin_geo_nodes_path
end
def toggle
if @node.primary?
flash[:alert] = "Primary node can't be disabled."
else
if @node.toggle!(:enabled)
new_status = @node.enabled? ? 'enabled' : 'disabled'
flash[:notice] = "Node #{@node.url} was successfully #{new_status}."
else
action = @node.enabled? ? 'disabling' : 'enabling'
flash[:alert] = "There was a problem #{action} node #{@node.url}."
end
end
redirect_to admin_geo_nodes_path
end
def status
status = Geo::NodeStatusFetchService.new.call(@node)
respond_to do |format|
format.json do
render json: GeoNodeStatusSerializer.new.represent(status)
end
end
end
private
def geo_node_params
......
# Shared actions between Groups::BoardsController and Projects::BoardsController
module EE
module Boards
module BoardsController
include ::Gitlab::Utils::StrongMemoize
extend ActiveSupport::Concern
prepended do
before_action :check_multiple_issue_boards_available!, only: [:create]
before_action :authorize_create_board!, only: [:create]
before_action :authorize_admin_board!, only: [:create, :update, :destroy]
before_action :find_board, only: [:update, :destroy]
end
def create
......@@ -24,28 +25,26 @@ module EE
end
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def update
service = ::Boards::UpdateService.new(parent, current_user, board_params)
service.execute(@board)
service.execute(board)
respond_to do |format|
format.json do
if @board.valid?
extra_json = { board_path: board_path(@board) }
render json: serialize_as_json(@board).merge(extra_json)
if board.valid?
extra_json = { board_path: board_path(board) }
render json: serialize_as_json(board).merge(extra_json)
else
render json: @board.errors, status: :unprocessable_entity
render json: board.errors, status: :unprocessable_entity
end
end
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def destroy
service = ::Boards::DestroyService.new(parent, current_user)
service.execute(@board) # rubocop:disable Gitlab/ModuleWithInstanceVariables
service.execute(board)
respond_to do |format|
format.json { head :ok }
......@@ -55,36 +54,22 @@ module EE
private
def authorize_admin_board!
return render_404 unless can?(current_user, :admin_board, parent)
end
def board_params
params.require(:board).permit(:name, :weight, :milestone_id, :assignee_id, label_ids: [])
end
def find_board
@board = parent.boards.find(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def parent
@parent ||= @project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
def board
strong_memoize(:board) do
parent.boards.find(params[:id])
end
end
def boards_path
if @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
group_boards_path(parent)
def authorize_create_board!
if group?
check_multiple_group_issue_boards_available!
else
project_boards_path(parent)
check_multiple_project_issue_boards_available!
end
end
def board_path(board)
if @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
group_board_path(parent, board)
else
project_board_path(parent, board)
end
def authorize_admin_board!
return render_404 unless can?(current_user, :admin_board, parent)
end
def serialize_as_json(resource)
......
module EE
module Boards
module IssuesController
extend ActiveSupport::Concern
include ControllerWithCrossProjectAccessCheck
prepended do
requires_cross_project_access if: -> { board.group_board? }
end
def issues_finder
return super unless board.group_board?
::IssuesFinder.new(current_user, group_id: board_parent.id)
end
def project
@project ||= begin
if board.group_board?
::Project.find(issue_params[:project_id])
else
super
end
end
end
end
end
end
module EE
module BoardsResponses
# Shared authorizations between projects and groups which
# have different policies on EE.
def authorize_read_list
ability = board.group_board? ? :read_group : :read_list
authorize_action_for!(board.parent, ability)
end
def authorize_read_issue
ability = board.group_board? ? :read_group : :read_issue
authorize_action_for!(board.parent, ability)
end
end
end
......@@ -17,7 +17,13 @@ class EpicsFinder < IssuableFinder
end
def row_count
execute.count
count = execute.count
# When filtering by multiple labels, count returns a hash of
# records grouped by id - so we just have to get length of the Hash.
# Once we have state for epics, we can use default issuables row_count
# method.
count.is_a?(Hash) ? count.length : count
end
# we don't have states for epics for now this method (#4017)
......
......@@ -6,7 +6,7 @@ module EE
def board_data
show_feature_promotion = (@project && show_promotions? &&
(!@project.feature_available?(:multiple_issue_boards) ||
(!@project.feature_available?(:multiple_project_issue_boards) ||
!@project.feature_available?(:scoped_issue_board) ||
!@project.feature_available?(:issue_board_focus_mode)))
......@@ -24,12 +24,6 @@ module EE
super.merge(data)
end
def build_issue_link_base
return super unless @board.group_board?
"#{group_path(@board.group)}/:project_path/issues"
end
def current_board_json
board = @board || @boards.first
......@@ -43,42 +37,8 @@ module EE
)
end
def board_base_url
if board.group_board?
group_boards_url(@group)
else
super
end
end
def current_board_path(board)
@current_board_path ||= begin
if board.group_board?
group_board_path(current_board_parent, board)
else
super(board)
end
end
end
def current_board_parent
@current_board_parent ||= @group || super
end
def can_admin_issue?
can?(current_user, :admin_issue, current_board_parent)
end
def board_list_data
super.merge(group_path: @group&.path)
end
def board_sidebar_user_data
super.merge(group_id: @group&.id)
end
def boards_link_text
if @project.multiple_issue_boards_available?(current_user)
if parent.multiple_issue_boards_available?
s_("IssueBoards|Boards")
else
s_("IssueBoards|Board")
......
......@@ -13,7 +13,6 @@ module EE
{
primary_version: version.to_s,
primary_revision: revision.to_s,
node_details_path: admin_geo_nodes_path.to_s,
node_actions_allowed: ::Gitlab::Database.read_write?.to_s,
node_edit_allowed: ::Gitlab::Geo.license_allows?.to_s
}
......
module EpicsHelper
def epic_meta_data
author = @epic.author
def epic_show_app_data(epic, opts)
author = epic.author
group = epic.group
data = {
created: @epic.created_at,
epic_meta = {
created: epic.created_at,
author: {
name: author.name,
url: user_path(author),
username: "@#{author.username}",
src: avatar_icon_for_user(@epic.author)
src: opts[:author_icon]
},
start_date: @epic.start_date,
end_date: @epic.end_date
start_date: epic.start_date,
end_date: epic.end_date
}
data.to_json
{
initial: opts[:initial].merge(labels: epic.labels).to_json,
meta: epic_meta.to_json,
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group)
}
end
def epic_endpoint_query_params(opts)
opts[:data] ||= {}
opts[:data][:endpoint_query_params] = {
only_group_labels: true,
include_ancestor_groups: true,
include_descendant_groups: true
}.to_json
opts
end
end
......@@ -6,7 +6,6 @@ module EE
EMPTY_SCOPE_STATE = [nil, -1].freeze
prepended do
belongs_to :group
belongs_to :milestone
has_many :board_labels
......@@ -20,19 +19,6 @@ module EE
has_many :labels, through: :board_labels
validates :name, presence: true
validates :group, presence: true, unless: :project
end
def project_needed?
!group
end
def parent
@parent ||= group || project
end
def group_board?
group_id.present?
end
def milestone
......
......@@ -5,9 +5,9 @@ module EE
# and be included in the `Group` model
module Group
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
included do
has_many :boards
has_many :epics
state_machine :ldap_sync_status, namespace: :ldap_sync, initial: :ready do
......@@ -62,5 +62,10 @@ module EE
def project_creation_level
super || ::Gitlab::CurrentSettings.default_project_creation
end
override :multiple_issue_boards_available?
def multiple_issue_boards_available?
feature_available?(:multiple_group_issue_boards)
end
end
end
module EE
module Label
extend ActiveSupport::Concern
prepended do
scope :on_group_boards, ->(group_id) { with_lists_and_board.where(boards: { group_id: group_id }) }
end
end
end
......@@ -223,6 +223,11 @@ module EE
end
end
override :multiple_issue_boards_available?
def multiple_issue_boards_available?
feature_available?(:multiple_project_issue_boards)
end
def service_desk_enabled
::EE::Gitlab::ServiceDesk.enabled?(project: self) && super
end
......
......@@ -20,6 +20,12 @@ module EE
end
end
after_transition started: :failed do |project, _|
if project.mirror? && project.mirror_hard_failed?
::NotificationService.new.mirror_was_hard_failed(project)
end
end
after_transition [:scheduled, :started] => [:finished, :failed] do |project, _|
::Gitlab::Mirror.decrement_capacity(project.id) if project.mirror?
end
......
......@@ -24,7 +24,7 @@ class License < ActiveRecord::Base
merge_request_squash
multiple_ldap_servers
multiple_issue_assignees
multiple_issue_boards
multiple_project_issue_boards
push_rules
protected_refs_for_users
related_issues
......@@ -42,10 +42,10 @@ class License < ActiveRecord::Base
extended_audit_events
file_locks
geo
group_issue_boards
jira_dev_panel_integration
ldap_group_sync_filter
multiple_clusters
multiple_group_issue_boards
merge_request_performance_metrics
object_storage
service_desk
......@@ -87,7 +87,8 @@ class License < ActiveRecord::Base
merge_request_approvers
merge_request_squash
multiple_issue_assignees
multiple_issue_boards
multiple_project_issue_boards
multiple_group_issue_boards
protected_refs_for_users
push_rules
related_issues
......
......@@ -20,7 +20,6 @@ module EE
rule { reporter }.policy do
enable :admin_list
enable :admin_board
enable :admin_issue
end
condition(:can_owners_manage_ldap, scope: :global) do
......
......@@ -5,7 +5,7 @@ module EE
override :can_create_board?
def can_create_board?
parent.feature_available?(:multiple_issue_boards) || super
parent.multiple_issue_boards_available? || super
end
end
end
......
......@@ -2,14 +2,6 @@ module EE
module Boards
module Issues
module ListService
def set_parent
if parent.is_a?(Group)
params[:group_id] = parent.id
else
super
end
end
def issues_label_links
if has_valid_milestone?
super.where("issues.milestone_id = ?", board.milestone_id)
......
......@@ -5,7 +5,7 @@ module EE
override :execute
def execute
if parent.multiple_issue_boards_available?(current_user)
if parent.multiple_issue_boards_available?
super
else
super.limit(1)
......
module EE
module Boards
module Lists
module CreateService
def available_labels_for(board)
if board.group_board?
parent.labels
else
super
end
end
end
end
end
end
module EE
module Boards
module MoveService
def remove_label_ids
label_ids =
if moving_to_list.movable?
moving_from_list.label_id
elsif board.group_board?
::Label.on_group_boards(parent.id).pluck(:label_id)
else
::Label.on_project_boards(parent.id).pluck(:label_id)
end
Array(label_ids).compact
end
end
end
end
......@@ -20,7 +20,19 @@ module EE
return if note.author == support_bot
return unless issue.subscribed?(support_bot, issue.project)
Notify.service_desk_new_note_email(issue.id, note.id).deliver_later
mailer.service_desk_new_note_email(issue.id, note.id).deliver_later
end
def mirror_was_hard_failed(project)
recipients = project.members.owners_and_masters
unless recipients.present?
recipients = project.group.members.owners_and_masters
end
recipients.each do |recipient|
mailer.mirror_was_hard_failed_email(project.id, recipient.user.id).deliver_later
end
end
end
end
......@@ -12,3 +12,7 @@
&middot;
opened #{time_ago_with_tooltip(epic.created_at, placement: 'bottom')}
by #{link_to_member(@group, epic.author, avatar: false)}
- if epic.labels.any?
&nbsp;
- epic.labels.each do |label|
= link_to render_colored_label(label, tooltip: true), group_epics_path(@group, label_name:[label.name]), class: 'label-link'
......@@ -15,4 +15,4 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
#epic-show-app{ data: { initial: issuable_initial_data(@epic).to_json, meta: epic_meta_data } }
#epic-show-app{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
%p
Repository mirroring on #{@project.full_path} has been paused due to too many failures. The last failure was:
%pre
= @project.import_error
%p
To resume mirroring update your #{link_to("repository mirroring settings", project_settings_repository_path(@project))}.
Repository mirroring on <%= @project.full_path %> has been paused due to too many failures. The last failure was:
<%= @project.import_error %>
To resume mirroring update your repository settings at <%= project_settings_repository_url(@project) %>.
......@@ -28,7 +28,7 @@
- if can?(current_user, :admin_board, parent)
.dropdown-footer
%ul.dropdown-footer-list
- if parent.feature_available?(:multiple_issue_boards)
- if parent.multiple_issue_boards_available?
%li
%a{ "href" => "#", "v-on:click.prevent" => "showPage('new')" }
Create new board
......
......@@ -19,7 +19,7 @@
.scroll-container
%ul.tokens-container.list-unstyled
%li.input-token
%input.form-control.filtered-search{ search_filter_input_options(type) }
%input.form-control.filtered-search{ epic_endpoint_query_params(search_filter_input_options(type)) }
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
......@@ -46,6 +46,18 @@
= render 'shared/issuable/user_dropdown_item',
user: User.new(username: '{{username}}', name: '{{name}}'),
avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-label.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link{ type: 'button' }
= _("No Label")
%li.divider.droplab-item-ignore
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
%li.filter-dropdown-item{ type: 'button' }
%button.btn.btn-link
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{ title }}
%button.clear-search.hidden{ type: 'button' }
= icon('times')
......
......@@ -9,7 +9,7 @@
- else
= _('Improve Issue boards with GitLab Enterprise Edition.')
%ul
- unless @project.feature_available?(:multiple_issue_boards)
- unless @project.multiple_issue_boards_available?
%li
= link_to _('Multiple issue boards'), help_page_path('user/project/issue_board.html', anchor:'use-cases-for-multiple-issue-boards'), target: '_blank'
- unless @project.feature_available?(:scoped_issue_board)
......
......@@ -62,10 +62,9 @@ class RepositoryUpdateMirrorWorker
end
def fail_mirror(project, message)
error_message = "Mirror update for #{project.full_path} failed with the following message: #{message}"
project.mark_import_as_failed(error_message)
project.mark_import_as_failed(message)
Rails.logger.error(error_message)
Rails.logger.error("Mirror update for #{project.full_path} failed with the following message: #{message}")
Gitlab::Metrics.add_event(:mirrors_failed, path: project.full_path)
end
......
---
title: Allow to add or remove labels from epics and filter epics by labels
merge_request: 4773
author:
type: added
---
title: Repository mirroring notifies when hard failed
merge_request: 4699
author:
type: added
---
title: Fixes and enhancements for Geo admin dashboard
merge_request: 4536
author:
type: fixed
---
title: Allow adding or removing labels from epics and filter epics by labels
merge_request:
author:
type: added
......@@ -66,6 +66,8 @@ module API
strong_memoize(:geo_node_status) do
if geo_node.current?
GeoNodeStatus.current_node_status
elsif to_boolean(declared_params(include_missing: false)[:refresh])
::Geo::NodeStatusFetchService.new.call(geo_node)
else
geo_node.status
end
......@@ -93,6 +95,9 @@ module API
desc 'Get metrics for a single Geo node' do
success EE::API::Entities::GeoNodeStatus
end
params do
optional :refresh, type: Boolean, desc: 'Attempt to fetch the latest status from the Geo node directly, ignoring the cache'
end
get 'status' do
not_found!('GeoNode') unless geo_node
......@@ -145,6 +150,20 @@ module API
render_validation_error!(geo_node)
end
end
# Delete an existing Geo node
#
# Example request:
# DELETE /geo_nodes/:id
desc 'Delete an existing Geo secondary node' do
success EE::API::Entities::GeoNode
end
delete do
not_found!('GeoNode') unless geo_node
geo_node.destroy!
status 204
end
end
end
end
......
......@@ -6,7 +6,7 @@ module EE
included do
helpers do
def create_board
forbidden! unless ::License.feature_available?(:multiple_issue_boards)
forbidden! unless board_parent.multiple_issue_boards_available?
board =
::Boards::CreateService.new(board_parent, current_user, { name: params[:name] }).execute
......@@ -15,7 +15,7 @@ module EE
end
def delete_board
forbidden! unless ::License.feature_available?(:multiple_issue_boards)
forbidden! unless board_parent.multiple_issue_boards_available?
destroy_conditionally!(board) do |board|
service = ::Boards::DestroyService.new(board_parent, current_user)
......
......@@ -224,11 +224,19 @@ module EE
'http'
end
expose :web_edit_url do |geo_node|
::Gitlab::Routing.url_helpers.edit_admin_geo_node_url(geo_node)
end
expose :_links do
expose :self do |geo_node|
expose_url api_v4_geo_nodes_path(id: geo_node.id)
end
expose :status do |geo_node|
expose_url api_v4_geo_nodes_status_path(id: geo_node.id)
end
expose :repair do |geo_node|
expose_url api_v4_geo_nodes_repair_path(id: geo_node.id)
end
......
module EE
module API
class GroupBoards < ::Grape::API
include ::API::PaginationParams
include ::API::BoardsResponses
include BoardsResponses
before do
authenticate!
end
helpers do
def board_parent
user_group
end
end
params do
requires :id, type: String, desc: 'The ID of a group'
end
resource :groups, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
segment ':id/boards' do
desc 'Create a group board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
end
params do
requires :name, type: String, desc: 'The board name'
end
post '/' do
authorize!(:admin_board, board_parent)
create_board
end
desc 'Delete a group board' do
detail 'This feature was introduced in 10.4'
success ::API::Entities::Board
end
delete '/:board_id' do
authorize!(:admin_board, board_parent)
delete_board
end
end
end
end
end
end
module EE
module API
module JobArtifacts
extend ActiveSupport::Concern
prepended do
helpers do
def authorize_download_artifacts!
super
check_cross_project_pipelines_feature!
end
def check_cross_project_pipelines_feature!
if job_token_authentication? && !@project.feature_available?(:cross_project_pipelines)
not_found!('Project')
end
end
end
end
end
end
end
module EE
module API
module MergeRequests
extend ActiveSupport::Concern
class_methods do
def update_params_at_least_one_of
super.push(*%i[
squash
])
end
end
prepended do
helpers do
params :merge_params_ee do
optional :squash, type: Grape::API::Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
end
params :optional_params_ee do
optional :approvals_before_merge, type: Integer, desc: 'Number of approvals required before this can be merged'
use :merge_params_ee
end
def update_merge_request_ee(merge_request)
if params[:squash] && merge_request.project.feature_available?(:merge_request_squash)
merge_request.update(squash: params[:squash])
end
end
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: ::API::API::PROJECT_ENDPOINT_REQUIREMENTS do
# Get the status of the merge request's approvals
#
# Parameters:
# id (required) - The ID of a project
# merge_request_idd (required) - IID of MR
# Examples:
# GET /projects/:id/merge_requests/:merge_request_iid/approvals
#
desc "List a merge request's approvals" do
success EE::API::Entities::MergeRequestApprovals
end
get ':id/merge_requests/:merge_request_iid/approvals' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
# Approve a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_iid (required) - IID of MR
# Examples:
# POST /projects/:id/merge_requests/:merge_request_iid/approve
#
desc 'Approve a merge request' do
success EE::API::Entities::MergeRequestApprovals
end
params do
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
post ':id/merge_requests/:merge_request_iid/approve' do
merge_request = find_project_merge_request(params[:merge_request_iid])
unauthorized! unless merge_request.can_approve?(current_user)
check_sha_param!(params, merge_request)
::MergeRequests::ApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
desc 'Remove an approval from a merge request' do
success EE::API::Entities::MergeRequestApprovals
end
post ':id/merge_requests/:merge_request_iid/unapprove' do
merge_request = find_project_merge_request(params[:merge_request_iid])
not_found! unless merge_request.has_approved?(current_user)
::MergeRequests::RemoveApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
end
end
end
end
end
......@@ -55,34 +55,6 @@ describe Admin::GeoNodesController, :postgresql do
end
end
describe '#destroy' do
let!(:geo_node) { create(:geo_node) }
def go
delete(:destroy, id: geo_node)
end
context 'without add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(false)
end
it 'deletes the node' do
expect { go }.to change { GeoNode.count }.by(-1)
end
end
context 'with add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
end
it 'deletes the node' do
expect { go }.to change { GeoNode.count }.by(-1)
end
end
end
describe '#create' do
let(:geo_node_attributes) { { url: 'http://example.com' } }
......@@ -149,126 +121,4 @@ describe Admin::GeoNodesController, :postgresql do
end
end
end
describe '#repair' do
let(:geo_node) { create(:geo_node) }
def go
post :repair, id: geo_node
end
before do
allow(Gitlab::Geo).to receive(:license_allows?) { false }
go
end
it_behaves_like 'unlicensed geo action'
end
describe '#toggle' do
context 'without add-on license' do
let(:geo_node) { create(:geo_node) }
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(false)
post :toggle, id: geo_node
end
it_behaves_like 'unlicensed geo action'
end
context 'with add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
end
context 'with a primary node' do
before do
post :toggle, id: geo_node
end
let(:geo_node) { create(:geo_node, :primary, enabled: true) }
it 'does not disable the node' do
expect(geo_node.reload).to be_enabled
end
it 'displays a flash message' do
expect(controller).to set_flash[:alert].to("Primary node can't be disabled.")
end
it 'redirects to the geo nodes page' do
expect(response).to redirect_to(admin_geo_nodes_path)
end
end
context 'with a secondary node' do
let(:geo_node) { create(:geo_node, url: 'http://example.com') }
context 'when succeed' do
before do
post :toggle, id: geo_node
end
it 'disables the node' do
expect(geo_node.reload).not_to be_enabled
end
it 'displays a flash message' do
expect(controller).to set_flash[:notice].to('Node http://example.com/ was successfully disabled.')
end
it 'redirects to the geo nodes page' do
expect(response).to redirect_to(admin_geo_nodes_path)
end
end
context 'when fail' do
before do
allow_any_instance_of(GeoNode).to receive(:toggle!).and_return(false)
post :toggle, id: geo_node
end
it 'does not disable the node' do
expect(geo_node.reload).to be_enabled
end
it 'displays a flash message' do
expect(controller).to set_flash[:alert].to('There was a problem disabling node http://example.com/.')
end
it 'redirects to the geo nodes page' do
expect(response).to redirect_to(admin_geo_nodes_path)
end
end
end
end
end
describe '#status' do
let(:geo_node) { create(:geo_node) }
context 'without add-on license' do
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(false)
get :status, id: geo_node, format: :json
end
it_behaves_like 'unlicensed geo action'
end
context 'with add-on license' do
let(:geo_node_status) { build(:geo_node_status, :healthy, geo_node: geo_node) }
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
allow_any_instance_of(Geo::NodeStatusFetchService).to receive(:call).and_return(geo_node_status)
end
it 'returns the status' do
get :status, id: geo_node, format: :json
expect(response).to match_response_schema('public_api/v4/geo_node_status', dir: 'ee')
end
end
end
end
......@@ -8,7 +8,7 @@ describe Groups::BoardsController do
allow(Ability).to receive(:allowed?).and_call_original
group.add_master(user)
sign_in(user)
stub_licensed_features(group_issue_boards: true)
stub_licensed_features(multiple_group_issue_boards: true)
end
describe 'GET index' do
......@@ -16,28 +16,6 @@ describe Groups::BoardsController do
expect { list_boards }.to change(group.boards, :count).by(1)
end
context 'when format is HTML' do
it 'renders template' do
list_boards
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
list_boards
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
it 'returns a list of group boards' do
create(:board, group: group, milestone: create(:milestone, group: group))
......@@ -73,71 +51,4 @@ describe Groups::BoardsController do
get :index, group_id: group, format: format
end
end
describe 'GET show' do
let!(:board) { create(:board, group: group) }
context 'when format is HTML' do
it 'renders template' do
read_board board: board
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
it 'returns project board' do
read_board board: board, format: :json
expect(response).to match_response_schema('board')
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board, format: :json
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
context 'when board does not belong to group' do
it 'returns a not found 404 response' do
another_board = create(:board)
read_board board: another_board
expect(response).to have_gitlab_http_status(404)
end
end
it_behaves_like 'disabled when using an external authorization service' do
subject { read_board board: board }
end
def read_board(board:, format: :html)
get :show, group_id: group,
id: board.to_param,
format: format
end
end
end
......@@ -83,7 +83,10 @@ describe 'admin Geo Nodes', :js do
it 'removes an existing Geo Node' do
page.within(find('.geo-node-actions', match: :first)) do
page.click_link('Remove')
page.click_button('Remove')
end
page.within('.modal') do
page.click_button('Remove')
end
expect(current_path).to eq admin_geo_nodes_path
......
......@@ -8,10 +8,6 @@ describe 'Multiple Issue Boards', :js do
let!(:board2) { create(:board, project: project) }
context 'with multiple issue boards enabled' do
before do
stub_licensed_features(multiple_issue_boards: true)
end
context 'authorized user' do
before do
project.add_master(user)
......@@ -150,7 +146,7 @@ describe 'Multiple Issue Boards', :js do
context 'with multiple issue boards disabled' do
before do
stub_licensed_features(multiple_issue_boards: false)
stub_licensed_features(multiple_project_issue_boards: false)
project.add_master(user)
login_as(user)
......
......@@ -23,7 +23,6 @@ describe 'Scoped issue boards', :js do
before do
allow_any_instance_of(ApplicationHelper).to receive(:collapsed_sidebar?).and_return(true)
stub_licensed_features(multiple_issue_boards: true)
stub_licensed_features(scoped_issue_boards: true)
end
......@@ -86,7 +85,7 @@ describe 'Scoped issue boards', :js do
end
it 'only shows group labels in list on group boards' do
stub_licensed_features(group_issue_boards: true)
stub_licensed_features(multiple_group_issue_boards: true)
visit group_boards_path(group)
wait_for_requests
......
......@@ -130,4 +130,22 @@ describe EpicsFinder do
end
end
end
describe '#row_count' do
let(:label) { create(:label) }
let(:label2) { create(:label) }
let!(:labeled_epic) { create(:labeled_epic, group: group, labels: [label]) }
let!(:labeled_epic2) { create(:labeled_epic, group: group, labels: [label, label2]) }
before do
group.add_developer(search_user)
stub_licensed_features(epics: true)
end
it 'returns number of rows when epics are grouped' do
params = { group_id: group.id, label_name: [label.title, label2.title] }
expect(described_class.new(search_user, params).row_count).to eq(1)
end
end
end
......@@ -20,11 +20,13 @@
"files_max_capacity": { "type": "integer" },
"repos_max_capacity": { "type": "integer" },
"clone_protocol": { "type": ["string"] },
"web_edit_url": { "type": "string" },
"_links": {
"type": "object",
"required": ["self", "repair"],
"properties" : {
"self": { "type": "string" },
"status": { "type": "string" },
"repair": { "type": "string" }
},
"additionalProperties": false
......
......@@ -3,18 +3,32 @@ require 'spec_helper'
describe EpicsHelper do
include ApplicationHelper
describe '#epic_meta_data' do
describe '#epic_show_app_data' do
it 'returns the correct json' do
user = create(:user)
@epic = create(:epic, author: user)
expect(JSON.parse(epic_meta_data).keys).to match_array(%w[created author start_date end_date])
expect(JSON.parse(epic_meta_data)['author']).to eq({
data = epic_show_app_data(@epic, initial: {}, author_icon: 'icon_path')
meta_data = JSON.parse(data[:meta])
expected_keys = %i(initial meta namespace labels_path labels_web_url epics_web_url)
expect(data.keys).to match_array(expected_keys)
expect(meta_data.keys).to match_array(%w[created author start_date end_date])
expect(meta_data['author']).to eq({
'name' => user.name,
'url' => "/#{user.username}",
'username' => "@#{user.username}",
'src' => "#{avatar_icon_for_user(user)}"
'src' => 'icon_path'
})
end
end
describe '#epic_endpoint_query_params' do
it 'it includes epic specific options in JSON format' do
opts = epic_endpoint_query_params({})
expected = "{\"only_group_labels\":true,\"include_ancestor_groups\":true,\"include_descendant_groups\":true}"
expect(opts[:data][:endpoint_query_params]).to eq(expected)
end
end
end
......@@ -102,6 +102,17 @@ describe Project do
end
end
describe 'hard failing a mirror' do
it 'sends a notification' do
project = create(:project, :mirror, :import_started)
project.mirror_data.update_attributes(retry_count: Gitlab::Mirror::MAX_RETRY)
expect_any_instance_of(EE::NotificationService).to receive(:mirror_was_hard_failed).with(project)
project.import_fail
end
end
describe '#push_rule' do
let(:project) { create(:project, push_rule: create(:push_rule)) }
......
require 'spec_helper'
describe API::Boards do
set(:user) { create(:user) }
set(:user) { create(:user) }
set(:board_parent) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
set(:milestone) { create(:milestone, project: board_parent) }
set(:board) { create(:board, project: board_parent, milestone: milestone) }
......
......@@ -34,9 +34,11 @@ describe API::GeoNodes, :geo, api: true do
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/geo_node', dir: 'ee')
expect(json_response['web_edit_url']).to end_with("/admin/geo_nodes/#{primary.id}/edit")
links = json_response['_links']
expect(links['self']).to end_with("/api/v4/geo_nodes/#{primary.id}")
expect(links['status']).to end_with("/api/v4/geo_nodes/#{primary.id}/status")
expect(links['repair']).to end_with("/api/v4/geo_nodes/#{primary.id}/repair")
end
......@@ -99,6 +101,32 @@ describe API::GeoNodes, :geo, api: true do
expect(response).to match_response_schema('public_api/v4/geo_node_status', dir: 'ee')
end
it 'fetches the real-time status with `refresh=true`' do
stub_current_geo_node(primary)
new_status = build(:geo_node_status, :healthy, geo_node: secondary, attachments_count: 923, lfs_objects_count: 652)
expect(GeoNode).to receive(:find).and_return(secondary)
expect_any_instance_of(Geo::NodeStatusFetchService).to receive(:call).and_return(new_status)
get api("/geo_nodes/#{secondary.id}/status", admin), refresh: true
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/geo_node_status', dir: 'ee')
expect(json_response['attachments_count']).to eq(923)
expect(json_response['lfs_objects_count']).to eq(652)
end
it 'returns 404 when no Geo Node status is not found' do
stub_current_geo_node(primary)
secondary_status.destroy!
expect(GeoNode).to receive(:find).and_return(secondary)
get api("/geo_nodes/#{secondary.id}/status", admin)
expect(response).to have_gitlab_http_status(404)
end
it_behaves_like '404 response' do
let(:request) { get api("/geo_nodes/#{unexisting_node_id}/status", admin) }
end
......@@ -149,7 +177,7 @@ describe API::GeoNodes, :geo, api: true do
describe 'PUT /geo_nodes/:id' do
it_behaves_like '404 response' do
let(:request) { get api("/geo_nodes/#{unexisting_node_id}/status", admin) }
let(:request) { put api("/geo_nodes/#{unexisting_node_id}", admin), {} }
end
it 'denies access if not admin' do
......@@ -174,6 +202,32 @@ describe API::GeoNodes, :geo, api: true do
end
end
describe 'DELETE /geo_nodes/:id' do
it_behaves_like '404 response' do
let(:request) { delete api("/geo_nodes/#{unexisting_node_id}", admin) }
end
it 'denies access if not admin' do
delete api("/geo_nodes/#{secondary.id}", user)
expect(response).to have_gitlab_http_status(403)
end
it 'deletes the node' do
delete api("/geo_nodes/#{secondary.id}", admin)
expect(response).to have_gitlab_http_status(204)
end
it 'returns 400 if Geo Node could not be deleted' do
allow_any_instance_of(GeoNode).to receive(:destroy!).and_raise(StandardError, 'Something wrong')
delete api("/geo_nodes/#{secondary.id}", admin)
expect(response).to have_gitlab_http_status(500)
end
end
describe 'GET /geo_nodes/current/failures/:type' do
it 'fetches the current node failures' do
create(:geo_project_registry, :sync_failed)
......
......@@ -8,7 +8,6 @@ describe API::GroupBoards do
set(:board_parent) { create(:group, :public) }
before do
stub_licensed_features(group_issue_boards: true)
board_parent.add_owner(user)
end
......@@ -37,7 +36,6 @@ describe API::GroupBoards do
set(:milestone) { create(:milestone, group: board_parent) }
set(:board_label) { create(:group_label, group: board_parent) }
# EE only
set(:board) do
create(:board, group: board_parent,
milestone: milestone,
......@@ -48,16 +46,4 @@ describe API::GroupBoards do
it_behaves_like 'group and project boards', "/groups/:id/boards", true
it_behaves_like 'multiple and scoped issue boards', "/groups/:id/boards"
describe 'POST /groups/:id/boards/lists' do
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
it 'does not create lists for child project labels' do
project_label = create(:label, project: project)
post api(url, user), label_id: project_label.id
expect(response).to have_gitlab_http_status(400)
end
end
end
......@@ -18,7 +18,7 @@ describe Boards::CreateService, services: true do
shared_examples 'boards create service' do
context 'With the feature available' do
before do
stub_licensed_features(multiple_issue_boards: true)
stub_licensed_features(multiple_group_issue_boards: true)
end
context 'with valid params' do
......@@ -68,7 +68,7 @@ describe Boards::CreateService, services: true do
end
it 'skips creating a second board when the feature is not available' do
stub_licensed_features(multiple_issue_boards: false)
stub_licensed_features(multiple_project_issue_boards: false)
service = described_class.new(parent, double)
expect(service.execute).not_to be_nil
......
......@@ -39,16 +39,10 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) }
before do
group.add_developer(user)
end
it 'delegates search to IssuesFinder' do
params = { board_id: board.id, id: list1.id }
expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
let(:parent) { group }
described_class.new(group, user, params).execute
before do
parent.add_developer(user)
end
context 'when list_id is missing' do
......@@ -56,7 +50,7 @@ describe Boards::Issues::ListService, services: true do
it 'returns opened issues without board labels applied' do
params = { board_id: board.id }
issues = described_class.new(group, user, params).execute
issues = described_class.new(parent, user, params).execute
expect(issues).to match_array([opened_issue2, reopened_issue1, opened_issue1])
end
......@@ -67,7 +61,7 @@ describe Boards::Issues::ListService, services: true do
params = { board_id: board.id }
board.update_attribute(:milestone, m1)
issues = described_class.new(group, user, params).execute
issues = described_class.new(parent, user, params).execute
expect(issues).to match_array([opened_issue2, list1_issue2, reopened_issue1, opened_issue1])
end
......@@ -81,7 +75,7 @@ describe Boards::Issues::ListService, services: true do
end
it 'returns open issue for backlog without board label' do
issues = described_class.new(group, user, params).execute
issues = described_class.new(parent, user, params).execute
expect(issues).to match_array([opened_issue2, reopened_issue1, opened_issue1])
end
......@@ -93,7 +87,7 @@ describe Boards::Issues::ListService, services: true do
end
it 'returns open issue for backlog without board label' do
issues = described_class.new(group, user, params).execute
issues = described_class.new(parent, user, params).execute
expect(issues).to match_array([opened_issue2, reopened_issue1, opened_issue1])
end
......@@ -101,56 +95,5 @@ describe Boards::Issues::ListService, services: true do
end
end
end
context 'issues are ordered by priority' do
it 'returns opened issues when list_id is missing' do
params = { board_id: board.id }
issues = described_class.new(group, user, params).execute
expect(issues).to match_array([opened_issue2, reopened_issue1, opened_issue1])
end
it 'returns opened issues when listing issues from Backlog' do
params = { board_id: board.id, id: backlog.id }
issues = described_class.new(group, user, params).execute
expect(issues).to match_array([opened_issue2, reopened_issue1, opened_issue1])
end
it 'returns closed issues when listing issues from Closed' do
params = { board_id: board.id, id: closed.id }
issues = described_class.new(group, user, params).execute
expect(issues).to match_array([closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1])
end
it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { board_id: board.id, id: list1.id }
issues = described_class.new(group, user, params).execute
expect(issues).to match_array([list1_issue3, list1_issue1, list1_issue2])
end
end
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
service = described_class.new(group, user, board_id: board.id, id: list.id)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'with invalid list id' do
it 'raises an error' do
service = described_class.new(group, user, board_id: board.id, id: nil)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
require 'spec_helper'
describe Boards::Issues::MoveService, services: true do
describe '#execute' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:board1) { create(:board, group: group) }
let(:bug) { create(:group_label, group: group, name: 'Bug') }
let(:development) { create(:group_label, group: group, name: 'Development') }
let(:testing) { create(:group_label, group: group, name: 'Testing') }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board1) }
before do
group.add_developer(user)
end
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
it 'delegates the label changes to Issues::UpdateService' do
expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
described_class.new(group, user, params).execute(issue)
end
it 'removes the label from the list it came from and adds the label of the list it goes to' do
described_class.new(group, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, testing)
end
end
context 'when moving to closed' do
let(:board2) { create(:board, group: group) }
let(:regression) { create(:group_label, group: group, name: 'Regression') }
let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.id } }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
described_class.new(group, user, params).execute(issue)
end
it 'removes all list-labels from project boards and close the issue' do
described_class.new(group, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug)
expect(issue).to be_closed
end
end
context 'when moving from closed' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
described_class.new(group, user, params).execute(issue)
end
it 'adds the label of the list it goes to and reopen the issue' do
described_class.new(group, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug, testing)
expect(issue.state).to eq("opened")
end
end
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(group, user, params).execute(issue)).to eq false
end
it 'keeps issues labels' do
described_class.new(group, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, development)
end
it 'sorts issues' do
[issue, issue1, issue2].each do |issue|
issue.move_to_end && issue.save!
end
params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
described_class.new(group, user, params).execute(issue)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end
end
end
end
......@@ -8,14 +8,18 @@ describe Boards::ListService do
end
describe '#execute' do
it 'returns all issue boards when `multiple_issue_boards` is enabled' do
stub_licensed_features(multiple_issue_boards: true)
it 'returns all issue boards when multiple issue boards is enabled' do
if parent.is_a?(Group)
stub_licensed_features(multiple_group_issue_boards: true)
end
expect(service.execute.size).to eq(2)
end
it 'returns the first issue board when `multiple_issue_boards` is disabled' do
stub_licensed_features(multiple_issue_boards: false)
it 'returns the first issue board when multiple issue boards is disabled' do
if parent.is_a?(Project)
stub_licensed_features(multiple_project_issue_boards: false)
end
expect(service.execute.size).to eq(1)
end
......
require 'spec_helper'
describe Boards::Lists::CreateService, services: true do
describe '#execute' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:user) { create(:user) }
let(:label) { create(:group_label, group: group, name: 'in-progress') }
subject(:service) { described_class.new(group, user, label_id: label.id) }
before do
group.add_developer(user)
end
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
expect(list.position).to eq 0
end
end
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
expect(list.position).to eq 0
end
end
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
list = service.execute(board)
expect(list.position).to eq 2
end
end
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
list2 = service.execute(board)
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
end
end
context 'when provided label does not belongs to the group' do
it 'raises an error' do
label = create(:label, name: 'in-development')
service = described_class.new(group, user, label_id: label.id)
expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
end
require 'spec_helper'
describe Boards::Lists::DestroyService, services: true do
describe '#execute' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:user) { create(:user) }
context 'when list type is label' do
it 'removes list from board' do
list = create(:list, board: board)
service = described_class.new(group, user)
expect { service.execute(list) }.to change(board.lists, :count).by(-1)
end
it 'decrements position of higher lists' do
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
closed = board.closed_list
described_class.new(group, user).execute(development)
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(closed.reload.position).to be_nil
end
end
it 'does not remove list from board when list type is closed' do
list = board.closed_list
service = described_class.new(group, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
end
end
require 'spec_helper'
describe Boards::Lists::ListService, services: true do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:label) { create(:group_label, group: group) }
let!(:list) { create(:list, board: board, label: label) }
let(:service) { described_class.new(group, double) }
describe '#execute' do
context 'when the board has a backlog list' do
let!(:backlog_list) { create(:backlog_list, board: board) }
it 'does not create a backlog list' do
expect { service.execute(board) }.not_to change(board.lists, :count)
end
it "returns board's lists" do
expect(service.execute(board)).to eq [backlog_list, list, board.closed_list]
end
end
context 'when the board does not have a backlog list' do
it 'creates a backlog list' do
expect { service.execute(board) }.to change(board.lists, :count).by(1)
end
it "returns board's lists" do
expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
end
end
end
end
require 'spec_helper'
describe Boards::Lists::MoveService, services: true do
describe '#execute' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:user) { create(:user) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
let!(:closed) { create(:closed_list, board: board) }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
service = described_class.new(group, user, position: nil)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to old position' do
service = described_class.new(group, user, position: planning.position)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is negative' do
service = described_class.new(group, user, position: -1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to number of labels lists' do
service = described_class.new(group, user, position: board.lists.label.size)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is greater than number of labels lists' do
service = described_class.new(group, user, position: board.lists.label.size + 1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'increments position of intermediate lists when new positon is equal to first position' do
service = described_class.new(group, user, position: 0)
service.execute(staging)
expect(current_list_positions).to eq [1, 2, 3, 0]
end
it 'decrements position of intermediate lists when new positon is equal to last position' do
service = described_class.new(group, user, position: board.lists.label.last.position)
service.execute(planning)
expect(current_list_positions).to eq [3, 0, 1, 2]
end
it 'decrements position of intermediate lists when new position is greater than old position' do
service = described_class.new(group, user, position: 2)
service.execute(planning)
expect(current_list_positions).to eq [2, 0, 1, 3]
end
it 'increments position of intermediate lists when new position is lower than old position' do
service = described_class.new(group, user, position: 1)
service.execute(staging)
expect(current_list_positions).to eq [0, 2, 3, 1]
end
end
it 'keeps position of lists when list type is closed' do
service = described_class.new(group, user, position: 2)
service.execute(closed)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
end
def current_list_positions
[planning, development, review, staging].map { |list| list.reload.position }
end
end
......@@ -119,4 +119,70 @@ describe EE::NotificationService, :mailer do
end
end
end
describe 'mirror hard failed' do
let(:user) { create(:user) }
context 'when user is owner' do
it 'sends email' do
project = create(:project, :mirror, :import_hard_failed)
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, project.owner.id).and_call_original
subject.mirror_was_hard_failed(project)
end
end
context 'when user is master' do
it 'sends email' do
project = create(:project, :mirror, :import_hard_failed)
project.add_master(user)
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, project.owner.id).and_call_original
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, user.id).and_call_original
subject.mirror_was_hard_failed(project)
end
end
context 'when user is not owner nor master' do
it 'does not send email' do
project = create(:project, :mirror, :import_hard_failed)
project.add_developer(user)
expect(Notify).not_to receive(:mirror_was_hard_failed_email).with(project.id, user.id).and_call_original
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, project.creator.id).and_call_original
subject.mirror_was_hard_failed(project)
end
context 'when user is group owner' do
it 'sends email' do
group = create(:group, :public) do |group|
group.add_owner(user)
end
project = create(:project, :mirror, :import_hard_failed, namespace: group)
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, user.id).and_call_original
subject.mirror_was_hard_failed(project)
end
end
context 'when user is group master' do
it 'sends email' do
group = create(:group, :public) do |group|
group.add_master(user)
end
project = create(:project, :mirror, :import_hard_failed, namespace: group)
expect(Notify).to receive(:mirror_was_hard_failed_email).with(project.id, user.id).and_call_original
subject.mirror_was_hard_failed(project)
end
end
end
end
end
......@@ -4,7 +4,7 @@ shared_examples_for 'multiple and scoped issue boards' do |route_definition|
context 'multiple issue boards' do
before do
board_parent.add_reporter(user)
stub_licensed_features(multiple_issue_boards: true, group_issue_boards: true)
stub_licensed_features(multiple_group_issue_boards: true, multiple_project_issue_boards: true)
end
describe "POST #{route_definition}" do
......@@ -30,7 +30,7 @@ shared_examples_for 'multiple and scoped issue boards' do |route_definition|
context 'with the scoped_issue_board-feature available' do
it 'returns the milestone when the `scoped_issue_board` feature is enabled' do
stub_licensed_features(scoped_issue_board: true, group_issue_boards: true)
stub_licensed_features(scoped_issue_board: true)
get api(root_url, user)
......@@ -38,7 +38,7 @@ shared_examples_for 'multiple and scoped issue boards' do |route_definition|
end
it 'hides the milestone when the `scoped_issue_board` feature is disabled' do
stub_licensed_features(scoped_issue_board: false, group_issue_boards: true)
stub_licensed_features(scoped_issue_board: false)
get api(root_url, user)
......
......@@ -43,20 +43,24 @@ describe 'layouts/nav/sidebar/_group' do
end
describe 'group issue boards link' do
it 'is not visible when there is no valid license' do
stub_licensed_features(group_issue_boards: false)
context 'when multiple issue board is disabled' do
it 'shows link text in singular' do
render
render
expect(rendered).not_to have_text 'Boards'
expect(rendered).to have_text 'Board'
end
end
it 'is visible when there is valid license' do
stub_licensed_features(group_issue_boards: true)
context 'when multiple issue board is enabled' do
before do
stub_licensed_features(multiple_group_issue_boards: true)
end
render
it 'shows link text in plural' do
render
expect(rendered).to have_text 'Boards'
expect(rendered).to have_text 'Boards'
end
end
end
end
......
......@@ -133,6 +133,7 @@ module API
mount ::API::Features
mount ::API::Files
mount ::API::Groups
mount ::API::GroupBoards
mount ::API::GroupMilestones
mount ::API::Internal
mount ::API::Issues
......@@ -178,17 +179,18 @@ module API
mount ::API::Wikis
## EE-specific API V4 endpoints START
mount ::EE::API::Boards
mount ::EE::API::GroupBoards
mount ::API::EpicIssues
mount ::API::Epics
mount ::API::Geo
mount ::API::GeoNodes
mount ::API::GroupBoards
mount ::API::IssueLinks
mount ::API::Ldap
mount ::API::LdapGroupLinks
mount ::API::License
mount ::API::ProjectPushRule
mount ::EE::API::Boards
## EE-specific API V4 endpoints END
route :any, '*path' do
......
......@@ -6,17 +6,12 @@ module API
before do
authenticate!
check_group_issue_boards!
end
helpers do
def board_parent
user_group
end
def check_group_issue_boards!
forbidden! unless ::License.feature_available?(:group_issue_boards)
end
end
params do
......@@ -25,47 +20,23 @@ module API
resource :groups, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
segment ':id/boards' do
desc 'Get all group boards' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
params do
use :pagination
end
get '/' do
present paginate(board_parent.boards), with: Entities::Board
end
desc 'Find a group board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
success ::API::Entities::Board
end
get '/:board_id' do
present board, with: Entities::Board
present board, with: ::API::Entities::Board
end
desc 'Create a group board' do
desc 'Get all group boards' do
detail 'This feature was introduced in 10.4'
success Entities::Board
end
params do
requires :name, type: String, desc: 'The board name'
end
post '/' do
authorize!(:admin_board, board_parent)
create_board
end
desc 'Delete a group board' do
detail 'This feature was introduced in 10.4'
success Entities::Board
use :pagination
end
delete '/:board_id' do
authorize!(:admin_board, board_parent)
delete_board
get '/' do
present paginate(board_parent.boards), with: Entities::Board
end
end
......
......@@ -2,6 +2,15 @@ module API
class JobArtifacts < Grape::API
before { authenticate_non_get! }
# EE::API::JobArtifacts would override the following helpers
helpers do
def authorize_download_artifacts!
authorize_read_builds!
end
end
prepend EE::API::JobArtifacts
params do
requires :id, type: String, desc: 'The ID of a project'
end
......@@ -16,8 +25,7 @@ module API
route_setting :authentication, job_token_allowed: true
get ':id/jobs/artifacts/:ref_name/download',
requirements: { ref_name: /.+/ } do
authorize_read_builds!
check_cross_project_pipelines_feature!
authorize_download_artifacts!
builds = user_project.latest_successful_builds_for(params[:ref_name])
latest_build = builds.find_by!(name: params[:job])
......@@ -33,8 +41,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/jobs/:job_id/artifacts' do
authorize_read_builds!
check_cross_project_pipelines_feature!
authorize_download_artifacts!
build = find_build!(params[:job_id])
......@@ -80,13 +87,5 @@ module API
present build, with: Entities::Job
end
end
helpers do
def check_cross_project_pipelines_feature!
if job_token_authentication? && !@project.feature_available?(:cross_project_pipelines)
not_found!('Project')
end
end
end
end
end
......@@ -6,6 +6,34 @@ module API
helpers ::Gitlab::IssuableMetadata
# EE::API::MergeRequests would override the following helpers
helpers do
params :optional_params_ee do
end
params :merge_params_ee do
end
def update_merge_request_ee(merge_request)
end
end
def self.update_params_at_least_one_of
%i[
assignee_id
description
labels
milestone_id
remove_source_branch
state_event
target_branch
title
discussion_locked
]
end
prepend EE::API::MergeRequests
helpers do
def find_merge_requests(args = {})
args = declared_params.merge(args)
......@@ -31,6 +59,12 @@ module API
mr.all_pipelines
end
def check_sha_param!(params, merge_request)
if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
end
params :merge_requests_params do
optional :state, type: String, values: %w[opened closed merged all], default: 'all',
desc: 'Return opened, closed, merged, or all merge requests'
......@@ -106,27 +140,13 @@ module API
render_api_error!(errors, 400)
end
def check_sha_param!(params, merge_request)
if params[:sha] && merge_request.diff_head_sha != params[:sha]
render_api_error!("SHA does not match HEAD of source branch: #{merge_request.diff_head_sha}", 409)
end
end
params :optional_params_ce do
params :optional_params do
optional :description, type: String, desc: 'The description of the merge request'
optional :assignee_id, type: Integer, desc: 'The ID of a user to assign the merge request'
optional :milestone_id, type: Integer, desc: 'The ID of a milestone to assign the merge request'
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :remove_source_branch, type: Boolean, desc: 'Remove source branch when merging'
end
params :optional_params_ee do
optional :approvals_before_merge, type: Integer, desc: 'Number of approvals required before this can be merged'
optional :squash, type: Boolean, desc: 'Squash commits when merging'
end
params :optional_params do
use :optional_params_ce
use :optional_params_ee
end
end
......@@ -252,31 +272,14 @@ module API
success Entities::MergeRequest
end
params do
# CE
at_least_one_of_ce = [
:assignee_id,
:description,
:labels,
:milestone_id,
:remove_source_branch,
:state_event,
:target_branch,
:title,
:discussion_locked
]
optional :title, type: String, allow_blank: false, desc: 'The title of the merge request'
optional :target_branch, type: String, allow_blank: false, desc: 'The target branch'
optional :state_event, type: String, values: %w[close reopen],
desc: 'Status of the merge request'
optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked'
# EE
at_least_one_of_ee = [
:squash
]
use :optional_params
at_least_one_of(*(at_least_one_of_ce + at_least_one_of_ee))
at_least_one_of(*::API::MergeRequests.update_params_at_least_one_of)
end
put ':id/merge_requests/:merge_request_iid' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42318')
......@@ -299,7 +302,6 @@ module API
success Entities::MergeRequest
end
params do
# CE
optional :merge_commit_message, type: String, desc: 'Custom merge commit message'
optional :should_remove_source_branch, type: Boolean,
desc: 'When true, the source branch will be deleted if possible'
......@@ -307,8 +309,7 @@ module API
desc: 'When true, this merge request will be merged when the pipeline succeeds'
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
# EE
optional :squash, type: Boolean, desc: 'When true, the commits will be squashed into a single commit on merge'
use :merge_params_ee
end
put ':id/merge_requests/:merge_request_iid/merge' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42317')
......@@ -326,9 +327,7 @@ module API
check_sha_param!(params, merge_request)
if params[:squash] && merge_request.project.feature_available?(:merge_request_squash)
merge_request.update(squash: params[:squash])
end
update_merge_request_ee(merge_request)
merge_params = {
commit_message: params[:merge_commit_message],
......@@ -361,66 +360,6 @@ module API
.cancel(merge_request)
end
# Get the status of the merge request's approvals
#
# Parameters:
# id (required) - The ID of a project
# merge_request_idd (required) - IID of MR
# Examples:
# GET /projects/:id/merge_requests/:merge_request_iid/approvals
#
desc "List a merge request's approvals" do
success EE::API::Entities::MergeRequestApprovals
end
get ':id/merge_requests/:merge_request_iid/approvals' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
# Approve a merge request
#
# Parameters:
# id (required) - The ID of a project
# merge_request_iid (required) - IID of MR
# Examples:
# POST /projects/:id/merge_requests/:merge_request_iid/approve
#
desc 'Approve a merge request' do
success EE::API::Entities::MergeRequestApprovals
end
params do
optional :sha, type: String, desc: 'When present, must have the HEAD SHA of the source branch'
end
post ':id/merge_requests/:merge_request_iid/approve' do
merge_request = find_project_merge_request(params[:merge_request_iid])
unauthorized! unless merge_request.can_approve?(current_user)
check_sha_param!(params, merge_request)
::MergeRequests::ApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
desc 'Remove an approval from a merge request' do
success EE::API::Entities::MergeRequestApprovals
end
post ':id/merge_requests/:merge_request_iid/unapprove' do
merge_request = find_project_merge_request(params[:merge_request_iid])
not_found! unless merge_request.has_approved?(current_user)
::MergeRequests::RemoveApprovalService
.new(user_project, current_user)
.execute(merge_request)
present merge_request, with: EE::API::Entities::MergeRequestApprovals, current_user: current_user
end
desc 'List issues that will be closed on merge' do
success Entities::MRNote
end
......
module Gitlab
module Ci
class Trace
ArchiveError = Class.new(StandardError)
attr_reader :job
delegate :old_trace, to: :job
......@@ -93,8 +95,52 @@ module Gitlab
job.erase_old_trace!
end
def archive!
raise ArchiveError, 'Already archived' if trace_artifact
raise ArchiveError, 'Job is not finished yet' unless job.complete?
if current_path
File.open(current_path) do |stream|
archive_stream!(stream)
FileUtils.rm(current_path)
end
elsif old_trace
StringIO.new(old_trace, 'rb').tap do |stream|
archive_stream!(stream)
job.erase_old_trace!
end
end
end
private
def archive_stream!(stream)
clone_file!(stream, JobArtifactUploader.workhorse_upload_path) do |clone_path|
create_job_trace!(job, clone_path)
end
end
def clone_file!(src_stream, temp_dir)
FileUtils.mkdir_p(temp_dir)
Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.touch(temp_path)
size = IO.copy_stream(src_stream, temp_path)
raise ArchiveError, 'Failed to copy stream' unless size == src_stream.size
yield(temp_path)
end
end
def create_job_trace!(job, path)
File.open(path) do |stream|
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
def ensure_path
return current_path if current_path
......
require 'logger'
require 'resolv-replace'
desc "GitLab | Archive legacy traces to trace artifacts"
namespace :gitlab do
namespace :traces do
task archive: :environment do
logger = Logger.new(STDOUT)
logger.info('Archiving legacy traces')
Ci::Build.finished
.where('NOT EXISTS (?)',
Ci::JobArtifact.select(1).trace.where('ci_builds.id = ci_job_artifacts.job_id'))
.order(id: :asc)
.find_in_batches(batch_size: 1000) do |jobs|
job_ids = jobs.map { |job| [job.id] }
ArchiveTraceWorker.bulk_perform_async(job_ids)
logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}")
end
end
end
end
require 'spec_helper'
describe Groups::BoardsController do
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_master(user)
sign_in(user)
end
describe 'GET index' do
it 'creates a new board when group does not have one' do
expect { list_boards }.to change(group.boards, :count).by(1)
end
context 'when format is HTML' do
it 'renders template' do
list_boards
expect(response).to render_template :index
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
list_boards
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
it 'return an array with one group board' do
create(:board, group: group, milestone: create(:milestone, group: group))
list_boards format: :json
parsed_response = JSON.parse(response.body)
expect(response).to match_response_schema('boards')
expect(parsed_response.length).to eq 1
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
list_boards format: :json
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
def list_boards(format: :html)
get :index, group_id: group, format: format
end
end
describe 'GET show' do
let!(:board) { create(:board, group: group) }
context 'when format is HTML' do
it 'renders template' do
read_board board: board
expect(response).to render_template :show
expect(response.content_type).to eq 'text/html'
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'text/html'
end
end
end
context 'when format is JSON' do
it 'returns project board' do
read_board board: board, format: :json
expect(response).to match_response_schema('board')
end
context 'with unauthorized user' do
before do
allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(true)
allow(Ability).to receive(:allowed?).with(user, :read_group, group).and_return(false)
end
it 'returns a not found 404 response' do
read_board board: board, format: :json
expect(response).to have_gitlab_http_status(404)
expect(response.content_type).to eq 'application/json'
end
end
end
context 'when board does not belong to group' do
it 'returns a not found 404 response' do
another_board = create(:board)
read_board board: another_board
expect(response).to have_gitlab_http_status(404)
end
end
def read_board(board:, format: :html)
get :show, group_id: group,
id: board.to_param,
format: format
end
end
end
......@@ -2,11 +2,13 @@ require 'spec_helper'
describe BoardsHelper do
describe '#build_issue_link_base' do
it 'returns correct path for project board' do
@project = create(:project)
@board = create(:board, project: @project)
context 'project board' do
it 'returns correct path for project board' do
@project = create(:project)
@board = create(:board, project: @project)
expect(build_issue_link_base).to eq("/#{@project.namespace.path}/#{@project.path}/issues")
expect(build_issue_link_base).to eq("/#{@project.namespace.path}/#{@project.path}/issues")
end
end
context 'group board' do
......
......@@ -36,6 +36,11 @@ describe('EpicShowApp', () => {
markdownDocsPath,
author,
created,
namespace,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
} = props;
const EpicShowApp = Vue.extend(epicShowApp);
......@@ -72,6 +77,12 @@ describe('EpicShowApp', () => {
editable: canUpdate,
initialStartDate: startDate,
initialEndDate: endDate,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
namespace,
});
setTimeout(done);
......
export const mockLabels = [
{
id: 26,
title: 'Foo Label',
description: 'Foobar',
color: '#BADA55',
text_color: '#FFFFFF',
},
];
export const contentProps = {
endpoint: '',
updateEndpoint: gl.TEST_HOST,
......@@ -8,10 +18,15 @@ export const contentProps = {
markdownDocsPath: '',
issueLinksEndpoint: '/',
groupPath: '',
namespace: 'gitlab-org',
labelsPath: '',
labelsWebUrl: '',
epicsWebUrl: '',
initialTitleHtml: '',
initialTitleText: '',
startDate: '2017-01-01',
endDate: '2017-10-10',
labels: mockLabels,
};
export const headerProps = {
......
......@@ -3,15 +3,23 @@ import _ from 'underscore';
import Cookies from 'js-cookie';
import epicSidebar from 'ee/epics/sidebar/components/sidebar_app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { props } from '../../epic_show/mock_data';
describe('epicSidebar', () => {
let vm;
let originalCookieState;
let EpicSidebar;
const {
updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
labels,
} = props;
beforeEach(() => {
setFixtures(`
<div class="page-with-sidebar right-sidebar-expanded">
<div class="page-with-contextual-sidebar right-sidebar-expanded">
<div id="epic-sidebar"></div>
</div>
`);
......@@ -21,6 +29,11 @@ describe('epicSidebar', () => {
EpicSidebar = Vue.extend(epicSidebar);
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
}, '#epic-sidebar');
});
......@@ -36,6 +49,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2017');
......@@ -45,6 +63,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialEndDate: '2018-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
});
expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jan 1, 2018');
......@@ -55,6 +78,11 @@ describe('epicSidebar', () => {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
initialEndDate: '2018-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
});
const datePickers = vm.$el.querySelectorAll('.block');
......@@ -68,6 +96,11 @@ describe('epicSidebar', () => {
vm = mountComponent(EpicSidebar, {
endpoint: gl.TEST_HOST,
initialStartDate: '2017-01-01',
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
});
});
......@@ -89,7 +122,7 @@ describe('epicSidebar', () => {
});
it('should toggle contentContainer css class', () => {
const contentContainer = document.querySelector('.page-with-sidebar');
const contentContainer = document.querySelector('.page-with-contextual-sidebar');
expect(contentContainer.classList.contains('right-sidebar-expanded')).toEqual(true);
expect(contentContainer.classList.contains('right-sidebar-collapsed')).toEqual(false);
......@@ -113,6 +146,11 @@ describe('epicSidebar', () => {
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
},
});
});
......@@ -146,6 +184,38 @@ describe('epicSidebar', () => {
it('should handle errors gracefully', () => {});
});
describe('handleLabelClick', () => {
const label = {
id: 1,
title: 'Foo',
color: ['#BADA55'],
text_color: '#FFFFFF',
};
it('initializes `epicContext.labels` as empty array when `label.isAny` is `true`', () => {
const labelIsAny = { isAny: true };
vm.handleLabelClick(labelIsAny);
expect(Array.isArray(vm.epicContext.labels)).toBe(true);
expect(vm.epicContext.labels.length).toBe(0);
});
it('adds provided `label` to epicContext.labels', () => {
vm.handleLabelClick(label);
// epicContext.labels gets initialized with initialLabels, hence
// newly insert label will be at second position (index `1`)
expect(vm.epicContext.labels.length).toBe(2);
expect(vm.epicContext.labels[1].id).toBe(label.id);
vm.handleLabelClick(label);
});
it('filters epicContext.labels to exclude provided `label` if it is already present in `epicContext.labels`', () => {
vm.handleLabelClick(label); // Select
vm.handleLabelClick(label); // Un-select
expect(vm.epicContext.labels.length).toBe(1);
expect(vm.epicContext.labels[0].id).toBe(labels[0].id);
});
});
describe('saveDate error', () => {
let interceptor;
let component;
......@@ -160,6 +230,11 @@ describe('epicSidebar', () => {
component = new EpicSidebar({
propsData: {
endpoint: gl.TEST_HOST,
initialLabels: labels,
updatePath: updateEndpoint,
labelsPath,
labelsWebUrl,
epicsWebUrl,
},
});
});
......
......@@ -7,7 +7,8 @@ import eventHub from 'ee/geo_nodes/event_hub';
import GeoNodesStore from 'ee/geo_nodes/store/geo_nodes_store';
import GeoNodesService from 'ee/geo_nodes/service/geo_nodes_service';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { PRIMARY_VERSION, NODE_DETAILS_PATH, mockNodes, rawMockNodeDetails } from '../mock_data';
import { NODE_ACTIONS } from 'ee/geo_nodes/constants';
import { PRIMARY_VERSION, NODE_DETAILS_PATH, mockNodes, mockNode, rawMockNodeDetails } from '../mock_data';
const createComponent = () => {
const Component = Vue.extend(appComponent);
......@@ -34,19 +35,27 @@ describe('AppComponent', () => {
mock = new MockAdapter(axios);
document.body.innerHTML += '<div class="flash-container"></div>';
mock.onGet(/(.*)\/geo_nodes$/).reply(() => [statusCode, response]);
vm = createComponent();
});
afterEach(() => {
document.querySelector('.flash-container').remove();
vm.$destroy();
mock.restore();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.isLoading).toBeTruthy();
expect(vm.hasError).toBeFalsy();
expect(vm.isLoading).toBe(true);
expect(vm.hasError).toBe(false);
expect(vm.showModal).toBe(false);
expect(vm.targetNode).toBeNull();
expect(vm.targetNodeActionType).toBe('');
expect(vm.modalKind).toBe('warning');
expect(vm.modalMessage).toBe('');
expect(vm.modalActionLabel).toBe('');
expect(vm.errorMessage).toBe('');
});
});
......@@ -60,6 +69,23 @@ describe('AppComponent', () => {
});
describe('methods', () => {
describe('setNodeActionStatus', () => {
it('sets `nodeActionActive` property with value of `status` parameter for provided `node` parameter', () => {
const node = {
nodeActionActive: false,
};
vm.setNodeActionStatus(node, true);
expect(node.nodeActionActive).toBe(true);
});
});
describe('initNodeDetailsPolling', () => {
it('initializes SmartInterval and sets it to component', () => {
vm.initNodeDetailsPolling(2);
expect(vm.nodePollingInterval).toBeDefined();
});
});
describe('fetchGeoNodes', () => {
it('calls service.getGeoNodes and sets response to the store on success', (done) => {
spyOn(vm.store, 'setNodes');
......@@ -89,35 +115,244 @@ describe('AppComponent', () => {
describe('fetchNodeDetails', () => {
it('calls service.getGeoNodeDetails and sets response to the store on success', (done) => {
mock.onGet(`${vm.service.geoNodeDetailsBasePath}/2/status.json`).reply(200, rawMockNodeDetails);
mock.onGet(mockNode.statusPath).reply(200, rawMockNodeDetails);
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(2);
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(vm.service.getGeoNodeDetails).toHaveBeenCalled();
expect(Object.keys(vm.store.state.nodeDetails).length).toBe(1);
expect(vm.store.state.nodeDetails['2']).toBeDefined();
expect(Object.keys(vm.store.state.nodeDetails).length).not.toBe(0);
expect(vm.store.state.nodeDetails['1']).toBeDefined();
done();
}, 0);
});
it('emits `nodeDetailsLoadFailed` event on failure', (done) => {
const err = 'Something went wrong';
spyOn(eventHub, '$emit');
mock.onGet(`${vm.service.geoNodeDetailsBasePath}/2/status.json`).reply(500, err);
mock.onGet(mockNode.statusPath).reply(500, {});
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoadFailed', mockNode.id, jasmine.any(Object));
done();
}, 0);
});
it('emits `nodeDetailsLoaded` event with fake nodeDetails object on 404 failure', (done) => {
spyOn(eventHub, '$emit');
mock.onGet(mockNode.statusPath).reply(404, {});
spyOn(vm.service, 'getGeoNodeDetails').and.callThrough();
vm.fetchNodeDetails(2);
vm.fetchNodeDetails(mockNode);
setTimeout(() => {
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoadFailed', 2, jasmine.any(Object));
expect(eventHub.$emit).toHaveBeenCalledWith('nodeDetailsLoaded', jasmine.any(Object));
const nodeDetails = vm.store.state.nodeDetails['1'];
expect(nodeDetails).toBeDefined();
expect(nodeDetails.syncStatusUnavailable).toBe(true);
expect(nodeDetails.health).toBe('Request failed with status code 404');
done();
}, 0);
});
});
describe('initNodeDetailsPolling', () => {
it('initializes SmartInterval and sets it to component', () => {
vm.initNodeDetailsPolling(2);
expect(vm.nodePollingInterval).toBeDefined();
describe('repairNode', () => {
it('calls service.repairNode and shows success Flash message on request success', (done) => {
const node = { ...mockNode };
mock.onPost(node.repairPath).reply(200);
spyOn(vm.service, 'repairNode').and.callThrough();
vm.repairNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.repairNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Node Authentication was successfully repaired.');
expect(node.nodeActionActive).toBe(false);
done();
});
});
it('calls service.repairNode and shows failure Flash message on request failure', (done) => {
const node = { ...mockNode };
mock.onPost(node.repairPath).reply(500);
spyOn(vm.service, 'repairNode').and.callThrough();
vm.repairNode(node);
setTimeout(() => {
expect(vm.service.repairNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while repairing node');
expect(node.nodeActionActive).toBe(false);
done();
});
});
});
describe('toggleNode', () => {
it('calls service.toggleNode for enabling node and updates toggle button on request success', (done) => {
const node = { ...mockNode };
mock.onPut(node.basePath).reply(200, {
enabled: true,
});
spyOn(vm.service, 'toggleNode').and.callThrough();
node.enabled = false;
vm.toggleNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
expect(node.enabled).toBe(true);
expect(node.nodeActionActive).toBe(false);
done();
});
});
it('calls service.toggleNode and shows Flash error on request failure', (done) => {
const node = { ...mockNode };
mock.onPut(node.basePath).reply(500);
spyOn(vm.service, 'toggleNode').and.callThrough();
node.enabled = false;
vm.toggleNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.toggleNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while changing node status');
expect(node.nodeActionActive).toBe(false);
done();
});
});
});
describe('removeNode', () => {
it('calls service.removeNode for removing node and shows Flash message on request success', (done) => {
const node = { ...mockNode };
mock.onDelete(node.basePath).reply(200);
spyOn(vm.service, 'removeNode').and.callThrough();
spyOn(vm.store, 'removeNode').and.stub();
vm.removeNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.removeNode).toHaveBeenCalledWith(node);
expect(vm.store.removeNode).toHaveBeenCalledWith(node);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Node was successfully removed.');
done();
});
});
it('calls service.removeNode and shows Flash message on request failure', (done) => {
const node = { ...mockNode };
mock.onDelete(node.basePath).reply(500);
spyOn(vm.service, 'removeNode').and.callThrough();
spyOn(vm.store, 'removeNode').and.stub();
vm.removeNode(node);
expect(node.nodeActionActive).toBe(true);
setTimeout(() => {
expect(vm.service.removeNode).toHaveBeenCalledWith(node);
expect(vm.store.removeNode).not.toHaveBeenCalled();
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while removing node');
done();
});
});
});
describe('handleNodeAction', () => {
it('sets `showModal` to false and calls `toggleNode` when `targetNodeActionType` is `toggle`', () => {
vm.targetNode = { ...mockNode };
vm.targetNodeActionType = NODE_ACTIONS.TOGGLE;
vm.showModal = true;
spyOn(vm, 'toggleNode').and.stub();
vm.handleNodeAction();
expect(vm.showModal).toBe(false);
expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
});
it('sets `showModal` to false and calls `removeNode` when `targetNodeActionType` is `remove`', () => {
vm.targetNode = { ...mockNode };
vm.targetNodeActionType = NODE_ACTIONS.REMOVE;
vm.showModal = true;
spyOn(vm, 'removeNode').and.stub();
vm.handleNodeAction();
expect(vm.showModal).toBe(false);
expect(vm.removeNode).toHaveBeenCalledWith(vm.targetNode);
});
});
describe('showNodeActionModal', () => {
let node;
let modalKind;
let modalMessage;
let modalActionLabel;
beforeEach(() => {
node = { ...mockNode };
modalKind = 'warning';
modalMessage = 'Foobar message';
modalActionLabel = 'Disable';
});
it('sets target node and modal config props on component', () => {
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.targetNode).toBe(node);
expect(vm.targetNodeActionType).toBe(NODE_ACTIONS.TOGGLE);
expect(vm.modalKind).toBe(modalKind);
expect(vm.modalMessage).toBe(modalMessage);
expect(vm.modalActionLabel).toBe(modalActionLabel);
});
it('sets showModal to `true` when actionType is `toggle` and node is enabled', () => {
node.enabled = true;
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.showModal).toBe(true);
});
it('calls toggleNode when actionType is `toggle` and node.enabled is `false`', () => {
node.enabled = false;
spyOn(vm, 'toggleNode').and.stub();
vm.showNodeActionModal({
actionType: NODE_ACTIONS.TOGGLE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.toggleNode).toHaveBeenCalledWith(vm.targetNode);
});
it('sets showModal to `true` when actionType is not `toggle`', () => {
node.enabled = true;
vm.showNodeActionModal({
actionType: NODE_ACTIONS.REMOVE,
node,
modalKind,
modalMessage,
modalActionLabel,
});
expect(vm.showModal).toBe(true);
});
});
describe('hideNodeActionModal', () => {
it('sets `showModal` to `false`', () => {
vm.showModal = true;
vm.hideNodeActionModal();
expect(vm.showModal).toBe(false);
});
});
});
......@@ -127,6 +362,8 @@ describe('AppComponent', () => {
spyOn(eventHub, '$on');
const vmX = createComponent();
expect(eventHub.$on).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
expect(eventHub.$on).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
vmX.$destroy();
});
});
......@@ -137,6 +374,8 @@ describe('AppComponent', () => {
const vmX = createComponent();
vmX.$destroy();
expect(eventHub.$off).toHaveBeenCalledWith('pollNodeDetails', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('showNodeActionModal', jasmine.any(Function));
expect(eventHub.$off).toHaveBeenCalledWith('repairNode', jasmine.any(Function));
});
});
......
......@@ -2,6 +2,8 @@ import Vue from 'vue';
import geoNodeActionsComponent from 'ee/geo_nodes/components/geo_node_actions.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import eventHub from 'ee/geo_nodes/event_hub';
import { NODE_ACTIONS } from 'ee/geo_nodes/constants';
import { mockNodes } from '../mock_data';
const createComponent = (node = mockNodes[0], nodeEditAllowed = true, nodeMissingOauth = false) => {
......@@ -25,14 +27,6 @@ describe('GeoNodeActionsComponent', () => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
const vmX = createComponent();
expect(vmX.isNodeToggleInProgress).toBeFalsy();
vmX.$destroy();
});
});
describe('computed', () => {
describe('isToggleAllowed', () => {
it('returns boolean value representing if toggle on node can be allowed', () => {
......@@ -59,49 +53,48 @@ describe('GeoNodeActionsComponent', () => {
vmX.$destroy();
});
});
});
describe('nodeDisableMessage', () => {
it('returns node toggle message', () => {
let mockNode = Object.assign({}, mockNodes[1]);
let vmX = createComponent(mockNode);
expect(vmX.nodeDisableMessage).toBe('Disabling a node stops the sync process. Are you sure?');
vmX.$destroy();
mockNode = Object.assign({}, mockNodes[1], { enabled: false });
vmX = createComponent(mockNode);
expect(vmX.nodeDisableMessage).toBe('');
vmX.$destroy();
});
});
describe('nodePath', () => {
it('returns node path', () => {
expect(vm.nodePath).toBe('/admin/geo_nodes/1');
});
});
describe('nodeRepairAuthPath', () => {
it('returns node repair authentication path', () => {
expect(vm.nodeRepairAuthPath).toBe('/admin/geo_nodes/1/repair');
describe('methods', () => {
describe('onToggleNode', () => {
it('emits showNodeActionModal with actionType `toggle`, node reference, modalMessage and modalActionLabel', () => {
spyOn(eventHub, '$emit');
vm.onToggleNode();
expect(eventHub.$emit).toHaveBeenCalledWith('showNodeActionModal', {
actionType: NODE_ACTIONS.TOGGLE,
node: vm.node,
modalMessage: 'Disabling a node stops the sync process. Are you sure?',
modalActionLabel: vm.nodeToggleLabel,
});
});
});
describe('nodeTogglePath', () => {
it('returns node toggle path', () => {
expect(vm.nodeTogglePath).toBe('/admin/geo_nodes/1/toggle');
describe('onRemoveNode', () => {
it('emits showNodeActionModal with actionType `remove`, node reference, modalKind, modalMessage and modalActionLabel', () => {
spyOn(eventHub, '$emit');
vm.onRemoveNode();
expect(eventHub.$emit).toHaveBeenCalledWith('showNodeActionModal', {
actionType: NODE_ACTIONS.REMOVE,
node: vm.node,
modalKind: 'danger',
modalMessage: 'Removing a node stops the sync process. Are you sure?',
modalActionLabel: 'Remove',
});
});
});
describe('nodeEditPath', () => {
it('returns node edit path', () => {
expect(vm.nodeEditPath).toBe('/admin/geo_nodes/1/edit');
describe('onRepairNode', () => {
it('emits `repairNode` event with node reference', () => {
spyOn(eventHub, '$emit');
vm.onRepairNode();
expect(eventHub.$emit).toHaveBeenCalledWith('repairNode', vm.node);
});
});
});
describe('template', () => {
it('renders container elements correctly', () => {
expect(vm.$el.classList.contains('geo-node-actions')).toBeTruthy();
expect(vm.$el.classList.contains('geo-node-actions')).toBe(true);
expect(vm.$el.querySelectorAll('.node-action-container').length).not.toBe(0);
expect(vm.$el.querySelectorAll('.btn-node-action').length).not.toBe(0);
});
......
......@@ -77,6 +77,16 @@ describe('GeoNodeDetailsComponent', () => {
});
describe('nodeVersion', () => {
it('returns `Unknown` when `version` and `revision` are null', () => {
const nodeDetailsVersionNull = Object.assign({}, mockNodeDetails, {
version: null,
revision: null,
});
const vmVersionNull = createComponent(nodeDetailsVersionNull);
expect(vmVersionNull.nodeVersion).toBe('Unknown');
vmVersionNull.$destroy();
});
it('returns version string', () => {
expect(vm.nodeVersion).toBe('10.4.0-pre (b93c51849b)');
});
......@@ -92,6 +102,15 @@ describe('GeoNodeDetailsComponent', () => {
it('returns DB replication lag time duration', () => {
expect(vm.dbReplicationLag).toBe('0m');
});
it('returns `Unknown` when `dbReplicationLag` is null', () => {
const nodeDetailsLagNull = Object.assign({}, mockNodeDetails, {
dbReplicationLag: null,
});
const vmLagNull = createComponent(nodeDetailsLagNull);
expect(vmLagNull.dbReplicationLag).toBe('Unknown');
vmLagNull.$destroy();
});
});
describe('lastEventStatus', () => {
......@@ -158,10 +177,17 @@ describe('GeoNodeDetailsComponent', () => {
describe('syncSettings', () => {
it('returns sync settings object', () => {
const syncSettings = vm.syncSettings();
const nodeDetailsUnknownSync = Object.assign({}, mockNodeDetails, {
syncStatusUnavailable: true,
});
const vmUnknownSync = createComponent(nodeDetailsUnknownSync);
const syncSettings = vmUnknownSync.syncSettings();
expect(syncSettings.syncStatusUnavailable).toBe(true);
expect(syncSettings.namespaces).toBe(mockNodeDetails.namespaces);
expect(syncSettings.lastEvent).toBe(mockNodeDetails.lastEvent);
expect(syncSettings.cursorLastEvent).toBe(mockNodeDetails.cursorLastEvent);
vmUnknownSync.$destroy();
});
});
......
......@@ -184,7 +184,7 @@ describe('GeoNodeItemComponent', () => {
spyOn(eventHub, '$emit');
vm.handleMounted();
expect(eventHub.$emit).toHaveBeenCalledWith('pollNodeDetails', mockNodes[0].id);
expect(eventHub.$emit).toHaveBeenCalledWith('pollNodeDetails', vm.node);
});
});
});
......
......@@ -5,12 +5,14 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockNodeDetails } from '../mock_data';
const createComponent = (
syncStatusUnavailable = false,
selectiveSyncType = mockNodeDetails.selectiveSyncType,
lastEvent = mockNodeDetails.lastEvent,
cursorLastEvent = mockNodeDetails.cursorLastEvent) => {
const Component = Vue.extend(geoNodeSyncSettingsComponent);
return mountComponent(Component, {
syncStatusUnavailable,
selectiveSyncType,
lastEvent,
cursorLastEvent,
......@@ -29,7 +31,7 @@ describe('GeoNodeSyncSettingsComponent', () => {
describe('eventTimestampEmpty', () => {
it('returns `true` if one of the event timestamp is empty', () => {
const vmEmptyTimestamp = createComponent(mockNodeDetails.namespaces, {
const vmEmptyTimestamp = createComponent(false, mockNodeDetails.namespaces, {
id: 0,
timeStamp: 0,
}, {
......@@ -87,4 +89,12 @@ describe('GeoNodeSyncSettingsComponent', () => {
});
});
});
describe('template', () => {
it('renders `Unknown` when `syncStatusUnavailable` prop is true', () => {
const vmSyncUnavailable = createComponent(true);
expect(vmSyncUnavailable.$el.innerText.trim()).toBe('Unknown');
vmSyncUnavailable.$destroy();
});
});
});
......@@ -15,6 +15,12 @@ export const mockNodes = [
files_max_capacity: 10,
repos_max_capacity: 25,
clone_protocol: 'http',
_links: {
self: 'http://127.0.0.1:3001/api/v4/geo_nodes/1',
repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/repair',
status: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/status',
web_edit: 'http://127.0.0.1:3001/admin/geo_nodes/1/edit',
},
},
{
id: 2,
......@@ -25,9 +31,28 @@ export const mockNodes = [
files_max_capacity: 10,
repos_max_capacity: 25,
clone_protocol: 'http',
_links: {
self: 'http://127.0.0.1:3001/api/v4/geo_nodes/2',
repair: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/repair',
status: 'http://127.0.0.1:3001/api/v4/geo_nodes/2/status',
web_edit: 'http://127.0.0.1:3001/admin/geo_nodes/2/edit',
},
},
];
export const mockNode = {
id: 1,
url: 'http://127.0.0.1:3001/',
primary: true,
current: true,
enabled: true,
nodeActionActive: false,
basePath: 'http://127.0.0.1:3001/api/v4/geo_nodes/1',
repairPath: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/repair',
statusPath: 'http://127.0.0.1:3001/api/v4/geo_nodes/1/status?refresh=true',
editPath: 'http://127.0.0.1:3001/admin/geo_nodes/1/edit',
};
export const rawMockNodeDetails = {
geo_node_id: 2,
healthy: true,
......
......@@ -22,7 +22,7 @@ describe('GeoNodesStore', () => {
describe('setNodes', () => {
it('sets nodes list to state', () => {
store.setNodes(mockNodes);
expect(store.getNodes()).toBe(mockNodes);
expect(store.getNodes().length).toBe(mockNodes.length);
});
});
......@@ -33,6 +33,28 @@ describe('GeoNodesStore', () => {
});
});
describe('removeNode', () => {
it('removes node from store state', () => {
store.setNodes(mockNodes);
const nodeToBeRemoved = store.getNodes()[1];
store.removeNode(nodeToBeRemoved);
store.getNodes().forEach((node) => {
expect(node.id).not.toBe(nodeToBeRemoved);
});
});
});
describe('formatNode', () => {
it('returns formatted raw node object', () => {
const node = GeoNodesStore.formatNode(mockNodes[0]);
expect(node.id).toBe(mockNodes[0].id);
expect(node.url).toBe(mockNodes[0].url);
expect(node.basePath).toBe(mockNodes[0]._links.self);
expect(node.repairPath).toBe(mockNodes[0]._links.repair);
expect(node.nodeActionActive).toBe(false);
});
});
describe('formatNodeDetails', () => {
it('returns formatted raw node details object', () => {
const nodeDetails = GeoNodesStore.formatNodeDetails(rawMockNodeDetails);
......
......@@ -64,7 +64,7 @@ describe('Multi-file editor library model', () => {
model.getModel().setValue('123');
setTimeout(() => {
expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
expect(spy).toHaveBeenCalledWith(model, jasmine.anything());
done();
});
});
......
......@@ -178,6 +178,7 @@ describe('IDE commit module actions', () => {
f = file('changedFile');
Object.assign(f, {
active: true,
changed: true,
content: 'file content',
});
......@@ -191,8 +192,11 @@ describe('IDE commit module actions', () => {
},
},
};
store.state.changedFiles.push(f);
store.state.openFiles.push(f);
store.state.changedFiles.push(f, {
...file('changedFile2'),
changed: true,
});
store.state.openFiles = store.state.changedFiles;
});
it('updates stores working reference', (done) => {
......@@ -209,6 +213,20 @@ describe('IDE commit module actions', () => {
.catch(done.fail);
});
it('resets all files changed status', (done) => {
store.dispatch('commit/updateFilesAfterCommit', {
data,
branch,
})
.then(() => {
store.state.openFiles.forEach((entry) => {
expect(entry.changed).toBeFalsy();
});
})
.then(done)
.catch(done.fail);
});
it('removes all changed files', (done) => {
store.dispatch('commit/updateFilesAfterCommit', {
data,
......
......@@ -161,4 +161,17 @@ describe('Multi-file store file mutations', () => {
expect(localState.changedFiles.length).toBe(0);
});
});
describe('TOGGLE_FILE_CHANGED', () => {
it('updates file changed status', () => {
const f = file();
mutations.TOGGLE_FILE_CHANGED(localState, {
file: f,
changed: true,
});
expect(f.changed).toBeTruthy();
});
});
});
......@@ -399,4 +399,138 @@ describe Gitlab::Ci::Trace do
end
end
end
describe '#archive!' do
subject { trace.archive! }
shared_examples 'archive trace file' do
it do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
build.reload
expect(build.trace.exist?).to be_truthy
expect(build.job_artifacts_trace.file.exists?).to be_truthy
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(File.exist?(src_path)).to be_falsy
expect(src_checksum)
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest)
end
end
shared_examples 'source trace file stays intact' do |error:|
it do
expect { subject }.to raise_error(error)
build.reload
expect(build.trace.exist?).to be_truthy
expect(build.job_artifacts_trace).to be_nil
expect(File.exist?(src_path)).to be_truthy
end
end
shared_examples 'archive trace in database' do
it do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
build.reload
expect(build.trace.exist?).to be_truthy
expect(build.job_artifacts_trace.file.exists?).to be_truthy
expect(build.job_artifacts_trace.file.filename).to eq('job.log')
expect(build.old_trace).to be_nil
expect(src_checksum)
.to eq(Digest::SHA256.file(build.job_artifacts_trace.file.path).digest)
end
end
shared_examples 'source trace in database stays intact' do |error:|
it do
expect { subject }.to raise_error(error)
build.reload
expect(build.trace.exist?).to be_truthy
expect(build.job_artifacts_trace).to be_nil
expect(build.old_trace).to eq(trace_content)
end
end
context 'when job does not have trace artifact' do
context 'when trace file stored in default path' do
let!(:build) { create(:ci_build, :success, :trace_live) }
let!(:src_path) { trace.read { |s| return s.path } }
let!(:src_checksum) { Digest::SHA256.file(src_path).digest }
it_behaves_like 'archive trace file'
context 'when failed to create clone file' do
before do
allow(IO).to receive(:copy_stream).and_return(0)
end
it_behaves_like 'source trace file stays intact', error: Gitlab::Ci::Trace::ArchiveError
end
context 'when failed to create job artifact record' do
before do
allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
.and_return(%w[Error Error])
end
it_behaves_like 'source trace file stays intact', error: ActiveRecord::RecordInvalid
end
end
context 'when trace is stored in database' do
let(:build) { create(:ci_build, :success) }
let(:trace_content) { 'Sample trace' }
let!(:src_checksum) { Digest::SHA256.digest(trace_content) }
before do
build.update_column(:trace, trace_content)
end
it_behaves_like 'archive trace in database'
context 'when failed to create clone file' do
before do
allow(IO).to receive(:copy_stream).and_return(0)
end
it_behaves_like 'source trace in database stays intact', error: Gitlab::Ci::Trace::ArchiveError
end
context 'when failed to create job artifact record' do
before do
allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
.and_return(%w[Error Error])
end
it_behaves_like 'source trace in database stays intact', error: ActiveRecord::RecordInvalid
end
end
end
context 'when job has trace artifact' do
before do
create(:ci_job_artifact, :trace, job: build)
end
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
expect { subject }.to raise_error('Already archived')
expect(build.job_artifacts_trace.file.exists?).to be_truthy
end
end
context 'when job is not finished yet' do
let!(:build) { create(:ci_build, :running, :trace_live) }
it 'does not archive' do
expect_any_instance_of(described_class).not_to receive(:archive_stream!)
expect { subject }.to raise_error('Job is not finished yet')
expect(build.trace.exist?).to be_truthy
end
end
end
end
......@@ -1419,6 +1419,21 @@ describe Notify do
end
end
describe 'mirror was hard failed' do
let(:project) { create(:project, :mirror, :import_hard_failed) }
subject { described_class.mirror_was_hard_failed_email(project.id, user.id) }
it_behaves_like 'an email sent from GitLab'
it_behaves_like 'it should not have Gmail Actions links'
it_behaves_like "a user cannot unsubscribe through footer link"
it 'has the correct subject and body' do
is_expected.to have_subject("#{project.name} | Repository mirroring paused")
is_expected.to have_html_escaped_body_text(project.full_path)
end
end
describe 'admin notification' do
let(:example_site_path) { root_path }
let(:user) { create(:user) }
......
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20180306074045_migrate_create_trace_artifact_sidekiq_queue.rb')
describe MigrateCreateTraceArtifactSidekiqQueue, :sidekiq, :redis do
include Gitlab::Database::MigrationHelpers
context 'when there are jobs in the queues' do
it 'correctly migrates queue when migrating up' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_default:create_trace_artifact').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('pipeline_default:create_trace_artifact')).to eq 0
expect(sidekiq_queue_length('pipeline_background:archive_trace')).to eq 2
end
end
it 'does not affect other queues under the same namespace' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_default:build_coverage').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:build_trace_sections').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:pipeline_metrics').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:pipeline_notification').perform_async('Something', [1])
stubbed_worker(queue: 'pipeline_default:update_head_pipeline_for_merge_request').perform_async('Something', [1])
described_class.new.up
expect(sidekiq_queue_length('pipeline_default:build_coverage')).to eq 1
expect(sidekiq_queue_length('pipeline_default:build_trace_sections')).to eq 1
expect(sidekiq_queue_length('pipeline_default:pipeline_metrics')).to eq 1
expect(sidekiq_queue_length('pipeline_default:pipeline_notification')).to eq 1
expect(sidekiq_queue_length('pipeline_default:update_head_pipeline_for_merge_request')).to eq 1
end
end
it 'correctly migrates queue when migrating down' do
Sidekiq::Testing.disable! do
stubbed_worker(queue: 'pipeline_background:archive_trace').perform_async('Something', [1])
described_class.new.down
expect(sidekiq_queue_length('pipeline_default:create_trace_artifact')).to eq 1
expect(sidekiq_queue_length('pipeline_background:archive_trace')).to eq 0
end
end
end
context 'when there are no jobs in the queues' do
it 'does not raise error when migrating up' do
expect { described_class.new.up }.not_to raise_error
end
it 'does not raise error when migrating down' do
expect { described_class.new.down }.not_to raise_error
end
end
def stubbed_worker(queue:)
Class.new do
include Sidekiq::Worker
sidekiq_options queue: queue
end
end
end
require 'spec_helper'
describe API::GroupBoards do
set(:user) { create(:user) }
set(:non_member) { create(:user) }
set(:guest) { create(:user) }
set(:admin) { create(:user, :admin) }
set(:board_parent) { create(:group, :public) }
before do
board_parent.add_owner(user)
end
set(:project) { create(:project, :public, namespace: board_parent ) }
set(:dev_label) do
create(:group_label, title: 'Development', color: '#FFAABB', group: board_parent)
end
set(:test_label) do
create(:group_label, title: 'Testing', color: '#FFAACC', group: board_parent)
end
set(:ux_label) do
create(:group_label, title: 'UX', color: '#FF0000', group: board_parent)
end
set(:dev_list) do
create(:list, label: dev_label, position: 1)
end
set(:test_list) do
create(:list, label: test_label, position: 2)
end
set(:milestone) { create(:milestone, group: board_parent) }
set(:board_label) { create(:group_label, group: board_parent) }
set(:board) { create(:board, group: board_parent, lists: [dev_list, test_list]) }
it_behaves_like 'group and project boards', "/groups/:id/boards", false
describe 'POST /groups/:id/boards/lists' do
let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" }
it 'does not create lists for child project labels' do
project_label = create(:label, project: project)
post api(url, user), label_id: project_label.id
expect(response).to have_gitlab_http_status(400)
end
end
end
......@@ -700,10 +700,10 @@ describe API::Runner do
end
end
context 'when tace is given' do
context 'when trace is given' do
it 'creates a trace artifact' do
allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
CreateTraceArtifactWorker.new.perform(job.id)
ArchiveTraceWorker.new.perform(job.id)
end
update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
......
......@@ -2,34 +2,20 @@ require 'spec_helper'
describe Boards::CreateService do
describe '#execute' do
let(:project) { create(:project) }
context 'when board parent is a project' do
let(:parent) { create(:project) }
subject(:service) { described_class.new(project, double) }
subject(:service) { described_class.new(parent, double) }
context 'when project does not have a board' do
it 'creates a new board' do
expect { service.execute }.to change(Board, :count).by(1)
end
it 'creates the default lists' do
board = service.execute
expect(board.lists.size).to eq 2
expect(board.lists.first).to be_backlog
expect(board.lists.last).to be_closed
end
it_behaves_like 'boards create service'
end
context 'when project has a board' do
before do
create(:board, project: project)
end
context 'when board parent is a group' do
let(:parent) { create(:group) }
it 'does not create a new board' do
expect(service).to receive(:can_create_board?) { false }
subject(:service) { described_class.new(parent, double) }
expect { service.execute }.not_to change(project.boards, :count)
end
it_behaves_like 'boards create service'
end
end
end
......@@ -2,124 +2,103 @@ require 'spec_helper'
describe Boards::Issues::ListService do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:m1) { create(:milestone, project: project) }
let(:m2) { create(:milestone, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board) }
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) }
let!(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Issue 3' ) }
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
let!(:list1_issue3) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1]) }
let!(:list2_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [testing]) }
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
before do
project.add_developer(user)
end
it 'delegates search to IssuesFinder' do
params = { board_id: board.id, id: list1.id }
expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
described_class.new(project, user, params).execute
end
context 'when list_id is missing' do
context 'when board does not have a milestone' do
it 'returns opened issues without board labels applied' do
params = { board_id: board.id }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
context 'when parent is a project' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:m1) { create(:milestone, project: project) }
let(:m2) { create(:milestone, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:p1) { create(:label, title: 'P1', project: project, priority: 1) }
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board) }
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) }
let!(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Issue 3' ) }
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
let!(:list1_issue3) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1]) }
let!(:list2_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [testing]) }
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) }
let(:parent) { project }
before do
project.add_developer(user)
end
context 'when board have a milestone' do
it 'returns opened issues without board labels and milestone applied' do
params = { board_id: board.id }
board.update_attribute(:milestone, m1)
issues = described_class.new(project, user, params).execute
expect(issues).to eq [opened_issue2, list1_issue2, reopened_issue1, opened_issue1]
end
end
it_behaves_like 'issues list service'
end
context 'issues are ordered by priority' do
it 'returns opened issues when list_id is missing' do
params = { board_id: board.id }
context 'when parent is a group' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :empty_repo, namespace: group) }
let(:project1) { create(:project, :empty_repo, namespace: group) }
let(:board) { create(:board, group: group) }
issues = described_class.new(project, user, params).execute
let(:m1) { create(:milestone, group: group) }
let(:m2) { create(:milestone, group: group) }
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
it 'returns opened issues when listing issues from Backlog' do
params = { board_id: board.id, id: backlog.id }
let(:bug) { create(:group_label, group: group, name: 'Bug') }
let(:development) { create(:group_label, group: group, name: 'Development') }
let(:testing) { create(:group_label, group: group, name: 'Testing') }
issues = described_class.new(project, user, params).execute
let(:p1) { create(:group_label, title: 'P1', group: group) }
let(:p2) { create(:group_label, title: 'P2', group: group) }
let(:p3) { create(:group_label, title: 'P3', group: group) }
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
let(:p1_project) { create(:label, title: 'P1_project', project: project, priority: 1) }
let(:p2_project) { create(:label, title: 'P2_project', project: project, priority: 2) }
let(:p3_project) { create(:label, title: 'P3_project', project: project, priority: 3) }
it 'returns closed issues when listing issues from Closed' do
params = { board_id: board.id, id: closed.id }
let(:p1_project1) { create(:label, title: 'P1_project1', project: project1, priority: 1) }
let(:p2_project1) { create(:label, title: 'P2_project1', project: project1, priority: 2) }
let(:p3_project1) { create(:label, title: 'P3_project1', project: project1, priority: 3) }
issues = described_class.new(project, user, params).execute
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
end
let!(:backlog) { create(:backlog_list, board: board) }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board) }
it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { board_id: board.id, id: list1.id }
let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) }
let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Issue 3', closed_at: Time.now ) }
issues = described_class.new(project, user, params).execute
let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, p2_project, development]) }
let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) }
let!(:list1_issue3) { create(:labeled_issue, project: project1, milestone: m1, labels: [development, p1, p1_project1]) }
let!(:list2_issue1) { create(:labeled_issue, project: project1, milestone: m1, labels: [testing]) }
expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3, p3_project]) }
let!(:closed_issue3) { create(:issue, :closed, project: project1) }
let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1]) }
let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) }
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
service = described_class.new(project, user, board_id: board.id, id: list.id)
let(:parent) { group }
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
before do
group.add_developer(user)
end
end
context 'with invalid list id' do
it 'raises an error' do
service = described_class.new(project, user, board_id: board.id, id: nil)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
it_behaves_like 'issues list service'
end
end
end
......@@ -2,108 +2,53 @@ require 'spec_helper'
describe Boards::Issues::MoveService do
describe '#execute' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:board1) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board1) }
before do
project.add_developer(user)
end
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
it 'delegates the label changes to Issues::UpdateService' do
expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
described_class.new(project, user, params).execute(issue)
end
it 'removes the label from the list it came from and adds the label of the list it goes to' do
described_class.new(project, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, testing)
end
end
context 'when moving to closed' do
context 'when parent is a project' do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:board1) { create(:board, project: project) }
let(:board2) { create(:board, project: project) }
let(:bug) { create(:label, project: project, name: 'Bug') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:regression) { create(:label, project: project, name: 'Regression') }
let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.id } }
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board1) }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
let(:parent) { project }
described_class.new(project, user, params).execute(issue)
before do
parent.add_developer(user)
end
it 'removes all list-labels from project boards and close the issue' do
described_class.new(project, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug)
expect(issue).to be_closed
end
it_behaves_like 'issues move service'
end
context 'when moving from closed' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
described_class.new(project, user, params).execute(issue)
end
it 'adds the label of the list it goes to and reopen the issue' do
described_class.new(project, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug, testing)
expect(issue).to be_opened
end
end
context 'when parent is a group' do
let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
let(:board1) { create(:board, group: group) }
let(:board2) { create(:board, group: group) }
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
let(:bug) { create(:group_label, group: group, name: 'Bug') }
let(:development) { create(:group_label, group: group, name: 'Development') }
let(:testing) { create(:group_label, group: group, name: 'Testing') }
let(:regression) { create(:group_label, group: group, name: 'Regression') }
it 'returns false' do
expect(described_class.new(project, user, params).execute(issue)).to eq false
end
let!(:list1) { create(:list, board: board1, label: development, position: 0) }
let!(:list2) { create(:list, board: board1, label: testing, position: 1) }
let!(:closed) { create(:closed_list, board: board1) }
it 'keeps issues labels' do
described_class.new(project, user, params).execute(issue)
let(:parent) { group }
expect(issue.reload.labels).to contain_exactly(bug, development)
before do
parent.add_developer(user)
end
it 'sorts issues' do
[issue, issue1, issue2].each do |issue|
issue.move_to_end && issue.save!
end
params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
described_class.new(project, user, params).execute(issue)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end
it_behaves_like 'issues move service'
end
end
end
......@@ -2,37 +2,20 @@ require 'spec_helper'
describe Boards::ListService do
describe '#execute' do
let(:project) { create(:project) }
context 'when board parent is a project' do
let(:parent) { create(:project) }
subject(:service) { described_class.new(project, double) }
subject(:service) { described_class.new(parent, double) }
context 'when project does not have a board' do
it 'creates a new project board' do
expect { service.execute }.to change(project.boards, :count).by(1)
end
it 'delegates the project board creation to Boards::CreateService' do
expect_any_instance_of(Boards::CreateService).to receive(:execute).once
service.execute
end
it_behaves_like 'boards list service'
end
context 'when project has a board' do
before do
create(:board, project: project)
end
it 'does not create a new board' do
expect { service.execute }.not_to change(project.boards, :count)
end
end
context 'when board parent is a group' do
let(:parent) { create(:group) }
it 'returns project boards' do
board1 = create(:board, project: project)
board2 = create(:board, project: project)
subject(:service) { described_class.new(parent, double) }
expect(service.execute).to match_array [board1, board2]
it_behaves_like 'boards list service'
end
end
end
......@@ -2,62 +2,77 @@ require 'spec_helper'
describe Boards::Lists::CreateService do
describe '#execute' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let(:label) { create(:label, project: project, name: 'in-progress') }
shared_examples 'creating board lists' do
let(:user) { create(:user) }
subject(:service) { described_class.new(project, user, label_id: label.id) }
subject(:service) { described_class.new(parent, user, label_id: label.id) }
before do
project.add_developer(user)
end
before do
parent.add_developer(user)
end
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
context 'when board lists is empty' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
expect(list.position).to eq 0
expect(list.position).to eq 0
end
end
end
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
context 'when board lists has the done list' do
it 'creates a new list at beginning of the list' do
list = service.execute(board)
expect(list.position).to eq 0
expect(list.position).to eq 0
end
end
end
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
list = service.execute(board)
list = service.execute(board)
expect(list.position).to eq 2
expect(list.position).to eq 2
end
end
end
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
context 'when board lists has label and done lists' do
it 'creates a new list at end of the label lists' do
list1 = create(:list, board: board, position: 0)
list2 = service.execute(board)
list2 = service.execute(board)
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
expect(list1.reload.position).to eq 0
expect(list2.reload.position).to eq 1
end
end
end
context 'when provided label does not belongs to the project' do
it 'raises an error' do
label = create(:label, name: 'in-development')
service = described_class.new(project, user, label_id: label.id)
context 'when provided label does not belongs to the parent' do
it 'raises an error' do
label = create(:label, name: 'in-development')
service = described_class.new(parent, user, label_id: label.id)
expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
expect { service.execute(board) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
context 'when board parent is a project' do
let(:parent) { create(:project) }
let(:board) { create(:board, project: parent) }
let(:label) { create(:label, project: parent, name: 'in-progress') }
it_behaves_like 'creating board lists'
end
context 'when board parent is a group' do
let(:parent) { create(:group) }
let(:board) { create(:board, group: parent) }
let(:label) { create(:group_label, group: parent, name: 'in-progress') }
it_behaves_like 'creating board lists'
end
end
end
......@@ -2,37 +2,24 @@ require 'spec_helper'
describe Boards::Lists::DestroyService do
describe '#execute' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'when board parent is a project' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'when list type is label' do
it 'removes list from board' do
list = create(:list, board: board)
service = described_class.new(project, user)
let(:parent) { project }
expect { service.execute(list) }.to change(board.lists, :count).by(-1)
end
it 'decrements position of higher lists' do
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
closed = board.closed_list
described_class.new(project, user).execute(development)
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(closed.reload.position).to be_nil
end
it_behaves_like 'lists destroy service'
end
it 'does not remove list from board when list type is closed' do
list = board.closed_list
service = described_class.new(project, user)
context 'when board parent is a group' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:user) { create(:user) }
let(:parent) { group }
expect { service.execute(list) }.not_to change(board.lists, :count)
it_behaves_like 'lists destroy service'
end
end
end
require 'spec_helper'
describe Boards::Lists::ListService do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:label) { create(:label, project: project) }
let!(:list) { create(:list, board: board, label: label) }
let(:service) { described_class.new(project, double) }
describe '#execute' do
context 'when the board has a backlog list' do
let!(:backlog_list) { create(:backlog_list, board: board) }
it 'does not create a backlog list' do
expect { service.execute(board) }.not_to change(board.lists, :count)
end
context 'when board parent is a project' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:label) { create(:label, project: project) }
let!(:list) { create(:list, board: board, label: label) }
let(:service) { described_class.new(project, double) }
it "returns board's lists" do
expect(service.execute(board)).to eq [backlog_list, list, board.closed_list]
end
it_behaves_like 'lists list service'
end
context 'when the board does not have a backlog list' do
it 'creates a backlog list' do
expect { service.execute(board) }.to change(board.lists, :count).by(1)
end
context 'when board parent is a group' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:label) { create(:group_label, group: group) }
let!(:list) { create(:list, board: board, label: label) }
let(:service) { described_class.new(group, double) }
it "returns board's lists" do
expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
end
it_behaves_like 'lists list service'
end
end
end
......@@ -2,100 +2,24 @@ require 'spec_helper'
describe Boards::Lists::MoveService do
describe '#execute' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
context 'when board parent is a project' do
let(:project) { create(:project) }
let(:board) { create(:board, project: project) }
let(:user) { create(:user) }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
let!(:closed) { create(:closed_list, board: board) }
let(:parent) { project }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
service = described_class.new(project, user, position: nil)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to old position' do
service = described_class.new(project, user, position: planning.position)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is negative' do
service = described_class.new(project, user, position: -1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to number of labels lists' do
service = described_class.new(project, user, position: board.lists.label.size)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is greater than number of labels lists' do
service = described_class.new(project, user, position: board.lists.label.size + 1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'increments position of intermediate lists when new positon is equal to first position' do
service = described_class.new(project, user, position: 0)
service.execute(staging)
expect(current_list_positions).to eq [1, 2, 3, 0]
end
it 'decrements position of intermediate lists when new positon is equal to last position' do
service = described_class.new(project, user, position: board.lists.label.last.position)
service.execute(planning)
expect(current_list_positions).to eq [3, 0, 1, 2]
end
it 'decrements position of intermediate lists when new position is greater than old position' do
service = described_class.new(project, user, position: 2)
service.execute(planning)
expect(current_list_positions).to eq [2, 0, 1, 3]
end
it 'increments position of intermediate lists when new position is lower than old position' do
service = described_class.new(project, user, position: 1)
service.execute(staging)
expect(current_list_positions).to eq [0, 2, 3, 1]
end
it_behaves_like 'lists move service'
end
it 'keeps position of lists when list type is closed' do
service = described_class.new(project, user, position: 2)
context 'when board parent is a group' do
let(:group) { create(:group) }
let(:board) { create(:board, group: group) }
let(:user) { create(:user) }
service.execute(closed)
let(:parent) { group }
expect(current_list_positions).to eq [0, 1, 2, 3]
it_behaves_like 'lists move service'
end
end
def current_list_positions
[planning, development, review, staging].map { |list| list.reload.position }
end
end
require 'spec_helper'
describe Ci::CreateTraceArtifactService do
describe '#execute' do
subject { described_class.new(nil, nil).execute(job) }
context 'when the job does not have trace artifact' do
context 'when the job has a trace file' do
let!(:job) { create(:ci_build, :trace_live) }
let!(:legacy_path) { job.trace.read { |stream| return stream.path } }
let!(:legacy_checksum) { Digest::SHA256.file(legacy_path).hexdigest }
let(:new_path) { job.job_artifacts_trace.file.path }
let(:new_checksum) { Digest::SHA256.file(new_path).hexdigest }
it { expect(File.exist?(legacy_path)).to be_truthy }
it 'creates trace artifact' do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
expect(File.exist?(legacy_path)).to be_falsy
expect(File.exist?(new_path)).to be_truthy
expect(new_checksum).to eq(legacy_checksum)
expect(job.job_artifacts_trace.file.exists?).to be_truthy
expect(job.job_artifacts_trace.file.filename).to eq('job.log')
end
context 'when failed to create trace artifact record' do
before do
# When ActiveRecord error happens
allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
.and_return("Error")
subject rescue nil
job.reload
end
it 'keeps legacy trace and removes trace artifact' do
expect(File.exist?(legacy_path)).to be_truthy
expect(job.job_artifacts_trace).to be_nil
end
end
end
context 'when the job does not have a trace file' do
let!(:job) { create(:ci_build) }
it 'does not create trace artifact' do
expect { subject }.not_to change { Ci::JobArtifact.count }
end
end
end
context 'when the job has already had trace artifact' do
let!(:job) { create(:ci_build, :trace_artifact) }
it 'does not create trace artifact' do
expect { subject }.not_to change { Ci::JobArtifact.count }
end
end
end
end
shared_examples 'boards create service' do
context 'when parent does not have a board' do
it 'creates a new board' do
expect { service.execute }.to change(Board, :count).by(1)
end
it 'creates the default lists' do
board = service.execute
expect(board.lists.size).to eq 2
expect(board.lists.first).to be_backlog
expect(board.lists.last).to be_closed
end
end
context 'when parent has a board' do
before do
create(:board, parent: parent)
end
it 'does not create a new board' do
expect(service).to receive(:can_create_board?) { false }
expect { service.execute }.not_to change(parent.boards, :count)
end
end
end
shared_examples 'boards list service' do
context 'when parent does not have a board' do
it 'creates a new parent board' do
expect { service.execute }.to change(parent.boards, :count).by(1)
end
it 'delegates the parent board creation to Boards::CreateService' do
expect_any_instance_of(Boards::CreateService).to receive(:execute).once
service.execute
end
end
context 'when parent has a board' do
before do
create(:board, parent: parent)
end
it 'does not create a new board' do
expect { service.execute }.not_to change(parent.boards, :count)
end
end
it 'returns parent boards' do
board = create(:board, parent: parent)
expect(service.execute).to eq [board]
end
end
shared_examples 'issues list service' do
it 'delegates search to IssuesFinder' do
params = { board_id: board.id, id: list1.id }
expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original
described_class.new(parent, user, params).execute
end
context 'issues are ordered by priority' do
it 'returns opened issues when list_id is missing' do
params = { board_id: board.id }
issues = described_class.new(parent, user, params).execute
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
it 'returns opened issues when listing issues from Backlog' do
params = { board_id: board.id, id: backlog.id }
issues = described_class.new(parent, user, params).execute
expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1]
end
it 'returns closed issues when listing issues from Closed' do
params = { board_id: board.id, id: closed.id }
issues = described_class.new(parent, user, params).execute
expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1]
end
it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { board_id: board.id, id: list1.id }
issues = described_class.new(parent, user, params).execute
expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
context 'with list that does not belong to the board' do
it 'raises an error' do
list = create(:list)
service = described_class.new(parent, user, board_id: board.id, id: list.id)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
context 'with invalid list id' do
it 'raises an error' do
service = described_class.new(parent, user, board_id: board.id, id: nil)
expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end
shared_examples 'issues move service' do
context 'when moving an issue between lists' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } }
it 'delegates the label changes to Issues::UpdateService' do
expect_any_instance_of(Issues::UpdateService).to receive(:execute).with(issue).once
described_class.new(parent, user, params).execute(issue)
end
it 'removes the label from the list it came from and adds the label of the list it goes to' do
described_class.new(parent, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, testing)
end
end
context 'when moving to closed' do
let!(:list3) { create(:list, board: board2, label: regression, position: 1) }
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development, testing, regression]) }
let(:params) { { board_id: board1.id, from_list_id: list2.id, to_list_id: closed.id } }
it 'delegates the close proceedings to Issues::CloseService' do
expect_any_instance_of(Issues::CloseService).to receive(:execute).with(issue).once
described_class.new(parent, user, params).execute(issue)
end
it 'removes all list-labels from boards and close the issue' do
described_class.new(parent, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug)
expect(issue).to be_closed
end
end
context 'when moving from closed' do
let(:issue) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let(:params) { { board_id: board1.id, from_list_id: closed.id, to_list_id: list2.id } }
it 'delegates the re-open proceedings to Issues::ReopenService' do
expect_any_instance_of(Issues::ReopenService).to receive(:execute).with(issue).once
described_class.new(parent, user, params).execute(issue)
end
it 'adds the label of the list it goes to and reopen the issue' do
described_class.new(parent, user, params).execute(issue)
issue.reload
expect(issue.labels).to contain_exactly(bug, testing)
expect(issue).to be_opened
end
end
context 'when moving to same list' do
let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue1) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:issue2) { create(:labeled_issue, project: project, labels: [bug, development]) }
let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list1.id } }
it 'returns false' do
expect(described_class.new(parent, user, params).execute(issue)).to eq false
end
it 'keeps issues labels' do
described_class.new(parent, user, params).execute(issue)
expect(issue.reload.labels).to contain_exactly(bug, development)
end
it 'sorts issues' do
[issue, issue1, issue2].each do |issue|
issue.move_to_end && issue.save!
end
params.merge!(move_after_id: issue1.id, move_before_id: issue2.id)
described_class.new(parent, user, params).execute(issue)
expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position)
end
end
end
shared_examples 'lists destroy service' do
context 'when list type is label' do
it 'removes list from board' do
list = create(:list, board: board)
service = described_class.new(parent, user)
expect { service.execute(list) }.to change(board.lists, :count).by(-1)
end
it 'decrements position of higher lists' do
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
closed = board.closed_list
described_class.new(parent, user).execute(development)
expect(review.reload.position).to eq 0
expect(staging.reload.position).to eq 1
expect(closed.reload.position).to be_nil
end
end
it 'does not remove list from board when list type is closed' do
list = board.closed_list
service = described_class.new(parent, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
end
shared_examples 'lists list service' do
context 'when the board has a backlog list' do
let!(:backlog_list) { create(:backlog_list, board: board) }
it 'does not create a backlog list' do
expect { service.execute(board) }.not_to change(board.lists, :count)
end
it "returns board's lists" do
expect(service.execute(board)).to eq [backlog_list, list, board.closed_list]
end
end
context 'when the board does not have a backlog list' do
it 'creates a backlog list' do
expect { service.execute(board) }.to change(board.lists, :count).by(1)
end
it "returns board's lists" do
expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list]
end
end
end
shared_examples 'lists move service' do
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
let!(:closed) { create(:closed_list, board: board) }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
service = described_class.new(parent, user, position: nil)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to old position' do
service = described_class.new(parent, user, position: planning.position)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is negative' do
service = described_class.new(parent, user, position: -1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is equal to number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'keeps position of lists when new positon is greater than number of labels lists' do
service = described_class.new(parent, user, position: board.lists.label.size + 1)
service.execute(planning)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
it 'increments position of intermediate lists when new positon is equal to first position' do
service = described_class.new(parent, user, position: 0)
service.execute(staging)
expect(current_list_positions).to eq [1, 2, 3, 0]
end
it 'decrements position of intermediate lists when new positon is equal to last position' do
service = described_class.new(parent, user, position: board.lists.label.last.position)
service.execute(planning)
expect(current_list_positions).to eq [3, 0, 1, 2]
end
it 'decrements position of intermediate lists when new position is greater than old position' do
service = described_class.new(parent, user, position: 2)
service.execute(planning)
expect(current_list_positions).to eq [2, 0, 1, 3]
end
it 'increments position of intermediate lists when new position is lower than old position' do
service = described_class.new(parent, user, position: 1)
service.execute(staging)
expect(current_list_positions).to eq [0, 2, 3, 1]
end
end
it 'keeps position of lists when list type is closed' do
service = described_class.new(parent, user, position: 2)
service.execute(closed)
expect(current_list_positions).to eq [0, 1, 2, 3]
end
def current_list_positions
[planning, development, review, staging].map { |list| list.reload.position }
end
end
require 'rake_helper'
describe 'gitlab:traces rake tasks' do
before do
Rake.application.rake_require 'tasks/gitlab/traces'
end
shared_examples 'passes the job id to worker' do
it do
expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]])
run_rake_task('gitlab:traces:archive')
end
end
shared_examples 'does not pass the job id to worker' do
it do
expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async)
run_rake_task('gitlab:traces:archive')
end
end
context 'when trace file stored in default path' do
let!(:job) { create(:ci_build, :success, :trace_live) }
it_behaves_like 'passes the job id to worker'
end
context 'when trace is stored in database' do
let!(:job) { create(:ci_build, :success) }
before do
job.update_column(:trace, 'trace in db')
end
it_behaves_like 'passes the job id to worker'
end
context 'when job has trace artifact' do
let!(:job) { create(:ci_build, :success) }
before do
create(:ci_job_artifact, :trace, job: job)
end
it_behaves_like 'does not pass the job id to worker'
end
context 'when job is not finished yet' do
let!(:build) { create(:ci_build, :running, :trace_live) }
it_behaves_like 'does not pass the job id to worker'
end
end
......@@ -12,7 +12,7 @@ describe 'layouts/nav/sidebar/_project' do
end
describe 'issue boards' do
it 'has boards tab when multiple issue boards available' do
it 'has board tab' do
render
expect(rendered).to have_css('a[title="Boards"]')
......@@ -20,7 +20,7 @@ describe 'layouts/nav/sidebar/_project' do
it 'has board tab when multiple issue boards is not available' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:multiple_issue_boards) { false }
allow(License).to receive(:feature_available?).with(:multiple_project_issue_boards) { false }
render
......
require 'spec_helper'
describe CreateTraceArtifactWorker do
describe ArchiveTraceWorker do
describe '#perform' do
subject { described_class.new.perform(job&.id) }
......@@ -8,8 +8,7 @@ describe CreateTraceArtifactWorker do
let(:job) { create(:ci_build) }
it 'executes service' do
expect_any_instance_of(Ci::CreateTraceArtifactService)
.to receive(:execute).with(job)
expect_any_instance_of(Gitlab::Ci::Trace).to receive(:archive!)
subject
end
......@@ -19,8 +18,7 @@ describe CreateTraceArtifactWorker do
let(:job) { nil }
it 'does not execute service' do
expect_any_instance_of(Ci::CreateTraceArtifactService)
.not_to receive(:execute)
expect_any_instance_of(Gitlab::Ci::Trace).not_to receive(:archive!)
subject
end
......
......@@ -14,7 +14,7 @@ describe BuildFinishedWorker do
expect_any_instance_of(BuildTraceSectionsWorker).to receive(:perform)
expect_any_instance_of(BuildCoverageWorker).to receive(:perform)
expect(BuildHooksWorker).to receive(:perform_async)
expect(CreateTraceArtifactWorker).to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_async)
described_class.new.perform(build.id)
end
......
require 'spec_helper'
describe PipelineBackgroundQueue do
let(:worker) do
Class.new do
def self.name
'DummyWorker'
end
include ApplicationWorker
include PipelineBackgroundQueue
end
end
it 'sets a default object storage queue automatically' do
expect(worker.sidekiq_options['queue'])
.to eq 'pipeline_background:dummy'
end
end
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