diff options
40 files changed, 716 insertions, 214 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 3f315550536..d04069df885 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -249,7 +249,7 @@ teaspoon: - curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - apt-get install --assume-yes nodejs - npm install --global istanbul - - teaspoon + - rake teaspoon artifacts: name: coverage-javascript expire_in: 31d diff --git a/CHANGELOG.md b/CHANGELOG.md index 0120e3bb07e..efa5a213570 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Please view this file on the master branch, on stable branches it's out of date. - API: Fix booleans not recognized as such when using the `to_boolean` helper - Removed delete branch tooltip !6954 - Stop unauthorized users dragging on milestone page (blackst0ne) + - Restore issue boards welcome message when a project is created !6899 - Escape ref and path for relative links !6050 (winniehell) - Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose) - Fix filtering of milestones with quotes in title (airatshigapov) @@ -32,14 +33,14 @@ Please view this file on the master branch, on stable branches it's out of date. - Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens - Show full status link on MR & commit pipelines - Fix documents and comments on Build API `scope` - - Fix applying labels for GitHub-imported MRs - - Fix importing MR comments from GitHub - - Modify GitHub importer to be retryable - Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov) - Shortened merge request modal to let clipboard button not overlap ## 8.13.2 - Fix builds dropdown overlapping bug !7124 + - Fix applying labels for GitHub-imported MRs !7139 + - Fix importing MR comments from GitHub !7139 + - Modify GitHub importer to be retryable !7003 - Fix and improve `Sortable.highest_label_priority` ## 8.13.1 (2016-10-25) @@ -91,6 +92,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Updating verbiage on git basics to be more intuitive - Fix project_feature record not generated on project creation - Clarify documentation for Runners API (Gennady Trafimenkov) + - Use optimistic locking for pipelines and builds - The instrumentation for Banzai::Renderer has been restored - Change user & group landing page routing from /u/:username to /:username - Added documentation for .gitattributes files @@ -158,6 +160,7 @@ Please view this file on the master branch, on stable branches it's out of date. - Only update issuable labels if they have been changed - Take filters in account in issuable counters. !6496 - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*) + - Replace static issue fixtures by script !6059 (winniehell) - Append issue template to existing description !6149 (Joseph Frazier) - Trending projects now only show public projects and the list of projects is cached for a day - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro) diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6 index 534845cd8a2..175e034afed 100644 --- a/app/assets/javascripts/boards/stores/boards_store.js.es6 +++ b/app/assets/javascripts/boards/stores/boards_store.js.es6 @@ -63,7 +63,8 @@ this.removeList('blank'); Cookies.set('issue_board_welcome_hidden', 'true', { - expires: 365 * 10 + expires: 365 * 10, + path: '' }); }, welcomeIsHidden () { diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 76b730198d4..8c148ecfaeb 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -30,6 +30,8 @@ class ProjectsController < Projects::ApplicationController @project = ::Projects::CreateService.new(current_user, project_params).execute if @project.saved? + cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) } + redirect_to( project_path(@project), notice: "Project '#{@project.name}' was successfully created." diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb index 44484d64567..865f093f04a 100644 --- a/app/finders/labels_finder.rb +++ b/app/finders/labels_finder.rb @@ -4,9 +4,8 @@ class LabelsFinder < UnionFinder @params = params end - def execute(authorized_only: true) - @authorized_only = authorized_only - + def execute(skip_authorization: false) + @skip_authorization = skip_authorization items = find_union(label_ids, Label) items = with_title(items) sort(items) @@ -14,7 +13,7 @@ class LabelsFinder < UnionFinder private - attr_reader :current_user, :params, :authorized_only + attr_reader :current_user, :params, :skip_authorization def label_ids label_ids = [] @@ -70,17 +69,17 @@ class LabelsFinder < UnionFinder end def find_project - if authorized_only - available_projects.find_by(id: project_id) - else + if skip_authorization Project.find_by(id: project_id) + else + available_projects.find_by(id: project_id) end end def projects return @projects if defined?(@projects) - @projects = authorized_only ? available_projects : Project.all + @projects = skip_authorization ? Project.all : available_projects @projects = @projects.in_namespace(group_id) if group_id @projects = @projects.where(id: projects_ids) if projects_ids @projects = @projects.reorder(nil) diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb index adda3b8f40c..d3432632899 100644 --- a/app/models/ci/pipeline.rb +++ b/app/models/ci/pipeline.rb @@ -30,23 +30,23 @@ module Ci end event :run do - transition any => :running + transition any - [:running] => :running end event :skip do - transition any => :skipped + transition any - [:skipped] => :skipped end event :drop do - transition any => :failed + transition any - [:failed] => :failed end event :succeed do - transition any => :success + transition any - [:success] => :success end event :cancel do - transition any => :canceled + transition any - [:canceled] => :canceled end # IMPORTANT @@ -260,7 +260,7 @@ module Ci end def update_status - with_lock do + Gitlab::OptimisticLocking.retry_lock(self) do case latest_builds_status when 'pending' then enqueue when 'running' then run diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb index 4cb3a69416e..d159fc6c5c7 100644 --- a/app/models/commit_status.rb +++ b/app/models/commit_status.rb @@ -73,16 +73,16 @@ class CommitStatus < ActiveRecord::Base transition [:created, :pending, :running] => :canceled end - after_transition created: [:pending, :running] do |commit_status| - commit_status.update_attributes queued_at: Time.now + before_transition created: [:pending, :running] do |commit_status| + commit_status.queued_at = Time.now end - after_transition [:created, :pending] => :running do |commit_status| - commit_status.update_attributes started_at: Time.now + before_transition [:created, :pending] => :running do |commit_status| + commit_status.started_at = Time.now end - after_transition any => [:success, :failed, :canceled] do |commit_status| - commit_status.update_attributes finished_at: Time.now + before_transition any => [:success, :failed, :canceled] do |commit_status| + commit_status.finished_at = Time.now end after_transition do |commit_status, transition| diff --git a/app/models/project.rb b/app/models/project.rb index fbf7012972e..ae57815c620 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -738,7 +738,7 @@ class Project < ActiveRecord::Base def create_labels Label.templates.each do |label| params = label.attributes.except('id', 'template', 'created_at', 'updated_at') - Labels::FindOrCreateService.new(owner, self, params).execute + Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true) end end diff --git a/app/models/project_team.rb b/app/models/project_team.rb index 79d041d2775..a6e911df9bd 100644 --- a/app/models/project_team.rb +++ b/app/models/project_team.rb @@ -125,14 +125,8 @@ class ProjectTeam max_member_access(user.id) == Gitlab::Access::MASTER end - def member?(user, min_member_access = nil) - member = !!find_member(user.id) - - if min_member_access - member && max_member_access(user.id) >= min_member_access - else - member - end + def member?(user, min_member_access = Gitlab::Access::GUEST) + max_member_access(user.id) >= min_member_access end def human_max_access(user_id) diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb index d3dd30b2588..8face432d97 100644 --- a/app/services/ci/process_pipeline_service.rb +++ b/app/services/ci/process_pipeline_service.rb @@ -10,17 +10,14 @@ module Ci create_builds! end - @pipeline.with_lock do - new_builds = - stage_indexes_of_created_builds.map do |index| - process_stage(index) - end + new_builds = + stage_indexes_of_created_builds.map do |index| + process_stage(index) + end - @pipeline.update_status + @pipeline.update_status - # Return a flag if a when builds got enqueued - new_builds.flatten.any? - end + new_builds.flatten.any? end private @@ -32,9 +29,11 @@ module Ci def process_stage(index) current_status = status_for_prior_stages(index) - created_builds_in_stage(index).select do |build| - if HasStatus::COMPLETED_STATUSES.include?(current_status) - process_build(build, current_status) + if HasStatus::COMPLETED_STATUSES.include?(current_status) + created_builds_in_stage(index).select do |build| + Gitlab::OptimisticLocking.retry_lock(build) do |subject| + process_build(subject, current_status) + end end end end diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb index 6973191b203..74b5ebf372b 100644 --- a/app/services/ci/register_build_service.rb +++ b/app/services/ci/register_build_service.rb @@ -28,17 +28,14 @@ module Ci if build # In case when 2 runners try to assign the same build, second runner will be declined - # with StateMachines::InvalidTransition in run! method. - build.with_lock do - build.runner_id = current_runner.id - build.save! - build.run! - end + # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method. + build.runner_id = current_runner.id + build.run! end build - rescue StateMachines::InvalidTransition + rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError nil end diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb index 74291312c4e..d622f9edd33 100644 --- a/app/services/labels/find_or_create_service.rb +++ b/app/services/labels/find_or_create_service.rb @@ -2,21 +2,24 @@ module Labels class FindOrCreateService def initialize(current_user, project, params = {}) @current_user = current_user - @group = project.group @project = project @params = params.dup end - def execute + def execute(skip_authorization: false) + @skip_authorization = skip_authorization find_or_create_label end private - attr_reader :current_user, :group, :project, :params + attr_reader :current_user, :project, :params, :skip_authorization def available_labels - @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute + @available_labels ||= LabelsFinder.new( + current_user, + project_id: project.id + ).execute(skip_authorization: skip_authorization) end def find_or_create_label diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb index 416aee2ab51..c13f289f61e 100644 --- a/app/services/members/approve_access_request_service.rb +++ b/app/services/members/approve_access_request_service.rb @@ -4,17 +4,25 @@ module Members attr_accessor :source + # source - The source object that respond to `#requesters` (i.g. project or group) + # current_user - The user that performs the access request approval + # params - A hash of parameters + # :user_id - User ID used to retrieve the access requester + # :id - Member ID used to retrieve the access requester + # :access_level - Optional access level set when the request is accepted def initialize(source, current_user, params = {}) @source = source @current_user = current_user - @params = params + @params = params.slice(:user_id, :id, :access_level) end - def execute + # opts - A hash of options + # :force - Bypass permission check: current_user can be nil in that case + def execute(opts = {}) condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] } access_requester = source.requesters.find_by!(condition) - raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester) + raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts) access_requester.access_level = params[:access_level] if params[:access_level] access_requester.accept_request @@ -24,8 +32,11 @@ module Members private - def can_update_access_requester?(access_requester) - access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester) + def can_update_access_requester?(access_requester, opts = {}) + access_requester && ( + opts[:force] || + can?(current_user, action_member_permission(:update, access_requester), access_requester) + ) end end end diff --git a/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb new file mode 100644 index 00000000000..b47f3aa2810 --- /dev/null +++ b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb @@ -0,0 +1,14 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class AddLockVersionToBuildAndPipelines < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + # Set this constant to true if this migration requires downtime. + DOWNTIME = false + + def change + add_column :ci_builds, :lock_version, :integer + add_column :ci_commits, :lock_version, :integer + end +end diff --git a/db/schema.rb b/db/schema.rb index 5bbfba0d7dd..54b5fc83be0 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 20161025231710) do t.text "yaml_variables" t.datetime "queued_at" t.string "token" + t.integer "lock_version" end add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree @@ -219,6 +220,7 @@ ActiveRecord::Schema.define(version: 20161025231710) do t.datetime "finished_at" t.integer "duration" t.integer "user_id" + t.integer "lock_version" end add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree diff --git a/doc/development/frontend.md b/doc/development/frontend.md index 56c8516508e..4fb56444917 100644 --- a/doc/development/frontend.md +++ b/doc/development/frontend.md @@ -185,6 +185,20 @@ again in the future. See [the Testing Standards and Style Guidelines](testing.md) for more information. +### Running frontend tests + +`rake teaspoon` runs the frontend-only (JavaScript) tests. +It consists of two subtasks: + +- `rake teaspoon:fixtures` (re-)generates fixtures +- `rake teaspoon:tests` actually executes the tests + +As long as the fixtures don't change, `rake teaspoon:tests` is sufficient +(and saves you some time). + +Please note: Not all of the frontend fixtures are generated. Some are still static +files. These will not be touched by `rake teaspoon:fixtures`. + ## Supported browsers For our currently-supported browsers, see our [requirements][requirements]. diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb index c24831e68ee..9f9a96cdc65 100644 --- a/lib/banzai/filter/label_reference_filter.rb +++ b/lib/banzai/filter/label_reference_filter.rb @@ -39,7 +39,7 @@ module Banzai end def find_labels(project) - LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false) + LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true) end # Parameters to pass to `Label.find_by` based on the given arguments diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb index 65ee85ca5a9..222bcdcbf9c 100644 --- a/lib/gitlab/fogbugz_import/importer.rb +++ b/lib/gitlab/fogbugz_import/importer.rb @@ -75,7 +75,7 @@ module Gitlab def create_label(name) params = { title: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def user_info(person_id) @@ -133,7 +133,7 @@ module Gitlab updated_at: DateTime.parse(bug['dtLastUpdated']) ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb index 002494739e9..211ccdc51bb 100644 --- a/lib/gitlab/github_import/label_formatter.rb +++ b/lib/gitlab/github_import/label_formatter.rb @@ -15,8 +15,8 @@ module Gitlab def create! params = attributes.except(:project) - service = ::Labels::FindOrCreateService.new(project.owner, project, params) - label = service.execute + service = ::Labels::FindOrCreateService.new(nil, project, params) + label = service.execute(skip_authorization: true) raise ActiveRecord::RecordInvalid.new(label) unless label.persisted? diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb index 6a68e786b4f..1f4edc36928 100644 --- a/lib/gitlab/google_code_import/importer.rb +++ b/lib/gitlab/google_code_import/importer.rb @@ -101,7 +101,7 @@ module Gitlab state: raw_issue['state'] == 'closed' ? 'closed' : 'opened' ) - issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute + issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true) issue.update_attribute(:label_ids, issue_labels.pluck(:id)) import_issue_comments(issue, comments) @@ -235,7 +235,7 @@ module Gitlab def create_label(name) params = { name: name, color: nice_label_color(name) } - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end def format_content(raw_content) diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb index dbc759367eb..b8ca7f2f55f 100644 --- a/lib/gitlab/issues_labels.rb +++ b/lib/gitlab/issues_labels.rb @@ -19,7 +19,7 @@ module Gitlab ] labels.each do |params| - ::Labels::FindOrCreateService.new(project.owner, project, params).execute + ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true) end end end diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb new file mode 100644 index 00000000000..879d46446b3 --- /dev/null +++ b/lib/gitlab/optimistic_locking.rb @@ -0,0 +1,19 @@ +module Gitlab + module OptimisticLocking + extend self + + def retry_lock(subject, retries = 100, &block) + loop do + begin + ActiveRecord::Base.transaction do + return block.call(subject) + end + rescue ActiveRecord::StaleObjectError + retries -= 1 + raise unless retries >= 0 + subject.reload + end + end + end + end +end diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake new file mode 100644 index 00000000000..156fa90537d --- /dev/null +++ b/lib/tasks/teaspoon.rake @@ -0,0 +1,23 @@ +Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon') + +namespace :teaspoon do + desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests' + RSpec::Core::RakeTask.new(:fixtures) do |t| + ENV['NO_KNAPSACK'] = 'true' + t.pattern = 'spec/javascripts/fixtures/*.rb' + t.rspec_opts = '--format documentation' + end + + desc 'GitLab | Teaspoon | Run JavaScript tests' + task :tests do + require "teaspoon/console" + options = {} + abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures? + end +end + +desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests' +task :teaspoon do + Rake::Task['teaspoon:fixtures'].invoke + Rake::Task['teaspoon:tests'].invoke +end diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb index 41df63d445a..8faecec0063 100644 --- a/spec/controllers/projects/labels_controller_spec.rb +++ b/spec/controllers/projects/labels_controller_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Projects::LabelsController do let(:group) { create(:group) } - let(:project) { create(:project, namespace: group) } + let(:project) { create(:empty_project, namespace: group) } let(:user) { create(:user) } before do @@ -73,16 +73,27 @@ describe Projects::LabelsController do describe 'POST #generate' do let(:admin) { create(:admin) } - let(:project) { create(:empty_project) } before do sign_in(admin) end - it 'creates labels' do - post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + context 'personal project' do + let(:personal_project) { create(:empty_project) } - expect(response).to have_http_status(302) + it 'creates labels' do + post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param + + expect(response).to have_http_status(302) + end + end + + context 'project belonging to a group' do + it 'creates labels' do + post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to have_http_status(302) + end end end end diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index 63e487a7ad3..6208c2386b0 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -18,7 +18,10 @@ gl.boardService = new BoardService('/test/issue-boards/board', '1'); gl.issueBoards.BoardsStore.create(); - Cookies.set('issue_board_welcome_hidden', 'false'); + Cookies.set('issue_board_welcome_hidden', 'false', { + expires: 365 * 10, + path: '' + }); }); describe('Store', () => { diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore new file mode 100644 index 00000000000..009b68d5d1c --- /dev/null +++ b/spec/javascripts/fixtures/.gitignore @@ -0,0 +1 @@ +*.html.raw diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb new file mode 100644 index 00000000000..d95eb851421 --- /dev/null +++ b/spec/javascripts/fixtures/issues.rb @@ -0,0 +1,44 @@ +require 'spec_helper' + +describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let(:project) { create(:project_empty_repo) } + + render_views + + before(:all) do + clean_frontend_fixtures('issues/') + end + + before(:each) do + sign_in(admin) + end + + it 'issues/open-issue.html.raw' do |example| + render_issue(example.description, create(:issue, project: project)) + end + + it 'issues/closed-issue.html.raw' do |example| + render_issue(example.description, create(:closed_issue, project: project)) + end + + it 'issues/issue-with-task-list.html.raw' do |example| + issue = create(:issue, project: project) + issue.update(description: '- [ ] Task List Item') + render_issue(example.description, issue) + end + + private + + def render_issue(fixture_file_name, issue) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: issue.to_param + + expect(response).to be_success + store_frontend_fixture(response, fixture_file_name) + end +end diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml deleted file mode 100644 index 06c2ab1e823..00000000000 --- a/spec/javascripts/fixtures/issues_show.html.haml +++ /dev/null @@ -1,23 +0,0 @@ -:css - .hidden { display: none !important; } - -.flash-container.flash-container-page - .flash-alert - .flash-notice - -.status-box.status-box-open Open -.status-box.status-box-closed.hidden Closed -%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close -%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen - -.detail-page-description - .description.js-task-list-container - .wiki - %ul.task-list - %li.task-list-item - %input.task-list-item-checkbox{type: 'checkbox'} - Task List Item - %textarea.js-task-list-field - \- [ ] Task List Item - -%form.js-issuable-update{action: '/foo'} diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index c27fb856081..949114185cf 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -4,116 +4,160 @@ /*= require issue */ (function() { + var INVALID_URL = 'http://goesnowhere.nothing/whereami'; + var $boxClosed, $boxOpen, $btnClose, $btnReopen; + + fixture.preload('issues/closed-issue.html'); + fixture.preload('issues/issue-with-task-list.html'); + fixture.preload('issues/open-issue.html'); + + function expectErrorMessage() { + var $flashMessage = $('div.flash-alert'); + expect($flashMessage).toExist(); + expect($flashMessage).toBeVisible(); + expect($flashMessage).toHaveText('Unable to update this issue at this time.'); + } + + function expectIssueState(isIssueOpen) { + expectVisibility($boxClosed, !isIssueOpen); + expectVisibility($boxOpen, isIssueOpen); + + expectVisibility($btnClose, isIssueOpen); + expectVisibility($btnReopen, !isIssueOpen); + } + + function expectPendingRequest(req, $triggeredButton) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe($triggeredButton.attr('href')); + expect($triggeredButton).toHaveProp('disabled', true); + } + + function expectVisibility($element, shouldBeVisible) { + if (shouldBeVisible) { + expect($element).not.toHaveClass('hidden'); + } else { + expect($element).toHaveClass('hidden'); + } + } + + function findElements() { + $boxClosed = $('div.status-box-closed'); + expect($boxClosed).toExist(); + expect($boxClosed).toHaveText('Closed'); + + $boxOpen = $('div.status-box-open'); + expect($boxOpen).toExist(); + expect($boxOpen).toHaveText('Open'); + + $btnClose = $('.btn-close.btn-grouped'); + expect($btnClose).toExist(); + expect($btnClose).toHaveText('Close issue'); + + $btnReopen = $('.btn-reopen.btn-grouped'); + expect($btnReopen).toExist(); + expect($btnReopen).toHaveText('Reopen issue'); + } + describe('Issue', function() { - return describe('task lists', function() { - fixture.preload('issues_show.html'); + describe('task lists', function() { + fixture.load('issues/issue-with-task-list.html'); beforeEach(function() { - fixture.load('issues_show.html'); - return this.issue = new Issue(); + this.issue = new Issue(); }); + it('modifies the Markdown field', function() { spyOn(jQuery, 'ajax').and.stub(); $('input[type=checkbox]').attr('checked', true).trigger('change'); - return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); }); - return it('submits an ajax request on tasklist:changed', function() { + + it('submits an ajax request on tasklist:changed', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); - expect(req.url).toBe('/foo'); - return expect(req.data.issue.description).not.toBe(null); + expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json'); + expect(req.data.issue.description).not.toBe(null); }); - return $('.js-task-list-field').trigger('tasklist:changed'); + + $('.js-task-list-field').trigger('tasklist:changed'); }); }); }); - describe('reopen/close issue', function() { - fixture.preload('issues_show.html'); + describe('close issue', function() { beforeEach(function() { - fixture.load('issues_show.html'); - return this.issue = new Issue(); + fixture.load('issues/open-issue.html'); + findElements(); + this.issue = new Issue(); + + expectIssueState(true); }); + it('closes an issue', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://gitlab.com/issues/6/close'); - return req.success({ + expectPendingRequest(req, $btnClose); + req.success({ id: 34 }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); - expect($btnReopen).toBeVisible(); - expect($btnClose).toBeHidden(); - expect($('div.status-box-closed')).toBeVisible(); - return expect($('div.status-box-open')).toBeHidden(); + + expectIssueState(false); + expect($btnClose).toHaveProp('disabled', false); }); + it('fails to close an issue with success:false', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://goesnowhere.nothing/whereami'); - return req.success({ + expectPendingRequest(req, $btnClose); + req.success({ saved: false }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + + $btnClose.attr('href', INVALID_URL); $btnClose.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-closed')).toBeHidden(); - expect($('div.status-box-open')).toBeVisible(); - expect($('div.flash-alert')).toBeVisible(); - return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + + expectIssueState(true); + expect($btnClose).toHaveProp('disabled', false); + expectErrorMessage(); }); + it('fails to closes an issue with HTTP error', function() { - var $btnClose, $btnReopen; spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://goesnowhere.nothing/whereami'); - return req.error(); + expectPendingRequest(req, $btnClose); + req.error(); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); - expect($btnReopen).toBeHidden(); - expect($btnClose.text()).toBe('Close'); - expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + + $btnClose.attr('href', INVALID_URL); $btnClose.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-closed')).toBeHidden(); - expect($('div.status-box-open')).toBeVisible(); - expect($('div.flash-alert')).toBeVisible(); - return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + + expectIssueState(true); + expect($btnClose).toHaveProp('disabled', true); + expectErrorMessage(); }); - return it('reopens an issue', function() { - var $btnClose, $btnReopen; + }); + + describe('reopen issue', function() { + beforeEach(function() { + fixture.load('issues/closed-issue.html'); + findElements(); + this.issue = new Issue(); + + expectIssueState(false); + }); + + it('reopens an issue', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { - expect(req.type).toBe('PUT'); - expect(req.url).toBe('http://gitlab.com/issues/6/reopen'); - return req.success({ + expectPendingRequest(req, $btnReopen); + req.success({ id: 34 }); }); - $btnClose = $('a.btn-close'); - $btnReopen = $('a.btn-reopen'); - expect($btnReopen.text()).toBe('Reopen'); + $btnReopen.trigger('click'); - expect($btnReopen).toBeHidden(); - expect($btnClose).toBeVisible(); - expect($('div.status-box-open')).toBeVisible(); - return expect($('div.status-box-closed')).toBeHidden(); + + expectIssueState(true); + expect($btnReopen).toHaveProp('disabled', false); }); }); diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index feee0f025d8..07a2c316899 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -178,6 +178,7 @@ Ci::Pipeline: - finished_at - duration - user_id +- lock_version CommitStatus: - id - project_id @@ -217,6 +218,7 @@ CommitStatus: - yaml_variables - queued_at - token +- lock_version Ci::Variable: - id - project_id diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb new file mode 100644 index 00000000000..498dc514c8c --- /dev/null +++ b/spec/lib/gitlab/optimistic_locking_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::OptimisticLocking, lib: true do + describe '#retry_lock' do + let!(:pipeline) { create(:ci_pipeline) } + let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) } + + it 'does not reload object if state changes' do + expect(pipeline).not_to receive(:reload) + expect(pipeline).to receive(:succeed).and_call_original + + described_class.retry_lock(pipeline) do |subject| + subject.succeed + end + end + + it 'retries action if exception is raised' do + pipeline.succeed + + expect(pipeline2).to receive(:reload).and_call_original + expect(pipeline2).to receive(:drop).twice.and_call_original + + described_class.retry_lock(pipeline2) do |subject| + subject.drop + end + end + + it 'raises exception when too many retries' do + expect(pipeline).to receive(:drop).twice.and_call_original + + expect do + described_class.retry_lock(pipeline, 1) do |subject| + subject.lock_version = 100 + subject.drop + end + end.to raise_error(ActiveRecord::StaleObjectError) + end + end +end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 43397c5ae39..5eb14dc6bd2 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -138,32 +138,26 @@ describe Ci::Pipeline, models: true do describe 'state machine' do let(:current) { Time.now.change(usec: 0) } - let(:build) { create_build('build1', current, 10) } - let(:build_b) { create_build('build2', current, 20) } - let(:build_c) { create_build('build3', current + 50, 10) } + let(:build) { create_build('build1', 0) } + let(:build_b) { create_build('build2', 0) } + let(:build_c) { create_build('build3', 0) } describe '#duration' do before do - pipeline.update(created_at: current) - - travel_to(current + 5) do - pipeline.run - pipeline.save - end - travel_to(current + 30) do - build.success + build.run! + build.success! + build_b.run! + build_c.run! end travel_to(current + 40) do - build_b.drop + build_b.drop! end travel_to(current + 70) do - build_c.success + build_c.success! end - - pipeline.drop end it 'matches sum of builds duration' do @@ -455,7 +449,9 @@ describe Ci::Pipeline, models: true do context 'when all builds succeed' do before do build_a.success - build_b.success + + # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker + build_b.reload.success end it 'receives a success event once' do diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb new file mode 100644 index 00000000000..2b7b6cad654 --- /dev/null +++ b/spec/policies/issues_policy_spec.rb @@ -0,0 +1,193 @@ +require 'spec_helper' + +describe IssuePolicy, models: true do + let(:guest) { create(:user) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:reporter) { create(:user) } + let(:group) { create(:group, :public) } + let(:reporter_from_group_link) { create(:user) } + + def permissions(user, issue) + IssuePolicy.abilities(user, issue).to_set + end + + context 'a private project' do + let(:non_member) { create(:user) } + let(:project) { create(:empty_project, :private) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [author, :guest] + project.team << [assignee, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'does not allow non-members to read issues' do + expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow non-members to read confidential issues' do + expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end + + context 'a public project' do + let(:project) { create(:empty_project, :public) } + let(:issue) { create(:issue, project: project, assignee: assignee, author: author) } + let(:issue_no_assignee) { create(:issue, project: project) } + + before do + project.team << [guest, :guest] + project.team << [reporter, :reporter] + + group.add_reporter(reporter_from_group_link) + + create(:project_group_link, group: group, project: project) + end + + it 'allows guests to read issues' do + expect(permissions(guest, issue)).to include(:read_issue) + expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue) + + expect(permissions(guest, issue_no_assignee)).to include(:read_issue) + expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin issues' do + expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters from group links to read, update, and admin issues' do + expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their issues' do + expect(permissions(author, issue)).to include(:read_issue, :update_issue) + expect(permissions(author, issue)).not_to include(:admin_issue) + + expect(permissions(author, issue_no_assignee)).to include(:read_issue) + expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their issues' do + expect(permissions(assignee, issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, issue)).not_to include(:admin_issue) + + expect(permissions(assignee, issue_no_assignee)).to include(:read_issue) + expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue) + end + + context 'with confidential issues' do + let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) } + let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } + + it 'does not allow guests to read confidential issues' do + expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporters to read, update, and admin confidential issues' do + expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows reporter from group links to read, update, and admin confidential issues' do + expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue authors to read and update their confidential issues' do + expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + + it 'allows issue assignees to read and update their confidential issues' do + expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue) + + expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue) + end + end + end +end diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index 95c7bbf99c9..fc72a44d663 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -277,6 +277,7 @@ describe API::API, api: true do context 'with regular branch' do before do + pipeline.reload pipeline.update(ref: 'master', sha: project.commit('master').sha) @@ -288,6 +289,7 @@ describe API::API, api: true do context 'with branch name containing slash' do before do + pipeline.reload pipeline.update(ref: 'improve/awesome', sha: project.commit('improve/awesome').sha) end diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb index 1e21a32a062..a3fc23ba177 100644 --- a/spec/services/ci/register_build_service_spec.rb +++ b/spec/services/ci/register_build_service_spec.rb @@ -101,11 +101,11 @@ module Ci it 'equalises number of running builds' do # after finishing the first build for project 1, get a second build from the same project expect(service.execute(shared_runner)).to eq(build1_project1) - build1_project1.success + build1_project1.reload.success expect(service.execute(shared_runner)).to eq(build2_project1) expect(service.execute(shared_runner)).to eq(build1_project2) - build1_project2.success + build1_project2.reload.success expect(service.execute(shared_runner)).to eq(build2_project2) expect(service.execute(shared_runner)).to eq(build1_project3) expect(service.execute(shared_runner)).to eq(build3_project1) diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index cbfc63de811..7a9b34f9f96 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe Labels::FindOrCreateService, services: true do describe '#execute' do - let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, namespace: group) } @@ -14,37 +13,49 @@ describe Labels::FindOrCreateService, services: true do } end - subject(:service) { described_class.new(user, project, params) } - - before do - project.team << [user, :developer] - end + context 'when acting on behalf of a specific user' do + let(:user) { create(:user) } + subject(:service) { described_class.new(user, project, params) } + before do + project.team << [user, :developer] + end - context 'when label does not exist at group level' do - it 'creates a new label at project level' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project level' do + expect { service.execute }.to change(project.labels, :count).by(1) + end end - end - context 'when label exists at group level' do - it 'returns the group label' do - group_label = create(:group_label, group: group, title: 'Security') + context 'when label exists at group level' do + it 'returns the group label' do + group_label = create(:group_label, group: group, title: 'Security') - expect(service.execute).to eq group_label + expect(service.execute).to eq group_label + end end - end - context 'when label does not exist at group level' do - it 'creates a new label at project leve' do - expect { service.execute }.to change(project.labels, :count).by(1) + context 'when label does not exist at group level' do + it 'creates a new label at project leve' do + expect { service.execute }.to change(project.labels, :count).by(1) + end + end + + context 'when label exists at project level' do + it 'returns the project label' do + project_label = create(:label, project: project, title: 'Security') + + expect(service.execute).to eq project_label + end end end - context 'when label exists at project level' do + context 'when authorization is not required' do + subject(:service) { described_class.new(nil, project, params) } + it 'returns the project label' do project_label = create(:label, project: project, title: 'Security') - expect(service.execute).to eq project_label + expect(service.execute(skip_authorization: true)).to eq project_label end end end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index 03e296259f9..7b090343a3e 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do let(:access_requester) { create(:user) } let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } + let(:opts) { {} } shared_examples 'a service raising ActiveRecord::RecordNotFound' do it 'raises ActiveRecord::RecordNotFound' do - expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound) end end shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do it 'raises Gitlab::Access::AccessDeniedError' do - expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError) end end shared_examples 'a service approving an access request' do it 'succeeds' do - expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1) + expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1) end it 'returns a <Source>Member' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params).execute(opts) expect(member).to be_a "#{source.class}Member".constantize expect(member.requested_at).to be_nil end context 'with a custom access level' do - let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } } + let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) } it 'returns a ProjectMember with the custom access level' do - member = described_class.new(source, user, params).execute + member = described_class.new(source, user, params2).execute(opts) expect(member.access_level).to eq Gitlab::Access::MASTER end @@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do end let(:params) { { user_id: access_requester.id } } + context 'when current user is nil' do + let(:user) { nil } + + context 'and :force option is not given' do + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is false' do + let(:opts) { { force: false } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + + context 'and :force option is true' do + let(:opts) { { force: true } } + + it_behaves_like 'a service approving an access request' do + let(:source) { project } + end + + it_behaves_like 'a service approving an access request' do + let(:source) { group } + end + end + + context 'and :force param is true' do + let(:params) { { user_id: access_requester.id, force: true } } + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { project } + end + + it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do + let(:source) { group } + end + end + end + context 'when current user cannot approve access request to the project' do it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { project } diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index b80cfd8f450..1f90efdbd6a 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -147,6 +147,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do expect(MergeWorker).not_to receive(:perform_async) build.success + test.reload test.drop end @@ -154,6 +155,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do expect(MergeWorker).to receive(:perform_async) build.success + test.reload test.success end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 06d52f0f735..b2ca856f89f 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -9,7 +9,7 @@ require 'shoulda/matchers' require 'sidekiq/testing/inline' require 'rspec/retry' -if ENV['CI'] +if ENV['CI'] && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind end diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb new file mode 100644 index 00000000000..adc3f48b434 --- /dev/null +++ b/spec/support/javascript_fixtures_helpers.rb @@ -0,0 +1,45 @@ +require 'fileutils' +require 'gitlab/popen' + +module JavaScriptFixturesHelpers + include Gitlab::Popen + + FIXTURE_PATH = 'spec/javascripts/fixtures' + + # Public: Removes all fixture files from given directory + # + # directory_name - directory of the fixtures (relative to FIXTURE_PATH) + # + def clean_frontend_fixtures(directory_name) + directory_name = File.expand_path(directory_name, FIXTURE_PATH) + Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name| + FileUtils.rm(file_name) + end + end + + # Public: Store a response object as fixture file + # + # response - response object to store + # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH) + # + def store_frontend_fixture(response, fixture_file_name) + fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH) + fixture = response.body + + response_mime_type = Mime::Type.lookup(response.content_type) + if response_mime_type.html? + doc = Nokogiri::HTML::DocumentFragment.parse(fixture) + + scripts = doc.css('script') + scripts.remove + + fixture = doc.to_html + + # replace relative links + fixture.gsub!(%r{="/}, '="https://fixture.invalid/') + end + + FileUtils.mkdir_p(File.dirname(fixture_file_name)) + File.write(fixture_file_name, fixture) + end +end |