Commit c0261f77 authored by Dmitriy Zaporozhets's avatar Dmitriy Zaporozhets

Merge branch 'ce-to-ee' into 'master'

CE Upstream

Closes gitlab-com/gitlab-docs#121

See merge request !2774
parents 8e3e1d43 c83d0973
...@@ -56,13 +56,18 @@ const Api = { ...@@ -56,13 +56,18 @@ const Api = {
// Return projects list. Filtered by query // Return projects list. Filtered by query
projects(query, options, callback) { projects(query, options, callback) {
const url = Api.buildUrl(Api.projectsPath); const url = Api.buildUrl(Api.projectsPath);
const defaults = {
search: query,
per_page: 20,
};
if (gon.current_user_id) {
defaults.membership = true;
}
return $.ajax({ return $.ajax({
url, url,
data: Object.assign({ data: Object.assign(defaults, options),
search: query,
per_page: 20,
membership: true,
}, options),
dataType: 'json', dataType: 'json',
}) })
.done(projects => callback(projects)); .done(projects => callback(projects));
......
...@@ -29,12 +29,14 @@ showTooltip = function(target, title) { ...@@ -29,12 +29,14 @@ showTooltip = function(target, title) {
var $target = $(target); var $target = $(target);
var originalTitle = $target.data('original-title'); var originalTitle = $target.data('original-title');
$target if (!$target.data('hideTooltip')) {
.attr('title', 'Copied') $target
.tooltip('fixTitle') .attr('title', 'Copied')
.tooltip('show') .tooltip('fixTitle')
.attr('title', originalTitle) .tooltip('show')
.tooltip('fixTitle'); .attr('title', originalTitle)
.tooltip('fixTitle');
}
}; };
$(function() { $(function() {
......
...@@ -16,6 +16,14 @@ body.modal-open { ...@@ -16,6 +16,14 @@ body.modal-open {
overflow: hidden; overflow: hidden;
} }
.modal-no-backdrop {
@extend .modal-dialog;
.modal-content {
box-shadow: none;
}
}
@media (min-width: $screen-md-min) { @media (min-width: $screen-md-min) {
.modal-dialog { .modal-dialog {
width: 860px; width: 860px;
......
...@@ -182,7 +182,6 @@ ...@@ -182,7 +182,6 @@
padding: 5px 10px; padding: 5px 10px;
position: relative; position: relative;
border-top: 1px solid $white-normal; border-top: 1px solid $white-normal;
margin-top: -5px;
} }
#binary-viewer { #binary-viewer {
......
# GroupsFinder
#
# Used to filter Groups by a set of params
#
# Arguments:
# current_user - which user is requesting groups
# params:
# owned: boolean
# parent: Group
# all_available: boolean (defaults to true)
#
# Users with full private access can see all groups. The `owned` and `parent`
# params can be used to restrict the groups that are returned.
#
# Anonymous users will never return any `owned` groups. They will return all
# public groups instead, even if `all_available` is set to false.
class GroupsFinder < UnionFinder class GroupsFinder < UnionFinder
def initialize(current_user = nil, params = {}) def initialize(current_user = nil, params = {})
@current_user = current_user @current_user = current_user
...@@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder ...@@ -16,13 +32,13 @@ class GroupsFinder < UnionFinder
attr_reader :current_user, :params attr_reader :current_user, :params
def all_groups def all_groups
groups = [] return [owned_groups] if params[:owned]
return [Group.all] if current_user&.full_private_access?
if current_user
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups
end
groups << Group.unscoped.public_to_user(current_user)
groups = []
groups << Gitlab::GroupHierarchy.new(groups_for_ancestors, groups_for_descendants).all_groups if current_user
groups << Group.unscoped.public_to_user(current_user) if include_public_groups?
groups << Group.none if groups.empty?
groups groups
end end
...@@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder ...@@ -39,4 +55,12 @@ class GroupsFinder < UnionFinder
groups.where(parent: params[:parent]) groups.where(parent: params[:parent])
end end
def owned_groups
current_user&.groups || Group.none
end
def include_public_groups?
current_user.nil? || params.fetch(:all_available, true)
end
end end
...@@ -20,6 +20,9 @@ module ButtonHelper ...@@ -20,6 +20,9 @@ module ButtonHelper
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard' title = data[:title] || 'Copy to clipboard'
button_text = data[:button_text] || ''
hide_tooltip = data[:hide_tooltip] || false
hide_button_icon = data[:hide_button_icon] || false
# This supports code in app/assets/javascripts/copy_to_clipboard.js that # This supports code in app/assets/javascripts/copy_to_clipboard.js that
# works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM. # works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
...@@ -35,17 +38,22 @@ module ButtonHelper ...@@ -35,17 +38,22 @@ module ButtonHelper
target = data.delete(:target) target = data.delete(:target)
data[:clipboard_target] = target if target data[:clipboard_target] = target if target
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) unless hide_tooltip
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
end
content_tag :button, button_attributes = {
icon('clipboard', 'aria-hidden': 'true'),
class: "btn #{css_class}", class: "btn #{css_class}",
data: data, data: data,
type: :button, type: :button,
title: title, title: title,
aria: { aria: { label: title }
label: title }
}
content_tag :button, button_attributes do
concat(icon('clipboard', 'aria-hidden': 'true')) unless hide_button_icon
concat(button_text)
end
end end
def http_clone_button(project, placement = 'right', append_link: true) def http_clone_button(project, placement = 'right', append_link: true)
......
module Users
module NewUserNotifier
def notify_new_user(user, reset_token)
log_info("User \"#{user.name}\" (#{user.email}) was created")
notification_service.new_user(user, reset_token) if reset_token
system_hook_service.execute_hooks_for(user, :create)
end
end
end
module Users module Users
class CreateService < BaseService class CreateService < BaseService
include NewUserNotifier
def initialize(current_user, params = {}) def initialize(current_user, params = {})
@current_user = current_user @current_user = current_user
@params = params.dup @params = params.dup
...@@ -10,11 +12,7 @@ module Users ...@@ -10,11 +12,7 @@ module Users
@reset_token = user.generate_reset_token if user.recently_sent_password_reset? @reset_token = user.generate_reset_token if user.recently_sent_password_reset?
if user.save notify_new_user(user, @reset_token) if user.save
log_info("User \"#{user.name}\" (#{user.email}) was created")
notification_service.new_user(user, @reset_token) if @reset_token
system_hook_service.execute_hooks_for(user, :create)
end
user user
end end
......
module Users module Users
class UpdateService < BaseService class UpdateService < BaseService
include NewUserNotifier
def initialize(user, params = {}) def initialize(user, params = {})
@user = user @user = user
@params = params.dup @params = params.dup
...@@ -10,7 +12,11 @@ module Users ...@@ -10,7 +12,11 @@ module Users
assign_attributes(&block) assign_attributes(&block)
user_exists = @user.persisted?
if @user.save(validate: validate) if @user.save(validate: validate)
notify_new_user(@user, nil) unless user_exists
success success
else else
error(@user.errors.full_messages.uniq.join('. ')) error(@user.errors.full_messages.uniq.join('. '))
......
%h3.page-title Authorization required
%main{ :role => "main" } %main{ :role => "main" }
%p.h4 .modal-no-backdrop
Authorize .modal-content
%strong.text-info= @pre_auth.client.name .modal-header
to use your account? %h3.page-title
Authorize
= link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
to use your account?
- if current_user.admin? .modal-body
.text-warning.prepend-top-20 - if current_user.admin?
%p .text-warning
= icon("exclamation-triangle fw") %p
You are an admin, which means granting access to = icon("exclamation-triangle fw")
%strong= @pre_auth.client.name You are an admin, which means granting access to
will allow them to interact with GitLab as an admin as well. Proceed with caution. %strong= @pre_auth.client.name
will allow them to interact with GitLab as an admin as well. Proceed with caution.
- if @pre_auth.scopes %p
#oauth-permissions You are about to authorize
%p This application will be able to: = link_to @pre_auth.client.name, @pre_auth.redirect_uri, target: '_blank', rel: 'noopener noreferrer'
%ul.text-info to use your account.
- @pre_auth.scopes.each do |scope| - if @pre_auth.scopes
%li= t scope, scope: [:doorkeeper, :scopes] This application will be able to:
%hr/ %ul
.actions - @pre_auth.scopes.each do |scope|
= form_tag oauth_authorization_path, method: :post do %li= t scope, scope: [:doorkeeper, :scopes]
= hidden_field_tag :client_id, @pre_auth.client.uid .form-actions.text-right
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri = form_tag oauth_authorization_path, method: :delete, class: 'inline' do
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :nonce, @pre_auth.nonce = hidden_field_tag :response_type, @pre_auth.response_type
= submit_tag "Authorize", class: "btn btn-success wide pull-left" = hidden_field_tag :scope, @pre_auth.scope
= form_tag oauth_authorization_path, method: :delete do = hidden_field_tag :nonce, @pre_auth.nonce
= hidden_field_tag :client_id, @pre_auth.client.uid = submit_tag "Deny", class: "btn btn-danger"
= hidden_field_tag :redirect_uri, @pre_auth.redirect_uri = form_tag oauth_authorization_path, method: :post, class: 'inline' do
= hidden_field_tag :state, @pre_auth.state = hidden_field_tag :client_id, @pre_auth.client.uid
= hidden_field_tag :response_type, @pre_auth.response_type = hidden_field_tag :redirect_uri, @pre_auth.redirect_uri
= hidden_field_tag :scope, @pre_auth.scope = hidden_field_tag :state, @pre_auth.state
= hidden_field_tag :nonce, @pre_auth.nonce = hidden_field_tag :response_type, @pre_auth.response_type
= submit_tag "Deny", class: "btn btn-danger prepend-left-10" = hidden_field_tag :scope, @pre_auth.scope
= hidden_field_tag :nonce, @pre_auth.nonce
= submit_tag "Authorize", class: "btn btn-success prepend-left-10"
...@@ -35,7 +35,8 @@ ...@@ -35,7 +35,8 @@
%ul %ul
- if can_update_issue - if can_update_issue
%li= link_to 'Edit', edit_project_issue_path(@project, @issue) %li= link_to 'Edit', edit_project_issue_path(@project, @issue)
- unless current_user == @issue.author / TODO: simplify condition back #36860
- if @issue.author && current_user != @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue)) %li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue - if can_update_issue
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue' %li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
%span.icon %span.icon
= custom_icon('ellipsis_v') = custom_icon('ellipsis_v')
%ul.dropdown-menu.more-actions-dropdown.dropdown-open-left %ul.dropdown-menu.more-actions-dropdown.dropdown-open-left
%li
= clipboard_button(text: noteable_note_url(note), title: "Copy reference to clipboard", button_text: 'Copy link', hide_tooltip: true, hide_button_icon: true)
- unless is_current_user - unless is_current_user
%li %li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do = link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
%span.sr-only %span.sr-only
Clear search Clear search
- unless params[:snippets].eql? 'true' - unless params[:snippets].eql? 'true'
= render 'filter' if current_user = render 'filter'
= button_tag "Search", class: "btn btn-success btn-search" = button_tag "Search", class: "btn btn-success btn-search"
- if current_application_settings.elasticsearch_search? - if current_application_settings.elasticsearch_search?
.help-block .help-block
......
...@@ -9,6 +9,7 @@ ...@@ -9,6 +9,7 @@
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}" class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
- elsif can_update && !is_current_user - elsif can_update && !is_current_user
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable = render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
- else - elsif issuable.author
/ TODO: change back to else #36860
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), = link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse' class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
...@@ -37,13 +37,15 @@ ...@@ -37,13 +37,15 @@
%li.divider.droplab-item-ignore %li.divider.droplab-item-ignore
%li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)), / TODO: remove condition #36860
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } } - if issuable.author
%button.btn.btn-transparent %li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
= icon('check', class: 'icon') button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
.description %button.btn.btn-transparent
%strong.title Report abuse = icon('check', class: 'icon')
%p.text .description
Report %strong.title Report abuse
= display_issuable_type.pluralize %p.text
that are abusive, inappropriate or spam. Report
= display_issuable_type.pluralize
that are abusive, inappropriate or spam.
# Concern for enabling a few lines of exception backtraces in Sidekiq
module ExceptionBacktrace
extend ActiveSupport::Concern
included do
sidekiq_options backtrace: 5
end
end
class GroupDestroyWorker class GroupDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def perform(group_id, user_id) def perform(group_id, user_id)
begin begin
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
class NamespacelessProjectDestroyWorker class NamespacelessProjectDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def self.bulk_perform_async(args_list) def self.bulk_perform_async(args_list)
Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list) Sidekiq::Client.push_bulk('class' => self, 'queue' => sidekiq_options['queue'], 'args' => args_list)
......
class ProjectDestroyWorker class ProjectDestroyWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
def perform(project_id, user_id, params) def perform(project_id, user_id, params)
project = Project.find(project_id) project = Project.find(project_id)
......
class ProjectExportWorker class ProjectExportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
sidekiq_options retry: 3 sidekiq_options retry: 3
......
...@@ -3,6 +3,7 @@ class RepositoryImportWorker ...@@ -3,6 +3,7 @@ class RepositoryImportWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
include ExceptionBacktrace
sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
......
---
title: Fix group and project search for anonymous users
merge_request: 13745
author:
type: fixed
---
title: restyling of OAuth authorization confirmation
merge_request:
author: Jacopo Beschi @jacopo-beschi
type: changed
---
title: Add support for copying permalink to notes via more actions dropdown
merge_request: 13299
author:
type: added
---
title: Fix failure when issue is authored by a deleted user
merge_request: 13807
author:
type: fixed
---
title: Document version Group Milestones API introduced
merge_request:
author:
type: changed
---
title: Replace 'source/search_code.feature' spinach test with an rspec analog
merge_request: 13697
author: blackst0ne
type: other
---
title: Fire system hooks when a user is created via LDAP
merge_request:
author:
type: fixed
...@@ -177,7 +177,7 @@ var config = { ...@@ -177,7 +177,7 @@ var config = {
if (chunk.name) { if (chunk.name) {
return chunk.name; return chunk.name;
} }
return chunk.modules.map((m) => { return chunk.mapModules((m) => {
var chunkPath = m.request.split('!').pop(); var chunkPath = m.request.split('!').pop();
return path.relative(m.context, chunkPath); return path.relative(m.context, chunkPath);
}).join('_'); }).join('_');
......
...@@ -6,7 +6,7 @@ class MigrateStagesStatuses < ActiveRecord::Migration ...@@ -6,7 +6,7 @@ class MigrateStagesStatuses < ActiveRecord::Migration
disable_ddl_transaction! disable_ddl_transaction!
BATCH_SIZE = 10000 BATCH_SIZE = 10000
RANGE_SIZE = 1000 RANGE_SIZE = 100
MIGRATION = 'MigrateStageStatus'.freeze MIGRATION = 'MigrateStageStatus'.freeze
class Stage < ActiveRecord::Base class Stage < ActiveRecord::Base
...@@ -17,10 +17,10 @@ class MigrateStagesStatuses < ActiveRecord::Migration ...@@ -17,10 +17,10 @@ class MigrateStagesStatuses < ActiveRecord::Migration
def up def up
Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index| Stage.where(status: nil).each_batch(of: BATCH_SIZE) do |relation, index|
relation.each_batch(of: RANGE_SIZE) do |batch| relation.each_batch(of: RANGE_SIZE) do |batch|
range = relation.pluck('MIN(id)', 'MAX(id)').first range = batch.pluck('MIN(id)', 'MAX(id)').first
schedule = index * 5.minutes delay = index * 5.minutes
BackgroundMigrationWorker.perform_in(schedule, MIGRATION, range) BackgroundMigrationWorker.perform_in(delay, MIGRATION, range)
end end
end end
end end
......
...@@ -4,7 +4,7 @@ To enable the Authentiq OmniAuth provider for passwordless authentication you mu ...@@ -4,7 +4,7 @@ To enable the Authentiq OmniAuth provider for passwordless authentication you mu
Authentiq will generate a Client ID and the accompanying Client Secret for you to use. Authentiq will generate a Client ID and the accompanying Client Secret for you to use.
1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/register). 1. Get your Client credentials (Client ID and Client Secret) at [Authentiq](https://www.authentiq.com/developers).
2. On your GitLab server, open the configuration file: 2. On your GitLab server, open the configuration file:
......
# Group milestones API # Group milestones API
> **Notes:**
> [Introduced][ce-12819] in GitLab 9.5.
## List group milestones ## List group milestones
Returns a list of group milestones. Returns a list of group milestones.
...@@ -118,3 +121,5 @@ Parameters: ...@@ -118,3 +121,5 @@ Parameters:
- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user - `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
- `milestone_id` (required) - The ID of a group milestone - `milestone_id` (required) - The ID of a group milestone
[ce-12819]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/12819
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
## List groups ## List groups
Get a list of groups. (As user: my groups or all available, as admin: all groups). Get a list of visible groups for the authenticated user. When accessed without
authentication, only public groups are returned.
Parameters: Parameters:
...@@ -43,7 +44,8 @@ You can search for groups by name or path, see below. ...@@ -43,7 +44,8 @@ You can search for groups by name or path, see below.
## List a group's projects ## List a group's projects
Get a list of projects in this group. Get a list of projects in this group. When accessed without authentication, only
public projects are returned.
``` ```
GET /groups/:id/projects GET /groups/:id/projects
...@@ -109,7 +111,8 @@ Example response: ...@@ -109,7 +111,8 @@ Example response:
## Details of a group ## Details of a group
Get all details of a group. Get all details of a group. This endpoint can be accessed without authentication
if the group is publicly accessible.
``` ```
GET /groups/:id GET /groups/:id
......
...@@ -87,6 +87,11 @@ To follow conventions of naming across GitLab, and to futher move away from the ...@@ -87,6 +87,11 @@ To follow conventions of naming across GitLab, and to futher move away from the
`build` term and toward `job` CI variables have been renamed for the 9.0 `build` term and toward `job` CI variables have been renamed for the 9.0
release. release.
>**Note:**
Starting with GitLab 9.0, we have deprecated the `$CI_BUILD_*` variables. **You are
strongly advised to use the new variables as we will remove the old ones in
future GitLab releases.**
| 8.x name | 9.0+ name | | 8.x name | 9.0+ name |
| --------------------- |------------------------ | | --------------------- |------------------------ |
| `CI_BUILD_ID` | `CI_JOB_ID` | | `CI_BUILD_ID` | `CI_JOB_ID` |
......
...@@ -1065,6 +1065,8 @@ a list of all previous jobs from which the artifacts should be downloaded. ...@@ -1065,6 +1065,8 @@ a list of all previous jobs from which the artifacts should be downloaded.
You can only define jobs from stages that are executed before the current one. You can only define jobs from stages that are executed before the current one.
An error will be shown if you define jobs from the current stage or next ones. An error will be shown if you define jobs from the current stage or next ones.
Defining an empty array will skip downloading any artifacts for that job. Defining an empty array will skip downloading any artifacts for that job.
The status of the previous job is not considered when using `dependencies`, so
if it failed or it is a manual job that was not run, no error occurs.
--- ---
......
...@@ -17,7 +17,7 @@ the hardware requirements. ...@@ -17,7 +17,7 @@ the hardware requirements.
- [Installation from source](installation.md) - Install GitLab from source. - [Installation from source](installation.md) - Install GitLab from source.
Useful for unsupported systems like *BSD. For an overview of the directory Useful for unsupported systems like *BSD. For an overview of the directory
structure, read the [structure documentation](structure.md). structure, read the [structure documentation](structure.md).
- [Docker](https://docs.gitlab.com/omnibus/docker/) - Install GitLab using Docker. - [Docker](docker.md) - Install GitLab using Docker.
## Install GitLab on cloud providers ## Install GitLab on cloud providers
......
...@@ -10,7 +10,7 @@ like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that y ...@@ -10,7 +10,7 @@ like Ubuntu, Red Hat Enterprise Linux, and of course - GitLab! This means that y
pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes. pre-configured GitLab VM and have your very own private GitLab up and running in around 30 minutes.
Let's get started. Let's get started.
### Getting started ## Getting started
First, you'll need an account on Azure. There are three ways to do this: First, you'll need an account on Azure. There are three ways to do this:
...@@ -25,7 +25,7 @@ This is a great way to try out Azure and cloud computing, and you can ...@@ -25,7 +25,7 @@ This is a great way to try out Azure and cloud computing, and you can
subscription gives you recurring Azure credits every month, so why not put those credits to use and subscription gives you recurring Azure credits every month, so why not put those credits to use and
try out GitLab right now? try out GitLab right now?
### Working with Azure ## Working with Azure
Once you have an Azure account, you can get started. Login to Azure using Once you have an Azure account, you can get started. Login to Azure using
[portal.azure.com](https://portal.azure.com) and the first thing you will see is the Dashboard: [portal.azure.com](https://portal.azure.com) and the first thing you will see is the Dashboard:
...@@ -35,7 +35,7 @@ Once you have an Azure account, you can get started. Login to Azure using ...@@ -35,7 +35,7 @@ Once you have an Azure account, you can get started. Login to Azure using
The Dashboard gives you a quick overview of Azure resources, and from here you you can build VMs, The Dashboard gives you a quick overview of Azure resources, and from here you you can build VMs,
create SQL Databases, author websites, and perform lots of other cloud tasks. create SQL Databases, author websites, and perform lots of other cloud tasks.
### Create New VM ## Create New VM
The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and The [Azure Marketplace][Azure-Marketplace] is an online store for pre-configured applications and
services which have been optimized for the cloud by software vendors like GitLab, and both services which have been optimized for the cloud by software vendors like GitLab, and both
...@@ -56,7 +56,7 @@ Click **"Create"** and you will be presented with the "Create virtual machine" b ...@@ -56,7 +56,7 @@ Click **"Create"** and you will be presented with the "Create virtual machine" b
![Azure - Create Virtual Machine - Basics](img/azure-create-virtual-machine-basics.png) ![Azure - Create Virtual Machine - Basics](img/azure-create-virtual-machine-basics.png)
### Basics ## Basics
The first items we need to configure are the basic settings of the underlying virtual machine: The first items we need to configure are the basic settings of the underlying virtual machine:
...@@ -84,7 +84,7 @@ Here are the settings we've used: ...@@ -84,7 +84,7 @@ Here are the settings we've used:
Check the settings you have entered, and then click **"OK"** when you're ready to proceed. Check the settings you have entered, and then click **"OK"** when you're ready to proceed.
### Size ## Size
Next, you need to choose the size of your VM - selecting features such as the number of CPU cores, Next, you need to choose the size of your VM - selecting features such as the number of CPU cores,
the amount of RAM, the size of storage (and its speed), etc. the amount of RAM, the size of storage (and its speed), etc.
...@@ -108,7 +108,7 @@ free trial credits, you'll likely want to learn ...@@ -108,7 +108,7 @@ free trial credits, you'll likely want to learn
Go ahead and click your chosen size, then click **"Select"** when you're ready to proceed to the Go ahead and click your chosen size, then click **"Select"** when you're ready to proceed to the
next step. next step.
### Settings ## Settings
On the next blade, you're asked to configure the Storage, Network and Extension settings. On the next blade, you're asked to configure the Storage, Network and Extension settings.
We've gone with the default settings as they're sufficient for test-driving GitLab, but please We've gone with the default settings as they're sufficient for test-driving GitLab, but please
...@@ -118,7 +118,7 @@ choose the settings which best meet your own requirements: ...@@ -118,7 +118,7 @@ choose the settings which best meet your own requirements:
Review the settings and then click **"OK"** when you're ready to proceed to the last step. Review the settings and then click **"OK"** when you're ready to proceed to the last step.
### Purchase ## Purchase
The Purchase page is the last step and here you will be presented with the price per hour for your The Purchase page is the last step and here you will be presented with the price per hour for your
new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the new VM. You'll be billed only for the VM itself (e.g. "Standard DS1 v2") because the
...@@ -131,7 +131,7 @@ previous steps, just click on any of the four steps to re-open them. ...@@ -131,7 +131,7 @@ previous steps, just click on any of the four steps to re-open them.
When you have read and agreed to the terms of use and are ready to proceed, click **"Purchase"**. When you have read and agreed to the terms of use and are ready to proceed, click **"Purchase"**.
### Deployment ## Deployment
At this point, Azure will begin deploying your new VM. The deployment process will take a few At this point, Azure will begin deploying your new VM. The deployment process will take a few
minutes to complete, with progress displayed on the **"Deployment"** blade: minutes to complete, with progress displayed on the **"Deployment"** blade:
...@@ -146,7 +146,7 @@ on the Azure Dashboard (you may need to refresh the page): ...@@ -146,7 +146,7 @@ on the Azure Dashboard (you may need to refresh the page):
The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the The new VM can also be accessed by clicking the `All resources` or `Virtual machines` icons in the
Azure Portal sidebar navigation menu. Azure Portal sidebar navigation menu.
### Setup a domain name ## Setup a domain name
The VM will have a public IP address (static by default), but Azure allows us to assign a friendly The VM will have a public IP address (static by default), but Azure allows us to assign a friendly
DNS name to the VM, so let's go ahead and do that. DNS name to the VM, so let's go ahead and do that.
...@@ -174,7 +174,7 @@ to make sure your VM is configured to use a _static_ public IP address (i.e. not ...@@ -174,7 +174,7 @@ to make sure your VM is configured to use a _static_ public IP address (i.e. not
or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP or you will have to reconfigure the DNS `A` record each time Azure reassigns your VM a new public IP
address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more. address. Read [IP address types and allocation methods in Azure][Azure-IP-Address-Types] to learn more.
### Let's open some ports! ## Let's open some ports!
At this stage you should have a running and fully operational VM. However, none of the services on At this stage you should have a running and fully operational VM. However, none of the services on
your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the your VM (e.g. GitLab) will be publicly accessible via the internet until you have opened up the
...@@ -202,7 +202,7 @@ Next, click **"Add"**: ...@@ -202,7 +202,7 @@ Next, click **"Add"**:
![Azure - Network security group - Inbound security rules - Add](img/azure-nsg-inbound-sec-rules-add-highlight.png) ![Azure - Network security group - Inbound security rules - Add](img/azure-nsg-inbound-sec-rules-add-highlight.png)
#### Which ports to open? ### Which ports to open?
Like all servers, our VM will be running many services. However, we want to open up the correct Like all servers, our VM will be running many services. However, we want to open up the correct
ports to enable public internet access to two services in particular: ports to enable public internet access to two services in particular:
...@@ -213,7 +213,7 @@ public access to the instance of GitLab running on our VM. ...@@ -213,7 +213,7 @@ public access to the instance of GitLab running on our VM.
allowing public access (with authentication) to remote terminal sessions allowing public access (with authentication) to remote terminal sessions
_(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_ _(you'll see why we need [SSH] access to our VM [later on in this tutorial](#maintaining-your-gitlab-instance))_
#### Open HTTP on Port 80 ### Open HTTP on Port 80
In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP In the **"Add inbound security rule"** blade, let's open port 80 so that our VM will accept HTTP
connections: connections:
...@@ -225,7 +225,7 @@ connections: ...@@ -225,7 +225,7 @@ connections:
1. Make sure the `Action` is set to **Allow** 1. Make sure the `Action` is set to **Allow**
1. Click **"OK"** 1. Click **"OK"**
#### Open SSH on Port 22 ### Open SSH on Port 22
Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to Repeat the above process, adding a second Inbound security rule to open port 22, enabling our VM to
accept [SSH] connections: accept [SSH] connections:
......
# GitLab Docker images
[Docker](https://www.docker.com) and container technology have been revolutionizing the software world for the past few years. They combine the performance and efficiency of native execution with the abstraction, security, and immutability of virtualization.
GitLab provides official Docker images to allowing you to easily take advantage of the benefits of containerization while operating your GitLab instance.
## Omnibus GitLab based images
GitLab maintains a set of [official Docker images](https://hub.docker.com/r/gitlab) based on our [Omnibus GitLab package](https://docs.gitlab.com/omnibus/README.html). These images include:
* [GitLab Community Edition](https://hub.docker.com/r/gitlab/gitlab-ce/)
* [GitLab Enterprise Edition](https://hub.docker.com/r/gitlab/gitlab-ee/)
* [GitLab Runner](https://hub.docker.com/r/gitlab/gitlab-runner/)
A [complete usage guide](https://docs.gitlab.com/omnibus/docker/) to these images is available, as well as the [Dockerfile used for building the images](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker).
## Cloud native images
GitLab is also working towards a [cloud native set of containers](https://gitlab.com/charts/helm.gitlab.io#docker-container-images), with a single image for each component service. We intend for these images to eventually replace the [Omnibus GitLab based images](#omnibus-gitlab-based-images).
# GitLab Docker images # GitLab Docker images
* The official GitLab Community Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ce/). This content has been moved to [our documentation site](https://docs.gitlab.com/ce/install/docker.html).
* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://hub.docker.com/r/gitlab/gitlab-ee/).
* The complete usage guide can be found in [Using GitLab Docker images](https://docs.gitlab.com/omnibus/docker/)
* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
* Check the guide for [creating Omnibus-based Docker Image](https://docs.gitlab.com/omnibus/build/README.html#build-docker-image)
Feature: Project Source Search Code
Background:
Given I sign in as a user
Scenario: Search for term "coffee"
Given I own project "Shop"
And I visit project source page
When I search for term "coffee"
Then I should see files from repository containing "coffee"
Scenario: Search on empty project
Given I own an empty project
And I visit my project's home page
When I search for term "coffee"
Then I should see empty result
class Spinach::Features::ProjectSourceSearchCode < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
include SharedPaths
step 'I search for term "coffee"' do
fill_in "search", with: "coffee"
click_button "Go"
end
step 'I should see files from repository containing "coffee"' do
expect(page).to have_content 'coffee'
expect(page).to have_content 'CONTRIBUTING.md'
end
step 'I should see empty result' do
expect(page).to have_content "We couldn't find any"
end
end
...@@ -328,10 +328,6 @@ module SharedPaths ...@@ -328,10 +328,6 @@ module SharedPaths
visit project_commits_path(@project, 'stable', { limit: 5 }) visit project_commits_path(@project, 'stable', { limit: 5 })
end end
step 'I visit project source page' do
visit project_tree_path(@project, root_ref)
end
step 'I visit blob file from repo' do step 'I visit blob file from repo' do
visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path)) visit project_blob_path(@project, File.join(sample_commit.id, sample_blob.path))
end end
......
...@@ -2,7 +2,7 @@ module API ...@@ -2,7 +2,7 @@ module API
class Groups < Grape::API class Groups < Grape::API
include PaginationParams include PaginationParams
before { authenticate! } before { authenticate_non_get! }
helpers do helpers do
params :optional_params_ce do params :optional_params_ce do
...@@ -56,16 +56,8 @@ module API ...@@ -56,16 +56,8 @@ module API
use :pagination use :pagination
end end
get do get do
groups = if params[:owned] find_params = { all_available: params[:all_available], owned: params[:owned] }
current_user.owned_groups groups = GroupsFinder.new(current_user, find_params).execute
elsif current_user.admin
Group.all
elsif params[:all_available]
GroupsFinder.new(current_user).execute
else
current_user.groups
end
groups = groups.search(params[:search]) if params[:search].present? groups = groups.search(params[:search]) if params[:search].present?
groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present? groups = groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
groups = groups.reorder(params[:order_by] => params[:sort]) groups = groups.reorder(params[:order_by] => params[:sort])
......
...@@ -40,4 +40,18 @@ feature 'Issue Detail', :js do ...@@ -40,4 +40,18 @@ feature 'Issue Detail', :js do
end end
end end
end end
context 'when authored by a user who is later deleted' do
before do
issue.update_attribute(:author_id, nil)
sign_in(user)
visit project_issue_path(project, issue)
end
it 'shows the issue' do
page.within('.issuable-details') do
expect(find('h2')).to have_content(issue.title)
end
end
end
end end
require 'spec_helper'
feature 'Find files button in the tree header' do
given(:user) { create(:user) }
given(:project) { create(:project, :repository) }
background do
sign_in(user)
project.team << [user, :developer]
end
scenario 'project main screen' do
visit project_path(project)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
scenario 'project tree screen' do
visit project_tree_path(project, project.default_branch)
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
end
require 'spec_helper'
describe 'User searches for files' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
before do
sign_in(user)
end
describe 'project main screen' do
context 'when project is empty' do
let(:empty_project) { create(:project) }
before do
empty_project.add_developer(user)
visit project_path(empty_project)
end
it 'does not show any result' do
fill_in('search', with: 'coffee')
click_button('Go')
expect(page).to have_content("We couldn't find any")
end
end
context 'when project is not empty' do
before do
project.add_developer(user)
visit project_path(project)
end
it 'shows "Find file" button' do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
end
end
describe 'project tree screen' do
before do
project.add_developer(user)
visit project_tree_path(project, project.default_branch)
end
it 'shows "Find file" button' do
expect(page).to have_selector('.tree-controls .shortcuts-find-file')
end
it 'shows found files' do
fill_in('search', with: 'coffee')
click_button('Go')
expect(page).to have_content('coffee')
expect(page).to have_content('CONTRIBUTING.md')
end
end
end
...@@ -281,4 +281,30 @@ describe "Search" do ...@@ -281,4 +281,30 @@ describe "Search" do
expect(page).to have_selector('.commit-row-description', count: 9) expect(page).to have_selector('.commit-row-description', count: 9)
end end
end end
context 'anonymous user' do
let(:project) { create(:project, :public) }
before do
sign_out(user)
end
it 'preserves the group being searched in' do
visit search_path(group_id: project.namespace.id)
fill_in 'search', with: 'foo'
click_button 'Search'
expect(find('#group_id').value).to eq(project.namespace.id.to_s)
end
it 'preserves the project being searched in' do
visit search_path(project_id: project.id)
fill_in 'search', with: 'foo'
click_button 'Search'
expect(find('#project_id').value).to eq(project.id.to_s)
end
end
end end
...@@ -62,4 +62,67 @@ describe ButtonHelper do ...@@ -62,4 +62,67 @@ describe ButtonHelper do
end end
end end
end end
describe 'clipboard_button' do
let(:user) { create(:user) }
let(:project) { build_stubbed(:project) }
def element(data = {})
element = helper.clipboard_button(data)
Nokogiri::HTML::DocumentFragment.parse(element).first_element_child
end
before do
allow(helper).to receive(:current_user).and_return(user)
end
context 'with default options' do
context 'when no `text` attribute is not provided' do
it 'shows copy to clipboard button with default configuration and no text set to copy' do
expect(element.attr('class')).to eq('btn btn-clipboard btn-transparent')
expect(element.attr('type')).to eq('button')
expect(element.attr('aria-label')).to eq('Copy to clipboard')
expect(element.attr('data-toggle')).to eq('tooltip')
expect(element.attr('data-placement')).to eq('bottom')
expect(element.attr('data-container')).to eq('body')
expect(element.attr('data-clipboard-text')).to eq(nil)
expect(element.inner_text).to eq("")
expect(element).to have_selector('.fa.fa-clipboard')
end
end
context 'when `text` attribute is provided' do
it 'shows copy to clipboard button with provided `text` to copy' do
expect(element(text: 'Hello World!').attr('data-clipboard-text')).to eq('Hello World!')
end
end
context 'when `title` attribute is provided' do
it 'shows copy to clipboard button with provided `title` as tooltip' do
expect(element(title: 'Copy to my clipboard!').attr('aria-label')).to eq('Copy to my clipboard!')
end
end
end
context 'with `button_text` attribute provided' do
it 'shows copy to clipboard button with provided `button_text` as button label' do
expect(element(button_text: 'Copy text').inner_text).to eq('Copy text')
end
end
context 'with `hide_tooltip` attribute provided' do
it 'shows copy to clipboard button without tooltip support' do
expect(element(hide_tooltip: true).attr('data-placement')).to eq(nil)
expect(element(hide_tooltip: true).attr('data-toggle')).to eq(nil)
expect(element(hide_tooltip: true).attr('data-container')).to eq(nil)
end
end
context 'with `hide_button_icon` attribute provided' do
it 'shows copy to clipboard button without tooltip support' do
expect(element(hide_button_icon: true)).not_to have_selector('.fa.fa-clipboard')
end
end
end
end end
...@@ -17,7 +17,7 @@ describe('Api', () => { ...@@ -17,7 +17,7 @@ describe('Api', () => {
beforeEach(() => { beforeEach(() => {
originalGon = window.gon; originalGon = window.gon;
window.gon = dummyGon; window.gon = Object.assign({}, dummyGon);
}); });
afterEach(() => { afterEach(() => {
...@@ -98,10 +98,11 @@ describe('Api', () => { ...@@ -98,10 +98,11 @@ describe('Api', () => {
}); });
describe('projects', () => { describe('projects', () => {
it('fetches projects', (done) => { it('fetches projects with membership when logged in', (done) => {
const query = 'dummy query'; const query = 'dummy query';
const options = { unused: 'option' }; const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
window.gon.current_user_id = 1;
const expectedData = Object.assign({ const expectedData = Object.assign({
search: query, search: query,
per_page: 20, per_page: 20,
...@@ -119,6 +120,27 @@ describe('Api', () => { ...@@ -119,6 +120,27 @@ describe('Api', () => {
done(); done();
}); });
}); });
it('fetches projects without membership when not logged in', (done) => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json?simple=true`;
const expectedData = Object.assign({
search: query,
per_page: 20,
}, options);
spyOn(jQuery, 'ajax').and.callFake((request) => {
expect(request.url).toEqual(expectedUrl);
expect(request.dataType).toEqual('json');
expect(request.data).toEqual(expectedData);
return sendDummyResponse();
});
Api.projects(query, options, (response) => {
expect(response).toBe(dummyResponse);
done();
});
});
}); });
describe('newLabel', () => { describe('newLabel', () => {
......
...@@ -7,6 +7,7 @@ import '~/project_select'; ...@@ -7,6 +7,7 @@ import '~/project_select';
import '~/project'; import '~/project';
describe('Project Title', () => { describe('Project Title', () => {
const dummyApiVersion = 'v3000';
preloadFixtures('issues/open-issue.html.raw'); preloadFixtures('issues/open-issue.html.raw');
loadJSONFixtures('projects.json'); loadJSONFixtures('projects.json');
...@@ -14,7 +15,7 @@ describe('Project Title', () => { ...@@ -14,7 +15,7 @@ describe('Project Title', () => {
loadFixtures('issues/open-issue.html.raw'); loadFixtures('issues/open-issue.html.raw');
window.gon = {}; window.gon = {};
window.gon.api_version = 'v3'; window.gon.api_version = dummyApiVersion;
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Project(); new Project();
...@@ -37,9 +38,10 @@ describe('Project Title', () => { ...@@ -37,9 +38,10 @@ describe('Project Title', () => {
it('toggles dropdown', () => { it('toggles dropdown', () => {
const $menu = $('.js-dropdown-menu-projects'); const $menu = $('.js-dropdown-menu-projects');
window.gon.current_user_id = 1;
$('.js-projects-dropdown-toggle').click(); $('.js-projects-dropdown-toggle').click();
expect($menu).toHaveClass('open'); expect($menu).toHaveClass('open');
expect(reqUrl).toBe('/api/v3/projects.json?simple=true'); expect(reqUrl).toBe(`/api/${dummyApiVersion}/projects.json?simple=true`);
expect(reqData).toEqual({ expect(reqData).toEqual({
search: '', search: '',
order_by: 'last_activity_at', order_by: 'last_activity_at',
......
...@@ -12,7 +12,7 @@ describe MigrateStagesStatuses, :migration do ...@@ -12,7 +12,7 @@ describe MigrateStagesStatuses, :migration do
before do before do
stub_const("#{described_class.name}::BATCH_SIZE", 2) stub_const("#{described_class.name}::BATCH_SIZE", 2)
stub_const("#{described_class.name}::RANGE_SIZE", 2) stub_const("#{described_class.name}::RANGE_SIZE", 1)
projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1') projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1')
projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2') projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2')
...@@ -50,9 +50,10 @@ describe MigrateStagesStatuses, :migration do ...@@ -50,9 +50,10 @@ describe MigrateStagesStatuses, :migration do
Timecop.freeze do Timecop.freeze do
migrate! migrate!
expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 2) expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 1, 1)
expect(described_class::MIGRATION).to be_scheduled_migration(5.minutes, 2, 2)
expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3) expect(described_class::MIGRATION).to be_scheduled_migration(10.minutes, 3, 3)
expect(BackgroundMigrationWorker.jobs.size).to eq 2 expect(BackgroundMigrationWorker.jobs.size).to eq 3
end end
end end
end end
......
...@@ -21,10 +21,15 @@ describe API::Groups do ...@@ -21,10 +21,15 @@ describe API::Groups do
describe "GET /groups" do describe "GET /groups" do
context "when unauthenticated" do context "when unauthenticated" do
it "returns authentication error" do it "returns public groups" do
get api("/groups") get api("/groups")
expect(response).to have_http_status(401) expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response)
.to satisfy_one { |group| group['name'] == group1.name }
end end
end end
...@@ -179,6 +184,18 @@ describe API::Groups do ...@@ -179,6 +184,18 @@ describe API::Groups do
end end
describe "GET /groups/:id" do describe "GET /groups/:id" do
context 'when unauthenticated' do
it 'returns 404 for a private group' do
get api("/groups/#{group2.id}")
expect(response).to have_http_status(404)
end
it 'returns 200 for a public group' do
get api("/groups/#{group1.id}")
expect(response).to have_http_status(200)
end
end
context "when authenticated as user" do context "when authenticated as user" do
it "returns one of user1's groups" do it "returns one of user1's groups" do
project = create(:project, namespace: group2, path: 'Foo') project = create(:project, namespace: group2, path: 'Foo')
......
...@@ -37,7 +37,10 @@ describe Users::UpdateService do ...@@ -37,7 +37,10 @@ describe Users::UpdateService do
describe '#execute!' do describe '#execute!' do
it 'updates the name' do it 'updates the name' do
result = update_user(user, name: 'New Name') service = described_class.new(user, name: 'New Name')
expect(service).not_to receive(:notify_new_user)
result = service.execute!
expect(result).to be true expect(result).to be true
expect(user.name).to eq('New Name') expect(user.name).to eq('New Name')
...@@ -49,6 +52,18 @@ describe Users::UpdateService do ...@@ -49,6 +52,18 @@ describe Users::UpdateService do
end.to raise_error(ActiveRecord::RecordInvalid) end.to raise_error(ActiveRecord::RecordInvalid)
end end
it 'fires system hooks when a new user is saved' do
system_hook_service = spy(:system_hook_service)
user = build(:user)
service = described_class.new(user, name: 'John Doe')
expect(service).to receive(:notify_new_user).and_call_original
expect(service).to receive(:system_hook_service).and_return(system_hook_service)
service.execute
expect(system_hook_service).to have_received(:execute_hooks_for).with(user, :create)
end
def update_user(user, opts) def update_user(user, opts)
described_class.new(user, opts).execute! described_class.new(user, opts).execute!
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