From b9287208523e1a5c05939fe0db038df51a9082fc Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Wed, 30 Aug 2017 16:57:50 +0200 Subject: Support discussion locking in the backend --- app/controllers/projects/issues_controller.rb | 1 + .../merge_requests/application_controller.rb | 1 + app/controllers/projects/notes_controller.rb | 14 ++++++ app/helpers/notes_helper.rb | 4 ++ app/models/system_note_metadata.rb | 2 +- app/policies/issuable_policy.rb | 5 ++ app/policies/note_policy.rb | 10 ++++ app/services/issuable_base_service.rb | 1 + app/services/issues/update_service.rb | 8 ++++ app/services/system_note_service.rb | 12 +++++ ...61207221154_add_dicussion_locked_to_issuable.rb | 16 +++++++ db/schema.rb | 2 + spec/controllers/projects/notes_controller_spec.rb | 41 ++++++++++++++++ .../gitlab/import_export/safe_model_attributes.yml | 2 + spec/policies/issuable_policy_spec.rb | 28 +++++++++++ spec/policies/note_policy_spec.rb | 54 ++++++++++++++++++++++ spec/services/issues/update_service_spec.rb | 5 +- .../services/merge_requests/update_service_spec.rb | 4 +- 18 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb create mode 100644 spec/policies/issuable_policy_spec.rb create mode 100644 spec/policies/note_policy_spec.rb diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 8990c919ca0..ab75a68e56a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -289,6 +289,7 @@ class Projects::IssuesController < Projects::ApplicationController state_event task_num lock_version + discussion_locked ] + [{ label_ids: [], assignee_ids: [] }] end diff --git a/app/controllers/projects/merge_requests/application_controller.rb b/app/controllers/projects/merge_requests/application_controller.rb index 6602b204fcb..eb7d7bf374c 100644 --- a/app/controllers/projects/merge_requests/application_controller.rb +++ b/app/controllers/projects/merge_requests/application_controller.rb @@ -34,6 +34,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont :target_project_id, :task_num, :title, + :discussion_locked, label_ids: [] ] diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index 41a13f6f577..dd3dc71c004 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -66,7 +66,21 @@ class Projects::NotesController < Projects::ApplicationController params.merge(last_fetched_at: last_fetched_at) end + def authorize_admin_note! + return access_denied! unless can?(current_user, :admin_note, note) + end + def authorize_resolve_note! return access_denied! unless can?(current_user, :resolve_note, note) end + + def authorize_create_note! + noteable_type = note_params[:noteable_type] + + return unless ['MergeRequest', 'Issue'].include?(noteable_type) + return access_denied! unless can?(current_user, :create_note, project) + + noteable = noteable_type.constantize.find(note_params[:noteable_id]) + access_denied! unless can?(current_user, :create_note, noteable) + end end diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb index ce028195e51..c219aa3d6a9 100644 --- a/app/helpers/notes_helper.rb +++ b/app/helpers/notes_helper.rb @@ -130,8 +130,12 @@ module NotesHelper end def can_create_note? + issuable = @issue || @merge_request + if @snippet.is_a?(PersonalSnippet) can?(current_user, :comment_personal_snippet, @snippet) + elsif issuable + can?(current_user, :create_note, issuable) else can?(current_user, :create_note, @project) end diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb index 0b33e45473b..1f9f8d7286b 100644 --- a/app/models/system_note_metadata.rb +++ b/app/models/system_note_metadata.rb @@ -2,7 +2,7 @@ class SystemNoteMetadata < ActiveRecord::Base ICON_TYPES = %w[ commit description merge confidential visible label assignee cross_reference title time_tracking branch milestone discussion task moved - opened closed merged duplicate + opened closed merged duplicate locked unlocked outdated ].freeze diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index daf6fa9e18a..212f4989557 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -1,6 +1,9 @@ class IssuablePolicy < BasePolicy delegate { @subject.project } + condition(:locked) { @subject.discussion_locked? } + condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } + desc "User is the assignee or author" condition(:assignee_or_author) do @user && @subject.assignee_or_author?(@user) @@ -12,4 +15,6 @@ class IssuablePolicy < BasePolicy enable :read_merge_request enable :update_merge_request end + + rule { locked & ~is_project_member }.prevent :create_note end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 20cd51cfb99..5d51fbf4f4a 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -2,14 +2,18 @@ class NotePolicy < BasePolicy delegate { @subject.project } condition(:is_author) { @user && @subject.author == @user } + condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } condition(:editable, scope: :subject) { @subject.editable? } + condition(:locked) { @subject.noteable.discussion_locked? } rule { ~editable | anonymous }.prevent :edit_note + rule { is_author | admin }.enable :edit_note rule { can?(:master_access) }.enable :edit_note + rule { locked & ~is_author & ~is_project_member }.prevent :edit_note rule { is_author }.policy do enable :read_note @@ -21,4 +25,10 @@ class NotePolicy < BasePolicy rule { for_merge_request & is_noteable_author }.policy do enable :resolve_note end + + rule { locked & ~is_project_member }.policy do + prevent :update_note + prevent :admin_note + prevent :resolve_note + end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 8b967b78052..40793201664 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -57,6 +57,7 @@ class IssuableBaseService < BaseService params.delete(:due_date) params.delete(:canonical_issue_id) params.delete(:project) + params.delete(:discussion_locked) end filter_assignee(issuable) diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index b4ca3966505..2a24ee85c45 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -41,6 +41,10 @@ module Issues create_confidentiality_note(issue) end + if issue.previous_changes.include?('discussion_locked') + create_discussion_lock_note(issue) + end + added_labels = issue.labels - old_labels if added_labels.present? @@ -95,5 +99,9 @@ module Issues def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) end + + def create_discussion_lock_note(issue) + SystemNoteService.discussion_lock(issue, current_user) + end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index 1f66a2668f9..cec0a1b6efa 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -591,6 +591,18 @@ module SystemNoteService create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate')) end + def discussion_lock(issuable, author) + if issuable.discussion_locked + body = 'locked this issue' + action = 'locked' + else + body = 'unlocked this issue' + action = 'unlocked' + end + + create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) + end + private def notes_for_mentioner(mentioner, noteable, notes) diff --git a/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb b/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb new file mode 100644 index 00000000000..bb60ac2a410 --- /dev/null +++ b/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb @@ -0,0 +1,16 @@ +class AddDicussionLockedToIssuable < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + disable_ddl_transaction! + + def up + add_column(:merge_requests, :discussion_locked, :boolean) + add_column(:issues, :discussion_locked, :boolean) + end + + def down + remove_column(:merge_requests, :discussion_locked) + remove_column(:issues, :discussion_locked) + end +end diff --git a/db/schema.rb b/db/schema.rb index 2149f5ad23d..16f38f7b60b 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -660,6 +660,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "cached_markdown_version" t.datetime "last_edited_at" t.integer "last_edited_by_id" + t.boolean "discussion_locked", default: false, null: false end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -882,6 +883,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "head_pipeline_id" t.boolean "ref_fetched" t.string "merge_jid" + t.boolean "discussion_locked", default: false, null: false end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 6ffe41b8608..26429b57bd5 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -232,6 +232,47 @@ describe Projects::NotesController do end end end + + context 'when the merge request discussion is locked' do + before do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) + merge_request.update_attribute(:discussion_locked, true) + end + + context 'when a user is a team member' do + it 'returns 302 status for html' do + post :create, request_params + + expect(response).to have_http_status(302) + end + + it 'returns 200 status for json' do + post :create, request_params.merge(format: :json) + + expect(response).to have_http_status(200) + end + + it 'creates a new note' do + expect{ post :create, request_params }.to change { Note.count }.by(1) + end + end + + context 'when a user is not a team member' do + before do + project.project_member(user).destroy + end + + it 'returns 404 status' do + post :create, request_params + + expect(response).to have_http_status(404) + end + + it 'does not create a new note' do + expect{ post :create, request_params }.not_to change { Note.count } + end + end + end end describe 'DELETE destroy' do diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index 899d17d97c2..763ab029051 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -25,6 +25,7 @@ Issue: - relative_position - last_edited_at - last_edited_by_id +- discussion_locked Event: - id - target_type @@ -168,6 +169,7 @@ MergeRequest: - last_edited_at - last_edited_by_id - head_pipeline_id +- discussion_locked MergeRequestDiff: - id - state diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb new file mode 100644 index 00000000000..9b399d764ea --- /dev/null +++ b/spec/policies/issuable_policy_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe IssuablePolicy, models: true do + describe '#rules' do + context 'when discussion is locked for the issuable' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, discussion_locked: true) } + let(:policies) { described_class.new(user, issue) } + + context 'when the user is not a project member' do + it 'can not create a note' do + expect(policies).to be_disallowed(:create_note) + end + end + + context 'when the user is a project member' do + before do + project.team << [user, :guest] + end + + it 'can create a note' do + expect(policies).to be_allowed(:create_note) + end + end + end + end +end diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb new file mode 100644 index 00000000000..70a99ed0198 --- /dev/null +++ b/spec/policies/note_policy_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe NotePolicy, mdoels: true do + describe '#rules' do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, noteable: issue, author: user, project: project) } + + let(:policies) { described_class.new(user, note) } + + context 'when the project is public' do + context 'when the note author is not a project member' do + it 'can edit a note' do + expect(policies).to be_allowed(:update_note) + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + end + + context 'when a discussion is locked' do + before do + issue.update_attribute(:discussion_locked, true) + end + + context 'when the note author is a project member' do + before do + project.add_developer(user) + end + + it 'can eddit a note' do + expect(policies).to be_allowed(:update_note) + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + end + + context 'when the note author is not a project member' do + it 'can not edit a note' do + expect(policies).to be_disallowed(:update_note) + expect(policies).to be_disallowed(:admin_note) + expect(policies).to be_disallowed(:resolve_note) + end + + it 'can read a note' do + expect(policies).to be_allowed(:read_note) + end + end + end + end + end +end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 15a50b85f19..0ed4c2152bb 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -48,7 +48,8 @@ describe Issues::UpdateService, :mailer do assignee_ids: [user2.id], state_event: 'close', label_ids: [label.id], - due_date: Date.tomorrow + due_date: Date.tomorrow, + discussion_locked: true } end @@ -62,6 +63,7 @@ describe Issues::UpdateService, :mailer do expect(issue).to be_closed expect(issue.labels).to match_array [label] expect(issue.due_date).to eq Date.tomorrow + expect(issue.discussion_locked).to be_truthy end it 'updates open issue counter for assignees when issue is reassigned' do @@ -103,6 +105,7 @@ describe Issues::UpdateService, :mailer do expect(issue.labels).to be_empty expect(issue.milestone).to be_nil expect(issue.due_date).to be_nil + expect(issue.discussion_locked).to be_falsey end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 681feee61d1..a07b7ac2218 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -49,7 +49,8 @@ describe MergeRequests::UpdateService, :mailer do state_event: 'close', label_ids: [label.id], target_branch: 'target', - force_remove_source_branch: '1' + force_remove_source_branch: '1', + discussion_locked: true } end @@ -73,6 +74,7 @@ describe MergeRequests::UpdateService, :mailer do expect(@merge_request.labels.first.title).to eq(label.name) expect(@merge_request.target_branch).to eq('target') expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') + expect(@merge_request.discussion_locked).to be_truthy end it 'executes hooks with update action' do -- cgit v1.2.1 From 073ba05d315881730de3995042cc4256c116e2c4 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 31 Aug 2017 12:38:32 +0200 Subject: Support discussion lock in the API --- doc/api/issues.md | 57 ++++++++++++++++++++-- doc/api/merge_requests.md | 11 ++++- lib/api/entities.rb | 2 + lib/api/issues.rb | 3 +- lib/api/merge_requests.rb | 4 +- lib/api/notes.rb | 7 +++ .../fixtures/api/schemas/public_api/v4/issues.json | 1 + .../api/schemas/public_api/v4/merge_requests.json | 1 + 8 files changed, 79 insertions(+), 7 deletions(-) diff --git a/doc/api/issues.md b/doc/api/issues.md index 8ca66049d31..61e42345153 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -109,7 +109,8 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, - "confidential": false + "confidential": false, + "discussion_locked": false } ] ``` @@ -214,7 +215,8 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, - "confidential": false + "confidential": false, + "discussion_locked": false } ] ``` @@ -320,7 +322,8 @@ Example response: "human_time_estimate": null, "human_total_time_spent": null }, - "confidential": false + "confidential": false, + "discussion_locked": false } ] ``` @@ -403,6 +406,7 @@ Example response: "human_total_time_spent": null }, "confidential": false, + "discussion_locked": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", "notes": "http://example.com/api/v4/projects/1/issues/2/notes", @@ -477,6 +481,7 @@ Example response: "human_total_time_spent": null }, "confidential": false, + "discussion_locked": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", "notes": "http://example.com/api/v4/projects/1/issues/2/notes", @@ -510,6 +515,8 @@ PUT /projects/:id/issues/:issue_iid | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | +| `discussion_locked` | boolean | no | Updates an issue to lock or unlock its discussion | + ```bash curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues/85?state_event=close @@ -552,6 +559,7 @@ Example response: "human_total_time_spent": null }, "confidential": false, + "discussion_locked": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", "notes": "http://example.com/api/v4/projects/1/issues/2/notes", @@ -650,6 +658,7 @@ Example response: "human_total_time_spent": null }, "confidential": false, + "discussion_locked": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", "notes": "http://example.com/api/v4/projects/1/issues/2/notes", @@ -727,6 +736,7 @@ Example response: "human_total_time_spent": null }, "confidential": false, + "discussion_locked": false, "_links": { "self": "http://example.com/api/v4/projects/1/issues/2", "notes": "http://example.com/api/v4/projects/1/issues/2/notes", @@ -757,6 +767,44 @@ POST /projects/:id/issues/:issue_iid/unsubscribe curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/unsubscribe ``` +Example response: + +```json +{ + "id": 93, + "iid": 12, + "project_id": 5, + "title": "Incidunt et rerum ea expedita iure quibusdam.", + "description": "Et cumque architecto sed aut ipsam.", + "state": "opened", + "created_at": "2016-04-05T21:41:45.217Z", + "updated_at": "2016-04-07T13:02:37.905Z", + "labels": [], + "milestone": null, + "assignee": { + "name": "Edwardo Grady", + "username": "keyon", + "id": 21, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/3e6f06a86cf27fa8b56f3f74f7615987?s=80&d=identicon", + "web_url": "https://gitlab.example.com/keyon" + }, + "author": { + "name": "Vivian Hermann", + "username": "orville", + "id": 11, + "state": "active", + "avatar_url": "http://www.gravatar.com/avatar/5224fd70153710e92fb8bcf79ac29d67?s=80&d=identicon", + "web_url": "https://gitlab.example.com/orville" + }, + "subscribed": false, + "due_date": null, + "web_url": "http://example.com/example/example/issues/12", + "confidential": false, + "discussion_locked": false +} +``` + ## Create a todo Manually creates a todo for the current user on an issue. If @@ -849,7 +897,8 @@ Example response: "downvotes": 0, "due_date": null, "web_url": "http://example.com/example/example/issues/110", - "confidential": false + "confidential": false, + "discussion_locked": false }, "target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10", "body": "Vel voluptas atque dicta mollitia adipisci qui at.", diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index bff8a2d3e4d..e5d1ebb9cfb 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -192,6 +192,7 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -267,6 +268,7 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -378,6 +380,7 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -471,6 +474,7 @@ POST /projects/:id/merge_requests "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -500,6 +504,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `labels` | string | no | Labels for MR as a comma-separated list | | `milestone_id` | integer | no | The ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | +| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked | Must include at least one non-required attribute from above. @@ -554,6 +559,7 @@ Must include at least one non-required attribute from above. "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -658,6 +664,7 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -734,6 +741,7 @@ Parameters: "should_remove_source_branch": true, "force_remove_source_branch": false, "web_url": "http://example.com/example/example/merge_requests/1", + "discussion_locked": false, "time_stats": { "time_estimate": 0, "total_time_spent": 0, @@ -1028,7 +1036,8 @@ Example response: "id": 14, "state": "active", "avatar_url": "http://www.gravatar.com/avatar/a7fa515d53450023c83d62986d0658a8?s=80&d=identicon", - "web_url": "https://gitlab.example.com/francisca" + "web_url": "https://gitlab.example.com/francisca", + "discussion_locked": false }, "assignee": { "name": "Dr. Gabrielle Strosin", diff --git a/lib/api/entities.rb b/lib/api/entities.rb index 52c49e5caa9..4b2ac1cce95 100644 --- a/lib/api/entities.rb +++ b/lib/api/entities.rb @@ -362,6 +362,7 @@ module API end expose :due_date expose :confidential + expose :discussion_locked expose :web_url do |issue, options| Gitlab::UrlBuilder.build(issue) @@ -458,6 +459,7 @@ module API expose :diff_head_sha, as: :sha expose :merge_commit_sha expose :user_notes_count + expose :discussion_locked expose :should_remove_source_branch?, as: :should_remove_source_branch expose :force_remove_source_branch?, as: :force_remove_source_branch diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 1729df2aad0..88b592083db 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -48,6 +48,7 @@ module API optional :labels, type: String, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' + optional :discussion_locked, type: Boolean, desc: "Boolean parameter if the issue's discussion should be locked" end params :issue_params do @@ -193,7 +194,7 @@ module API desc: 'Date time when the issue was updated. Available only for admins and project owners.' optional :state_event, type: String, values: %w[reopen close], desc: 'State of the issue' use :issue_params - at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, + at_least_one_of :title, :description, :assignee_ids, :assignee_id, :milestone_id, :discussion_locked, :labels, :created_at, :due_date, :confidential, :state_event end put ':id/issues/:issue_iid' do diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb index 56d72d511da..35395647fac 100644 --- a/lib/api/merge_requests.rb +++ b/lib/api/merge_requests.rb @@ -213,12 +213,14 @@ module API :remove_source_branch, :state_event, :target_branch, - :title + :title, + :discussion_locked ] optional :title, type: String, allow_blank: false, desc: 'The title of the merge request' optional :target_branch, type: String, allow_blank: false, desc: 'The target branch' optional :state_event, type: String, values: %w[close reopen], desc: 'Status of the merge request' + optional :discussion_locked, type: Boolean, desc: 'Whether the MR discussion is locked' use :optional_params at_least_one_of(*at_least_one_of_ce) diff --git a/lib/api/notes.rb b/lib/api/notes.rb index d6e7203adaf..b3db366d875 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -71,6 +71,8 @@ module API post ":id/#{noteables_str}/:noteable_id/notes" do noteable = find_project_noteable(noteables_str, params[:noteable_id]) + authorize! :create_note, user_project + opts = { note: params[:body], noteable_type: noteables_str.classify, @@ -82,6 +84,11 @@ module API opts[:created_at] = params[:created_at] end + noteable_type = opts[:noteable_type].to_s + noteable = Issue.find(opts[:noteable_id]) if noteable_type == 'Issue' + noteable = MergeRequest.find(opts[:noteable_id]) if noteable_type == 'MergeRequest' + authorize! :create_note, noteable if noteable + note = ::Notes::CreateService.new(user_project, current_user, opts).execute if note.valid? diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json index 8acd9488215..8c854a43fc6 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issues.json +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -9,6 +9,7 @@ "title": { "type": "string" }, "description": { "type": ["string", "null"] }, "state": { "type": "string" }, + "discussion_locked": { "type": "boolean" }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "labels": { diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 31b3f4ba946..3b42333bb10 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -72,6 +72,7 @@ "user_notes_count": { "type": "integer" }, "should_remove_source_branch": { "type": ["boolean", "null"] }, "force_remove_source_branch": { "type": ["boolean", "null"] }, + "discussion_locked": { "type": "boolean" }, "web_url": { "type": "uri" }, "time_stats": { "time_estimate": { "type": "integer" }, -- cgit v1.2.1 From 3d2917bf2e4799a7ba9bcb518c39605eca0a4b1d Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 31 Aug 2017 12:38:43 +0200 Subject: Add the possibility to lock issuables from the frontend --- app/assets/javascripts/discussion_lock.js | 46 +++++++++++++++++++++++ app/assets/javascripts/init_issuable_sidebar.js | 1 + app/assets/javascripts/main.js | 1 + app/assets/stylesheets/pages/notes.scss | 6 +++ app/views/shared/issuable/_sidebar.html.haml | 11 ++++++ app/views/shared/notes/_notes_with_form.html.haml | 10 ++++- 6 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 app/assets/javascripts/discussion_lock.js diff --git a/app/assets/javascripts/discussion_lock.js b/app/assets/javascripts/discussion_lock.js new file mode 100644 index 00000000000..7bf83e9b8a0 --- /dev/null +++ b/app/assets/javascripts/discussion_lock.js @@ -0,0 +1,46 @@ +class DiscussionLock { + constructor(containerElm) { + this.containerElm = containerElm; + + const lockButton = containerElm.querySelector('.js-discussion-lock-button'); + console.log(lockButton); + if (lockButton) { + // remove class so we don't bind twice + lockButton.classList.remove('js-discussion-lock-button'); + console.log(lockButton); + lockButton.addEventListener('click', this.toggleDiscussionLock.bind(this)); + } + } + + toggleDiscussionLock(event) { + const button = event.currentTarget; + const buttonSpan = button.querySelector('span'); + if (!buttonSpan || button.classList.contains('disabled')) { + return; + } + button.classList.add('disabled'); + + const url = this.containerElm.dataset.url; + const lock = this.containerElm.dataset.lock; + const issuableType = this.containerElm.dataset.issuableType; + + const data = {} + data[issuableType] = {} + data[issuableType].discussion_locked = lock + + $.ajax({ + url, + data: data, + type: 'PUT' + }).done((data) => { + button.classList.remove('disabled'); + }); + } + + static bindAll(selector) { + [].forEach.call(document.querySelectorAll(selector), elm => new DiscussionLock(elm)); + } +} + +window.gl = window.gl || {}; +window.gl.DiscussionLock = DiscussionLock; diff --git a/app/assets/javascripts/init_issuable_sidebar.js b/app/assets/javascripts/init_issuable_sidebar.js index 29e3d2ea94e..8857601f530 100644 --- a/app/assets/javascripts/init_issuable_sidebar.js +++ b/app/assets/javascripts/init_issuable_sidebar.js @@ -13,6 +13,7 @@ export default () => { new LabelsSelect(); new IssuableContext(sidebarOptions.currentUser); gl.Subscription.bindAll('.subscription'); + gl.DiscussionLock.bindAll('.discussion-lock'); new gl.DueDateSelectors(); window.sidebar = new Sidebar(); }; diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 0bc31a56684..ea1d50de965 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -80,6 +80,7 @@ import './copy_as_gfm'; import './copy_to_clipboard'; import './create_label'; import './diff'; +import './discussion_lock'; import './dropzone_input'; import './due_date_select'; import './files_comment_button'; diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss index e437bad4912..94439ed94a6 100644 --- a/app/assets/stylesheets/pages/notes.scss +++ b/app/assets/stylesheets/pages/notes.scss @@ -703,6 +703,12 @@ ul.notes { color: $note-disabled-comment-color; padding: 90px 0; + &.discussion-locked { + border: none; + background-color: $white-light; + } + + a { color: $gl-link-color; } diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml index 9cae3f51825..38a54c232d0 100644 --- a/app/views/shared/issuable/_sidebar.html.haml +++ b/app/views/shared/issuable/_sidebar.html.haml @@ -131,6 +131,17 @@ %button.btn.btn-default.pull-right.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" } %span= subscribed ? 'Unsubscribe' : 'Subscribe' + - if can_edit_issuable + - locked = issuable.discussion_locked? + .block.light.discussion-lock{ data: { lock: (!locked).to_s, url: "#{issuable_json_path(issuable)}?basic=true", issuable_type: issuable.class.to_s.underscore } } + .sidebar-collapsed-icon + = icon('rss', 'aria-hidden': 'true') + %span.issuable-header-text.hide-collapsed.pull-left + Discussion Lock + - subscribtion_status = locked ? 'locked' : 'not locked' + %button.btn.btn-default.pull-right.js-discussion-lock-button.issuable-discussion-lock-button.hide-collapsed{ type: "button" } + %span= locked ? 'Unlock' : 'Lock' + - project_ref = cross_project_reference(@project, issuable) .block.project-reference .sidebar-collapsed-icon.dont-change-state diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml index e3e86709b8f..371a9523de9 100644 --- a/app/views/shared/notes/_notes_with_form.html.haml +++ b/app/views/shared/notes/_notes_with_form.html.haml @@ -1,3 +1,6 @@ +- issuable = @issue || @merge_request +- discussion_locked = issuable&.discussion_locked? + %ul#notes-list.notes.main-notes-list.timeline = render "shared/notes/notes" @@ -21,5 +24,10 @@ or = link_to "sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'js-sign-in-link' to comment - +- elsif discussion_locked + .discussion_locked + %span + This + = issuable.class.to_s + has been locked. Posting comments has been restricted to project members. %script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe -- cgit v1.2.1 From 2b82f907abf2074ac332531d6142893d081f44b9 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Thu, 31 Aug 2017 14:31:14 +0200 Subject: Check the discussion lock only for issuables & clean style --- app/controllers/projects/notes_controller.rb | 2 +- app/policies/note_policy.rb | 2 +- spec/controllers/projects/notes_controller_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index dd3dc71c004..bb0c1869955 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -77,7 +77,7 @@ class Projects::NotesController < Projects::ApplicationController def authorize_create_note! noteable_type = note_params[:noteable_type] - return unless ['MergeRequest', 'Issue'].include?(noteable_type) + return unless %w[MergeRequest Issue].include?(noteable_type) return access_denied! unless can?(current_user, :create_note, project) noteable = noteable_type.constantize.find(note_params[:noteable_id]) diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 5d51fbf4f4a..307c514a74b 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -7,7 +7,7 @@ class NotePolicy < BasePolicy condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } condition(:editable, scope: :subject) { @subject.editable? } - condition(:locked) { @subject.noteable.discussion_locked? } + condition(:locked) { [MergeRequest, Issue].include?(@subject.noteable.class) && @subject.noteable.discussion_locked? } rule { ~editable | anonymous }.prevent :edit_note diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 26429b57bd5..10edad462c1 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -253,7 +253,7 @@ describe Projects::NotesController do end it 'creates a new note' do - expect{ post :create, request_params }.to change { Note.count }.by(1) + expect { post :create, request_params }.to change { Note.count }.by(1) end end @@ -269,7 +269,7 @@ describe Projects::NotesController do end it 'does not create a new note' do - expect{ post :create, request_params }.not_to change { Note.count } + expect { post :create, request_params }.not_to change { Note.count } end end end -- cgit v1.2.1 From 994e7d135947ca162c147c5e0992a0190de22808 Mon Sep 17 00:00:00 2001 From: Jarka Kadlecova Date: Fri, 1 Sep 2017 14:03:57 +0200 Subject: Create system notes for MR too, improve doc + clean up code --- app/controllers/projects/notes_controller.rb | 7 +---- app/models/concerns/noteable.rb | 4 +++ app/policies/issuable_policy.rb | 11 +++++-- app/policies/note_policy.rb | 10 +------ app/services/issuable_base_service.rb | 11 +++++++ app/services/issues/update_service.rb | 8 ----- app/services/system_note_service.rb | 9 ++---- changelogs/unreleased/18608-lock-issues.yml | 5 ++++ ...61207221154_add_dicussion_locked_to_issuable.rb | 16 ---------- ...0815221154_add_discussion_locked_to_issuable.rb | 13 +++++++++ db/schema.rb | 4 +-- doc/api/issues.md | 2 +- doc/api/merge_requests.md | 2 +- lib/api/issues.rb | 2 +- lib/api/notes.rb | 9 ++---- spec/controllers/projects/notes_controller_spec.rb | 9 ++++++ .../fixtures/api/schemas/public_api/v4/issues.json | 2 +- .../api/schemas/public_api/v4/merge_requests.json | 2 +- spec/policies/issuable_policy_spec.rb | 2 +- spec/policies/note_policy_spec.rb | 23 +++++++++++++-- spec/requests/api/notes_spec.rb | 34 ++++++++++++++++++++++ spec/services/issues/update_service_spec.rb | 7 +++++ .../services/merge_requests/update_service_spec.rb | 7 +++++ 23 files changed, 133 insertions(+), 66 deletions(-) create mode 100644 changelogs/unreleased/18608-lock-issues.yml delete mode 100644 db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb create mode 100644 db/migrate/20170815221154_add_discussion_locked_to_issuable.rb diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb index bb0c1869955..ef7d047b1ad 100644 --- a/app/controllers/projects/notes_controller.rb +++ b/app/controllers/projects/notes_controller.rb @@ -75,12 +75,7 @@ class Projects::NotesController < Projects::ApplicationController end def authorize_create_note! - noteable_type = note_params[:noteable_type] - - return unless %w[MergeRequest Issue].include?(noteable_type) - return access_denied! unless can?(current_user, :create_note, project) - - noteable = noteable_type.constantize.find(note_params[:noteable_id]) + return unless noteable.lockable? access_denied! unless can?(current_user, :create_note, noteable) end end diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb index 1c4ddabcad5..5d75b2aa6a3 100644 --- a/app/models/concerns/noteable.rb +++ b/app/models/concerns/noteable.rb @@ -74,4 +74,8 @@ module Noteable def discussions_can_be_resolved_by?(user) discussions_to_be_resolved.all? { |discussion| discussion.can_resolve?(user) } end + + def lockable? + [MergeRequest, Issue].include?(self.class) + end end diff --git a/app/policies/issuable_policy.rb b/app/policies/issuable_policy.rb index 212f4989557..f0aa16d2ecf 100644 --- a/app/policies/issuable_policy.rb +++ b/app/policies/issuable_policy.rb @@ -1,7 +1,8 @@ class IssuablePolicy < BasePolicy delegate { @subject.project } - condition(:locked) { @subject.discussion_locked? } + condition(:locked, scope: :subject, score: 0) { @subject.discussion_locked? } + condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } desc "User is the assignee or author" @@ -16,5 +17,11 @@ class IssuablePolicy < BasePolicy enable :update_merge_request end - rule { locked & ~is_project_member }.prevent :create_note + rule { locked & ~is_project_member }.policy do + prevent :create_note + prevent :update_note + prevent :admin_note + prevent :resolve_note + prevent :edit_note + end end diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb index 307c514a74b..d4cb5a77e63 100644 --- a/app/policies/note_policy.rb +++ b/app/policies/note_policy.rb @@ -1,19 +1,17 @@ class NotePolicy < BasePolicy delegate { @subject.project } + delegate { @subject.noteable if @subject.noteable.lockable? } condition(:is_author) { @user && @subject.author == @user } - condition(:is_project_member) { @user && @subject.project && @subject.project.team.member?(@user) } condition(:for_merge_request, scope: :subject) { @subject.for_merge_request? } condition(:is_noteable_author) { @user && @subject.noteable.author_id == @user.id } condition(:editable, scope: :subject) { @subject.editable? } - condition(:locked) { [MergeRequest, Issue].include?(@subject.noteable.class) && @subject.noteable.discussion_locked? } rule { ~editable | anonymous }.prevent :edit_note rule { is_author | admin }.enable :edit_note rule { can?(:master_access) }.enable :edit_note - rule { locked & ~is_author & ~is_project_member }.prevent :edit_note rule { is_author }.policy do enable :read_note @@ -25,10 +23,4 @@ class NotePolicy < BasePolicy rule { for_merge_request & is_noteable_author }.policy do enable :resolve_note end - - rule { locked & ~is_project_member }.policy do - prevent :update_note - prevent :admin_note - prevent :resolve_note - end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 40793201664..157539ee05b 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -43,6 +43,10 @@ class IssuableBaseService < BaseService SystemNoteService.change_time_spent(issuable, issuable.project, current_user) end + def create_discussion_lock_note(issuable) + SystemNoteService.discussion_lock(issuable, current_user) + end + def filter_params(issuable) ability_name = :"admin_#{issuable.to_ability_name}" @@ -236,6 +240,7 @@ class IssuableBaseService < BaseService handle_common_system_notes(issuable, old_labels: old_labels) end + change_discussion_lock(issuable) handle_changes( issuable, old_labels: old_labels, @@ -292,6 +297,12 @@ class IssuableBaseService < BaseService end end + def change_discussion_lock(issuable) + if issuable.previous_changes.include?('discussion_locked') + create_discussion_lock_note(issuable) + end + end + def toggle_award(issuable) award = params.delete(:emoji_award) if award diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index 2a24ee85c45..b4ca3966505 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -41,10 +41,6 @@ module Issues create_confidentiality_note(issue) end - if issue.previous_changes.include?('discussion_locked') - create_discussion_lock_note(issue) - end - added_labels = issue.labels - old_labels if added_labels.present? @@ -99,9 +95,5 @@ module Issues def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) end - - def create_discussion_lock_note(issue) - SystemNoteService.discussion_lock(issue, current_user) - end end end diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb index cec0a1b6efa..7b32e215c7f 100644 --- a/app/services/system_note_service.rb +++ b/app/services/system_note_service.rb @@ -592,13 +592,8 @@ module SystemNoteService end def discussion_lock(issuable, author) - if issuable.discussion_locked - body = 'locked this issue' - action = 'locked' - else - body = 'unlocked this issue' - action = 'unlocked' - end + action = issuable.discussion_locked? ? 'locked' : 'unlocked' + body = "#{action} this issue" create_note(NoteSummary.new(issuable, issuable.project, author, body, action: action)) end diff --git a/changelogs/unreleased/18608-lock-issues.yml b/changelogs/unreleased/18608-lock-issues.yml new file mode 100644 index 00000000000..6c0ae7cebf5 --- /dev/null +++ b/changelogs/unreleased/18608-lock-issues.yml @@ -0,0 +1,5 @@ +--- +title: Create system notes for MR too, improve doc + clean up code +merge_request: +author: +type: added diff --git a/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb b/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb deleted file mode 100644 index bb60ac2a410..00000000000 --- a/db/migrate/20161207221154_add_dicussion_locked_to_issuable.rb +++ /dev/null @@ -1,16 +0,0 @@ -class AddDicussionLockedToIssuable < ActiveRecord::Migration - include Gitlab::Database::MigrationHelpers - - DOWNTIME = false - disable_ddl_transaction! - - def up - add_column(:merge_requests, :discussion_locked, :boolean) - add_column(:issues, :discussion_locked, :boolean) - end - - def down - remove_column(:merge_requests, :discussion_locked) - remove_column(:issues, :discussion_locked) - end -end diff --git a/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb new file mode 100644 index 00000000000..5bd777c53a0 --- /dev/null +++ b/db/migrate/20170815221154_add_discussion_locked_to_issuable.rb @@ -0,0 +1,13 @@ +class AddDiscussionLockedToIssuable < ActiveRecord::Migration + DOWNTIME = false + + def up + add_column(:merge_requests, :discussion_locked, :boolean) + add_column(:issues, :discussion_locked, :boolean) + end + + def down + remove_column(:merge_requests, :discussion_locked) + remove_column(:issues, :discussion_locked) + end +end diff --git a/db/schema.rb b/db/schema.rb index 16f38f7b60b..6cdf929b1b6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -660,7 +660,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "cached_markdown_version" t.datetime "last_edited_at" t.integer "last_edited_by_id" - t.boolean "discussion_locked", default: false, null: false + t.boolean "discussion_locked" end add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree @@ -883,7 +883,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do t.integer "head_pipeline_id" t.boolean "ref_fetched" t.string "merge_jid" - t.boolean "discussion_locked", default: false, null: false + t.boolean "discussion_locked" end add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree diff --git a/doc/api/issues.md b/doc/api/issues.md index 61e42345153..479f8754bcc 100644 --- a/doc/api/issues.md +++ b/doc/api/issues.md @@ -515,7 +515,7 @@ PUT /projects/:id/issues/:issue_iid | `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it | | `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) | | `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` | -| `discussion_locked` | boolean | no | Updates an issue to lock or unlock its discussion | +| `discussion_locked` | boolean | no | Flag indicating if the issue's discussion is locked. If the discussion is locked only project members can add or edit comments. | ```bash diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md index e5d1ebb9cfb..64daed7c326 100644 --- a/doc/api/merge_requests.md +++ b/doc/api/merge_requests.md @@ -504,7 +504,7 @@ PUT /projects/:id/merge_requests/:merge_request_iid | `labels` | string | no | Labels for MR as a comma-separated list | | `milestone_id` | integer | no | The ID of a milestone | | `remove_source_branch` | boolean | no | Flag indicating if a merge request should remove the source branch when merging | -| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked | +| `discussion_locked` | boolean | no | Flag indicating if the merge request's discussion is locked. If the discussion is locked only project members can add, edit or resolve comments. | Must include at least one non-required attribute from above. diff --git a/lib/api/issues.rb b/lib/api/issues.rb index 88b592083db..0df41dcc903 100644 --- a/lib/api/issues.rb +++ b/lib/api/issues.rb @@ -48,7 +48,7 @@ module API optional :labels, type: String, desc: 'Comma-separated list of label names' optional :due_date, type: String, desc: 'Date string in the format YEAR-MONTH-DAY' optional :confidential, type: Boolean, desc: 'Boolean parameter if the issue should be confidential' - optional :discussion_locked, type: Boolean, desc: "Boolean parameter if the issue's discussion should be locked" + optional :discussion_locked, type: Boolean, desc: " Boolean parameter indicating if the issue's discussion is locked" end params :issue_params do diff --git a/lib/api/notes.rb b/lib/api/notes.rb index b3db366d875..0b9ab4eeb05 100644 --- a/lib/api/notes.rb +++ b/lib/api/notes.rb @@ -71,8 +71,6 @@ module API post ":id/#{noteables_str}/:noteable_id/notes" do noteable = find_project_noteable(noteables_str, params[:noteable_id]) - authorize! :create_note, user_project - opts = { note: params[:body], noteable_type: noteables_str.classify, @@ -80,15 +78,12 @@ module API } if can?(current_user, noteable_read_ability_name(noteable), noteable) + authorize! :create_note, noteable + if params[:created_at] && (current_user.admin? || user_project.owner == current_user) opts[:created_at] = params[:created_at] end - noteable_type = opts[:noteable_type].to_s - noteable = Issue.find(opts[:noteable_id]) if noteable_type == 'Issue' - noteable = MergeRequest.find(opts[:noteable_id]) if noteable_type == 'MergeRequest' - authorize! :create_note, noteable if noteable - note = ::Notes::CreateService.new(user_project, current_user, opts).execute if note.valid? diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 10edad462c1..6a6430dfc13 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -239,6 +239,15 @@ describe Projects::NotesController do merge_request.update_attribute(:discussion_locked, true) end + context 'when a noteable is not found' do + it 'returns 404 status' do + request_params[:note][:noteable_id] = 9999 + post :create, request_params.merge(format: :json) + + expect(response).to have_http_status(404) + end + end + context 'when a user is a team member' do it 'returns 302 status for html' do post :create, request_params diff --git a/spec/fixtures/api/schemas/public_api/v4/issues.json b/spec/fixtures/api/schemas/public_api/v4/issues.json index 8c854a43fc6..b7d6dbff031 100644 --- a/spec/fixtures/api/schemas/public_api/v4/issues.json +++ b/spec/fixtures/api/schemas/public_api/v4/issues.json @@ -9,7 +9,7 @@ "title": { "type": "string" }, "description": { "type": ["string", "null"] }, "state": { "type": "string" }, - "discussion_locked": { "type": "boolean" }, + "discussion_locked": { "type": ["boolean", "null"] }, "created_at": { "type": "date" }, "updated_at": { "type": "date" }, "labels": { diff --git a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json index 3b42333bb10..5828be5255b 100644 --- a/spec/fixtures/api/schemas/public_api/v4/merge_requests.json +++ b/spec/fixtures/api/schemas/public_api/v4/merge_requests.json @@ -72,7 +72,7 @@ "user_notes_count": { "type": "integer" }, "should_remove_source_branch": { "type": ["boolean", "null"] }, "force_remove_source_branch": { "type": ["boolean", "null"] }, - "discussion_locked": { "type": "boolean" }, + "discussion_locked": { "type": ["boolean", "null"] }, "web_url": { "type": "uri" }, "time_stats": { "time_estimate": { "type": "integer" }, diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb index 9b399d764ea..2cf669e8191 100644 --- a/spec/policies/issuable_policy_spec.rb +++ b/spec/policies/issuable_policy_spec.rb @@ -16,7 +16,7 @@ describe IssuablePolicy, models: true do context 'when the user is a project member' do before do - project.team << [user, :guest] + project.add_guest(user) end it 'can create a note' do diff --git a/spec/policies/note_policy_spec.rb b/spec/policies/note_policy_spec.rb index 70a99ed0198..58d36a2c84e 100644 --- a/spec/policies/note_policy_spec.rb +++ b/spec/policies/note_policy_spec.rb @@ -5,9 +5,15 @@ describe NotePolicy, mdoels: true do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } - let(:note) { create(:note, noteable: issue, author: user, project: project) } - let(:policies) { described_class.new(user, note) } + def policies(noteable = nil) + return @policies if @policies + + noteable ||= issue + note = create(:note, noteable: noteable, author: user, project: project) + + @policies = described_class.new(user, note) + end context 'when the project is public' do context 'when the note author is not a project member' do @@ -19,6 +25,17 @@ describe NotePolicy, mdoels: true do end end + context 'when the noteable is a snippet' do + it 'can edit note' do + policies = policies(create(:project_snippet, project: project)) + + expect(policies).to be_allowed(:update_note) + expect(policies).to be_allowed(:admin_note) + expect(policies).to be_allowed(:resolve_note) + expect(policies).to be_allowed(:read_note) + end + end + context 'when a discussion is locked' do before do issue.update_attribute(:discussion_locked, true) @@ -29,7 +46,7 @@ describe NotePolicy, mdoels: true do project.add_developer(user) end - it 'can eddit a note' do + it 'can edit a note' do expect(policies).to be_allowed(:update_note) expect(policies).to be_allowed(:admin_note) expect(policies).to be_allowed(:resolve_note) diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index f5882c0c74a..fb440fa551c 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -302,6 +302,40 @@ describe API::Notes do expect(private_issue.notes.reload).to be_empty end end + + context 'when the merge request discussion is locked' do + before do + merge_request.update_attribute(:discussion_locked, true) + end + + context 'when a user is a team member' do + subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), body: 'Hi!' } + + it 'returns 200 status' do + subject + + expect(response).to have_http_status(201) + end + + it 'creates a new note' do + expect { subject }.to change { Note.count }.by(1) + end + end + + context 'when a user is not a team member' do + subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", private_user), body: 'Hi!' } + + it 'returns 403 status' do + subject + + expect(response).to have_http_status(403) + end + + it 'does not create a new note' do + expect { subject }.not_to change { Note.count } + end + end + end end describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 0ed4c2152bb..bcce827fe71 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -144,6 +144,13 @@ describe Issues::UpdateService, :mailer do expect(note).not_to be_nil expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**' end + + it 'creates system note about discussion lock' do + note = find_note('locked this issue') + + expect(note).not_to be_nil + expect(note.note).to eq 'locked this issue' + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index a07b7ac2218..b11a1b31f32 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -125,6 +125,13 @@ describe MergeRequests::UpdateService, :mailer do expect(note.note).to eq 'changed target branch from `master` to `target`' end + it 'creates system note about discussion lock' do + note = find_note('locked this issue') + + expect(note).not_to be_nil + expect(note.note).to eq 'locked this issue' + end + context 'when not including source branch removal options' do before do opts.delete(:force_remove_source_branch) -- cgit v1.2.1 From a319418d9c050097a797fbf4f890cebd5256ed43 Mon Sep 17 00:00:00 2001 From: "Luke \"Jared\" Bennett" Date: Thu, 14 Sep 2017 12:01:07 +0100 Subject: Merge FE --- .../notes/components/issue_comment_form.vue | 27 ++++-- .../notes/components/issue_note_form.vue | 20 ++-- .../javascripts/notes/mixins/issuable_state.js | 15 +++ .../confidential/confidential_issue_sidebar.vue | 14 +-- .../sidebar/components/confidential/edit_form.vue | 9 +- .../components/confidential/edit_form_buttons.vue | 6 +- .../sidebar/components/lock/edit_form.vue | 62 ++++++++++++ .../sidebar/components/lock/edit_form_buttons.vue | 50 ++++++++++ .../sidebar/components/lock/lock_issue_sidebar.vue | 108 +++++++++++++++++++++ app/assets/javascripts/sidebar/sidebar_bundle.js | 70 +++++++++---- .../javascripts/sidebar/stores/sidebar_store.js | 1 + .../issue/confidential_issue_warning.vue | 16 --- .../vue_shared/components/issue/issue_warning.vue | 53 ++++++++++ .../javascripts/vue_shared/mixins/issuable.js | 7 ++ app/assets/stylesheets/framework/buttons.scss | 4 + app/assets/stylesheets/framework/variables.scss | 5 + app/assets/stylesheets/pages/issuable.scss | 27 +++--- app/assets/stylesheets/pages/note_form.scss | 16 +-- app/views/projects/_md_preview.html.haml | 7 ++ app/views/projects/issues/show.html.haml | 5 +- .../projects/merge_requests/_mr_title.html.haml | 2 + app/views/shared/issuable/_sidebar.html.haml | 4 + spec/features/issues_spec.rb | 12 +-- .../sidebar/lock/edit_form_buttons_spec.js | 36 +++++++ spec/javascripts/sidebar/lock/edit_form_spec.js | 41 ++++++++ .../sidebar/lock/lock_issue_sidebar_spec.js | 73 ++++++++++++++ .../issue/confidential_issue_warning_spec.js | 20 ---- .../components/issue/issue_warning_spec.js | 45 +++++++++ 28 files changed, 644 insertions(+), 111 deletions(-) create mode 100644 app/assets/javascripts/notes/mixins/issuable_state.js create mode 100644 app/assets/javascripts/sidebar/components/lock/edit_form.vue create mode 100644 app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue create mode 100644 app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue delete mode 100644 app/assets/javascripts/vue_shared/components/issue/confidential_issue_warning.vue create mode 100644 app/assets/javascripts/vue_shared/components/issue/issue_warning.vue create mode 100644 app/assets/javascripts/vue_shared/mixins/issuable.js create mode 100644 spec/javascripts/sidebar/lock/edit_form_buttons_spec.js create mode 100644 spec/javascripts/sidebar/lock/edit_form_spec.js create mode 100644 spec/javascripts/sidebar/lock/lock_issue_sidebar_spec.js delete mode 100644 spec/javascripts/vue_shared/components/issue/confidential_issue_warning_spec.js create mode 100644 spec/javascripts/vue_shared/components/issue/issue_warning_spec.js diff --git a/app/assets/javascripts/notes/components/issue_comment_form.vue b/app/assets/javascripts/notes/components/issue_comment_form.vue index 16f4e22aa9b..391a1960eae 100644 --- a/app/assets/javascripts/notes/components/issue_comment_form.vue +++ b/app/assets/javascripts/notes/components/issue_comment_form.vue @@ -6,10 +6,11 @@ import TaskList from '../../task_list'; import * as constants from '../constants'; import eventHub from '../event_hub'; - import confidentialIssue from '../../vue_shared/components/issue/confidential_issue_warning.vue'; + import issueWarning from '../../vue_shared/components/issue/issue_warning.vue'; import issueNoteSignedOutWidget from './issue_note_signed_out_widget.vue'; import markdownField from '../../vue_shared/components/markdown/field.vue'; import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue'; + import issuableStateMixin from '../mixins/issuable_state'; export default { name: 'issueCommentForm', @@ -25,7 +26,7 @@ }; }, components: { - confidentialIssue, + issueWarning, issueNoteSignedOutWidget, markdownField, userAvatarLink, @@ -54,6 +55,9 @@ isIssueOpen() { return this.issueState === constants.OPENED || this.issueState === constants.REOPENED; }, + canCreate() { + return this.getIssueData.current_user.can_create_note; + }, issueActionButtonTitle() { if (this.note.length) { const actionText = this.isIssueOpen ? 'close' : 'reopen'; @@ -89,9 +93,6 @@ endpoint() { return this.getIssueData.create_note_path; }, - isConfidentialIssue() { - return this.getIssueData.confidential; - }, }, methods: { ...mapActions([ @@ -206,6 +207,9 @@ }); }, }, + mixins: [ + issuableStateMixin, + ], mounted() { // jQuery is needed here because it is a custom event being dispatched with jQuery. $(document).on('issuable:change', (e, isClosed) => { @@ -239,15 +243,22 @@
- + class="new-note js-quick-submit common-note-form gfm-form js-main-target-form" + > + + +
+ :is-confidential-issue="isIssueConfidential(getIssueData)"> +
+ + `; + textAreaEl = containerEl.querySelector('textarea'); + + event = { + stopPropagation: () => {}, + currentTarget: containerEl.querySelector('button'), + }; + + spyOn(event, 'stopPropagation'); + spyOn(textAreaEl, 'focus'); + commentIndicatorHelper.commentIndicatorOnClick(event); + }); + + it('should stopPropagation', () => { + expect(event.stopPropagation).toHaveBeenCalled(); + }); + + it('should focus textAreaEl', () => { + expect(textAreaEl.focus).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/dom_helper_spec.js b/spec/javascripts/image_diff/helpers/dom_helper_spec.js new file mode 100644 index 00000000000..8dde924e8ae --- /dev/null +++ b/spec/javascripts/image_diff/helpers/dom_helper_spec.js @@ -0,0 +1,118 @@ +import * as domHelper from '~/image_diff/helpers/dom_helper'; +import * as mockData from '../mock_data'; + +describe('domHelper', () => { + const { imageMeta, badgeNumber } = mockData; + + describe('setPositionDataAttribute', () => { + let containerEl; + let attributeAfterCall; + const position = { + myProperty: 'myProperty', + }; + + beforeEach(() => { + containerEl = document.createElement('div'); + containerEl.dataset.position = JSON.stringify(position); + domHelper.setPositionDataAttribute(containerEl, imageMeta); + attributeAfterCall = JSON.parse(containerEl.dataset.position); + }); + + it('should set x, y, width, height', () => { + expect(attributeAfterCall.x).toEqual(imageMeta.x); + expect(attributeAfterCall.y).toEqual(imageMeta.y); + expect(attributeAfterCall.width).toEqual(imageMeta.width); + expect(attributeAfterCall.height).toEqual(imageMeta.height); + }); + + it('should not override other properties', () => { + expect(attributeAfterCall.myProperty).toEqual('myProperty'); + }); + }); + + describe('updateDiscussionAvatarBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` + +
+
+ `; + domHelper.updateDiscussionAvatarBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update avatar badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('updateDiscussionBadgeNumber', () => { + let discussionEl; + + beforeEach(() => { + discussionEl = document.createElement('div'); + discussionEl.innerHTML = ` +
+ `; + domHelper.updateDiscussionBadgeNumber(discussionEl, badgeNumber); + }); + + it('should update discussion badge number', () => { + expect(discussionEl.querySelector('.badge').innerText).toEqual(badgeNumber.toString()); + }); + }); + + describe('toggleCollapsed', () => { + let element; + let discussionNotesEl; + + beforeEach(() => { + element = document.createElement('div'); + element.innerHTML = ` +
+ + +
+ `; + discussionNotesEl = element.querySelector('.discussion-notes'); + }); + + describe('not collapsed', () => { + beforeEach(() => { + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should add collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(true); + }); + + it('should force formEl to display none', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('none'); + }); + }); + + describe('collapsed', () => { + beforeEach(() => { + discussionNotesEl.classList.add('collapsed'); + + domHelper.toggleCollapsed({ + currentTarget: element.querySelector('button'), + }); + }); + + it('should remove collapsed class', () => { + expect(discussionNotesEl.classList.contains('collapsed')).toEqual(false); + }); + + it('should force formEl to display block', () => { + const formEl = element.querySelector('.discussion-form'); + expect(formEl.style.display).toEqual('block'); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/helpers/utils_helper_spec.js b/spec/javascripts/image_diff/helpers/utils_helper_spec.js new file mode 100644 index 00000000000..56d77a05c4c --- /dev/null +++ b/spec/javascripts/image_diff/helpers/utils_helper_spec.js @@ -0,0 +1,207 @@ +import * as utilsHelper from '~/image_diff/helpers/utils_helper'; +import ImageDiff from '~/image_diff/image_diff'; +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageBadge from '~/image_diff/image_badge'; +import * as mockData from '../mock_data'; + +describe('utilsHelper', () => { + const { + noteId, + discussionId, + image, + imageProperties, + imageMeta, + } = mockData; + + describe('resizeCoordinatesToImageElement', () => { + let result; + + beforeEach(() => { + result = utilsHelper.resizeCoordinatesToImageElement(image, imageMeta); + }); + + it('should return x based on widthRatio', () => { + expect(result.x).toEqual(imageMeta.x * 0.5); + }); + + it('should return y based on heightRatio', () => { + expect(result.y).toEqual(imageMeta.y * 0.5); + }); + + it('should return image width', () => { + expect(result.width).toEqual(image.width); + }); + + it('should return image height', () => { + expect(result.height).toEqual(image.height); + }); + }); + + describe('generateBadgeFromDiscussionDOM', () => { + let discussionEl; + let result; + + beforeEach(() => { + const imageFrameEl = document.createElement('div'); + imageFrameEl.innerHTML = ` + + `; + discussionEl = document.createElement('div'); + discussionEl.dataset.discussionId = discussionId; + discussionEl.innerHTML = ` +
+ `; + discussionEl.dataset.position = JSON.stringify(imageMeta); + result = utilsHelper.generateBadgeFromDiscussionDOM(imageFrameEl, discussionEl); + }); + + it('should return actual image properties', () => { + const { actual } = result; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should return browser image properties', () => { + const { browser } = result; + expect(browser.x).toBeDefined(); + expect(browser.y).toBeDefined(); + expect(browser.width).toBeDefined(); + expect(browser.height).toBeDefined(); + }); + + it('should return instance of ImageBadge', () => { + expect(result instanceof ImageBadge).toEqual(true); + }); + + it('should return noteId', () => { + expect(result.noteId).toEqual(noteId); + }); + + it('should return discussionId', () => { + expect(result.discussionId).toEqual(discussionId); + }); + }); + + describe('getTargetSelection', () => { + let containerEl; + + beforeEach(() => { + containerEl = { + querySelector: () => imageProperties, + }; + }); + + function generateEvent(offsetX, offsetY) { + return { + currentTarget: containerEl, + offsetX, + offsetY, + }; + } + + it('should return browser properties', () => { + const event = generateEvent(25, 25); + const result = utilsHelper.getTargetSelection(event); + + const { browser } = result; + expect(browser.x).toEqual(event.offsetX); + expect(browser.y).toEqual(event.offsetY); + expect(browser.width).toEqual(imageProperties.width); + expect(browser.height).toEqual(imageProperties.height); + }); + + it('should return resized actual image properties', () => { + const event = generateEvent(50, 50); + const result = utilsHelper.getTargetSelection(event); + + const { actual } = result; + expect(actual.x).toEqual(100); + expect(actual.y).toEqual(100); + expect(actual.width).toEqual(imageProperties.naturalWidth); + expect(actual.height).toEqual(imageProperties.naturalHeight); + }); + + describe('normalize coordinates', () => { + it('should return x = 0 if x < 0', () => { + const event = generateEvent(-5, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(0); + }); + + it('should return x = width if x > width', () => { + const event = generateEvent(1000, 50); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.x).toEqual(imageProperties.width); + }); + + it('should return y = 0 if y < 0', () => { + const event = generateEvent(50, -10); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(0); + }); + + it('should return y = height if y > height', () => { + const event = generateEvent(50, 1000); + const result = utilsHelper.getTargetSelection(event); + expect(result.browser.y).toEqual(imageProperties.height); + }); + }); + }); + + describe('initImageDiff', () => { + let glCache; + let fileEl; + + beforeEach(() => { + window.gl = window.gl || (window.gl = {}); + glCache = window.gl; + window.gl.ImageFile = () => {}; + fileEl = document.createElement('div'); + fileEl.innerHTML = ` +
+ `; + + spyOn(ImageDiff.prototype, 'init').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'init').and.callFake(() => {}); + }); + + afterEach(() => { + window.gl = glCache; + }); + + it('should initialize gl.ImageFile', () => { + spyOn(window.gl, 'ImageFile'); + + utilsHelper.initImageDiff(fileEl, false, false); + expect(gl.ImageFile).toHaveBeenCalled(); + }); + + it('should initialize ImageDiff if js-single-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` +
+
+ `; + + const imageDiff = utilsHelper.initImageDiff(fileEl, true, false); + expect(ImageDiff.prototype.init).toHaveBeenCalled(); + expect(imageDiff.canCreateNote).toEqual(true); + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + + it('should initialize ReplacedImageDiff if js-replaced-image', () => { + const diffFileEl = fileEl.querySelector('.diff-file'); + diffFileEl.innerHTML = ` +
+
+ `; + + const replacedImageDiff = utilsHelper.initImageDiff(fileEl, false, true); + expect(ReplacedImageDiff.prototype.init).toHaveBeenCalled(); + expect(replacedImageDiff.canCreateNote).toEqual(false); + expect(replacedImageDiff.renderCommentBadge).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_badge_spec.js b/spec/javascripts/image_diff/image_badge_spec.js new file mode 100644 index 00000000000..87f98fc0926 --- /dev/null +++ b/spec/javascripts/image_diff/image_badge_spec.js @@ -0,0 +1,84 @@ +import ImageBadge from '~/image_diff/image_badge'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageBadge', () => { + const { noteId, discussionId, imageMeta } = mockData; + const options = { + noteId, + discussionId, + }; + + it('should save actual property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + actual: imageMeta, + })); + + const { actual } = imageBadge; + expect(actual.x).toEqual(imageMeta.x); + expect(actual.y).toEqual(imageMeta.y); + expect(actual.width).toEqual(imageMeta.width); + expect(actual.height).toEqual(imageMeta.height); + }); + + it('should save browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + browser: imageMeta, + })); + + const { browser } = imageBadge; + expect(browser.x).toEqual(imageMeta.x); + expect(browser.y).toEqual(imageMeta.y); + expect(browser.width).toEqual(imageMeta.width); + expect(browser.height).toEqual(imageMeta.height); + }); + + it('should save noteId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.noteId).toEqual(noteId); + }); + + it('should save discussionId', () => { + const imageBadge = new ImageBadge(options); + expect(imageBadge.discussionId).toEqual(discussionId); + }); + + describe('default values', () => { + let imageBadge; + + beforeEach(() => { + imageBadge = new ImageBadge(options); + }); + + it('should return defaultimageMeta if actual property is not provided', () => { + const { actual } = imageBadge; + expect(actual.x).toEqual(0); + expect(actual.y).toEqual(0); + expect(actual.width).toEqual(0); + expect(actual.height).toEqual(0); + }); + + it('should return defaultimageMeta if browser property is not provided', () => { + const { browser } = imageBadge; + expect(browser.x).toEqual(0); + expect(browser.y).toEqual(0); + expect(browser.width).toEqual(0); + expect(browser.height).toEqual(0); + }); + }); + + describe('imageEl property is provided and not browser property', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(true); + }); + + it('should generate browser property', () => { + const imageBadge = new ImageBadge(Object.assign({}, options, { + imageEl: document.createElement('img'), + })); + + expect(imageDiffHelper.resizeCoordinatesToImageElement).toHaveBeenCalled(); + expect(imageBadge.browser).toEqual(true); + }); + }); +}); diff --git a/spec/javascripts/image_diff/image_diff_spec.js b/spec/javascripts/image_diff/image_diff_spec.js new file mode 100644 index 00000000000..346282328c7 --- /dev/null +++ b/spec/javascripts/image_diff/image_diff_spec.js @@ -0,0 +1,361 @@ +import ImageDiff from '~/image_diff/image_diff'; +import * as imageUtility from '~/lib/utils/image_utility'; +import imageDiffHelper from '~/image_diff/helpers/index'; +import * as mockData from './mock_data'; + +describe('ImageDiff', () => { + let element; + let imageDiff; + + beforeEach(() => { + setFixtures(` +
+
+
+ +
+
1
+
2
+
3
+
+
+
+
+
+
+
+
+
+
+
+
+
+ `); + element = document.getElementById('element'); + }); + + describe('constructor', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element, { + canCreateNote: true, + renderCommentBadge: true, + }); + }); + + it('should set el', () => { + expect(imageDiff.el).toEqual(element); + }); + + it('should set canCreateNote', () => { + expect(imageDiff.canCreateNote).toEqual(true); + }); + + it('should set renderCommentBadge', () => { + expect(imageDiff.renderCommentBadge).toEqual(true); + }); + + it('should set $noteContainer', () => { + expect(imageDiff.$noteContainer[0]).toEqual(element.querySelector('.note-container')); + }); + + describe('default', () => { + beforeEach(() => { + imageDiff = new ImageDiff(element); + }); + + it('should set canCreateNote as false', () => { + expect(imageDiff.canCreateNote).toEqual(false); + }); + + it('should set renderCommentBadge as false', () => { + expect(imageDiff.renderCommentBadge).toEqual(false); + }); + }); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.init(); + }); + + it('should set imageFrameEl', () => { + expect(imageDiff.imageFrameEl).toEqual(element.querySelector('.diff-file .js-image-frame')); + }); + + it('should set imageEl', () => { + expect(imageDiff.imageEl).toEqual(element.querySelector('.diff-file .js-image-frame img')); + }); + + it('should call bindEvents', () => { + expect(imageDiff.bindEvents).toHaveBeenCalled(); + }); + }); + + describe('bindEvents', () => { + let imageEl; + + beforeEach(() => { + spyOn(imageDiffHelper, 'toggleCollapsed').and.callFake(() => {}); + spyOn(imageDiffHelper, 'commentIndicatorOnClick').and.callFake(() => {}); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'imageClicked').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'addBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'removeBadge').and.callFake(() => {}); + spyOn(ImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + imageEl = element.querySelector('.diff-file .js-image-frame img'); + }); + + describe('default', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click event delegation to js-diff-notes-toggle', () => { + element.querySelector('.js-diff-notes-toggle').click(); + expect(imageDiffHelper.toggleCollapsed).toHaveBeenCalled(); + }); + + it('should register click event delegation to comment-indicator', () => { + element.querySelector('.comment-indicator').click(); + expect(imageDiffHelper.commentIndicatorOnClick).toHaveBeenCalled(); + }); + }); + + describe('image loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(true); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + }); + + it('should renderBadges', () => {}); + }); + + describe('image not loaded', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should registers load eventListener', () => { + const loadEvent = new Event('load'); + imageEl.dispatchEvent(loadEvent); + expect(imageDiff.renderBadges).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element, { + canCreateNote: true, + }); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).toHaveBeenCalled(); + }); + + it('should register blur.imageDiff event', () => { + const event = new CustomEvent('blur.imageDiff'); + element.dispatchEvent(event); + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should register addBadge.imageDiff event', () => { + const event = new CustomEvent('addBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.addBadge).toHaveBeenCalled(); + }); + + it('should register removeBadge.imageDiff event', () => { + const event = new CustomEvent('removeBadge.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.removeBadge).toHaveBeenCalled(); + }); + }); + + describe('canCreateNote is false', () => { + beforeEach(() => { + spyOn(imageUtility, 'isImageLoaded').and.returnValue(false); + imageDiff = new ImageDiff(element); + imageDiff.imageEl = imageEl; + imageDiff.bindEvents(); + }); + + it('should not register click.imageDiff event', () => { + const event = new CustomEvent('click.imageDiff'); + element.dispatchEvent(event); + expect(imageDiff.imageClicked).not.toHaveBeenCalled(); + }); + }); + }); + + describe('imageClicked', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'getTargetSelection').and.returnValue({ + actual: {}, + browser: {}, + }); + spyOn(imageDiffHelper, 'setPositionDataAttribute').and.callFake(() => {}); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageClicked({ + detail: { + currentTarget: {}, + }, + }); + }); + + it('should call getTargetSelection', () => { + expect(imageDiffHelper.getTargetSelection).toHaveBeenCalled(); + }); + + it('should call setPositionDataAttribute', () => { + expect(imageDiffHelper.setPositionDataAttribute).toHaveBeenCalled(); + }); + + it('should call showCommentIndicator', () => { + expect(imageDiffHelper.showCommentIndicator).toHaveBeenCalled(); + }); + }); + + describe('renderBadges', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'renderBadge').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.renderBadges(); + }); + + it('should call renderBadge for each discussionEl', () => { + const discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + expect(imageDiff.renderBadge.calls.count()).toEqual(discussionEls.length); + }); + }); + + describe('renderBadge', () => { + let discussionEls; + + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addImageCommentBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'generateBadgeFromDiscussionDOM').and.returnValue({ + browser: {}, + noteId: 'noteId', + }); + discussionEls = element.querySelectorAll('.note-container .discussion-notes .notes'); + imageDiff = new ImageDiff(element); + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should populate imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + describe('renderCommentBadge', () => { + beforeEach(() => { + imageDiff.renderCommentBadge = true; + imageDiff.renderBadge(discussionEls[0], 0); + }); + + it('should call addImageCommentBadge', () => { + expect(imageDiffHelper.addImageCommentBadge).toHaveBeenCalled(); + }); + }); + + describe('renderCommentBadge is false', () => { + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + }); + }); + + describe('addBadge', () => { + beforeEach(() => { + spyOn(imageDiffHelper, 'addImageBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'addAvatarBadge').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.addBadge({ + detail: { + x: 0, + y: 1, + width: 25, + height: 50, + noteId: 'noteId', + discussionId: 'discussionId', + }, + }); + }); + + it('should add imageBadge to imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(1); + }); + + it('should call addImageBadge', () => { + expect(imageDiffHelper.addImageBadge).toHaveBeenCalled(); + }); + + it('should call addAvatarBadge', () => { + expect(imageDiffHelper.addAvatarBadge).toHaveBeenCalled(); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + }); + + describe('removeBadge', () => { + beforeEach(() => { + const { imageMeta } = mockData; + + spyOn(imageDiffHelper, 'updateDiscussionBadgeNumber').and.callFake(() => {}); + spyOn(imageDiffHelper, 'updateDiscussionAvatarBadgeNumber').and.callFake(() => {}); + imageDiff = new ImageDiff(element); + imageDiff.imageBadges = [imageMeta, imageMeta, imageMeta]; + imageDiff.imageFrameEl = element.querySelector('.diff-file .js-image-frame'); + imageDiff.removeBadge({ + detail: { + badgeNumber: 2, + }, + }); + }); + + describe('cascade badge count', () => { + it('should update next imageBadgeEl value', () => { + const imageBadgeEls = imageDiff.imageFrameEl.querySelectorAll('.badge'); + expect(imageBadgeEls[0].innerText).toEqual('1'); + expect(imageBadgeEls[1].innerText).toEqual('2'); + expect(imageBadgeEls.length).toEqual(2); + }); + + it('should call updateDiscussionBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionBadgeNumber).toHaveBeenCalled(); + }); + + it('should call updateDiscussionAvatarBadgeNumber', () => { + expect(imageDiffHelper.updateDiscussionAvatarBadgeNumber).toHaveBeenCalled(); + }); + }); + + it('should remove badge from imageBadges', () => { + expect(imageDiff.imageBadges.length).toEqual(2); + }); + + it('should remove imageBadgeEl', () => { + expect(imageDiff.imageFrameEl.querySelector('#badge-2')).toBeNull(); + }); + }); +}); diff --git a/spec/javascripts/image_diff/init_discussion_tab_spec.js b/spec/javascripts/image_diff/init_discussion_tab_spec.js new file mode 100644 index 00000000000..7c447d6f70d --- /dev/null +++ b/spec/javascripts/image_diff/init_discussion_tab_spec.js @@ -0,0 +1,37 @@ +import initDiscussionTab from '~/image_diff/init_discussion_tab'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('initDiscussionTab', () => { + beforeEach(() => { + setFixtures(` +
+
+
+
+ `); + }); + + it('should pass canCreateNote as false to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote) => { + expect(canCreateNote).toEqual(false); + done(); + }); + + initDiscussionTab(); + }); + + it('should pass renderCommentBadge as true to initImageDiff', (done) => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake((diffFileEl, canCreateNote, renderCommentBadge) => { + expect(renderCommentBadge).toEqual(true); + done(); + }); + + initDiscussionTab(); + }); + + it('should call initImageDiff for each diffFileEls', () => { + spyOn(imageDiffHelper, 'initImageDiff').and.callFake(() => {}); + initDiscussionTab(); + expect(imageDiffHelper.initImageDiff.calls.count()).toEqual(2); + }); +}); diff --git a/spec/javascripts/image_diff/mock_data.js b/spec/javascripts/image_diff/mock_data.js new file mode 100644 index 00000000000..a0d1732dd0a --- /dev/null +++ b/spec/javascripts/image_diff/mock_data.js @@ -0,0 +1,28 @@ +export const noteId = 'noteId'; +export const discussionId = 'discussionId'; +export const badgeText = 'badgeText'; +export const badgeNumber = 5; + +export const coordinate = { + x: 100, + y: 100, +}; + +export const image = { + width: 100, + height: 100, +}; + +export const imageProperties = { + width: image.width, + height: image.height, + naturalWidth: image.width * 2, + naturalHeight: image.height * 2, +}; + +export const imageMeta = { + x: coordinate.x, + y: coordinate.y, + width: imageProperties.naturalWidth, + height: imageProperties.naturalHeight, +}; diff --git a/spec/javascripts/image_diff/replaced_image_diff_spec.js b/spec/javascripts/image_diff/replaced_image_diff_spec.js new file mode 100644 index 00000000000..5f8cd7c531a --- /dev/null +++ b/spec/javascripts/image_diff/replaced_image_diff_spec.js @@ -0,0 +1,312 @@ +import ReplacedImageDiff from '~/image_diff/replaced_image_diff'; +import ImageDiff from '~/image_diff/image_diff'; +import { viewTypes } from '~/image_diff/view_types'; +import imageDiffHelper from '~/image_diff/helpers/index'; + +describe('ReplacedImageDiff', () => { + let element; + let replacedImageDiff; + + beforeEach(() => { + setFixtures(` +
+
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
2-up
+
Swipe
+
Onion skin
+
+
+ `); + element = document.getElementById('element'); + }); + + function setupImageFrameEls() { + replacedImageDiff.imageFrameEls = []; + replacedImageDiff.imageFrameEls[viewTypes.TWO_UP] = element.querySelector('.two-up .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.SWIPE] = element.querySelector('.swipe .js-image-frame'); + replacedImageDiff.imageFrameEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin .js-image-frame'); + } + + function setupViewModesEls() { + replacedImageDiff.viewModesEls = []; + replacedImageDiff.viewModesEls[viewTypes.TWO_UP] = element.querySelector('.view-modes-menu .two-up'); + replacedImageDiff.viewModesEls[viewTypes.SWIPE] = element.querySelector('.view-modes-menu .swipe'); + replacedImageDiff.viewModesEls[viewTypes.ONION_SKIN] = element.querySelector('.view-modes-menu .onion-skin'); + } + + function setupImageEls() { + replacedImageDiff.imageEls = []; + replacedImageDiff.imageEls[viewTypes.TWO_UP] = element.querySelector('.two-up img'); + replacedImageDiff.imageEls[viewTypes.SWIPE] = element.querySelector('.swipe img'); + replacedImageDiff.imageEls[viewTypes.ONION_SKIN] = element.querySelector('.onion-skin img'); + } + + it('should extend ImageDiff', () => { + replacedImageDiff = new ReplacedImageDiff(element); + expect(replacedImageDiff instanceof ImageDiff).toEqual(true); + }); + + describe('init', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + spyOn(ReplacedImageDiff.prototype, 'generateImageEls').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.init(); + }); + + it('should set imageFrameEls', () => { + const { imageFrameEls } = replacedImageDiff; + expect(imageFrameEls).toBeDefined(); + expect(imageFrameEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up .js-image-frame')); + expect(imageFrameEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe .js-image-frame')); + expect(imageFrameEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + + it('should set viewModesEls', () => { + const { viewModesEls } = replacedImageDiff; + expect(viewModesEls).toBeDefined(); + expect(viewModesEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.view-modes-menu .two-up')); + expect(viewModesEls[viewTypes.SWIPE]).toEqual(element.querySelector('.view-modes-menu .swipe')); + expect(viewModesEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.view-modes-menu .onion-skin')); + }); + + it('should generateImageEls', () => { + expect(ReplacedImageDiff.prototype.generateImageEls).toHaveBeenCalled(); + }); + + it('should bindEvents', () => { + expect(ReplacedImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + describe('currentView', () => { + it('should set currentView', () => { + replacedImageDiff.init(viewTypes.ONION_SKIN); + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should default to viewTypes.TWO_UP', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.TWO_UP); + }); + }); + }); + + describe('generateImageEls', () => { + beforeEach(() => { + spyOn(ReplacedImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + + replacedImageDiff = new ReplacedImageDiff(element, { + canCreateNote: false, + renderCommentBadge: false, + }); + + setupImageFrameEls(); + }); + + it('should set imageEls', () => { + replacedImageDiff.generateImageEls(); + const { imageEls } = replacedImageDiff; + expect(imageEls).toBeDefined(); + expect(imageEls[viewTypes.TWO_UP]).toEqual(element.querySelector('.two-up img')); + expect(imageEls[viewTypes.SWIPE]).toEqual(element.querySelector('.swipe img')); + expect(imageEls[viewTypes.ONION_SKIN]).toEqual(element.querySelector('.onion-skin img')); + }); + }); + + describe('bindEvents', () => { + beforeEach(() => { + spyOn(ImageDiff.prototype, 'bindEvents').and.callFake(() => {}); + replacedImageDiff = new ReplacedImageDiff(element); + + setupViewModesEls(); + }); + + it('should call super.bindEvents', () => { + replacedImageDiff.bindEvents(); + expect(ImageDiff.prototype.bindEvents).toHaveBeenCalled(); + }); + + it('should register click eventlistener to 2-up view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.TWO_UP); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.TWO_UP].click(); + }); + + it('should register click eventlistener to swipe view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + + it('should register click eventlistener to onion skin view mode', (done) => { + spyOn(ReplacedImageDiff.prototype, 'changeView').and.callFake((viewMode) => { + expect(viewMode).toEqual(viewTypes.SWIPE); + done(); + }); + + replacedImageDiff.bindEvents(); + replacedImageDiff.viewModesEls[viewTypes.SWIPE].click(); + }); + }); + + describe('getters', () => { + describe('imageEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageEls(); + }); + + it('should return imageEl based on currentView', () => { + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.two-up img')); + + replacedImageDiff.currentView = viewTypes.SWIPE; + expect(replacedImageDiff.imageEl).toEqual(element.querySelector('.swipe img')); + }); + }); + + describe('imageFrameEl', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + replacedImageDiff.currentView = viewTypes.TWO_UP; + setupImageFrameEls(); + }); + + it('should return imageFrameEl based on currentView', () => { + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.two-up .js-image-frame')); + + replacedImageDiff.currentView = viewTypes.ONION_SKIN; + expect(replacedImageDiff.imageFrameEl).toEqual(element.querySelector('.onion-skin .js-image-frame')); + }); + }); + }); + + describe('changeView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + spyOn(imageDiffHelper, 'removeCommentIndicator').and.returnValue({ + removed: false, + }); + setupImageFrameEls(); + }); + + describe('invalid viewType', () => { + beforeEach(() => { + replacedImageDiff.changeView('some-view-name'); + }); + + it('should not call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).not.toHaveBeenCalled(); + }); + }); + + describe('valid viewType', () => { + beforeEach(() => { + jasmine.clock().install(); + spyOn(ReplacedImageDiff.prototype, 'renderNewView').and.callFake(() => {}); + replacedImageDiff.changeView(viewTypes.ONION_SKIN); + }); + + afterEach(() => { + jasmine.clock().uninstall(); + }); + + it('should call removeCommentIndicator', () => { + expect(imageDiffHelper.removeCommentIndicator).toHaveBeenCalled(); + }); + + it('should update currentView to newView', () => { + expect(replacedImageDiff.currentView).toEqual(viewTypes.ONION_SKIN); + }); + + it('should clear imageBadges', () => { + expect(replacedImageDiff.imageBadges.length).toEqual(0); + }); + + it('should call renderNewView', () => { + jasmine.clock().tick(251); + expect(replacedImageDiff.renderNewView).toHaveBeenCalled(); + }); + }); + }); + + describe('renderNewView', () => { + beforeEach(() => { + replacedImageDiff = new ReplacedImageDiff(element); + }); + + it('should call renderBadges', () => { + spyOn(ReplacedImageDiff.prototype, 'renderBadges').and.callFake(() => {}); + + replacedImageDiff.renderNewView({ + removed: false, + }); + + expect(replacedImageDiff.renderBadges).toHaveBeenCalled(); + }); + + describe('removeIndicator', () => { + const indicator = { + removed: true, + x: 0, + y: 1, + image: { + width: 50, + height: 100, + }, + }; + + beforeEach(() => { + setupImageEls(); + setupImageFrameEls(); + }); + + it('should pass showCommentIndicator normalized indicator values', (done) => { + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake(() => {}); + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.callFake((imageEl, meta) => { + expect(meta.x).toEqual(indicator.x); + expect(meta.y).toEqual(indicator.y); + expect(meta.width).toEqual(indicator.image.width); + expect(meta.height).toEqual(indicator.image.height); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + + it('should call showCommentIndicator', (done) => { + const normalized = { + normalized: true, + }; + spyOn(imageDiffHelper, 'resizeCoordinatesToImageElement').and.returnValue(normalized); + spyOn(imageDiffHelper, 'showCommentIndicator').and.callFake((imageFrameEl, normalizedIndicator) => { + expect(normalizedIndicator).toEqual(normalized); + done(); + }); + replacedImageDiff.renderNewView(indicator); + }); + }); + }); +}); diff --git a/spec/javascripts/image_diff/view_types_spec.js b/spec/javascripts/image_diff/view_types_spec.js new file mode 100644 index 00000000000..e9639f46497 --- /dev/null +++ b/spec/javascripts/image_diff/view_types_spec.js @@ -0,0 +1,24 @@ +import { viewTypes, isValidViewType } from '~/image_diff/view_types'; + +describe('viewTypes', () => { + describe('isValidViewType', () => { + it('should return true for TWO_UP', () => { + expect(isValidViewType(viewTypes.TWO_UP)).toEqual(true); + }); + + it('should return true for SWIPE', () => { + expect(isValidViewType(viewTypes.SWIPE)).toEqual(true); + }); + + it('should return true for ONION_SKIN', () => { + expect(isValidViewType(viewTypes.ONION_SKIN)).toEqual(true); + }); + + it('should return false for non view types', () => { + expect(isValidViewType('some-view-type')).toEqual(false); + expect(isValidViewType(null)).toEqual(false); + expect(isValidViewType(undefined)).toEqual(false); + expect(isValidViewType('')).toEqual(false); + }); + }); +}); diff --git a/spec/javascripts/lib/utils/image_utility_spec.js b/spec/javascripts/lib/utils/image_utility_spec.js new file mode 100644 index 00000000000..75addfcc833 --- /dev/null +++ b/spec/javascripts/lib/utils/image_utility_spec.js @@ -0,0 +1,32 @@ +import * as imageUtility from '~/lib/utils/image_utility'; + +describe('imageUtility', () => { + describe('isImageLoaded', () => { + it('should return false when image.complete is false', () => { + const element = { + complete: false, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return false when naturalHeight = 0', () => { + const element = { + complete: true, + naturalHeight: 0, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(false); + }); + + it('should return true when image.complete and naturalHeight != 0', () => { + const element = { + complete: true, + naturalHeight: 100, + }; + + expect(imageUtility.isImageLoaded(element)).toEqual(true); + }); + }); +}); diff --git a/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb new file mode 100644 index 00000000000..2f99febe04e --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/image_formatter_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::ImageFormatter do + it_behaves_like "position formatter" do + let(:base_attrs) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_image.png', + new_path: 'new_image.png', + position_type: 'image' + } + end + + let(:attrs) do + base_attrs.merge(width: 100, height: 100, x: 1, y: 2) + end + end +end diff --git a/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb new file mode 100644 index 00000000000..897dc917f6a --- /dev/null +++ b/spec/lib/gitlab/diff/formatters/text_formatter_spec.rb @@ -0,0 +1,42 @@ +require 'spec_helper' + +describe Gitlab::Diff::Formatters::TextFormatter do + let!(:base) do + { + base_sha: 123, + start_sha: 456, + head_sha: 789, + old_path: 'old_path.txt', + new_path: 'new_path.txt' + } + end + + let!(:complete) do + base.merge(old_line: 1, new_line: 2) + end + + it_behaves_like "position formatter" do + let(:base_attrs) { base } + + let(:attrs) { complete } + end + + # Specific text formatter examples + let!(:formatter) { described_class.new(attrs) } + + describe '#line_age' do + subject { formatter.line_age } + + context ' when there is only new_line' do + let(:attrs) { base.merge(new_line: 1) } + + it { is_expected.to eq('new') } + end + + context ' when there is only old_line' do + let(:attrs) { base.merge(old_line: 1) } + + it { is_expected.to eq('old') } + end + end +end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index 7798736a4dc..9bf54fdecc4 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -5,7 +5,7 @@ describe Gitlab::Diff::Position do let(:project) { create(:project, :repository) } - describe "position for an added file" do + describe "position for an added text file" do let(:commit) { project.commit("2ea1f3dec713d940208fb5ce4a38765ecb5d3f73") } subject do @@ -47,6 +47,31 @@ describe Gitlab::Diff::Position do end end + describe "position for an added image file" do + let(:commit) { project.commit("33f3729a45c02fc67d00adb1b8bca394b0e761d9") } + + subject do + described_class.new( + old_path: "files/images/6049019_460s.jpg", + new_path: "files/images/6049019_460s.jpg", + width: 100, + height: 100, + x: 1, + y: 100, + diff_refs: commit.diff_refs, + position_type: "image" + ) + end + + it "returns the correct diff file" do + diff_file = subject.diff_file(project.repository) + + expect(diff_file.new_file?).to be true + expect(diff_file.new_path).to eq(subject.new_path) + expect(diff_file.diff_refs).to eq(subject.diff_refs) + end + end + describe "position for a changed file" do let(:commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } @@ -468,26 +493,54 @@ describe Gitlab::Diff::Position do end describe "#to_json" do - let(:hash) do - { - old_path: "files/ruby/popen.rb", - new_path: "files/ruby/popen.rb", - old_line: nil, - new_line: 14, - base_sha: nil, - head_sha: nil, - start_sha: nil - } + shared_examples "diff position json" do + it "returns the position as JSON" do + expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + end + + it "works when nested under another hash" do + expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + end end - let(:diff_position) { described_class.new(hash) } + context "for text positon" do + let(:hash) do + { + old_path: "files/ruby/popen.rb", + new_path: "files/ruby/popen.rb", + old_line: nil, + new_line: 14, + base_sha: nil, + head_sha: nil, + start_sha: nil, + position_type: "text" + } + end + + let(:diff_position) { described_class.new(hash) } - it "returns the position as JSON" do - expect(JSON.parse(diff_position.to_json)).to eq(hash.stringify_keys) + it_behaves_like "diff position json" end - it "works when nested under another hash" do - expect(JSON.parse(JSON.generate(pos: diff_position))).to eq('pos' => hash.stringify_keys) + context "for image positon" do + let(:hash) do + { + old_path: "files/any.img", + new_path: "files/any.img", + base_sha: nil, + head_sha: nil, + start_sha: nil, + width: 100, + height: 100, + x: 1, + y: 100, + position_type: "image" + } + end + + let(:diff_position) { described_class.new(hash) } + + it_behaves_like "diff position json" end end end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index 4fa30d8df8b..e5138705443 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -71,6 +71,10 @@ describe Gitlab::Diff::PositionTracer do Gitlab::Diff::DiffRefs.new(base_sha: base_commit.id, head_sha: head_commit.id) end + def text_position_attrs + [:old_line, :new_line] + end + def position(attrs = {}) attrs.reverse_merge!( diff_refs: old_diff_refs @@ -91,7 +95,11 @@ describe Gitlab::Diff::PositionTracer do expect(new_position.diff_refs).to eq(new_diff_refs) attrs.each do |attr, value| - expect(new_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(new_position.formatter.send(attr)).to eq(value) + else + expect(new_position.send(attr)).to eq(value) + end end end end @@ -110,7 +118,11 @@ describe Gitlab::Diff::PositionTracer do expect(change_position.diff_refs).to eq(change_diff_refs) attrs.each do |attr, value| - expect(change_position.send(attr)).to eq(value) + if text_position_attrs.include?(attr) + expect(change_position.formatter.send(attr)).to eq(value) + else + expect(change_position.send(attr)).to eq(value) + end end end end diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index 4aa9ec789a3..eb0a3e9e0d3 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe DiffNote do include RepoHelpers - let(:merge_request) { create(:merge_request) } + let!(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } let(:commit) { project.commit(sample_commit.id) } @@ -98,14 +98,14 @@ describe DiffNote do diff_line = subject.diff_line expect(diff_line.added?).to be true - expect(diff_line.new_line).to eq(position.new_line) + expect(diff_line.new_line).to eq(position.formatter.new_line) expect(diff_line.text).to eq("+ vars = {") end end describe "#line_code" do it "returns the correct line code" do - line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.new_line, 15) + line_code = Gitlab::Diff::LineCode.generate(position.file_path, position.formatter.new_line, 15) expect(subject.line_code).to eq(line_code) end @@ -255,4 +255,38 @@ describe DiffNote do end end end + + describe "image diff notes" do + let(:path) { "files/images/any_image.png" } + + let!(:position) do + Gitlab::Diff::Position.new( + old_path: path, + new_path: path, + width: 10, + height: 10, + x: 1, + y: 1, + diff_refs: merge_request.diff_refs, + position_type: "image" + ) + end + + describe "validations" do + subject { build(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) } + + it { is_expected.not_to validate_presence_of(:line_code) } + + it "does not validate diff line" do + diff_line = subject.diff_line + + expect(diff_line).to be nil + expect(subject).to be_valid + end + end + + it "returns true for on_image?" do + expect(subject.on_image?).to be_truthy + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index b214074fdce..1ecb50586c7 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -314,6 +314,56 @@ describe Note do expect(subject[active_diff_note1.line_code].first.id).to eq(active_diff_note1.discussion_id) expect(subject[active_diff_note3.line_code].first.id).to eq(active_diff_note3.discussion_id) end + + context 'with image discussions' do + let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images and changes") } + let(:image_path) { "files/images/ee_repo_logo.png" } + let(:text_path) { "bar/branch-test.txt" } + let!(:image_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position) } + let!(:text_note) { create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: text_position) } + + let(:image_position) do + Gitlab::Diff::Position.new( + old_path: image_path, + new_path: image_path, + width: 100, + height: 100, + x: 1, + y: 1, + position_type: "image", + diff_refs: merge_request2.diff_refs + ) + end + + let(:text_position) do + Gitlab::Diff::Position.new( + old_path: text_path, + new_path: text_path, + old_line: nil, + new_line: 2, + position_type: "text", + diff_refs: merge_request2.diff_refs + ) + end + + it "groups image discussions by file identifier" do + diff_discussion = DiffDiscussion.new([image_note]) + + discussions = merge_request2.notes.grouped_diff_discussions + + expect(discussions.size).to eq(2) + expect(discussions[image_note.diff_file.new_path]).to include(diff_discussion) + end + + it "groups text discussions by line code" do + diff_discussion = DiffDiscussion.new([text_note]) + + discussions = merge_request2.notes.grouped_diff_discussions + + expect(discussions.size).to eq(2) + expect(discussions[text_note.line_code]).to include(diff_discussion) + end + end end context 'diff discussions for older diff refs' do diff --git a/spec/services/discussions/update_diff_position_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index 82b156f5ebe..2b84206318f 100644 --- a/spec/services/discussions/update_diff_position_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -164,8 +164,8 @@ describe Discussions::UpdateDiffPositionService do change_position = discussion.change_position expect(change_position.start_sha).to eq(old_diff_refs.head_sha) expect(change_position.head_sha).to eq(new_diff_refs.head_sha) - expect(change_position.old_line).to eq(9) - expect(change_position.new_line).to be_nil + expect(change_position.formatter.old_line).to eq(9) + expect(change_position.formatter.new_line).to be_nil end it 'creates a system discussion' do @@ -184,7 +184,7 @@ describe Discussions::UpdateDiffPositionService do expect(discussion.original_position).to eq(old_position) expect(discussion.position).not_to eq(old_position) - expect(discussion.position.new_line).to eq(22) + expect(discussion.position.formatter.new_line).to eq(22) end context 'when the resolve_outdated_diff_discussions setting is set' do diff --git a/spec/support/features/discussion_comments_shared_example.rb b/spec/support/features/discussion_comments_shared_example.rb index 81cb94ab8c4..9f05cabf7ae 100644 --- a/spec/support/features/discussion_comments_shared_example.rb +++ b/spec/support/features/discussion_comments_shared_example.rb @@ -71,7 +71,7 @@ shared_examples 'discussion comments' do |resource_name| expect(page).not_to have_selector menu_selector find(toggle_selector).click - find('body').click + find('body').trigger 'click' expect(page).not_to have_selector menu_selector end diff --git a/spec/support/shared_examples/position_formatters.rb b/spec/support/shared_examples/position_formatters.rb new file mode 100644 index 00000000000..ffc9456dbc7 --- /dev/null +++ b/spec/support/shared_examples/position_formatters.rb @@ -0,0 +1,43 @@ +shared_examples_for "position formatter" do + let(:formatter) { described_class.new(attrs) } + + describe '#key' do + let(:key) { [123, 456, 789, Digest::SHA1.hexdigest(formatter.old_path), Digest::SHA1.hexdigest(formatter.new_path), 1, 2] } + + subject { formatter.key } + + it { is_expected.to eq(key) } + end + + describe '#complete?' do + subject { formatter.complete? } + + context 'when there are missing key attributes' do + it { is_expected.to be_truthy } + end + + context 'when old_line and new_line are nil' do + let(:attrs) { base_attrs } + + it { is_expected.to be_falsy } + end + end + + describe '#to_h' do + let(:formatter_hash) do + attrs.merge(position_type: base_attrs[:position_type] || 'text' ) + end + + subject { formatter.to_h } + + it { is_expected.to eq(formatter_hash) } + end + + describe '#==' do + subject { formatter } + + let(:other_formatter) { described_class.new(attrs) } + + it { is_expected.to eq(other_formatter) } + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 79395f4c564..a27bfdee3d2 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -46,7 +46,8 @@ module TestEnv 'v1.1.0' => 'b83d6e3', 'add-ipython-files' => '93ee732', 'add-pdf-file' => 'e774ebd', - 'add-pdf-text-binary' => '79faa7b' + 'add-pdf-text-binary' => '79faa7b', + 'add_images_and_changes' => '010d106' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily -- cgit v1.2.1 From 20727db1702849b78e6714197f16f602f68cecf8 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 28 Sep 2017 15:34:09 +0200 Subject: Add a model for `fork_networks` The fork network will keep track of the root project as long as it's present. --- app/models/fork_network.rb | 3 +++ db/migrate/20170928124105_create_fork_networks.rb | 28 +++++++++++++++++++++++ db/schema.rb | 8 +++++++ spec/lib/gitlab/import_export/all_models.yml | 3 +++ spec/models/fork_network_spec.rb | 5 ++++ 5 files changed, 47 insertions(+) create mode 100644 app/models/fork_network.rb create mode 100644 db/migrate/20170928124105_create_fork_networks.rb create mode 100644 spec/models/fork_network_spec.rb diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb new file mode 100644 index 00000000000..60c8c167b21 --- /dev/null +++ b/app/models/fork_network.rb @@ -0,0 +1,3 @@ +class ForkNetwork < ActiveRecord::Base + belongs_to :root_project +end diff --git a/db/migrate/20170928124105_create_fork_networks.rb b/db/migrate/20170928124105_create_fork_networks.rb new file mode 100644 index 00000000000..ca906b953a3 --- /dev/null +++ b/db/migrate/20170928124105_create_fork_networks.rb @@ -0,0 +1,28 @@ +class CreateForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :fork_networks do |t| + t.references :root_project, + references: :projects, + index: { unique: true } + + t.string :deleted_root_project_name + end + + add_concurrent_foreign_key :fork_networks, :projects, + column: :root_project_id, + on_delete: :nullify + end + + def down + if foreign_keys_for(:fork_networks, :root_project_id).any? + remove_foreign_key :fork_networks, column: :root_project_id + end + drop_table :fork_networks + end +end diff --git a/db/schema.rb b/db/schema.rb index 25aa15b837f..d7e1a2c94fa 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -591,6 +591,13 @@ ActiveRecord::Schema.define(version: 20171006091000) do add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree + create_table "fork_networks", force: :cascade do |t| + t.integer "root_project_id" + t.string "deleted_root_project_name" + end + + add_index "fork_networks", ["root_project_id"], name: "index_fork_networks_on_root_project_id", unique: true, using: :btree + create_table "forked_project_links", force: :cascade do |t| t.integer "forked_to_project_id", null: false t.integer "forked_from_project_id", null: false @@ -1793,6 +1800,7 @@ ActiveRecord::Schema.define(version: 20171006091000) do add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade + add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "gcp_clusters", "projects", on_delete: :cascade add_foreign_key "gcp_clusters", "services", on_delete: :nullify diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index c457bda9dcb..29baa70d5ae 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -272,6 +272,9 @@ project: - uploads - members_and_requesters - build_trace_section_names +- root_of_fork_network +- fork_network_member +- fork_network award_emoji: - awardable - user diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb new file mode 100644 index 00000000000..b7758a0fbb5 --- /dev/null +++ b/spec/models/fork_network_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe ForkNetwork, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end -- cgit v1.2.1 From d328007214786c7137c31d2c73e9ee76b025e6ed Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 28 Sep 2017 16:38:12 +0200 Subject: Create a fork network when forking a project When no fork network exists for the source projects, we create a new one with the correct source --- app/models/fork_network.rb | 10 ++++- app/models/fork_network_member.rb | 7 ++++ app/models/project.rb | 13 ++++++- app/services/projects/fork_service.rb | 20 ++++++++++ .../20170928133643_create_fork_network_members.rb | 26 +++++++++++++ db/schema.rb | 12 ++++++ spec/models/fork_network_member_spec.rb | 8 ++++ spec/models/fork_network_spec.rb | 43 ++++++++++++++++++++-- spec/services/projects/fork_service_spec.rb | 27 ++++++++++++++ 9 files changed, 160 insertions(+), 6 deletions(-) create mode 100644 app/models/fork_network_member.rb create mode 100644 db/migrate/20170928133643_create_fork_network_members.rb create mode 100644 spec/models/fork_network_member_spec.rb diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb index 60c8c167b21..fd2510d0a4c 100644 --- a/app/models/fork_network.rb +++ b/app/models/fork_network.rb @@ -1,3 +1,11 @@ class ForkNetwork < ActiveRecord::Base - belongs_to :root_project + belongs_to :root_project, class_name: 'Project' + has_many :fork_network_members + has_many :projects, through: :fork_network_members + + after_create :add_root_as_member, if: :root_project + + def add_root_as_member + projects << root_project + end end diff --git a/app/models/fork_network_member.rb b/app/models/fork_network_member.rb new file mode 100644 index 00000000000..6a9b52a1ef8 --- /dev/null +++ b/app/models/fork_network_member.rb @@ -0,0 +1,7 @@ +class ForkNetworkMember < ActiveRecord::Base + belongs_to :fork_network + belongs_to :project + belongs_to :forked_from_project, class_name: 'Project' + + validates :fork_network, :project, presence: true +end diff --git a/app/models/project.rb b/app/models/project.rb index 608b545f99f..aff8fd768f5 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -118,11 +118,20 @@ class Project < ActiveRecord::Base has_one :mock_monitoring_service has_one :microsoft_teams_service + # TODO: replace these relations with the fork network versions has_one :forked_project_link, foreign_key: "forked_to_project_id" has_one :forked_from_project, through: :forked_project_link has_many :forked_project_links, foreign_key: "forked_from_project_id" has_many :forks, through: :forked_project_links, source: :forked_to_project + # TODO: replace these relations with the fork network versions + + has_one :root_of_fork_network, + foreign_key: 'root_project_id', + inverse_of: :root_project, + class_name: 'ForkNetwork' + has_one :fork_network_member + has_one :fork_network, through: :fork_network_member # Merge Requests for target project should be removed with it has_many :merge_requests, foreign_key: 'target_project_id' @@ -1119,8 +1128,8 @@ class Project < ActiveRecord::Base end end - def forked_from?(project) - forked? && project == forked_from_project + def in_fork_network_of?(project) + forked? && project.fork_network == fork_network end def origin_merge_requests diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb index ad67e68a86a..eb5cce5ab98 100644 --- a/app/services/projects/fork_service.rb +++ b/app/services/projects/fork_service.rb @@ -23,11 +23,31 @@ module Projects refresh_forks_count + link_fork_network(new_project) + new_project end private + def fork_network + if @project.fork_network + @project.fork_network + elsif forked_from_project = @project.forked_from_project + # TODO: remove this case when all background migrations have completed + # this only happens when a project had a `forked_project_link` that was + # not migrated to the `fork_network` relation + forked_from_project.fork_network || forked_from_project.create_root_of_fork_network + else + @project.create_root_of_fork_network + end + end + + def link_fork_network(new_project) + fork_network.fork_network_members.create(project: new_project, + forked_from_project: @project) + end + def refresh_forks_count Projects::ForksCountService.new(@project).refresh_cache end diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb new file mode 100644 index 00000000000..c1d94e7d52e --- /dev/null +++ b/db/migrate/20170928133643_create_fork_network_members.rb @@ -0,0 +1,26 @@ +class CreateForkNetworkMembers < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + create_table :fork_network_members do |t| + t.references :fork_network, null: false, index: true, foreign_key: { on_delete: :cascade } + t.references :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade } + t.references :forked_from_project, references: :projects + end + + add_concurrent_foreign_key :fork_network_members, :projects, + column: :forked_from_project_id, + on_delete: :nullify + end + + def down + if foreign_key_exists?(:fork_network_members, column: :forked_from_project_id) + remove_foreign_key :fork_network_members, column: :forked_from_project_id + end + drop_table :fork_network_members + end +end diff --git a/db/schema.rb b/db/schema.rb index d7e1a2c94fa..aac37b6b455 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -591,6 +591,15 @@ ActiveRecord::Schema.define(version: 20171006091000) do add_index "features", ["key"], name: "index_features_on_key", unique: true, using: :btree + create_table "fork_network_members", force: :cascade do |t| + t.integer "fork_network_id", null: false + t.integer "project_id", null: false + t.integer "forked_from_project_id" + end + + add_index "fork_network_members", ["fork_network_id"], name: "index_fork_network_members_on_fork_network_id", using: :btree + add_index "fork_network_members", ["project_id"], name: "index_fork_network_members_on_project_id", unique: true, using: :btree + create_table "fork_networks", force: :cascade do |t| t.integer "root_project_id" t.string "deleted_root_project_name" @@ -1800,6 +1809,9 @@ ActiveRecord::Schema.define(version: 20171006091000) do add_foreign_key "environments", "projects", name: "fk_d1c8c1da6a", on_delete: :cascade add_foreign_key "events", "projects", on_delete: :cascade add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade + add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade + add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify + add_foreign_key "fork_network_members", "projects", on_delete: :cascade add_foreign_key "fork_networks", "projects", column: "root_project_id", name: "fk_e7b436b2b5", on_delete: :nullify add_foreign_key "forked_project_links", "projects", column: "forked_to_project_id", name: "fk_434510edb0", on_delete: :cascade add_foreign_key "gcp_clusters", "projects", on_delete: :cascade diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb new file mode 100644 index 00000000000..532ca1fca8c --- /dev/null +++ b/spec/models/fork_network_member_spec.rb @@ -0,0 +1,8 @@ +require 'spec_helper' + +describe ForkNetworkMember do + describe 'validations' do + it { is_expected.to validate_presence_of(:project) } + it { is_expected.to validate_presence_of(:fork_network) } + end +end diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb index b7758a0fbb5..4781a959846 100644 --- a/spec/models/fork_network_spec.rb +++ b/spec/models/fork_network_spec.rb @@ -1,5 +1,42 @@ -require 'rails_helper' +require 'spec_helper' -RSpec.describe ForkNetwork, type: :model do - pending "add some examples to (or delete) #{__FILE__}" +describe ForkNetwork do + include ProjectForksHelper + + describe '#add_root_as_member' do + it 'adds the root project as a member when creating a new root network' do + project = create(:project) + fork_network = described_class.create(root_project: project) + + expect(fork_network.projects).to include(project) + end + end + + context 'for a deleted project' do + it 'keeps the fork network' do + project = create(:project, :public) + forked = fork_project(project) + project.destroy! + + fork_network = forked.reload.fork_network + + expect(fork_network.projects).to contain_exactly(forked) + expect(fork_network.root_project).to be_nil + end + + it 'allows multiple fork networks where the root project is deleted' do + first_project = create(:project) + second_project = create(:project) + first_fork = fork_project(first_project) + second_fork = fork_project(second_project) + + first_project.destroy + second_project.destroy + + expect(first_fork.fork_network).not_to be_nil + expect(first_fork.fork_network.root_project).to be_nil + expect(second_fork.fork_network).not_to be_nil + expect(second_fork.fork_network.root_project).to be_nil + end + end end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index fa9d6969830..a5ec4111b70 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -60,6 +60,33 @@ describe Projects::ForkService do expect(@from_project.forks_count).to eq(1) end + + it 'creates a fork network with the new project and the root project set' do + to_project + fork_network = @from_project.reload.fork_network + + expect(fork_network).not_to be_nil + expect(fork_network.root_project).to eq(@from_project) + expect(fork_network.projects).to contain_exactly(@from_project, to_project) + end + end + + context 'creating a fork of a fork' do + let(:from_forked_project) { fork_project(@from_project, @to_user) } + let(:other_namespace) do + group = create(:group) + group.add_owner(@to_user) + group + end + let(:to_project) { fork_project(from_forked_project, @to_user, namespace: other_namespace) } + + it 'sets the root of the network to the root project' do + expect(to_project.fork_network.root_project).to eq(@from_project) + end + + it 'sets the forked_from_project on the membership' do + expect(to_project.fork_network_member.forked_from_project).to eq(from_forked_project) + end end end -- cgit v1.2.1 From 70716a1292ca5910908ba37a9d113c8b5a221bb7 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Wed, 20 Sep 2017 17:41:11 +0200 Subject: Allow creating merge requests across forks of a project --- app/finders/merge_request_target_project_finder.rb | 18 +++++++ app/helpers/merge_requests_helper.rb | 3 +- app/models/merge_request.rb | 2 +- app/models/project.rb | 15 +++++- changelogs/unreleased/bvl-fork-network-schema.yml | 5 ++ .../20170928133643_create_fork_network_members.rb | 2 +- lib/gitlab/closing_issue_extractor.rb | 3 +- .../merge_requests/create_new_mr_from_fork_spec.rb | 60 ++++++++++++++++++++++ .../merge_request_target_project_finder_spec.rb | 54 +++++++++++++++++++ spec/models/merge_request_spec.rb | 17 +++++- spec/models/project_spec.rb | 59 +++++++++++++++++++++ spec/requests/api/merge_requests_spec.rb | 10 ---- spec/requests/api/v3/merge_requests_spec.rb | 10 ---- 13 files changed, 230 insertions(+), 28 deletions(-) create mode 100644 app/finders/merge_request_target_project_finder.rb create mode 100644 changelogs/unreleased/bvl-fork-network-schema.yml create mode 100644 spec/features/merge_requests/create_new_mr_from_fork_spec.rb create mode 100644 spec/finders/merge_request_target_project_finder_spec.rb diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb new file mode 100644 index 00000000000..508b53a52c1 --- /dev/null +++ b/app/finders/merge_request_target_project_finder.rb @@ -0,0 +1,18 @@ +class MergeRequestTargetProjectFinder + attr_reader :current_user, :source_project + + def initialize(current_user: nil, source_project:, params: {}) + @current_user = current_user + @source_project = source_project + end + + def execute + if @source_project.fork_network + @source_project.fork_network.projects + .public_or_visible_to_user(current_user) + .with_feature_available_for_user(:merge_requests, current_user) + else + Project.where(id: source_project) + end + end +end diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb index c31023f2d9a..5b2c58d193d 100644 --- a/app/helpers/merge_requests_helper.rb +++ b/app/helpers/merge_requests_helper.rb @@ -73,7 +73,8 @@ module MergeRequestsHelper end def target_projects(project) - [project, project.default_merge_request_target].uniq + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: project) + .execute end def merge_request_button_visibility(merge_request, closed) diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index 086226618e6..9b312f7db6c 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -403,7 +403,7 @@ class MergeRequest < ActiveRecord::Base return false unless for_fork? return true unless source_project - !source_project.forked_from?(target_project) + !source_project.in_fork_network_of?(target_project) end def reopenable? diff --git a/app/models/project.rb b/app/models/project.rb index aff8fd768f5..4a883552a8d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1128,8 +1128,19 @@ class Project < ActiveRecord::Base end end - def in_fork_network_of?(project) - forked? && project.fork_network == fork_network + def forked_from?(other_project) + forked? && forked_from_project == other_project + end + + def in_fork_network_of?(other_project) + # TODO: Remove this in a next release when all fork_networks are populated + # This makes sure all MergeRequests remain valid while the projects don't + # have a fork_network yet. + return true if forked_from?(other_project) + + return false if fork_network.nil? || other_project.fork_network.nil? + + fork_network == other_project.fork_network end def origin_merge_requests diff --git a/changelogs/unreleased/bvl-fork-network-schema.yml b/changelogs/unreleased/bvl-fork-network-schema.yml new file mode 100644 index 00000000000..97b2d5acada --- /dev/null +++ b/changelogs/unreleased/bvl-fork-network-schema.yml @@ -0,0 +1,5 @@ +--- +title: Allow creating merge requests across a fork network +merge_request: 14422 +author: +type: changed diff --git a/db/migrate/20170928133643_create_fork_network_members.rb b/db/migrate/20170928133643_create_fork_network_members.rb index c1d94e7d52e..836f023efdc 100644 --- a/db/migrate/20170928133643_create_fork_network_members.rb +++ b/db/migrate/20170928133643_create_fork_network_members.rb @@ -18,7 +18,7 @@ class CreateForkNetworkMembers < ActiveRecord::Migration end def down - if foreign_key_exists?(:fork_network_members, column: :forked_from_project_id) + if foreign_keys_for(:fork_network_members, :forked_from_project_id).any? remove_foreign_key :fork_network_members, column: :forked_from_project_id end drop_table :fork_network_members diff --git a/lib/gitlab/closing_issue_extractor.rb b/lib/gitlab/closing_issue_extractor.rb index 243c1f1394d..7e7aaeeaa17 100644 --- a/lib/gitlab/closing_issue_extractor.rb +++ b/lib/gitlab/closing_issue_extractor.rb @@ -23,7 +23,8 @@ module Gitlab @extractor.analyze(closing_statements.join(" ")) @extractor.issues.reject do |issue| - @extractor.project.forked_from?(issue.project) # Don't extract issues on original project + # Don't extract issues from the project this project was forked from + @extractor.project.forked_from?(issue.project) end end end diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb new file mode 100644 index 00000000000..515818c5d42 --- /dev/null +++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb @@ -0,0 +1,60 @@ +require 'spec_helper' + +feature 'Creating a merge request from a fork', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let!(:source_project) { ::Projects::ForkService.new(project, user).execute } + + before do + source_project.add_master(user) + + sign_in(user) + end + + shared_examples 'create merge request to other project' do + it 'has all possible target projects' do + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + + within('.dropdown-target-project .dropdown-content') do + expect(page).to have_content(project.full_path) + expect(page).to have_content(target_project.full_path) + expect(page).to have_content(source_project.full_path) + end + end + + it 'allows creating the merge request to another target project' do + visit project_merge_requests_path(source_project) + + page.within '.content' do + click_link 'New merge request' + end + + find('.js-source-branch', match: :first).click + find('.dropdown-source-branch .dropdown-content a', match: :first).click + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + click_button 'Compare branches and continue' + + wait_for_requests + + expect { click_button 'Submit merge request' } + .to change { target_project.merge_requests.reload.size }.by(1) + end + end + + context 'creating to the source of a fork' do + let(:target_project) { project } + + it_behaves_like('create merge request to other project') + end + + context 'creating to a sibling of a fork' do + let!(:target_project) { ::Projects::ForkService.new(project, create(:user)).execute } + + it_behaves_like('create merge request to other project') + end +end diff --git a/spec/finders/merge_request_target_project_finder_spec.rb b/spec/finders/merge_request_target_project_finder_spec.rb new file mode 100644 index 00000000000..c81bfd7932c --- /dev/null +++ b/spec/finders/merge_request_target_project_finder_spec.rb @@ -0,0 +1,54 @@ +require 'spec_helper' + +describe MergeRequestTargetProjectFinder do + include ProjectForksHelper + + let(:user) { create(:user) } + subject(:finder) { described_class.new(current_user: user, source_project: forked_project) } + + shared_examples 'finding related projects' do + it 'finds sibling projects and base project' do + other_fork + + expect(finder.execute).to contain_exactly(base_project, other_fork, forked_project) + end + + it 'does not include projects that have merge requests turned off' do + other_fork.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + base_project.project_feature.update!(merge_requests_access_level: ProjectFeature::DISABLED) + + expect(finder.execute).to contain_exactly(forked_project) + end + end + + context 'public projects' do + let(:base_project) { create(:project, :public, path: 'base') } + let(:forked_project) { fork_project(base_project) } + let(:other_fork) { fork_project(base_project) } + + it_behaves_like 'finding related projects' + end + + context 'private projects' do + let(:base_project) { create(:project, :private, path: 'base') } + let(:forked_project) { fork_project(base_project, base_project.owner) } + let(:other_fork) { fork_project(base_project, base_project.owner) } + + context 'when the user is a member of all projects' do + before do + base_project.add_developer(user) + forked_project.add_developer(user) + other_fork.add_developer(user) + end + + it_behaves_like 'finding related projects' + end + + it 'only finds the projects the user is a member of' do + other_fork.add_developer(user) + base_project.add_developer(user) + + expect(finder.execute).to contain_exactly(other_fork, base_project) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 188a0a98ec3..5f3e3c05d78 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -49,6 +49,19 @@ describe MergeRequest do expect(subject).to be_valid end end + + context 'for forks' do + let(:project) { create(:project) } + let(:fork1) { create(:forked_project_link, forked_from_project: project).forked_to_project } + let(:fork2) { create(:forked_project_link, forked_from_project: project).forked_to_project } + + it 'allows merge requests for sibling-forks' do + subject.source_project = fork1 + subject.target_project = fork2 + + expect(subject).to be_valid + end + end end describe 'respond to' do @@ -1425,7 +1438,7 @@ describe MergeRequest do describe "#source_project_missing?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:fork_project) { create(:forked_project_link, forked_from_project: project).forked_to_project } let(:user) { create(:user) } let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } @@ -1446,7 +1459,7 @@ describe MergeRequest do end context "when the fork does not exist" do - let(:merge_request) do + let!(:merge_request) do create(:merge_request, source_project: fork_project, target_project: project) diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 760ea7da322..40b64cad475 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1818,6 +1818,65 @@ describe Project do end end + context 'forks' do + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project) } + + def fork_project(project) + Projects::ForkService.new(project, create(:user)).execute + end + + describe '#fork_network' do + it 'includes a fork of the project' do + expect(project.fork_network).to include(forked_project) + end + + it 'includes a fork of a fork' do + other_fork = fork_project(forked_project) + + expect(project.fork_network).to include(other_fork) + end + + it 'includes sibling forks' do + other_fork = fork_project(project) + + expect(forked_project.fork_network).to include(other_fork) + end + + it 'includes the base project' do + expect(forked_project.fork_network).to include(project) + end + end + + describe '#in_fork_network_of?' do + it 'is false when the project is not a fork' do + expect(project.in_fork_network_of?(double)).to be_falsy + end + + it 'is true for a real fork' do + expect(forked_project.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for a fork of a fork', :postgresql do + other_fork = fork_project(forked_project) + + expect(other_fork.in_fork_network_of?(project)).to be_truthy + end + + it 'is true for sibling forks' do + sibling = fork_project(project) + + expect(sibling.in_fork_network_of?(forked_project)).to be_truthy + end + + it 'is false when another project is given' do + other_project = build_stubbed(:project) + + expect(forked_project.in_fork_network_of?(other_project)).to be_falsy + end + end + end + describe '#pushes_since_gc' do let(:project) { create(:project) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index c4f6e97b915..377d1a8f877 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -676,16 +676,6 @@ describe API::MergeRequests do end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do post api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index 86f38dd4ec1..acd53fbff66 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -372,16 +372,6 @@ describe API::MergeRequests do end context 'when target_branch is specified' do - it 'returns 422 if not a forked project' do - post v3_api("/projects/#{project.id}/merge_requests", user), - title: 'Test merge_request', - target_branch: 'master', - source_branch: 'markdown', - author: user, - target_project_id: fork_project.id - expect(response).to have_gitlab_http_status(422) - end - it 'returns 422 if targeting a different fork' do post v3_api("/projects/#{fork_project.id}/merge_requests", user2), title: 'Test merge_request', -- cgit v1.2.1 From 7c00b53812895970fdb00cf1d27b059bb15815cd Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Thu, 21 Sep 2017 18:34:32 +0200 Subject: Find branches in all projects in the fork network --- .../projects/merge_requests/creations_controller.rb | 7 +++++-- app/finders/merge_request_target_project_finder.rb | 2 +- .../merge_requests/create_new_mr_from_fork_spec.rb | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb index 1096afbb798..99dc3dda9e7 100644 --- a/app/controllers/projects/merge_requests/creations_controller.rb +++ b/app/controllers/projects/merge_requests/creations_controller.rb @@ -120,10 +120,13 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap end def selected_target_project - if @project.id.to_s == params[:target_project_id] || @project.forked_project_link.nil? + if @project.id.to_s == params[:target_project_id] || !@project.forked? @project + elsif params[:target_project_id].present? + MergeRequestTargetProjectFinder.new(current_user: current_user, source_project: @project) + .execute.find(params[:target_project_id]) else - @project.forked_project_link.forked_from_project + @project.forked_from_project end end end diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb index 508b53a52c1..189eb3847eb 100644 --- a/app/finders/merge_request_target_project_finder.rb +++ b/app/finders/merge_request_target_project_finder.rb @@ -1,7 +1,7 @@ class MergeRequestTargetProjectFinder attr_reader :current_user, :source_project - def initialize(current_user: nil, source_project:, params: {}) + def initialize(current_user: nil, source_project:) @current_user = current_user @source_project = source_project end diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb index 515818c5d42..e6f827174e2 100644 --- a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb +++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb @@ -44,6 +44,25 @@ feature 'Creating a merge request from a fork', :js do expect { click_button 'Submit merge request' } .to change { target_project.merge_requests.reload.size }.by(1) end + + it 'updates the branches when selecting a new target project' do + target_project_member = target_project.owner + CreateBranchService.new(target_project, target_project_member) + .execute('a-brand-new-branch-to-test', 'master') + + visit project_new_merge_request_path(source_project) + + first('.js-target-project').click + find('.dropdown-target-project .dropdown-content a', text: target_project.full_path).click + + wait_for_requests + + first('.js-target-branch').click + + within('.dropdown-target-branch .dropdown-content') do + expect(page).to have_content('a-brand-new-branch-to-test') + end + end end context 'creating to the source of a fork' do -- cgit v1.2.1 From e8ca579d88703aeeaa64dbf4ac45f73a60181568 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 29 Sep 2017 10:04:50 +0200 Subject: Add a project forks spec helper The helper creates a fork of a project with all provided attributes, but skipping the creation of the repository on disk. --- features/steps/project/fork.rb | 4 +- features/steps/project/forked_merge_requests.rb | 5 +- features/support/env.rb | 2 +- spec/controllers/projects/blob_controller_spec.rb | 7 +-- .../merge_requests/diffs_controller_spec.rb | 10 ++-- .../projects/merge_requests_controller_spec.rb | 18 +++---- spec/controllers/projects/notes_controller_spec.rb | 20 ++++---- spec/controllers/projects_controller_spec.rb | 20 ++++---- spec/features/dashboard/merge_requests_spec.rb | 3 +- .../merge_requests/create_new_mr_from_fork_spec.rb | 18 +++++-- .../merge_requests/created_from_fork_spec.rb | 17 +++---- spec/features/merge_requests/diffs_spec.rb | 4 +- spec/features/merge_requests/form_spec.rb | 12 +++-- spec/features/projects/issuable_templates_spec.rb | 8 +-- .../projects/user_creates_directory_spec.rb | 2 +- spec/features/projects/user_creates_files_spec.rb | 2 +- spec/features/projects/user_deletes_files_spec.rb | 2 +- spec/features/projects/user_replaces_files_spec.rb | 2 +- spec/features/projects/user_uploads_files_spec.rb | 11 ++-- spec/features/projects_spec.rb | 7 +-- spec/finders/merge_requests_finder_spec.rb | 12 +++-- spec/helpers/merge_requests_helper_spec.rb | 7 +-- spec/lib/gitlab/import_export/fork_spec.rb | 6 ++- .../import_export/merge_request_parser_spec.rb | 7 +-- spec/lib/gitlab/search_results_spec.rb | 4 +- ...dd_head_pipeline_for_each_merge_request_spec.rb | 5 +- spec/models/forked_project_link_spec.rb | 12 +---- spec/models/merge_request_spec.rb | 43 ++++++++-------- spec/models/project_spec.rb | 18 +++---- spec/models/user_spec.rb | 3 +- spec/requests/api/merge_requests_spec.rb | 30 +++++------ spec/requests/api/projects_spec.rb | 5 +- spec/requests/api/v3/merge_requests_spec.rb | 30 +++++------ spec/requests/lfs_http_spec.rb | 6 +-- spec/serializers/build_details_entity_spec.rb | 10 ++-- spec/services/ci/create_pipeline_service_spec.rb | 8 ++- .../delete_merged_branches_service_spec.rb | 6 ++- .../conflicts/resolve_service_spec.rb | 20 ++++---- .../merge_requests/get_urls_service_spec.rb | 4 +- .../merge_requests/refresh_service_spec.rb | 8 +-- spec/services/projects/fork_service_spec.rb | 12 ++--- spec/services/projects/update_service_spec.rb | 10 ++-- spec/support/project_forks_helper.rb | 58 ++++++++++++++++++++++ .../merge_requests/_commits.html.haml_spec.rb | 5 +- .../projects/merge_requests/edit.html.haml_spec.rb | 9 ++-- .../projects/merge_requests/show.html.haml_spec.rb | 11 ++-- .../namespaceless_project_destroy_worker_spec.rb | 10 ++-- 47 files changed, 308 insertions(+), 225 deletions(-) create mode 100644 spec/support/project_forks_helper.rb diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb index 3490bbd968c..60707f26aee 100644 --- a/features/steps/project/fork.rb +++ b/features/steps/project/fork.rb @@ -58,13 +58,13 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps step 'I should see my fork on the list' do page.within('.js-projects-list-holder') do - project = @user.fork_of(@project) + project = @user.fork_of(@project.reload) expect(page).to have_content("#{project.namespace.human_name} / #{project.name}") end end step 'I make forked repo invalid' do - project = @user.fork_of(@project) + project = @user.fork_of(@project.reload) project.path = 'test-crappy-path' project.save! end diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb index 420ac8a695a..6781a906a94 100644 --- a/features/steps/project/forked_merge_requests.rb +++ b/features/steps/project/forked_merge_requests.rb @@ -5,6 +5,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps include SharedPaths include Select2Helper include WaitForRequests + include ProjectForksHelper step 'I am a member of project "Shop"' do @project = ::Project.find_by(name: "Shop") @@ -13,7 +14,9 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps end step 'I have a project forked off of "Shop" called "Forked Shop"' do - @forked_project = Projects::ForkService.new(@project, @user).execute + @forked_project = fork_project(@project, @user, + namespace: @user.namespace, + repository: true) end step 'I click link "New Merge Request"' do diff --git a/features/support/env.rb b/features/support/env.rb index 608d988755c..5962745d501 100644 --- a/features/support/env.rb +++ b/features/support/env.rb @@ -10,7 +10,7 @@ if ENV['CI'] Knapsack::Adapters::SpinachAdapter.bind end -%w(select2_helper test_env repo_helpers wait_for_requests sidekiq).each do |f| +%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper).each do |f| require Rails.root.join('spec', 'support', f) end diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb index 64b9af7b845..fb76b7fdf38 100644 --- a/spec/controllers/projects/blob_controller_spec.rb +++ b/spec/controllers/projects/blob_controller_spec.rb @@ -1,6 +1,8 @@ require 'rails_helper' describe Projects::BlobController do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } describe "GET show" do @@ -226,9 +228,8 @@ describe Projects::BlobController do end context 'when user has forked project' do - let(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:forked_project) { forked_project_link.forked_to_project } - let(:guest) { forked_project.owner } + let!(:forked_project) { fork_project(project, guest, namespace: guest.namespace, repository: true) } + let(:guest) { create(:user) } before do sign_in(guest) diff --git a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb index fad2c8f3ab7..7260350d5fb 100644 --- a/spec/controllers/projects/merge_requests/diffs_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/diffs_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequests::DiffsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -37,12 +39,12 @@ describe Projects::MergeRequests::DiffsController do render_views let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request_with_diffs, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save + project.add_developer(user) + merge_request.reload go end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index e46d1995498..707e7c32283 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::MergeRequestsController do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { project.owner } let(:merge_request) { create(:merge_request_with_diffs, target_project: project, source_project: project) } @@ -204,14 +206,11 @@ describe Projects::MergeRequestsController do context 'there is no source project' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:forked_project_with_submodules) } - let(:merge_request) { create(:merge_request, source_project: fork_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } + let(:forked_project) { fork_project_with_submodules(project) } + let!(:merge_request) { create(:merge_request, source_project: forked_project, source_branch: 'add-submodule-version-bump', target_branch: 'master', target_project: project) } before do - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - merge_request.reload - fork_project.destroy + forked_project.destroy end it 'closes MR without errors' do @@ -599,21 +598,16 @@ describe Projects::MergeRequestsController do describe 'GET ci_environments_status' do context 'the environment is from a forked project' do - let!(:forked) { create(:project, :repository) } + let!(:forked) { fork_project(project, user, repository: true) } let!(:environment) { create(:environment, project: forked) } let!(:deployment) { create(:deployment, environment: environment, sha: forked.commit.id, ref: 'master') } let(:admin) { create(:admin) } let(:merge_request) do - create(:forked_project_link, forked_to_project: forked, - forked_from_project: project) - create(:merge_request, source_project: forked, target_project: project) end before do - forked.team << [user, :master] - get :ci_environments_status, namespace_id: merge_request.project.namespace.to_param, project_id: merge_request.project, diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index e3114e5c06e..135fd6449ff 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::NotesController do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -210,18 +212,16 @@ describe Projects::NotesController do context 'when creating a commit comment from an MR fork' do let(:project) { create(:project, :repository) } - let(:fork_project) do - create(:project, :repository).tap do |fork| - create(:forked_project_link, forked_to_project: fork, forked_from_project: project) - end + let(:forked_project) do + fork_project(project, nil, repository: true) end let(:merge_request) do - create(:merge_request, source_project: fork_project, target_project: project, source_branch: 'feature', target_branch: 'master') + create(:merge_request, source_project: forked_project, target_project: project, source_branch: 'feature', target_branch: 'master') end let(:existing_comment) do - create(:note_on_commit, note: 'a note', project: fork_project, commit_id: merge_request.commit_shas.first) + create(:note_on_commit, note: 'a note', project: forked_project, commit_id: merge_request.commit_shas.first) end def post_create(extra_params = {}) @@ -231,7 +231,7 @@ describe Projects::NotesController do project_id: project, target_type: 'merge_request', target_id: merge_request.id, - note_project_id: fork_project.id, + note_project_id: forked_project.id, in_reply_to_discussion_id: existing_comment.discussion_id }.merge(extra_params) end @@ -253,16 +253,16 @@ describe Projects::NotesController do end context 'when the user has access to the fork' do - let(:discussion) { fork_project.notes.find_discussion(existing_comment.discussion_id) } + let(:discussion) { forked_project.notes.find_discussion(existing_comment.discussion_id) } before do - fork_project.add_developer(user) + forked_project.add_developer(user) existing_comment end it 'creates the note' do - expect { post_create }.to change { fork_project.notes.count }.by(1) + expect { post_create }.to change { forked_project.notes.count }.by(1) end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 491f35d0fb6..d7148e888ab 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -1,6 +1,8 @@ require('spec_helper') describe ProjectsController do + include ProjectForksHelper + let(:project) { create(:project) } let(:public_project) { create(:project, :public) } let(:user) { create(:user) } @@ -377,10 +379,10 @@ describe ProjectsController do context "when the project is forked" do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -388,7 +390,7 @@ describe ProjectsController do project.merge_requests << merge_request sign_in(admin) - delete :destroy, namespace_id: fork_project.namespace, id: fork_project + delete :destroy, namespace_id: forked_project.namespace, id: forked_project expect(merge_request.reload.state).to eq('closed') end @@ -455,18 +457,14 @@ describe ProjectsController do end context 'with forked project' do - let(:project_fork) { create(:project, :repository, namespace: user.namespace) } - - before do - create(:forked_project_link, forked_to_project: project_fork) - end + let(:forked_project) { fork_project(create(:project, :public), user) } it 'removes fork from project' do delete(:remove_fork, - namespace_id: project_fork.namespace.to_param, - id: project_fork.to_param, format: :js) + namespace_id: forked_project.namespace.to_param, + id: forked_project.to_param, format: :js) - expect(project_fork.forked?).to be_falsey + expect(forked_project.reload.forked?).to be_falsey expect(flash[:notice]).to eq('The fork relationship has been removed.') expect(response).to render_template(:remove_fork) end diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index 8204828b5b9..d26428ec286 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -3,12 +3,13 @@ require 'spec_helper' feature 'Dashboard Merge Requests' do include FilterItemSelectHelper include SortingHelper + include ProjectForksHelper let(:current_user) { create :user } let(:project) { create(:project) } let(:public_project) { create(:project, :public, :repository) } - let(:forked_project) { Projects::ForkService.new(public_project, current_user).execute } + let(:forked_project) { fork_project(public_project, current_user, repository: true) } before do project.add_master(current_user) diff --git a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb index e6f827174e2..93c40ff6443 100644 --- a/spec/features/merge_requests/create_new_mr_from_fork_spec.rb +++ b/spec/features/merge_requests/create_new_mr_from_fork_spec.rb @@ -1,9 +1,15 @@ require 'spec_helper' feature 'Creating a merge request from a fork', :js do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } - let!(:source_project) { ::Projects::ForkService.new(project, user).execute } + let!(:source_project) do + fork_project(project, user, + repository: true, + namespace: user.namespace) + end before do source_project.add_master(user) @@ -49,7 +55,6 @@ feature 'Creating a merge request from a fork', :js do target_project_member = target_project.owner CreateBranchService.new(target_project, target_project_member) .execute('a-brand-new-branch-to-test', 'master') - visit project_new_merge_request_path(source_project) first('.js-target-project').click @@ -66,13 +71,18 @@ feature 'Creating a merge request from a fork', :js do end context 'creating to the source of a fork' do - let(:target_project) { project } + let!(:target_project) { project } it_behaves_like('create merge request to other project') end context 'creating to a sibling of a fork' do - let!(:target_project) { ::Projects::ForkService.new(project, create(:user)).execute } + let!(:target_project) do + other_user = create(:user) + fork_project(project, other_user, + repository: true, + namespace: other_user.namespace) + end it_behaves_like('create merge request to other project') end diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index 09541873f71..f9bc3ee6c58 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -1,21 +1,20 @@ require 'spec_helper' feature 'Merge request created from fork' do + include ProjectForksHelper + given(:user) { create(:user) } given(:project) { create(:project, :public, :repository) } - given(:fork_project) { create(:project, :public, :repository) } + given(:forked_project) { fork_project(project, user, repository: true) } given!(:merge_request) do - create(:forked_project_link, forked_to_project: fork_project, - forked_from_project: project) - - create(:merge_request_with_diffs, source_project: fork_project, + create(:merge_request_with_diffs, source_project: forked_project, target_project: project, description: 'Test merge request') end background do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in user end @@ -31,7 +30,7 @@ feature 'Merge request created from fork' do background do create(:note_on_commit, note: comment, - project: fork_project, + project: forked_project, commit_id: merge_request.commit_shas.first) end @@ -55,7 +54,7 @@ feature 'Merge request created from fork' do context 'source project is deleted' do background do MergeRequests::MergeService.new(project, user).execute(merge_request) - fork_project.destroy! + forked_project.destroy! end scenario 'user can access merge request', js: true do @@ -69,7 +68,7 @@ feature 'Merge request created from fork' do context 'pipeline present in source project' do given(:pipeline) do create(:ci_pipeline, - project: fork_project, + project: forked_project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch) end diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index f4ba660ca39..ee9bb50a881 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Diffs URL', js: true do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } @@ -68,7 +70,7 @@ feature 'Diffs URL', js: true do context 'when editing file' do let(:author_user) { create(:user) } let(:user) { create(:user) } - let(:forked_project) { Projects::ForkService.new(project, author_user).execute } + let(:forked_project) { fork_project(project, author_user, repository: true) } let(:merge_request) { create(:merge_request_with_diffs, source_project: forked_project, target_project: project, author: author_user) } let(:changelog_id) { Digest::SHA1.hexdigest("CHANGELOG") } diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb index de98b147d04..758fc9b139d 100644 --- a/spec/features/merge_requests/form_spec.rb +++ b/spec/features/merge_requests/form_spec.rb @@ -1,8 +1,10 @@ require 'rails_helper' describe 'New/edit merge request', :js do + include ProjectForksHelper + let!(:project) { create(:project, :public, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } let!(:user) { create(:user) } let!(:user2) { create(:user) } let!(:milestone) { create(:milestone, project: project) } @@ -170,16 +172,16 @@ describe 'New/edit merge request', :js do context 'forked project' do before do - fork_project.team << [user, :master] + forked_project.team << [user, :master] sign_in(user) end context 'new merge request' do before do visit project_new_merge_request_path( - fork_project, + forked_project, merge_request: { - source_project_id: fork_project.id, + source_project_id: forked_project.id, target_project_id: project.id, source_branch: 'fix', target_branch: 'master' @@ -238,7 +240,7 @@ describe 'New/edit merge request', :js do context 'edit merge request' do before do merge_request = create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, source_branch: 'fix', target_branch: 'master' diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index 1f9b52dd998..c9b8ef4e37b 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'issuable templates', js: true do + include ProjectForksHelper + let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue_form_location) { '#content-body .issuable-details .detail-page-description' } @@ -116,15 +118,13 @@ feature 'issuable templates', js: true do context 'user creates a merge request from a forked project using templates' do let(:template_content) { 'this is a test "feature-proposal" template' } let(:fork_user) { create(:user) } - let(:fork_project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, :with_diffs, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project, fork_user) } + let(:merge_request) { create(:merge_request, :with_diffs, source_project: forked_project, target_project: project) } background do sign_out(:user) project.team << [fork_user, :developer] - fork_project.team << [fork_user, :master] - create(:forked_project_link, forked_to_project: fork_project, forked_from_project: project) sign_in(fork_user) diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb index 1ba5d83eadf..e8f2e4813c5 100644 --- a/spec/features/projects/user_creates_directory_spec.rb +++ b/spec/features/projects/user_creates_directory_spec.rb @@ -79,7 +79,7 @@ feature 'User creates a directory', js: true do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) end diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb index 3d335687510..51d918bc85d 100644 --- a/spec/features/projects/user_creates_files_spec.rb +++ b/spec/features/projects/user_creates_files_spec.rb @@ -142,7 +142,7 @@ describe 'User creates files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb index 95cd316be0e..7f48a69d9b7 100644 --- a/spec/features/projects/user_deletes_files_spec.rb +++ b/spec/features/projects/user_deletes_files_spec.rb @@ -59,7 +59,7 @@ describe 'User deletes files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Delete file') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) expect(page).to have_content('New commit message') diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb index e284fdefd4f..a9628198d5b 100644 --- a/spec/features/projects/user_replaces_files_spec.rb +++ b/spec/features/projects/user_replaces_files_spec.rb @@ -74,7 +74,7 @@ describe 'User replaces files' do expect(page).to have_content('Replacement file commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb index 98871317ca3..8014c299980 100644 --- a/spec/features/projects/user_uploads_files_spec.rb +++ b/spec/features/projects/user_uploads_files_spec.rb @@ -39,6 +39,9 @@ describe 'User uploads files' do expect(current_path).to eq(project_new_merge_request_path(project)) click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') @@ -51,7 +54,7 @@ describe 'User uploads files' do visit(project2_tree_path_root_ref) end - it 'uploads and commit a new fileto a forked project', js: true do + it 'uploads and commit a new file to a forked project', js: true do find('.add-to-tree').click click_link('Upload file') @@ -69,11 +72,13 @@ describe 'User uploads files' do expect(page).to have_content('New commit message') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) - click_link('Changes') + find("a[data-action='diffs']", text: 'Changes').click + + wait_for_requests expect(page).to have_content('Lorem ipsum dolor sit amet') expect(page).to have_content('Sed ut perspiciatis unde omnis') diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 81f7ab80a04..b957575a244 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Project' do + include ProjectForksHelper + describe 'creating from template' do let(:user) { create(:user) } let(:template) { Gitlab::ProjectTemplate.find(:rails) } @@ -57,11 +59,10 @@ feature 'Project' do describe 'remove forked relationship', js: true do let(:user) { create(:user) } - let(:project) { create(:project, namespace: user.namespace) } + let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) } before do sign_in user - create(:forked_project_link, forked_to_project: project) visit edit_project_path(project) end @@ -71,7 +72,7 @@ feature 'Project' do remove_with_confirm('Remove fork relationship', project.path) expect(page).to have_content 'The fork relationship has been removed.' - expect(project.forked?).to be_falsey + expect(project.reload.forked?).to be_falsey expect(page).not_to have_content 'Remove fork relationship' end end diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb index 95f445e7905..883bdf3746a 100644 --- a/spec/finders/merge_requests_finder_spec.rb +++ b/spec/finders/merge_requests_finder_spec.rb @@ -1,12 +1,18 @@ require 'spec_helper' describe MergeRequestsFinder do + include ProjectForksHelper + let(:user) { create :user } let(:user2) { create :user } - let(:project1) { create(:project) } - let(:project2) { create(:project, forked_from_project: project1) } - let(:project3) { create(:project, :archived, forked_from_project: project1) } + let(:project1) { create(:project, :public) } + let(:project2) { fork_project(project1, user) } + let(:project3) do + p = fork_project(project1, user) + p.update!(archived: true) + p + end let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) } let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') } diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb index 7d1c17909bf..fd7900c32f4 100644 --- a/spec/helpers/merge_requests_helper_spec.rb +++ b/spec/helpers/merge_requests_helper_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe MergeRequestsHelper do + include ProjectForksHelper describe 'ci_build_details_path' do let(:project) { create(:project) } let(:merge_request) { MergeRequest.new } @@ -31,10 +32,10 @@ describe MergeRequestsHelper do describe 'within different projects' do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) } + let(:forked_project) { fork_project(project) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project) } subject { format_mr_branch_names(merge_request) } - let(:source_title) { "#{fork_project.full_path}:#{merge_request.source_branch}" } + let(:source_title) { "#{forked_project.full_path}:#{merge_request.source_branch}" } let(:target_title) { "#{project.full_path}:#{merge_request.target_branch}" } it { is_expected.to eq([source_title, target_title]) } diff --git a/spec/lib/gitlab/import_export/fork_spec.rb b/spec/lib/gitlab/import_export/fork_spec.rb index c7fbc2bc92f..dd0ce0dae41 100644 --- a/spec/lib/gitlab/import_export/fork_spec.rb +++ b/spec/lib/gitlab/import_export/fork_spec.rb @@ -1,13 +1,15 @@ require 'spec_helper' describe 'forked project import' do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project_with_repo) } + let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } @@ -16,7 +18,7 @@ describe 'forked project import' do end let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project_with_repo) + create(:merge_request, source_project: forked_project, target_project: project_with_repo) end let(:saver) do diff --git a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb index 4d87f27ce05..473ba40fae7 100644 --- a/spec/lib/gitlab/import_export/merge_request_parser_spec.rb +++ b/spec/lib/gitlab/import_export/merge_request_parser_spec.rb @@ -1,13 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::MergeRequestParser do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } - let(:forked_from_project) { create(:project, :repository) } - let(:fork_link) { create(:forked_project_link, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let!(:merge_request) do - create(:merge_request, source_project: fork_link.forked_to_project, target_project: project) + create(:merge_request, source_project: forked_project, target_project: project) end let(:parsed_merge_request) do diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb index 4c5efbde69a..e44a7c23452 100644 --- a/spec/lib/gitlab/search_results_spec.rb +++ b/spec/lib/gitlab/search_results_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::SearchResults do + include ProjectForksHelper + let(:user) { create(:user) } let!(:project) { create(:project, name: 'foo') } let!(:issue) { create(:issue, project: project, title: 'foo') } @@ -42,7 +44,7 @@ describe Gitlab::SearchResults do end it 'includes merge requests from source and target projects' do - forked_project = create(:project, forked_from_project: project) + forked_project = fork_project(project, user) merge_request_2 = create(:merge_request, target_project: project, source_project: forked_project, title: 'foo') results = described_class.new(user, Project.where(id: forked_project.id), 'foo') diff --git a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb index 862907c5d01..84c2e9f7e52 100644 --- a/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb +++ b/spec/migrations/add_head_pipeline_for_each_merge_request_spec.rb @@ -2,11 +2,12 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20170508170547_add_head_pipeline_for_each_merge_request.rb') describe AddHeadPipelineForEachMergeRequest, :truncate do + include ProjectForksHelper + let(:migration) { described_class.new } let!(:project) { create(:project) } - let!(:forked_project_link) { create(:forked_project_link, forked_from_project: project) } - let!(:other_project) { forked_project_link.forked_to_project } + let!(:other_project) { fork_project(project) } let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: "branch_1") } let!(:pipeline_2) { create(:ci_pipeline, project: other_project, ref: "branch_1") } diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb index 7dbeb4d2e74..32e33e8f42f 100644 --- a/spec/models/forked_project_link_spec.rb +++ b/spec/models/forked_project_link_spec.rb @@ -1,10 +1,11 @@ require 'spec_helper' describe ForkedProjectLink, "add link on fork" do + include ProjectForksHelper + let(:project_from) { create(:project, :repository) } let(:project_to) { fork_project(project_from, user) } let(:user) { create(:user) } - let(:namespace) { user.namespace } before do project_from.add_reporter(user) @@ -64,13 +65,4 @@ describe ForkedProjectLink, "add link on fork" do expect(ForkedProjectLink.exists?(id: forked_project_link.id)).to eq(false) end end - - def fork_project(from_project, user) - service = Projects::ForkService.new(from_project, user) - shell = double('gitlab_shell', fork_repository: true) - - allow(service).to receive(:gitlab_shell).and_return(shell) - - service.execute - end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 5f3e3c05d78..950af653c80 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe MergeRequest do include RepoHelpers + include ProjectForksHelper subject { create(:merge_request) } @@ -52,8 +53,8 @@ describe MergeRequest do context 'for forks' do let(:project) { create(:project) } - let(:fork1) { create(:forked_project_link, forked_from_project: project).forked_to_project } - let(:fork2) { create(:forked_project_link, forked_from_project: project).forked_to_project } + let(:fork1) { fork_project(project) } + let(:fork2) { fork_project(project) } it 'allows merge requests for sibling-forks' do subject.source_project = fork1 @@ -685,7 +686,7 @@ describe MergeRequest do describe '#diverged_commits_count' do let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } + let(:forked_project) { fork_project(project, nil, repository: true) } context 'when the target branch does not exist anymore' do subject { create(:merge_request, source_project: project, target_project: project) } @@ -713,7 +714,7 @@ describe MergeRequest do end context 'diverged on fork' do - subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) } + subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(29) @@ -721,7 +722,7 @@ describe MergeRequest do end context 'rebased on fork' do - subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) } + subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: forked_project, target_project: project) } it 'counts commits that are on target branch but not on source branch' do expect(subject.diverged_commits_count).to eq(0) @@ -1270,11 +1271,7 @@ describe MergeRequest do end context 'with environments on source project' do - let(:source_project) do - create(:project, :repository) do |fork_project| - fork_project.create_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - end - end + let(:source_project) { fork_project(project, nil, repository: true) } let(:merge_request) do create(:merge_request, @@ -1438,14 +1435,14 @@ describe MergeRequest do describe "#source_project_missing?" do let(:project) { create(:project) } - let(:fork_project) { create(:forked_project_link, forked_from_project: project).forked_to_project } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the fork exists" do let(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1461,7 +1458,7 @@ describe MergeRequest do context "when the fork does not exist" do let!(:merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1484,14 +1481,14 @@ describe MergeRequest do describe "#closed_without_fork?" do let(:project) { create(:project) } - let(:fork_project) { create(:project, forked_from_project: project) } + let(:forked_project) { fork_project(project) } let(:user) { create(:user) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } context "when the merge request is closed" do let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1510,7 +1507,7 @@ describe MergeRequest do context "when the merge request is open" do let(:open_merge_request) do create(:merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end @@ -1529,24 +1526,24 @@ describe MergeRequest do end context 'forked project' do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } - let(:fork_project) { create(:project, forked_from_project: project, namespace: user.namespace) } + let(:forked_project) { fork_project(project, user) } let!(:merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project) end it 'returns false if unforked' do - Projects::UnlinkForkService.new(fork_project, user).execute + Projects::UnlinkForkService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end it 'returns false if the source project is deleted' do - Projects::DestroyService.new(fork_project, user).execute + Projects::DestroyService.new(forked_project, user).execute expect(merge_request.reload.reopenable?).to be_falsey end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 40b64cad475..f7155369666 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1819,40 +1819,34 @@ describe Project do end context 'forks' do + include ProjectForksHelper + let(:project) { create(:project, :public) } let!(:forked_project) { fork_project(project) } - def fork_project(project) - Projects::ForkService.new(project, create(:user)).execute - end - describe '#fork_network' do it 'includes a fork of the project' do - expect(project.fork_network).to include(forked_project) + expect(project.fork_network.projects).to include(forked_project) end it 'includes a fork of a fork' do other_fork = fork_project(forked_project) - expect(project.fork_network).to include(other_fork) + expect(project.fork_network.projects).to include(other_fork) end it 'includes sibling forks' do other_fork = fork_project(project) - expect(forked_project.fork_network).to include(other_fork) + expect(forked_project.fork_network.projects).to include(other_fork) end it 'includes the base project' do - expect(forked_project.fork_network).to include(project) + expect(forked_project.fork_network.projects).to include(project.reload) end end describe '#in_fork_network_of?' do - it 'is false when the project is not a fork' do - expect(project.in_fork_network_of?(double)).to be_falsy - end - it 'is true for a real fork' do expect(forked_project.in_fork_network_of?(project)).to be_truthy end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 995211845ce..52ca068f9a4 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe User do include Gitlab::CurrentSettings + include ProjectForksHelper describe 'modules' do subject { described_class } @@ -1431,7 +1432,7 @@ describe User do describe "#contributed_projects" do subject { create(:user) } let!(:project1) { create(:project) } - let!(:project2) { create(:project, forked_from_project: project3) } + let!(:project2) { fork_project(project3) } let!(:project3) { create(:project) } let!(:merge_request) { create(:merge_request, source_project: project2, target_project: project3, author: subject) } let!(:push_event) { create(:push_event, project: project1, author: subject) } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index 377d1a8f877..5e66e1607ba 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -616,17 +618,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -635,10 +637,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -647,7 +649,7 @@ describe API::MergeRequests do it 'returns 422 when target project has disabled merge requests' do project.project_feature.update(merge_requests_access_level: 0) - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: 'master', source_branch: 'markdown', @@ -658,26 +660,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do it 'returns 422 if targeting a different fork' do - post api("/projects/#{fork_project.id}/merge_requests", user2), + post api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -688,8 +690,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 18f6f7df1fa..5964244f8c5 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -64,9 +64,12 @@ describe API::Projects do create(:project, :public) end + # TODO: We're currently querying to detect if a project is a fork + # in 2 ways. Lower this back to 8 when `ForkedProjectLink` relation is + # removed expect do get api('/projects', current_user) - end.not_to exceed_query_limit(control).with_threshold(8) + end.not_to exceed_query_limit(control).with_threshold(9) end end diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb index acd53fbff66..df73c731c96 100644 --- a/spec/requests/api/v3/merge_requests_spec.rb +++ b/spec/requests/api/v3/merge_requests_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe API::MergeRequests do + include ProjectForksHelper + let(:base_time) { Time.now } let(:user) { create(:user) } let(:admin) { create(:user, :admin) } @@ -312,17 +314,17 @@ describe API::MergeRequests do context 'forked projects' do let!(:user2) { create(:user) } - let!(:fork_project) { create(:project, forked_from_project: project, namespace: user2.namespace, creator_id: user2.id) } + let!(:forked_project) { fork_project(project, user2) } let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) } before do - fork_project.add_reporter(user2) + forked_project.add_reporter(user2) allow_any_instance_of(MergeRequest).to receive(:write_ref) end it "returns merge_request" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "feature_conflict", target_branch: "master", author: user2, target_project_id: project.id, description: 'Test description for Test merge_request' expect(response).to have_gitlab_http_status(201) @@ -331,10 +333,10 @@ describe API::MergeRequests do end it "does not return 422 when source_branch equals target_branch" do - expect(project.id).not_to eq(fork_project.id) - expect(fork_project.forked?).to be_truthy - expect(fork_project.forked_from_project).to eq(project) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + expect(project.id).not_to eq(forked_project.id) + expect(forked_project.forked?).to be_truthy + expect(forked_project.forked_from_project).to eq(project) + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', source_branch: "master", target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(201) expect(json_response['title']).to eq('Test merge_request') @@ -343,7 +345,7 @@ describe API::MergeRequests do it "returns 422 when target project has disabled merge requests" do project.project_feature.update(merge_requests_access_level: 0) - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test', target_branch: "master", source_branch: 'markdown', @@ -354,26 +356,26 @@ describe API::MergeRequests do end it "returns 400 when source_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when target_branch is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end it "returns 400 when title is missing" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: project.id expect(response).to have_gitlab_http_status(400) end context 'when target_branch is specified' do it 'returns 422 if targeting a different fork' do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', @@ -384,8 +386,8 @@ describe API::MergeRequests do end it "returns 201 when target_branch is specified and for the same project" do - post v3_api("/projects/#{fork_project.id}/merge_requests", user2), - title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: fork_project.id + post v3_api("/projects/#{forked_project.id}/merge_requests", user2), + title: 'Test merge_request', target_branch: 'master', source_branch: 'markdown', author: user2, target_project_id: forked_project.id expect(response).to have_gitlab_http_status(201) end end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 27d09b8202e..badc8bccf84 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Git LFS API and storage' do include WorkhorseHelpers + include ProjectForksHelper let(:user) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } @@ -1173,11 +1174,6 @@ describe 'Git LFS API and storage' do ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token) end - def fork_project(project, user, object = nil) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(project, user, {}).execute - end - def post_lfs_json(url, body = nil, headers = nil) post(url, body.try(:to_json), (headers || {}).merge('Content-Type' => 'application/vnd.git-lfs+json')) end diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb index 5b7822d5d8e..f6bd6e9ede4 100644 --- a/spec/serializers/build_details_entity_spec.rb +++ b/spec/serializers/build_details_entity_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BuildDetailsEntity do + include ProjectForksHelper + set(:user) { create(:admin) } it 'inherits from JobEntity' do @@ -56,18 +58,16 @@ describe BuildDetailsEntity do end context 'when merge request is from a fork' do - let(:fork_project) do - create(:project, forked_from_project: project) - end + let(:forked_project) { fork_project(project) } - let(:pipeline) { create(:ci_pipeline, project: fork_project) } + let(:pipeline) { create(:ci_pipeline, project: forked_project) } before do allow(build).to receive(:merge_request).and_return(merge_request) end let(:merge_request) do - create(:merge_request, source_project: fork_project, + create(:merge_request, source_project: forked_project, target_project: project, source_branch: build.ref) end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index eb6e683cc23..08847183bf4 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Ci::CreatePipelineService do + include ProjectForksHelper + set(:project) { create(:project, :repository) } let(:user) { create(:admin) } let(:ref_name) { 'refs/heads/master' } @@ -82,13 +84,9 @@ describe Ci::CreatePipelineService do end context 'when merge request target project is different from source project' do + let!(:project) { fork_project(target_project, nil, repository: true) } let!(:target_project) { create(:project, :repository) } - let!(:forked_project_link) do - create(:forked_project_link, forked_to_project: project, - forked_from_project: target_project) - end - it 'updates head pipeline for merge request' do merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index 03c682ae0d7..5a9eb359ee1 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe DeleteMergedBranchesService do + include ProjectForksHelper + subject(:service) { described_class.new(project, project.owner) } let(:project) { create(:project, :repository) } @@ -50,9 +52,9 @@ describe DeleteMergedBranchesService do context 'open merge requests' do it 'does not delete branches from open merge requests' do - fork_link = create(:forked_project_link, forked_from_project: project) + forked_project = fork_project(project) create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') - create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') + create(:merge_request, :opened, source_project: forked_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') service.execute diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index 6f49a65d795..9c9b0c4c4a1 100644 --- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -1,14 +1,16 @@ require 'spec_helper' describe MergeRequests::Conflicts::ResolveService do + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - let(:fork_project) do - create(:forked_project_with_submodules) do |fork_project| - fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id) - fork_project.save - end + let(:project) { create(:project, :public, :repository) } + + let(:forked_project) do + forked_project = fork_project(project, user) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + forked_project end let(:merge_request) do @@ -19,7 +21,7 @@ describe MergeRequests::Conflicts::ResolveService do let(:merge_request_from_fork) do create(:merge_request, - source_branch: 'conflict-resolvable-fork', source_project: fork_project, + source_branch: 'conflict-resolvable-fork', source_project: forked_project, target_branch: 'conflict-start', target_project: project) end @@ -114,7 +116,7 @@ describe MergeRequests::Conflicts::ResolveService do end it 'gets conflicts from the source project' do - expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original + expect(forked_project.repository.rugged).to receive(:merge_commits).and_call_original expect(project.repository.rugged).not_to receive(:merge_commits) resolve_conflicts diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 25599dea19f..274624aa8bb 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -1,6 +1,8 @@ require "spec_helper" describe MergeRequests::GetUrlsService do + include ProjectForksHelper + let(:project) { create(:project, :public, :repository) } let(:service) { described_class.new(project) } let(:source_branch) { "merge-test" } @@ -85,7 +87,7 @@ describe MergeRequests::GetUrlsService do context 'pushing to existing branch from forked project' do let(:user) { create(:user) } - let!(:forked_project) { Projects::ForkService.new(project, user).execute } + let!(:forked_project) { fork_project(project, user, repository: true) } let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 64e676f22a0..62dbe362ec8 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe MergeRequests::RefreshService do + include ProjectForksHelper + let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class } @@ -12,7 +14,8 @@ describe MergeRequests::RefreshService do group.add_owner(@user) @project = create(:project, :repository, namespace: group) - @fork_project = Projects::ForkService.new(@project, @user).execute + @fork_project = fork_project(@project, @user, repository: true) + @merge_request = create(:merge_request, source_project: @project, source_branch: 'master', @@ -311,8 +314,7 @@ describe MergeRequests::RefreshService do context 'when the merge request is sourced from a different project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - forked_project = create(:project, :repository) - create(:forked_project_link, forked_to_project: forked_project, forked_from_project: @project) + forked_project = fork_project(@project, @user, repository: true) merge_request = create(:merge_request, target_branch: 'master', diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index a5ec4111b70..53862283a27 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Projects::ForkService do + include ProjectForksHelper let(:gitlab_shell) { Gitlab::Shell.new } describe 'fork by user' do @@ -33,7 +34,7 @@ describe Projects::ForkService do end describe "successfully creates project in the user namespace" do - let(:to_project) { fork_project(@from_project, @to_user) } + let(:to_project) { fork_project(@from_project, @to_user, namespace: @to_user.namespace) } it { expect(to_project).to be_persisted } it { expect(to_project.errors).to be_empty } @@ -93,7 +94,7 @@ describe Projects::ForkService do context 'project already exists' do it "fails due to validation, not transaction failure" do @existing_project = create(:project, :repository, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace) - @to_project = fork_project(@from_project, @to_user) + @to_project = fork_project(@from_project, @to_user, namespace: @to_namespace) expect(@existing_project).to be_persisted expect(@to_project).not_to be_persisted @@ -115,7 +116,7 @@ describe Projects::ForkService do end it 'does not allow creation' do - to_project = fork_project(@from_project, @to_user) + to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace) expect(to_project).not_to be_persisted expect(to_project.errors.messages).to have_key(:base) @@ -209,9 +210,4 @@ describe Projects::ForkService do end end end - - def fork_project(from_project, user, params = {}) - allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) - Projects::ForkService.new(from_project, user, params).execute - end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index d400304622e..3da222e2ed8 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Projects::UpdateService, '#execute' do + include ProjectForksHelper + let(:gitlab_shell) { Gitlab::Shell.new } let(:user) { create(:user) } let(:admin) { create(:admin) } @@ -76,13 +78,7 @@ describe Projects::UpdateService, '#execute' do describe 'when updating project that has forks' do let(:project) { create(:project, :internal) } - let(:forked_project) { create(:forked_project_with_submodules, :internal) } - - before do - forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, - forked_from_project_id: project.id) - forked_project.save - end + let(:forked_project) { fork_project(project) } it 'updates forks visibility level when parent set to more restrictive' do opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } diff --git a/spec/support/project_forks_helper.rb b/spec/support/project_forks_helper.rb new file mode 100644 index 00000000000..0d1c6792d13 --- /dev/null +++ b/spec/support/project_forks_helper.rb @@ -0,0 +1,58 @@ +module ProjectForksHelper + def fork_project(project, user = nil, params = {}) + # Load the `fork_network` for the project to fork as there might be one that + # wasn't loaded yet. + project.reload unless project.fork_network + + unless user + user = create(:user) + project.add_developer(user) + end + + unless params[:namespace] || params[:namespace_id] + params[:namespace] = create(:group) + params[:namespace].add_owner(user) + end + + service = Projects::ForkService.new(project, user, params) + + create_repository = params.delete(:repository) + # Avoid creating a repository + unless create_repository + allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) + shell = double('gitlab_shell', fork_repository: true) + allow(service).to receive(:gitlab_shell).and_return(shell) + end + + forked_project = service.execute + + # Reload the both projects so they know about their newly created fork_network + if forked_project.persisted? + project.reload + forked_project.reload + end + + if create_repository + # The call to project.repository.after_import in RepositoryForkWorker does + # not reset the @exists variable of this forked_project.repository + # so we have to explicitely call this method to clear the @exists variable. + # of the instance we're returning here. + forked_project.repository.after_import + + # We can't leave the hooks in place after a fork, as those would fail in tests + # The "internal" API is not available + FileUtils.rm_rf("#{forked_project.repository.path}/hooks") + end + + forked_project + end + + def fork_project_with_submodules(project, user = nil, params = {}) + forked_project = fork_project(project, user, params) + TestEnv.copy_repo(forked_project, + bare_repo: TestEnv.forked_repo_path_bare, + refs: TestEnv::FORKED_BRANCH_SHA) + + forked_project + end +end diff --git a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb index 98c7de9b709..efed2e02a1b 100644 --- a/spec/views/projects/merge_requests/_commits.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/_commits.html.haml_spec.rb @@ -2,10 +2,11 @@ require 'spec_helper' describe 'projects/merge_requests/_commits.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:target_project) { create(:project, :repository) } - let(:source_project) { create(:project, :repository, forked_from_project: target_project) } + let(:target_project) { create(:project, :public, :repository) } + let(:source_project) { fork_project(target_project, user, repository: true) } let(:merge_request) do create(:merge_request, :simple, diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb index 69c7d0cbf28..9b74a7e1946 100644 --- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb @@ -2,16 +2,19 @@ require 'spec_helper' describe 'projects/merge_requests/edit.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:milestone) { create(:milestone, project: project) } let(:closed_merge_request) do + project.add_developer(user) + create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user, assignee: user, diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb index 6f29d12373a..28d54c2fb77 100644 --- a/spec/views/projects/merge_requests/show.html.haml_spec.rb +++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb @@ -2,16 +2,17 @@ require 'spec_helper' describe 'projects/merge_requests/show.html.haml' do include Devise::Test::ControllerHelpers + include ProjectForksHelper let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:fork_project) { create(:project, :repository, forked_from_project: project) } - let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) } + let(:project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(project, user, repository: true) } + let(:unlink_project) { Projects::UnlinkForkService.new(forked_project, user) } let(:note) { create(:note_on_merge_request, project: project, noteable: closed_merge_request) } let(:closed_merge_request) do create(:closed_merge_request, - source_project: fork_project, + source_project: forked_project, target_project: project, author: user) end @@ -52,7 +53,7 @@ describe 'projects/merge_requests/show.html.haml' do context 'when the merge request is open' do it 'closes the merge request if the source project does not exist' do closed_merge_request.update_attributes(state: 'open') - fork_project.destroy + forked_project.destroy render diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb index 20cf580af8a..ed8cedc0079 100644 --- a/spec/workers/namespaceless_project_destroy_worker_spec.rb +++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe NamespacelessProjectDestroyWorker do + include ProjectForksHelper + subject { described_class.new } before do @@ -55,9 +57,11 @@ describe NamespacelessProjectDestroyWorker do context 'project forked from another' do let!(:parent_project) { create(:project) } - - before do - create(:forked_project_link, forked_to_project: project, forked_from_project: parent_project) + let(:project) do + namespaceless_project = fork_project(parent_project) + namespaceless_project.namespace_id = nil + namespaceless_project.save(validate: false) + namespaceless_project end it 'closes open merge requests' do -- cgit v1.2.1 From df7f530d843ca03cfdff65b2bb230e00ec60b371 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 29 Sep 2017 19:02:02 +0200 Subject: Add a migration to populate fork networks This uses the existing ForkedProjectLinks --- .../20170929131201_populate_fork_networks.rb | 30 ++++++ .../create_fork_network_memberships_range.rb | 60 +++++++++++ .../populate_fork_networks_range.rb | 54 ++++++++++ .../create_fork_network_memberships_range_spec.rb | 117 +++++++++++++++++++++ .../populate_fork_networks_range_spec.rb | 85 +++++++++++++++ 5 files changed, 346 insertions(+) create mode 100644 db/migrate/20170929131201_populate_fork_networks.rb create mode 100644 lib/gitlab/background_migration/create_fork_network_memberships_range.rb create mode 100644 lib/gitlab/background_migration/populate_fork_networks_range.rb create mode 100644 spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb create mode 100644 spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb diff --git a/db/migrate/20170929131201_populate_fork_networks.rb b/db/migrate/20170929131201_populate_fork_networks.rb new file mode 100644 index 00000000000..1214962770f --- /dev/null +++ b/db/migrate/20170929131201_populate_fork_networks.rb @@ -0,0 +1,30 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class PopulateForkNetworks < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + MIGRATION = 'PopulateForkNetworksRange'.freeze + BATCH_SIZE = 100 + DELAY_INTERVAL = 15.seconds + + disable_ddl_transaction! + + class ForkedProjectLink < ActiveRecord::Base + include EachBatch + + self.table_name = 'forked_project_links' + end + + def up + say 'Populating the `fork_networks` based on existing `forked_project_links`' + + queue_background_migration_jobs_by_range_at_intervals(ForkedProjectLink, MIGRATION, DELAY_INTERVAL, batch_size: BATCH_SIZE) + end + + def down + # nothing + end +end diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb new file mode 100644 index 00000000000..4b468e9cd58 --- /dev/null +++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb @@ -0,0 +1,60 @@ +module Gitlab + module BackgroundMigration + class CreateForkNetworkMembershipsRange + RESCHEDULE_DELAY = 15 + + class ForkedProjectLink < ActiveRecord::Base + self.table_name = 'forked_project_links' + end + + def perform(start_id, end_id) + log("Creating memberships for forks: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_MEMBERS + INSERT INTO fork_network_members (fork_network_id, project_id, forked_from_project_id) + + SELECT fork_network_members.fork_network_id, + forked_project_links.forked_to_project_id, + forked_project_links.forked_from_project_id + + FROM forked_project_links + + INNER JOIN fork_network_members + ON forked_project_links.forked_from_project_id = fork_network_members.project_id + + WHERE forked_project_links.id BETWEEN #{start_id} AND #{end_id} + AND NOT EXISTS ( + SELECT true + FROM fork_network_members existing_members + WHERE existing_members.project_id = forked_project_links.forked_to_project_id + ) + INSERT_MEMBERS + + if missing_members?(start_id, end_id) + BackgroundMigrationWorker.perform_in(RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [start_id, end_id]) + end + end + + def missing_members?(start_id, end_id) + count_sql = <<~MISSING_MEMBERS + SELECT COUNT(*) + + FROM forked_project_links + + WHERE NOT EXISTS ( + SELECT true + FROM fork_network_members + WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + MISSING_MEMBERS + + ForkNetworkMember.count_by_sql(count_sql) > 0 + end + + def log(message) + Rails.logger.info("#{self.class.name} - #{message}") + end + end + end +end diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb new file mode 100644 index 00000000000..6c355ed1e75 --- /dev/null +++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb @@ -0,0 +1,54 @@ +module Gitlab + module BackgroundMigration + class PopulateForkNetworksRange + def perform(start_id, end_id) + log("Creating fork networks for forked project links: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_NETWORKS + INSERT INTO fork_networks (root_project_id) + SELECT DISTINCT forked_project_links.forked_from_project_id + + FROM forked_project_links + + WHERE NOT EXISTS ( + SELECT true + FROM forked_project_links inner_links + WHERE inner_links.forked_to_project_id = forked_project_links.forked_from_project_id + ) + AND NOT EXISTS ( + SELECT true + FROM fork_networks + WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + INSERT_NETWORKS + + log("Creating memberships for root projects: #{start_id} - #{end_id}") + + ActiveRecord::Base.connection.execute <<~INSERT_ROOT + INSERT INTO fork_network_members (fork_network_id, project_id) + SELECT DISTINCT fork_networks.id, fork_networks.root_project_id + + FROM fork_networks + + INNER JOIN forked_project_links + ON forked_project_links.forked_from_project_id = fork_networks.root_project_id + + WHERE NOT EXISTS ( + SELECT true + FROM fork_network_members + WHERE fork_network_members.project_id = fork_networks.root_project_id + ) + AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} + INSERT_ROOT + + delay = BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + BackgroundMigrationWorker.perform_in(delay, "CreateForkNetworkMembershipsRange", [start_id, end_id]) + end + + def log(message) + Rails.logger.info("#{self.class.name} - #{message}") + end + end + end +end diff --git a/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb new file mode 100644 index 00000000000..1a4ea2bac48 --- /dev/null +++ b/spec/lib/gitlab/background_migration/create_fork_network_memberships_range_spec.rb @@ -0,0 +1,117 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let(:fork_of_fork) { create(:project) } + let(:fork_of_fork2) { create(:project) } + let(:second_level_fork) { create(:project) } + let(:third_level_fork) { create(:project) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + before do + # The fork-network relation created for the forked project + fork_networks.create(id: 1, root_project_id: base1.id) + fork_network_members.create(project_id: base1.id, fork_network_id: 1) + fork_networks.create(id: 2, root_project_id: base2.id) + fork_network_members.create(project_id: base2.id, fork_network_id: 2) + + # Normal fork links + forked_project_links.create(id: 1, forked_from_project_id: base1.id, forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, forked_from_project_id: base1.id, forked_to_project_id: base1_fork2.id) + forked_project_links.create(id: 3, forked_from_project_id: base2.id, forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, forked_from_project_id: base2.id, forked_to_project_id: base2_fork2.id) + + # Fork links + forked_project_links.create(id: 5, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork.id) + forked_project_links.create(id: 6, forked_from_project_id: base1_fork1.id, forked_to_project_id: fork_of_fork2.id) + + # Forks 3 levels down + forked_project_links.create(id: 7, forked_from_project_id: fork_of_fork.id, forked_to_project_id: second_level_fork.id) + forked_project_links.create(id: 8, forked_from_project_id: second_level_fork.id, forked_to_project_id: third_level_fork.id) + + migration.perform(1, 8) + end + + it 'creates a memberships for the direct forks' do + base1_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork1.id) + base1_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1_fork2.id) + base2_fork1_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork1.id) + base2_fork2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2_fork2.id) + + expect(base1_fork1_membership.forked_from_project_id).to eq(base1.id) + expect(base1_fork2_membership.forked_from_project_id).to eq(base1.id) + expect(base2_fork1_membership.forked_from_project_id).to eq(base2.id) + expect(base2_fork2_membership.forked_from_project_id).to eq(base2.id) + end + + it 'adds the fork network members for forks of forks' do + fork_of_fork_membership = fork_network_members.find_by(project_id: fork_of_fork.id, + fork_network_id: fork_network1.id) + fork_of_fork2_membership = fork_network_members.find_by(project_id: fork_of_fork2.id, + fork_network_id: fork_network1.id) + second_level_fork_membership = fork_network_members.find_by(project_id: second_level_fork.id, + fork_network_id: fork_network1.id) + third_level_fork_membership = fork_network_members.find_by(project_id: third_level_fork.id, + fork_network_id: fork_network1.id) + + expect(fork_of_fork_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(fork_of_fork2_membership.forked_from_project_id).to eq(base1_fork1.id) + expect(second_level_fork_membership.forked_from_project_id).to eq(fork_of_fork.id) + expect(third_level_fork_membership.forked_from_project_id).to eq(second_level_fork.id) + end + + it 'reschedules itself when there are missing members' do + allow(migration).to receive(:missing_members?).and_return(true) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(described_class::RESCHEDULE_DELAY, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'can be repeated without effect' do + expect { fork_network_members.count }.not_to change { migration.perform(1, 7) } + end + + it 'knows it is finished for this range' do + expect(migration.missing_members?(1, 7)).to be_falsy + end + + context 'with more forks' do + before do + forked_project_links.create(id: 9, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + forked_project_links.create(id: 10, forked_from_project_id: fork_of_fork.id, forked_to_project_id: create(:project).id) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(10) + + migration.perform(8, 10) + + expect(fork_network_members.count).to eq(12) + end + + it 'knows when not all memberships withing a batch have been created' do + expect(migration.missing_members?(8, 10)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb new file mode 100644 index 00000000000..3ef1873e615 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, schema: 20170929131201 do + let(:migration) { described_class.new } + let(:base1) { create(:project) } + let(:base1_fork1) { create(:project) } + let(:base1_fork2) { create(:project) } + + let(:base2) { create(:project) } + let(:base2_fork1) { create(:project) } + let(:base2_fork2) { create(:project) } + + let!(:forked_project_links) { table(:forked_project_links) } + let!(:fork_networks) { table(:fork_networks) } + let!(:fork_network_members) { table(:fork_network_members) } + + let(:fork_network1) { fork_networks.find_by(root_project_id: base1.id) } + let(:fork_network2) { fork_networks.find_by(root_project_id: base2.id) } + + before do + # A normal fork link + forked_project_links.create(id: 1, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork1.id) + forked_project_links.create(id: 2, + forked_from_project_id: base1.id, + forked_to_project_id: base1_fork2.id) + + forked_project_links.create(id: 3, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork1.id) + forked_project_links.create(id: 4, + forked_from_project_id: base2_fork1.id, + forked_to_project_id: create(:project).id) + + forked_project_links.create(id: 5, + forked_from_project_id: base2.id, + forked_to_project_id: base2_fork2.id) + + migration.perform(1, 3) + end + + it 'it creates the fork network' do + expect(fork_network1).not_to be_nil + expect(fork_network2).not_to be_nil + end + + it 'does not create a fork network for a fork-of-fork' do + # perfrom the entire batch + migration.perform(1, 5) + + expect(fork_networks.find_by(root_project_id: base2_fork1.id)).to be_nil + end + + it 'creates memberships for the root of fork networks' do + base1_membership = fork_network_members.find_by(fork_network_id: fork_network1.id, + project_id: base1.id) + base2_membership = fork_network_members.find_by(fork_network_id: fork_network2.id, + project_id: base2.id) + + expect(base1_membership).not_to be_nil + expect(base2_membership).not_to be_nil + end + + it 'schedules a job for inserting memberships for forks-of-forks' do + delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY + + expect(BackgroundMigrationWorker) + .to receive(:perform_in).with(delay, "CreateForkNetworkMembershipsRange", [1, 3]) + + migration.perform(1, 3) + end + + it 'only processes a single batch of links at a time' do + expect(fork_network_members.count).to eq(5) + + migration.perform(3, 5) + + expect(fork_network_members.count).to eq(7) + end + + it 'can be repeated without effect' do + expect { migration.perform(1, 3) }.not_to change { fork_network_members.count } + end +end -- cgit v1.2.1 From 8160550439d2027c12d5556c8ce1f8afd250628a Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sat, 30 Sep 2017 20:21:08 +0200 Subject: Remove membership from fork network when unlinking --- app/services/projects/unlink_fork_service.rb | 1 + spec/services/projects/unlink_fork_service_spec.rb | 30 ++++++++++++++++------ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb index f30b40423c8..abe414d0c05 100644 --- a/app/services/projects/unlink_fork_service.rb +++ b/app/services/projects/unlink_fork_service.rb @@ -16,6 +16,7 @@ module Projects refresh_forks_count(@project.forked_from_project) @project.forked_project_link.destroy + @project.fork_network_member.destroy end def refresh_forks_count(project) diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 4f1ab697460..50d3a4ec982 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -1,19 +1,22 @@ require 'spec_helper' describe Projects::UnlinkForkService do - subject { described_class.new(fork_project, user) } + include ProjectForksHelper - let(:fork_link) { create(:forked_project_link) } - let(:fork_project) { fork_link.forked_to_project } + subject { described_class.new(forked_project, user) } + + let(:fork_link) { forked_project.forked_project_link } + let(:project) { create(:project, :public) } + let(:forked_project) { fork_project(project, user) } let(:user) { create(:user) } context 'with opened merge request on the source project' do - let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: fork_link.forked_from_project) } - let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } + let(:merge_request) { create(:merge_request, source_project: forked_project, target_project: fork_link.forked_from_project) } + let(:mr_close_service) { MergeRequests::CloseService.new(forked_project, user) } before do allow(MergeRequests::CloseService).to receive(:new) - .with(fork_project, user) + .with(forked_project, user) .and_return(mr_close_service) end @@ -25,13 +28,24 @@ describe Projects::UnlinkForkService do end it 'remove fork relation' do - expect(fork_project.forked_project_link).to receive(:destroy) + expect(forked_project.forked_project_link).to receive(:destroy) + + subject.execute + end + + it 'removes the link to the fork network' do + expect(forked_project.fork_network_member).to be_present + expect(forked_project.fork_network).to be_present subject.execute + forked_project.reload + + expect(forked_project.fork_network_member).to be_nil + expect(forked_project.reload.fork_network).to be_nil end it 'refreshes the forks count cache of the source project' do - source = fork_project.forked_from_project + source = forked_project.forked_from_project expect(source.forks_count).to eq(1) -- cgit v1.2.1 From f90b27da7777b0c72782d2a930f770e2f27757e2 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 3 Oct 2017 17:06:09 +0200 Subject: Find forks within users/namespaces using fork memberships --- app/models/fork_network.rb | 4 ++++ app/models/namespace.rb | 4 +++- app/models/project.rb | 5 +++++ app/models/user.rb | 10 +--------- spec/factories/fork_networks.rb | 5 +++++ spec/models/fork_network_spec.rb | 12 ++++++++++++ spec/models/namespace_spec.rb | 23 +++++++++++++++++++++++ spec/models/user_spec.rb | 17 +++++++++++++++++ 8 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 spec/factories/fork_networks.rb diff --git a/app/models/fork_network.rb b/app/models/fork_network.rb index fd2510d0a4c..218e37a5312 100644 --- a/app/models/fork_network.rb +++ b/app/models/fork_network.rb @@ -8,4 +8,8 @@ class ForkNetwork < ActiveRecord::Base def add_root_as_member projects << root_project end + + def find_forks_in(other_projects) + projects.where(id: other_projects) + end end diff --git a/app/models/namespace.rb b/app/models/namespace.rb index e279d8dd8c5..4672881e220 100644 --- a/app/models/namespace.rb +++ b/app/models/namespace.rb @@ -139,7 +139,9 @@ class Namespace < ActiveRecord::Base end def find_fork_of(project) - projects.joins(:forked_project_link).find_by('forked_project_links.forked_from_project_id = ?', project.id) + return nil unless project.fork_network + + project.fork_network.find_forks_in(projects).first end def lfs_enabled? diff --git a/app/models/project.rb b/app/models/project.rb index 4a883552a8d..ad1c339ae78 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1009,6 +1009,11 @@ class Project < ActiveRecord::Base end def forked? + return true if fork_network && fork_network.root_project != self + + # TODO: Use only the above conditional using the `fork_network` + # This is the old conditional that looks at the `forked_project_link`, we + # fall back to this while we're migrating the new models !(forked_project_link.nil? || forked_project_link.forked_from_project.nil?) end diff --git a/app/models/user.rb b/app/models/user.rb index 959738ba608..c3f115ca074 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -697,15 +697,7 @@ class User < ActiveRecord::Base end def fork_of(project) - links = ForkedProjectLink.where( - forked_from_project_id: project, - forked_to_project_id: personal_projects.unscope(:order) - ) - if links.any? - links.first.forked_to_project - else - nil - end + namespace.find_fork_of(project) end def ldap_user? diff --git a/spec/factories/fork_networks.rb b/spec/factories/fork_networks.rb new file mode 100644 index 00000000000..f42d36f3d19 --- /dev/null +++ b/spec/factories/fork_networks.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :fork_network do + association :root_project, factory: :project + end +end diff --git a/spec/models/fork_network_spec.rb b/spec/models/fork_network_spec.rb index 4781a959846..605ccd6db06 100644 --- a/spec/models/fork_network_spec.rb +++ b/spec/models/fork_network_spec.rb @@ -12,6 +12,18 @@ describe ForkNetwork do end end + describe '#find_fork_in' do + it 'finds all fork of the current network in al collection' do + network = create(:fork_network) + root_project = network.root_project + another_project = fork_project(root_project) + create(:project) + + expect(network.find_forks_in(Project.all)) + .to contain_exactly(another_project, root_project) + end + end + context 'for a deleted project' do it 'keeps the fork network' do project = create(:project, :public) diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 3ea614776ca..2ebf6acd42a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Namespace do + include ProjectForksHelper + let!(:namespace) { create(:namespace) } describe 'associations' do @@ -520,4 +522,25 @@ describe Namespace do end end end + + describe '#has_forks_of?' do + let(:project) { create(:project, :public) } + let!(:forked_project) { fork_project(project, namespace.owner, namespace: namespace) } + + before do + # Reset the fork network relation + project.reload + end + + it 'knows if there is a direct fork in the namespace' do + expect(namespace.find_fork_of(project)).to eq(forked_project) + end + + it 'knows when there is as fork-of-fork in the namespace' do + other_namespace = create(:namespace) + other_fork = fork_project(forked_project, other_namespace.owner, namespace: other_namespace) + + expect(other_namespace.find_fork_of(project)).to eq(other_fork) + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 52ca068f9a4..ece6968dde6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1456,6 +1456,23 @@ describe User do end end + describe '#fork_of' do + let(:user) { create(:user) } + + it "returns a user's fork of a project" do + project = create(:project, :public) + user_fork = fork_project(project, user, namespace: user.namespace) + + expect(user.fork_of(project)).to eq(user_fork) + end + + it 'returns nil if the project does not have a fork network' do + project = create(:project) + + expect(user.fork_of(project)).to be_nil + end + end + describe '#can_be_removed?' do subject { create(:user) } -- cgit v1.2.1 From 14a6cebc9feefc45b587bf9b9b706194b9b5bff9 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 3 Oct 2017 18:27:51 +0200 Subject: Store the name of a project that's a root of a fork network So we can keep showing it to a user in his project page. --- app/services/projects/destroy_service.rb | 7 +++++++ spec/services/projects/destroy_service_spec.rb | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb index 54eb75ab9bf..19d75ff2efa 100644 --- a/app/services/projects/destroy_service.rb +++ b/app/services/projects/destroy_service.rb @@ -22,6 +22,13 @@ module Projects Projects::UnlinkForkService.new(project, current_user).execute + # The project is not necessarily a fork, so update the fork network originating + # from this project + if fork_network = project.root_of_fork_network + fork_network.update(root_project: nil, + deleted_root_project_name: project.full_name) + end + attempt_destroy_transaction(project) system_hook_service.execute_hooks_for(project, :destroy) diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index c867139d1de..c90bad46295 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -212,6 +212,19 @@ describe Projects::DestroyService do end end + context 'as the root of a fork network' do + let!(:fork_network) { create(:fork_network, root_project: project) } + + it 'updates the fork network with the project name' do + destroy_project(project, user) + + fork_network.reload + + expect(fork_network.deleted_root_project_name).to eq(project.full_name) + expect(fork_network.root_project).to be_nil + end + end + def destroy_project(project, user, params = {}) if async Projects::DestroyService.new(project, user, params).async_execute -- cgit v1.2.1 From 178f4e1e16b41f8e5992f484fdd6465090c55a08 Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Tue, 3 Oct 2017 19:41:37 +0200 Subject: Show fork information on the project panel --- app/views/projects/_home_panel.html.haml | 14 ++++-- locale/gitlab.pot | 78 +++++++++++++++++++++++++++++++- spec/features/projects_spec.rb | 49 ++++++++++++++++++++ 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml index 873b3045ea9..619b632918e 100644 --- a/app/views/projects/_home_panel.html.haml +++ b/app/views/projects/_home_panel.html.haml @@ -1,4 +1,6 @@ - empty_repo = @project.empty_repo? +- fork_network = @project.fork_network +- forked_from_project = @project.forked_from_project || fork_network&.root_project .project-home-panel.text-center{ class: ("empty-project" if empty_repo) } .limit-container-width{ class: container_class } .avatar-container.s70.project-avatar @@ -12,11 +14,15 @@ - if @project.description.present? = markdown_field(@project, :description) - - if forked_from_project = @project.forked_from_project + - if @project.forked? %p - #{ s_('ForkedFromProjectPath|Forked from') } - = link_to project_path(forked_from_project) do - = forked_from_project.namespace.try(:name) + - if forked_from_project + #{ s_('ForkedFromProjectPath|Forked from') } + = link_to project_path(forked_from_project) do + = forked_from_project.full_name + - else + - deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)') + = deleted_message % { project_name: fork_network.deleted_root_project_name } .project-repo-buttons .count-buttons diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7569af9d175..c73e582e608 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -8,8 +8,8 @@ msgid "" msgstr "" "Project-Id-Version: gitlab 1.0.0\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2017-10-04 23:47+0100\n" -"PO-Revision-Date: 2017-10-04 23:47+0100\n" +"POT-Creation-Date: 2017-10-06 18:33+0200\n" +"PO-Revision-Date: 2017-10-06 18:33+0200\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" @@ -23,6 +23,11 @@ msgid_plural "%d commits" msgstr[0] "" msgstr[1] "" +msgid "%d layer" +msgid_plural "%d layers" +msgstr[0] "" +msgstr[1] "" + msgid "%s additional commit has been omitted to prevent performance issues." msgid_plural "%s additional commits have been omitted to prevent performance issues." msgstr[0] "" @@ -59,6 +64,9 @@ msgstr[1] "" msgid "1st contribution!" msgstr "" +msgid "2FA enabled" +msgstr "" + msgid "A collection of graphs regarding Continuous Integration" msgstr "" @@ -528,6 +536,51 @@ msgstr "" msgid "Compare" msgstr "" +msgid "Container Registry" +msgstr "" + +msgid "ContainerRegistry|Created" +msgstr "" + +msgid "ContainerRegistry|First log in to GitLab’s Container Registry using your GitLab username and password. If you have %{link_2fa} you need to use a %{link_token}:" +msgstr "" + +msgid "ContainerRegistry|GitLab supports up to 3 levels of image names. The following examples of images are valid for your project:" +msgstr "" + +msgid "ContainerRegistry|How to use the Container Registry" +msgstr "" + +msgid "ContainerRegistry|Learn more about" +msgstr "" + +msgid "ContainerRegistry|No tags in Container Registry for this container image." +msgstr "" + +msgid "ContainerRegistry|Once you log in, you’re free to create and upload a container image using the common %{build} and %{push} commands" +msgstr "" + +msgid "ContainerRegistry|Remove repository" +msgstr "" + +msgid "ContainerRegistry|Remove tag" +msgstr "" + +msgid "ContainerRegistry|Size" +msgstr "" + +msgid "ContainerRegistry|Tag" +msgstr "" + +msgid "ContainerRegistry|Tag ID" +msgstr "" + +msgid "ContainerRegistry|Use different image names" +msgstr "" + +msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images." +msgstr "" + msgid "Contribution guide" msgstr "" @@ -739,6 +792,9 @@ msgstr[1] "" msgid "ForkedFromProjectPath|Forked from" msgstr "" +msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)" +msgstr "" + msgid "Format" msgstr "" @@ -949,6 +1005,9 @@ msgstr "" msgid "New tag" msgstr "" +msgid "No container images stored for this project. Add one by following the instructions above." +msgstr "" + msgid "No repository" msgstr "" @@ -1024,6 +1083,9 @@ msgstr "" msgid "OpenedNDaysAgo|Opened" msgstr "" +msgid "Opens in a new window" +msgstr "" + msgid "Options" msgstr "" @@ -1350,6 +1412,15 @@ msgstr[1] "" msgid "Snippets" msgstr "" +msgid "Something went wrong on our end." +msgstr "" + +msgid "Something went wrong while fetching the projects." +msgstr "" + +msgid "Something went wrong while fetching the registry list." +msgstr "" + msgid "SortOptions|Access level, ascending" msgstr "" @@ -1905,3 +1976,6 @@ msgid "parent" msgid_plural "parents" msgstr[0] "" msgstr[1] "" + +msgid "personal access token" +msgstr "" diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index b957575a244..4b2c54d54b5 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -77,6 +77,55 @@ feature 'Project' do end end + describe 'showing information about source of a project fork' do + let(:user) { create(:user) } + let(:base_project) { create(:project, :public, :repository) } + let(:forked_project) { fork_project(base_project, user, repository: true) } + + before do + sign_in user + end + + it 'shows a link to the source project when it is available' do + visit project_path(forked_project) + + expect(page).to have_content('Forked from') + expect(page).to have_link(base_project.full_name) + end + + it 'does not contain fork network information for the root project' do + forked_project + + visit project_path(base_project) + + expect(page).not_to have_content('In fork network of') + expect(page).not_to have_content('Forked from') + end + + it 'shows the name of the deleted project when the source was deleted' do + forked_project + Projects::DestroyService.new(base_project, base_project.owner).execute + + visit project_path(forked_project) + + expect(page).to have_content("Forked from #{base_project.full_name} (deleted)") + end + + context 'a fork of a fork' do + let(:fork_of_fork) { fork_project(forked_project, user, repository: true) } + + it 'links to the base project if the source project is removed' do + fork_of_fork + Projects::DestroyService.new(forked_project, user).execute + + visit project_path(fork_of_fork) + + expect(page).to have_content("Forked from") + expect(page).to have_link(base_project.full_name) + end + end + end + describe 'removal', js: true do let(:user) { create(:user, username: 'test', name: 'test') } let(:project) { create(:project, namespace: user.namespace, name: 'project1') } -- cgit v1.2.1 From fc51bde939a24347e21b765b8c0e4238e2ffc67d Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Fri, 6 Oct 2017 12:36:34 +0200 Subject: Add a spec for editing a file when a project was already forked --- spec/features/projects/user_edits_files_spec.rb | 32 ++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 19954313c23..2798041fa0c 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'User edits files' do + include ProjectForksHelper let(:project) { create(:project, :repository, name: 'Shop') } let(:project2) { create(:project, :repository, name: 'Another Project', path: 'another-project') } let(:project_tree_path_root_ref) { project_tree_path(project, project.repository.root_ref) } @@ -122,7 +123,7 @@ describe 'User edits files' do fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Commit changes') - fork = user.fork_of(project2) + fork = user.fork_of(project2.reload) expect(current_path).to eq(project_new_merge_request_path(fork)) @@ -130,5 +131,34 @@ describe 'User edits files' do expect(page).to have_content('New commit message') end + + context 'when the user already had a fork of the project', :js do + let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } + before do + visit(project2_tree_path_root_ref) + end + + it 'links to the forked project for editing' do + click_link('.gitignore') + find('.js-edit-blob').click + + expect(page).not_to have_link('Fork') + expect(page).not_to have_button('Cancel') + + execute_script("ace.edit('editor').setValue('*.rbca')") + fill_in(:commit_message, with: 'Another commit', visible: true) + click_button('Commit changes') + + fork = user.fork_of(project2) + + expect(current_path).to eq(project_new_merge_request_path(fork)) + + wait_for_requests + + expect(page).to have_content('Another commit') + expect(page).to have_content("From #{forked_project.full_path}") + expect(page).to have_content("into #{project2.full_path}") + end + end end end -- cgit v1.2.1 From 0ce6785851510ccb49f0d1edc0220aca46f815f5 Mon Sep 17 00:00:00 2001 From: Jacopo Date: Tue, 3 Oct 2017 10:35:01 +0200 Subject: Replaces `tag: true` into `:tag` in the specs Replaces all the explicit include metadata syntax in the specs (tag: true) into the implicit one (:tag). Added a cop to prevent future errors and handle autocorrection. --- .../unreleased/37552-replace-js-true-with-js.yml | 5 ++ rubocop/cop/rspec/verbose_include_metadata.rb | 74 ++++++++++++++++++++++ rubocop/rubocop.rb | 1 + spec/controllers/projects_controller_spec.rb | 2 +- spec/features/admin/admin_abuse_reports_spec.rb | 2 +- .../admin/admin_broadcast_messages_spec.rb | 2 +- .../admin/admin_disables_two_factor_spec.rb | 2 +- spec/features/admin/admin_groups_spec.rb | 10 +-- spec/features/admin/admin_health_check_spec.rb | 2 +- spec/features/admin/admin_hooks_spec.rb | 2 +- spec/features/admin/admin_labels_spec.rb | 2 +- spec/features/admin/admin_projects_spec.rb | 8 +-- .../admin/admin_users_impersonation_tokens_spec.rb | 2 +- spec/features/admin/admin_users_spec.rb | 4 +- .../admin/admin_uses_repository_checks_spec.rb | 2 +- spec/features/auto_deploy_spec.rb | 4 +- spec/features/boards/boards_spec.rb | 2 +- spec/features/boards/keyboard_shortcut_spec.rb | 2 +- spec/features/boards/new_issue_spec.rb | 2 +- spec/features/boards/sidebar_spec.rb | 2 +- spec/features/ci_lint_spec.rb | 2 +- spec/features/container_registry_spec.rb | 2 +- spec/features/copy_as_gfm_spec.rb | 2 +- spec/features/cycle_analytics_spec.rb | 2 +- spec/features/dashboard/active_tab_spec.rb | 2 +- .../dashboard/datetime_on_tooltips_spec.rb | 2 +- spec/features/dashboard/group_spec.rb | 2 +- spec/features/dashboard/issues_spec.rb | 8 +-- spec/features/dashboard/label_filter_spec.rb | 2 +- spec/features/dashboard/merge_requests_spec.rb | 6 +- .../project_member_activity_index_spec.rb | 2 +- spec/features/dashboard/projects_spec.rb | 2 +- .../dashboard/todos/todos_filtering_spec.rb | 2 +- spec/features/dashboard/todos/todos_spec.rb | 8 +-- spec/features/expand_collapse_diffs_spec.rb | 2 +- spec/features/gitlab_flavored_markdown_spec.rb | 2 +- spec/features/group_variables_spec.rb | 2 +- spec/features/groups/labels/subscription_spec.rb | 2 +- spec/features/groups_spec.rb | 6 +- spec/features/issuables/default_sort_order_spec.rb | 18 +++--- spec/features/issuables/user_sees_sidebar_spec.rb | 4 +- spec/features/issues/award_emoji_spec.rb | 18 +++--- spec/features/issues/award_spec.rb | 2 +- .../features/issues/bulk_assignment_labels_spec.rb | 2 +- ..._issue_for_discussions_in_merge_request_spec.rb | 2 +- ..._for_single_discussion_in_merge_request_spec.rb | 2 +- .../issues/filtered_search/dropdown_author_spec.rb | 2 +- .../issues/filtered_search/dropdown_emoji_spec.rb | 2 +- .../issues/filtered_search/dropdown_label_spec.rb | 2 +- .../issues/filtered_search/filter_issues_spec.rb | 2 +- .../issues/filtered_search/recent_searches_spec.rb | 2 +- .../issues/filtered_search/search_bar_spec.rb | 2 +- .../issues/filtered_search/visual_tokens_spec.rb | 2 +- spec/features/issues/gfm_autocomplete_spec.rb | 2 +- spec/features/issues/issue_sidebar_spec.rb | 8 +-- spec/features/issues/markdown_toolbar_spec.rb | 2 +- spec/features/issues/move_spec.rb | 6 +- spec/features/issues/spam_issues_spec.rb | 2 +- spec/features/issues/todo_spec.rb | 2 +- .../issues/user_uses_slash_commands_spec.rb | 2 +- spec/features/issues_spec.rb | 28 ++++---- spec/features/login_spec.rb | 8 +-- spec/features/merge_requests/assign_issues_spec.rb | 2 +- spec/features/merge_requests/award_spec.rb | 2 +- ...f_mergeable_with_unresolved_discussions_spec.rb | 2 +- spec/features/merge_requests/cherry_pick_spec.rb | 2 +- spec/features/merge_requests/closes_issues_spec.rb | 2 +- spec/features/merge_requests/conflicts_spec.rb | 2 +- spec/features/merge_requests/create_new_mr_spec.rb | 2 +- .../merge_requests/created_from_fork_spec.rb | 6 +- .../merge_requests/deleted_source_branch_spec.rb | 2 +- .../merge_requests/diff_notes_avatars_spec.rb | 2 +- .../merge_requests/diff_notes_resolve_spec.rb | 2 +- spec/features/merge_requests/diffs_spec.rb | 2 +- spec/features/merge_requests/edit_mr_spec.rb | 4 +- .../merge_requests/filter_by_milestone_spec.rb | 8 +-- .../merge_requests/filter_merge_requests_spec.rb | 16 ++--- spec/features/merge_requests/image_diff_notes.rb | 2 +- .../merge_commit_message_toggle_spec.rb | 2 +- .../only_allow_merge_if_build_succeeds_spec.rb | 6 +- spec/features/merge_requests/pipelines_spec.rb | 2 +- .../resolve_outdated_diff_discussions.rb | 2 +- spec/features/merge_requests/target_branch_spec.rb | 2 +- .../toggle_whitespace_changes_spec.rb | 2 +- .../merge_requests/toggler_behavior_spec.rb | 2 +- .../merge_requests/update_merge_requests_spec.rb | 6 +- .../user_uses_slash_commands_spec.rb | 2 +- spec/features/merge_requests/versions_spec.rb | 2 +- .../merge_requests/widget_deployments_spec.rb | 2 +- spec/features/merge_requests/widget_spec.rb | 2 +- spec/features/profiles/keys_spec.rb | 2 +- spec/features/profiles/oauth_applications_spec.rb | 2 +- .../profiles/personal_access_tokens_spec.rb | 2 +- .../user_changes_notified_of_own_activity_spec.rb | 2 +- .../profiles/user_visits_notifications_tab_spec.rb | 2 +- spec/features/projects/badges/list_spec.rb | 2 +- .../blobs/blob_line_permalink_updater_spec.rb | 2 +- spec/features/projects/blobs/edit_spec.rb | 2 +- .../features/projects/blobs/shortcuts_blob_spec.rb | 2 +- spec/features/projects/branches_spec.rb | 4 +- spec/features/projects/commit/builds_spec.rb | 2 +- spec/features/projects/commit/cherry_pick_spec.rb | 2 +- spec/features/projects/compare_spec.rb | 2 +- ...eloper_views_empty_project_instructions_spec.rb | 4 +- spec/features/projects/edit_spec.rb | 2 +- .../projects/environments/environments_spec.rb | 2 +- spec/features/projects/features_visibility_spec.rb | 4 +- spec/features/projects/files/browse_files_spec.rb | 2 +- .../projects/files/dockerfile_dropdown_spec.rb | 2 +- .../projects/files/edit_file_soft_wrap_spec.rb | 2 +- .../projects/files/find_file_keyboard_spec.rb | 2 +- .../projects/files/gitignore_dropdown_spec.rb | 2 +- .../projects/files/gitlab_ci_yml_dropdown_spec.rb | 2 +- .../project_owner_creates_license_file_spec.rb | 2 +- ...to_create_license_file_in_empty_project_spec.rb | 2 +- .../projects/files/template_type_dropdown_spec.rb | 2 +- spec/features/projects/files/undo_template_spec.rb | 2 +- .../projects/gfm_autocomplete_load_spec.rb | 2 +- .../projects/import_export/export_file_spec.rb | 2 +- .../projects/import_export/import_file_spec.rb | 2 +- .../import_export/namespace_export_file_spec.rb | 2 +- spec/features/projects/issuable_templates_spec.rb | 2 +- spec/features/projects/jobs_spec.rb | 2 +- spec/features/projects/labels/subscription_spec.rb | 2 +- .../projects/labels/update_prioritization_spec.rb | 10 +-- ...uester_cannot_request_access_to_project_spec.rb | 2 +- .../members/groups_with_access_list_spec.rb | 2 +- ...master_adds_member_with_expiration_date_spec.rb | 2 +- spec/features/projects/pipelines/pipelines_spec.rb | 4 +- spec/features/projects/project_settings_spec.rb | 4 +- spec/features/projects/ref_switcher_spec.rb | 2 +- .../projects/settings/integration_settings_spec.rb | 2 +- .../projects/settings/pipelines_settings_spec.rb | 2 +- .../projects/settings/repository_settings_spec.rb | 2 +- .../projects/settings/visibility_settings_spec.rb | 2 +- spec/features/projects/show_project_spec.rb | 2 +- spec/features/projects/user_browses_files_spec.rb | 8 +-- .../projects/user_creates_directory_spec.rb | 2 +- spec/features/projects/user_creates_files_spec.rb | 10 +-- .../features/projects/user_creates_project_spec.rb | 2 +- spec/features/projects/user_deletes_files_spec.rb | 4 +- spec/features/projects/user_edits_files_spec.rb | 12 ++-- .../projects/user_interacts_with_stars_spec.rb | 2 +- spec/features/projects/user_replaces_files_spec.rb | 4 +- spec/features/projects/user_uploads_files_spec.rb | 4 +- spec/features/projects/view_on_env_spec.rb | 2 +- .../projects/wiki/markdown_preview_spec.rb | 2 +- spec/features/projects_spec.rb | 4 +- spec/features/protected_tags_spec.rb | 2 +- spec/features/snippets/internal_snippet_spec.rb | 2 +- spec/features/tags/master_creates_tag_spec.rb | 2 +- spec/features/tags/master_deletes_tag_spec.rb | 6 +- spec/features/task_lists_spec.rb | 8 +-- spec/features/triggers_spec.rb | 2 +- .../uploads/user_uploads_file_to_note_spec.rb | 16 ++--- spec/features/users/snippets_spec.rb | 2 +- spec/features/users_spec.rb | 2 +- spec/features/variables_spec.rb | 2 +- spec/helpers/projects_helper_spec.rb | 2 +- .../migrate_system_uploads_to_new_folder_spec.rb | 2 +- spec/lib/gitlab/checks/force_push_spec.rb | 2 +- .../v1/rename_base_spec.rb | 4 +- .../v1/rename_namespaces_spec.rb | 2 +- .../v1/rename_projects_spec.rb | 2 +- spec/lib/gitlab/git/blame_spec.rb | 2 +- spec/lib/gitlab/git/blob_spec.rb | 4 +- spec/lib/gitlab/git/commit_spec.rb | 6 +- spec/lib/gitlab/git/repository_spec.rb | 24 +++---- spec/lib/gitlab/git/tag_spec.rb | 2 +- spec/lib/gitlab/git_access_spec.rb | 4 +- spec/lib/gitlab/gitaly_client/ref_service_spec.rb | 4 +- .../gitlab/health_checks/fs_shards_check_spec.rb | 2 +- spec/lib/gitlab/shell_spec.rb | 4 +- spec/lib/gitlab/workhorse_spec.rb | 4 +- .../update_upload_paths_to_system_spec.rb | 4 +- spec/models/member_spec.rb | 4 +- spec/models/project_group_link_spec.rb | 2 +- spec/models/repository_spec.rb | 32 +++++----- spec/models/user_spec.rb | 4 +- .../cop/rspec/verbose_include_metadata_spec.rb | 53 ++++++++++++++++ .../issuable_slash_commands_shared_examples.rb | 6 +- spec/workers/git_garbage_collect_worker_spec.rb | 2 +- 182 files changed, 461 insertions(+), 328 deletions(-) create mode 100644 changelogs/unreleased/37552-replace-js-true-with-js.yml create mode 100644 rubocop/cop/rspec/verbose_include_metadata.rb create mode 100644 spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb diff --git a/changelogs/unreleased/37552-replace-js-true-with-js.yml b/changelogs/unreleased/37552-replace-js-true-with-js.yml new file mode 100644 index 00000000000..f7b614a8839 --- /dev/null +++ b/changelogs/unreleased/37552-replace-js-true-with-js.yml @@ -0,0 +1,5 @@ +--- +title: 'Replace `tag: true` into `:tag` in the specs' +merge_request: 14653 +author: Jacopo Beschi @jacopo-beschi +type: added diff --git a/rubocop/cop/rspec/verbose_include_metadata.rb b/rubocop/cop/rspec/verbose_include_metadata.rb new file mode 100644 index 00000000000..58390622d60 --- /dev/null +++ b/rubocop/cop/rspec/verbose_include_metadata.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'rubocop-rspec' + +module RuboCop + module Cop + module RSpec + # Checks for verbose include metadata used in the specs. + # + # @example + # # bad + # describe MyClass, js: true do + # end + # + # # good + # describe MyClass, :js do + # end + class VerboseIncludeMetadata < Cop + MSG = 'Use `%s` instead of `%s`.' + + SELECTORS = %i[describe context feature example_group it specify example scenario its].freeze + + def_node_matcher :include_metadata, <<-PATTERN + (send {(const nil :RSpec) nil} {#{SELECTORS.map(&:inspect).join(' ')}} + !const + ... + (hash $...)) + PATTERN + + def_node_matcher :invalid_metadata?, <<-PATTERN + (pair + (sym $...) + (true)) + PATTERN + + def on_send(node) + invalid_metadata_matches(node) do |match| + add_offense(node, :expression, format(MSG, good(match), bad(match))) + end + end + + def autocorrect(node) + lambda do |corrector| + invalid_metadata_matches(node) do |match| + corrector.replace(match.loc.expression, good(match)) + end + end + end + + private + + def invalid_metadata_matches(node) + include_metadata(node) do |matches| + matches.select(&method(:invalid_metadata?)).each do |match| + yield match + end + end + end + + def bad(match) + "#{metadata_key(match)}: true" + end + + def good(match) + ":#{metadata_key(match)}" + end + + def metadata_key(match) + match.children[0].source + end + end + end + end +end diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb index 1b6e8991a17..1df23899efb 100644 --- a/rubocop/rubocop.rb +++ b/rubocop/rubocop.rb @@ -21,3 +21,4 @@ require_relative 'cop/migration/reversible_add_column_with_default' require_relative 'cop/migration/timestamps' require_relative 'cop/migration/update_column_in_batches' require_relative 'cop/rspec/single_line_hook' +require_relative 'cop/rspec/verbose_include_metadata' diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index d7148e888ab..0544afe31ed 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -141,7 +141,7 @@ describe ProjectsController do end end - context 'when the storage is not available', broken_storage: true do + context 'when the storage is not available', :broken_storage do set(:project) { create(:project, :broken_storage) } before do diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb index 2144f6ba635..766cd4b0090 100644 --- a/spec/features/admin/admin_abuse_reports_spec.rb +++ b/spec/features/admin/admin_abuse_reports_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Admin::AbuseReports", js: true do +describe "Admin::AbuseReports", :js do let(:user) { create(:user) } context 'as an admin' do diff --git a/spec/features/admin/admin_broadcast_messages_spec.rb b/spec/features/admin/admin_broadcast_messages_spec.rb index cbccf370514..9cb351282a0 100644 --- a/spec/features/admin/admin_broadcast_messages_spec.rb +++ b/spec/features/admin/admin_broadcast_messages_spec.rb @@ -40,7 +40,7 @@ feature 'Admin Broadcast Messages' do expect(page).not_to have_content 'Migration to new server' end - scenario 'Live preview a customized broadcast message', js: true do + scenario 'Live preview a customized broadcast message', :js do fill_in 'broadcast_message_message', with: "Live **Markdown** previews. :tada:" page.within('.broadcast-message-preview') do diff --git a/spec/features/admin/admin_disables_two_factor_spec.rb b/spec/features/admin/admin_disables_two_factor_spec.rb index e214ae6b78d..6a97378391b 100644 --- a/spec/features/admin/admin_disables_two_factor_spec.rb +++ b/spec/features/admin/admin_disables_two_factor_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' feature 'Admin disables 2FA for a user' do - scenario 'successfully', js: true do + scenario 'successfully', :js do sign_in(create(:admin)) user = create(:user, :two_factor) diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 3768727d8ae..771fb5253da 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -52,7 +52,7 @@ feature 'Admin Groups' do expect_selected_visibility(internal) end - scenario 'when entered in group path, it auto filled the group name', js: true do + scenario 'when entered in group path, it auto filled the group name', :js do visit admin_groups_path click_link "New group" group_path = 'gitlab' @@ -81,7 +81,7 @@ feature 'Admin Groups' do expect_selected_visibility(group.visibility_level) end - scenario 'edit group path does not change group name', js: true do + scenario 'edit group path does not change group name', :js do group = create(:group, :private) visit admin_group_edit_path(group) @@ -93,7 +93,7 @@ feature 'Admin Groups' do end end - describe 'add user into a group', js: true do + describe 'add user into a group', :js do shared_context 'adds user into a group' do it do visit admin_group_path(group) @@ -124,7 +124,7 @@ feature 'Admin Groups' do group.add_user(:user, Gitlab::Access::OWNER) end - it 'adds admin a to a group as developer', js: true do + it 'adds admin a to a group as developer', :js do visit group_group_members_path(group) page.within '.users-group-form' do @@ -141,7 +141,7 @@ feature 'Admin Groups' do end end - describe 'admin remove himself from a group', js: true do + describe 'admin remove himself from a group', :js do it 'removes admin from the group' do group.add_user(current_user, Gitlab::Access::DEVELOPER) diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 37fd3e171eb..09e6965849a 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "Admin Health Check", feature: true, broken_storage: true do +feature "Admin Health Check", :feature, :broken_storage do include StubENV before do diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb index 91f08dbad5d..2e65fcc5231 100644 --- a/spec/features/admin/admin_hooks_spec.rb +++ b/spec/features/admin/admin_hooks_spec.rb @@ -74,7 +74,7 @@ describe 'Admin::Hooks', :js do end end - describe 'Test', js: true do + describe 'Test', :js do before do WebMock.stub_request(:post, @system_hook.url) visit admin_hooks_path diff --git a/spec/features/admin/admin_labels_spec.rb b/spec/features/admin/admin_labels_spec.rb index ae9b47299e6..a5834056a1d 100644 --- a/spec/features/admin/admin_labels_spec.rb +++ b/spec/features/admin/admin_labels_spec.rb @@ -30,7 +30,7 @@ RSpec.describe 'admin issues labels' do end end - it 'deletes all labels', js: true do + it 'deletes all labels', :js do page.within '.labels' do page.all('.btn-remove').each do |remove| remove.click diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index f4f2505d436..94b12105088 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -28,7 +28,7 @@ describe "Admin::Projects" do expect(page).not_to have_content(archived_project.name) end - it 'renders all projects', js: true do + it 'renders all projects', :js do find(:css, '#sort-projects-dropdown').click click_link 'Show archived projects' @@ -37,7 +37,7 @@ describe "Admin::Projects" do expect(page).to have_xpath("//span[@class='label label-warning']", text: 'archived') end - it 'renders only archived projects', js: true do + it 'renders only archived projects', :js do find(:css, '#sort-projects-dropdown').click click_link 'Show archived projects only' @@ -74,7 +74,7 @@ describe "Admin::Projects" do .to receive(:move_uploads_to_new_namespace).and_return(true) end - it 'transfers project to group web', js: true do + it 'transfers project to group web', :js do visit admin_project_path(project) click_button 'Search for Namespace' @@ -91,7 +91,7 @@ describe "Admin::Projects" do project.team << [user, :master] end - it 'adds admin a to a project as developer', js: true do + it 'adds admin a to a project as developer', :js do visit project_project_members_path(project) page.within '.users-project-form' do diff --git a/spec/features/admin/admin_users_impersonation_tokens_spec.rb b/spec/features/admin/admin_users_impersonation_tokens_spec.rb index 034682dae27..388d30828a7 100644 --- a/spec/features/admin/admin_users_impersonation_tokens_spec.rb +++ b/spec/features/admin/admin_users_impersonation_tokens_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Admin > Users > Impersonation Tokens', js: true do +describe 'Admin > Users > Impersonation Tokens', :js do let(:admin) { create(:admin) } let!(:user) { create(:user) } diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb index e2e2b13cf8a..f9f4bd6f5b9 100644 --- a/spec/features/admin/admin_users_spec.rb +++ b/spec/features/admin/admin_users_spec.rb @@ -288,7 +288,7 @@ describe "Admin::Users" do end end - it 'allows group membership to be revoked', js: true do + it 'allows group membership to be revoked', :js do page.within(first('.group_member')) do find('.btn-remove').click end @@ -309,7 +309,7 @@ describe "Admin::Users" do end end - describe 'remove users secondary email', js: true do + describe 'remove users secondary email', :js do let!(:secondary_email) do create :email, email: 'secondary@example.com', user: user end diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index c2b7543a690..42f5b5eb8dc 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -32,7 +32,7 @@ feature 'Admin uses repository checks' do end end - scenario 'to clear all repository checks', js: true do + scenario 'to clear all repository checks', :js do visit admin_application_settings_path expect(RepositoryCheck::ClearWorker).to receive(:perform_async) diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb index dff6f96b663..4a7c3e4f1ab 100644 --- a/spec/features/auto_deploy_spec.rb +++ b/spec/features/auto_deploy_spec.rb @@ -31,7 +31,7 @@ describe 'Auto deploy' do expect(page).to have_link('Set up auto deploy') end - it 'includes OpenShift as an available template', js: true do + it 'includes OpenShift as an available template', :js do click_link 'Set up auto deploy' click_button 'Apply a GitLab CI Yaml template' @@ -40,7 +40,7 @@ describe 'Auto deploy' do end end - it 'creates a merge request using "auto-deploy" branch', js: true do + it 'creates a merge request using "auto-deploy" branch', :js do click_link 'Set up auto deploy' click_button 'Apply a GitLab CI Yaml template' within '.gitlab-ci-yml-selector' do diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 33aca6cb527..91c4e5037de 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Issue Boards', js: true do +describe 'Issue Boards', :js do include DragTo let(:group) { create(:group, :nested) } diff --git a/spec/features/boards/keyboard_shortcut_spec.rb b/spec/features/boards/keyboard_shortcut_spec.rb index 61b53aa5d1e..435de3861cf 100644 --- a/spec/features/boards/keyboard_shortcut_spec.rb +++ b/spec/features/boards/keyboard_shortcut_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Issue Boards shortcut', js: true do +describe 'Issue Boards shortcut', :js do let(:project) { create(:project) } before do diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index f67372337ec..5ac4d87e90b 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Issue Boards new issue', js: true do +describe 'Issue Boards new issue', :js do let(:project) { create(:project, :public) } let(:board) { create(:board, project: project) } let!(:list) { create(:list, board: board, position: 0) } diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index c3bf50ef9d1..c4817eb947b 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Issue Boards', js: true do +describe 'Issue Boards', :js do let(:user) { create(:user) } let(:user2) { create(:user) } let(:project) { create(:project, :public) } diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index af4cc00162a..9accd7bb07c 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'CI Lint', js: true do +describe 'CI Lint', :js do before do sign_in(create(:user)) end diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb index 45213dc6995..d5e9de20e59 100644 --- a/spec/features/container_registry_spec.rb +++ b/spec/features/container_registry_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe "Container Registry", js: true do +describe "Container Registry", :js do let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb index ebcd0ba0dcd..c6ba1211b9e 100644 --- a/spec/features/copy_as_gfm_spec.rb +++ b/spec/features/copy_as_gfm_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Copy as GFM', js: true do +describe 'Copy as GFM', :js do include MarkupHelper include RepoHelpers include ActionView::Helpers::JavaScriptHelper diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb index bfe9dac3bd4..177cd50dd72 100644 --- a/spec/features/cycle_analytics_spec.rb +++ b/spec/features/cycle_analytics_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Cycle Analytics', js: true do +feature 'Cycle Analytics', :js do let(:user) { create(:user) } let(:guest) { create(:user) } let(:project) { create(:project, :repository) } diff --git a/spec/features/dashboard/active_tab_spec.rb b/spec/features/dashboard/active_tab_spec.rb index 08d8cc7922b..8bab501134b 100644 --- a/spec/features/dashboard/active_tab_spec.rb +++ b/spec/features/dashboard/active_tab_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -RSpec.describe 'Dashboard Active Tab', js: true do +RSpec.describe 'Dashboard Active Tab', :js do before do sign_in(create(:user)) end diff --git a/spec/features/dashboard/datetime_on_tooltips_spec.rb b/spec/features/dashboard/datetime_on_tooltips_spec.rb index b6dce1b8ec4..349f9a47112 100644 --- a/spec/features/dashboard/datetime_on_tooltips_spec.rb +++ b/spec/features/dashboard/datetime_on_tooltips_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Tooltips on .timeago dates', js: true do +feature 'Tooltips on .timeago dates', :js do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:created_date) { Date.yesterday.to_time } diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb index 60a16830cdc..970173c7aaf 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -5,7 +5,7 @@ RSpec.describe 'Dashboard Group' do sign_in(create(:user)) end - it 'creates new group', js: true do + it 'creates new group', :js do visit dashboard_groups_path find('.btn-new').trigger('click') new_path = 'Samurai' diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb index 795335aa106..5610894fd9a 100644 --- a/spec/features/dashboard/issues_spec.rb +++ b/spec/features/dashboard/issues_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Dashboard Issues' do expect(page).not_to have_content(other_issue.title) end - it 'shows checkmark when unassigned is selected for assignee', js: true do + it 'shows checkmark when unassigned is selected for assignee', :js do find('.js-assignee-search').click find('li', text: 'Unassigned').click find('.js-assignee-search').click @@ -32,7 +32,7 @@ RSpec.describe 'Dashboard Issues' do expect(find('li[data-user-id="0"] a.is-active')).to be_visible end - it 'shows issues when current user is author', js: true do + it 'shows issues when current user is author', :js do find('#assignee_id', visible: false).set('') find('.js-author-search', match: :first).click @@ -70,7 +70,7 @@ RSpec.describe 'Dashboard Issues' do end describe 'new issue dropdown' do - it 'shows projects only with issues feature enabled', js: true do + it 'shows projects only with issues feature enabled', :js do find('.new-project-item-select-button').trigger('click') page.within('.select2-results') do @@ -79,7 +79,7 @@ RSpec.describe 'Dashboard Issues' do end end - it 'shows the new issue page', js: true do + it 'shows the new issue page', :js do find('.new-project-item-select-button').trigger('click') wait_for_requests diff --git a/spec/features/dashboard/label_filter_spec.rb b/spec/features/dashboard/label_filter_spec.rb index b1a207682c3..6802974c2ee 100644 --- a/spec/features/dashboard/label_filter_spec.rb +++ b/spec/features/dashboard/label_filter_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Dashboard > label filter', js: true do +describe 'Dashboard > label filter', :js do let(:user) { create(:user) } let(:project) { create(:project, name: 'test', namespace: user.namespace) } let(:project2) { create(:project, name: 'test2', path: 'test2', namespace: user.namespace) } diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb index d26428ec286..f01ba442e58 100644 --- a/spec/features/dashboard/merge_requests_spec.rb +++ b/spec/features/dashboard/merge_requests_spec.rb @@ -24,7 +24,7 @@ feature 'Dashboard Merge Requests' do visit merge_requests_dashboard_path end - it 'shows projects only with merge requests feature enabled', js: true do + it 'shows projects only with merge requests feature enabled', :js do find('.new-project-item-select-button').trigger('click') page.within('.select2-results') do @@ -89,7 +89,7 @@ feature 'Dashboard Merge Requests' do expect(page).not_to have_content(other_merge_request.title) end - it 'shows authored merge requests', js: true do + it 'shows authored merge requests', :js do filter_item_select('Any Assignee', '.js-assignee-search') filter_item_select(current_user.to_reference, '.js-author-search') @@ -101,7 +101,7 @@ feature 'Dashboard Merge Requests' do expect(page).not_to have_content(other_merge_request.title) end - it 'shows all merge requests', js: true do + it 'shows all merge requests', :js do filter_item_select('Any Assignee', '.js-assignee-search') filter_item_select('Any Author', '.js-author-search') diff --git a/spec/features/dashboard/project_member_activity_index_spec.rb b/spec/features/dashboard/project_member_activity_index_spec.rb index 4a004107408..8f96899fb4f 100644 --- a/spec/features/dashboard/project_member_activity_index_spec.rb +++ b/spec/features/dashboard/project_member_activity_index_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Project member activity', js: true do +feature 'Project member activity', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, name: 'x', namespace: user.namespace) } diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 4da95ccc169..fbf8b5c0db6 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -80,7 +80,7 @@ feature 'Dashboard Projects' do end end - describe 'with a pipeline', clean_gitlab_redis_shared_state: true do + describe 'with a pipeline', :clean_gitlab_redis_shared_state do let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.sha) } before do diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb index 54d477f7274..ad0f132da8c 100644 --- a/spec/features/dashboard/todos/todos_filtering_spec.rb +++ b/spec/features/dashboard/todos/todos_filtering_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Dashboard > User filters todos', js: true do +feature 'Dashboard > User filters todos', :js do let(:user_1) { create(:user, username: 'user_1', name: 'user_1') } let(:user_2) { create(:user, username: 'user_2', name: 'user_2') } diff --git a/spec/features/dashboard/todos/todos_spec.rb b/spec/features/dashboard/todos/todos_spec.rb index 30bab7eeaa7..01aca443f4a 100644 --- a/spec/features/dashboard/todos/todos_spec.rb +++ b/spec/features/dashboard/todos/todos_spec.rb @@ -17,7 +17,7 @@ feature 'Dashboard Todos' do end end - context 'User has a todo', js: true do + context 'User has a todo', :js do before do create(:todo, :mentioned, user: user, project: project, target: issue, author: author) sign_in(user) @@ -177,7 +177,7 @@ feature 'Dashboard Todos' do end end - context 'User has done todos', js: true do + context 'User has done todos', :js do before do create(:todo, :mentioned, :done, user: user, project: project, target: issue, author: author) sign_in(user) @@ -249,7 +249,7 @@ feature 'Dashboard Todos' do expect(page).to have_selector('.gl-pagination .page', count: 2) end - describe 'mark all as done', js: true do + describe 'mark all as done', :js do before do visit dashboard_todos_path find('.js-todos-mark-all').trigger('click') @@ -267,7 +267,7 @@ feature 'Dashboard Todos' do end end - describe 'undo mark all as done', js: true do + describe 'undo mark all as done', :js do before do visit dashboard_todos_path end diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb index 357d86497d9..1dd7547a7fc 100644 --- a/spec/features/expand_collapse_diffs_spec.rb +++ b/spec/features/expand_collapse_diffs_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Expand and collapse diffs', js: true do +feature 'Expand and collapse diffs', :js do let(:branch) { 'expand-collapse-diffs' } let(:project) { create(:project, :repository) } diff --git a/spec/features/gitlab_flavored_markdown_spec.rb b/spec/features/gitlab_flavored_markdown_spec.rb index 53b3bb3b65f..3c2186b3598 100644 --- a/spec/features/gitlab_flavored_markdown_spec.rb +++ b/spec/features/gitlab_flavored_markdown_spec.rb @@ -49,7 +49,7 @@ describe "GitLab Flavored Markdown" do end end - describe "for issues", js: true do + describe "for issues", :js do before do @other_issue = create(:issue, author: user, diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb index 37814ba6238..d2d0be35f1c 100644 --- a/spec/features/group_variables_spec.rb +++ b/spec/features/group_variables_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Group variables', js: true do +feature 'Group variables', :js do let(:user) { create(:user) } let(:group) { create(:group) } diff --git a/spec/features/groups/labels/subscription_spec.rb b/spec/features/groups/labels/subscription_spec.rb index 1dd09d4f203..2e06caf98f6 100644 --- a/spec/features/groups/labels/subscription_spec.rb +++ b/spec/features/groups/labels/subscription_spec.rb @@ -11,7 +11,7 @@ feature 'Labels subscription' do gitlab_sign_in user end - scenario 'users can subscribe/unsubscribe to group labels', js: true do + scenario 'users can subscribe/unsubscribe to group labels', :js do visit group_labels_path(group) expect(page).to have_content('feature') diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb index 4ec2e7e6012..862823d862e 100644 --- a/spec/features/groups_spec.rb +++ b/spec/features/groups_spec.rb @@ -85,7 +85,7 @@ feature 'Group' do end end - describe 'create a nested group', :nested_groups, js: true do + describe 'create a nested group', :nested_groups, :js do let(:group) { create(:group, path: 'foo') } context 'as admin' do @@ -142,7 +142,7 @@ feature 'Group' do expect(page).not_to have_content('secret-group') end - describe 'group edit', js: true do + describe 'group edit', :js do let(:group) { create(:group) } let(:path) { edit_group_path(group) } let(:new_name) { 'new-name' } @@ -207,7 +207,7 @@ feature 'Group' do end end - describe 'group page with nested groups', :nested_groups, js: true do + describe 'group page with nested groups', :nested_groups, :js do let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } let!(:path) { group_path(group) } diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb index 925d026ed61..caee7a67aec 100644 --- a/spec/features/issuables/default_sort_order_spec.rb +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -26,7 +26,7 @@ describe 'Projects > Issuables > Default sort order' do MergeRequest.all end - context 'in the "merge requests" tab', js: true do + context 'in the "merge requests" tab', :js do let(:issuable_type) { :merge_request } it 'is "last created"' do @@ -37,7 +37,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "merge requests / open" tab', js: true do + context 'in the "merge requests / open" tab', :js do let(:issuable_type) { :merge_request } it 'is "created date"' do @@ -49,7 +49,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "merge requests / merged" tab', js: true do + context 'in the "merge requests / merged" tab', :js do let(:issuable_type) { :merged_merge_request } it 'is "last updated"' do @@ -61,7 +61,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "merge requests / closed" tab', js: true do + context 'in the "merge requests / closed" tab', :js do let(:issuable_type) { :closed_merge_request } it 'is "last updated"' do @@ -73,7 +73,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "merge requests / all" tab', js: true do + context 'in the "merge requests / all" tab', :js do let(:issuable_type) { :merge_request } it 'is "created date"' do @@ -102,7 +102,7 @@ describe 'Projects > Issuables > Default sort order' do Issue.all end - context 'in the "issues" tab', js: true do + context 'in the "issues" tab', :js do let(:issuable_type) { :issue } it 'is "created date"' do @@ -114,7 +114,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "issues / open" tab', js: true do + context 'in the "issues / open" tab', :js do let(:issuable_type) { :issue } it 'is "created date"' do @@ -126,7 +126,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "issues / closed" tab', js: true do + context 'in the "issues / closed" tab', :js do let(:issuable_type) { :closed_issue } it 'is "last updated"' do @@ -138,7 +138,7 @@ describe 'Projects > Issuables > Default sort order' do end end - context 'in the "issues / all" tab', js: true do + context 'in the "issues / all" tab', :js do let(:issuable_type) { :issue } it 'is "created date"' do diff --git a/spec/features/issuables/user_sees_sidebar_spec.rb b/spec/features/issuables/user_sees_sidebar_spec.rb index 2bd1c8aab86..c6c2e58ecea 100644 --- a/spec/features/issuables/user_sees_sidebar_spec.rb +++ b/spec/features/issuables/user_sees_sidebar_spec.rb @@ -12,7 +12,7 @@ describe 'Issue Sidebar on Mobile' do sign_in(user) end - context 'mobile sidebar on merge requests', js: true do + context 'mobile sidebar on merge requests', :js do before do visit project_merge_request_path(merge_request.project, merge_request) end @@ -20,7 +20,7 @@ describe 'Issue Sidebar on Mobile' do it_behaves_like "issue sidebar stays collapsed on mobile" end - context 'mobile sidebar on issues', js: true do + context 'mobile sidebar on issues', :js do before do visit project_issue_path(project, issue) end diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb index a29acb30163..850b35c4467 100644 --- a/spec/features/issues/award_emoji_spec.rb +++ b/spec/features/issues/award_emoji_spec.rb @@ -24,7 +24,7 @@ describe 'Awards Emoji' do end # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529 - it 'does not shows a 500 page', js: true do + it 'does not shows a 500 page', :js do expect(page).to have_text(issue.title) end end @@ -37,37 +37,37 @@ describe 'Awards Emoji' do wait_for_requests end - it 'increments the thumbsdown emoji', js: true do + it 'increments the thumbsdown emoji', :js do find('[data-name="thumbsdown"]').click wait_for_requests expect(thumbsdown_emoji).to have_text("1") end context 'click the thumbsup emoji' do - it 'increments the thumbsup emoji', js: true do + it 'increments the thumbsup emoji', :js do find('[data-name="thumbsup"]').click wait_for_requests expect(thumbsup_emoji).to have_text("1") end - it 'decrements the thumbsdown emoji', js: true do + it 'decrements the thumbsdown emoji', :js do expect(thumbsdown_emoji).to have_text("0") end end context 'click the thumbsdown emoji' do - it 'increments the thumbsdown emoji', js: true do + it 'increments the thumbsdown emoji', :js do find('[data-name="thumbsdown"]').click wait_for_requests expect(thumbsdown_emoji).to have_text("1") end - it 'decrements the thumbsup emoji', js: true do + it 'decrements the thumbsup emoji', :js do expect(thumbsup_emoji).to have_text("0") end end - it 'toggles the smiley emoji on a note', js: true do + it 'toggles the smiley emoji on a note', :js do toggle_smiley_emoji(true) within('.note-body') do @@ -82,7 +82,7 @@ describe 'Awards Emoji' do end context 'execute /award quick action' do - it 'toggles the emoji award on noteable', js: true do + it 'toggles the emoji award on noteable', :js do execute_quick_action('/award :100:') expect(find(noteable_award_counter)).to have_text("1") @@ -95,7 +95,7 @@ describe 'Awards Emoji' do end end - context 'unauthorized user', js: true do + context 'unauthorized user', :js do before do visit project_issue_path(project, issue) end diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb index e95eb19f7d1..ddb69d414da 100644 --- a/spec/features/issues/award_spec.rb +++ b/spec/features/issues/award_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Issue awards', js: true do +feature 'Issue awards', :js do let(:user) { create(:user) } let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index a89dcdf41dc..3223eb20b55 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,7 +9,7 @@ feature 'Issues > Labels bulk assignment' do let!(:feature) { create(:label, project: project, title: 'feature') } let!(:wontfix) { create(:label, project: project, title: 'wontfix') } - context 'as an allowed user', js: true do + context 'as an allowed user', :js do before do project.team << [user, :master] diff --git a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb index 80cc8d22999..822ba48e005 100644 --- a/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_discussions_in_merge_request_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Resolving all open discussions in a merge request from an issue', js: true do +feature 'Resolving all open discussions in a merge request from an issue', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb index ad5fd0fd97b..f0bed85595c 100644 --- a/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb +++ b/spec/features/issues/create_issue_for_single_discussion_in_merge_request_spec.rb @@ -24,7 +24,7 @@ feature 'Resolve an open discussion in a merge request by creating an issue' do end end - context 'resolving the discussion', js: true do + context 'resolving the discussion', :js do before do click_button 'Resolve discussion' end diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb index 3cec59050ab..5e20fb48768 100644 --- a/spec/features/issues/filtered_search/dropdown_author_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Dropdown author', js: true do +describe 'Dropdown author', :js do include FilteredSearchHelpers let!(:project) { create(:project) } diff --git a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb index 44741bcc92d..3012c77f2b9 100644 --- a/spec/features/issues/filtered_search/dropdown_emoji_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_emoji_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Dropdown emoji', js: true do +describe 'Dropdown emoji', :js do include FilteredSearchHelpers let!(:project) { create(:project, :public) } diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb index c46803112a9..cbc4f8d4c50 100644 --- a/spec/features/issues/filtered_search/dropdown_label_spec.rb +++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Dropdown label', js: true do +describe 'Dropdown label', :js do include FilteredSearchHelpers let(:project) { create(:project) } diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index 630d6a10c9c..2974016c6a7 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Filter issues', js: true do +describe 'Filter issues', :js do include FilteredSearchHelpers let(:project) { create(:project) } diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb index 447281ed19d..eef7988e2bd 100644 --- a/spec/features/issues/filtered_search/recent_searches_spec.rb +++ b/spec/features/issues/filtered_search/recent_searches_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Recent searches', js: true do +describe 'Recent searches', :js do include FilteredSearchHelpers let(:project_1) { create(:project, :public) } diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb index d4dd570fb37..88688422dc7 100644 --- a/spec/features/issues/filtered_search/search_bar_spec.rb +++ b/spec/features/issues/filtered_search/search_bar_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Search bar', js: true do +describe 'Search bar', :js do include FilteredSearchHelpers let!(:project) { create(:project) } diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb index 2b624f4842d..920f5546eef 100644 --- a/spec/features/issues/filtered_search/visual_tokens_spec.rb +++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'Visual tokens', js: true do +describe 'Visual tokens', :js do include FilteredSearchHelpers include WaitForRequests diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index c6cf6265645..15041ff04ea 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'GFM autocomplete', js: true do +feature 'GFM autocomplete', :js do let(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let(:project) { create(:project) } let(:label) { create(:label, project: project, title: 'special+') } diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index af11b474842..bc9c3d825c1 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -13,7 +13,7 @@ feature 'Issue Sidebar' do sign_in(user) end - context 'assignee', js: true do + context 'assignee', :js do let(:user2) { create(:user) } let(:issue2) { create(:issue, project: project, author: user2) } @@ -82,7 +82,7 @@ feature 'Issue Sidebar' do visit_issue(project, issue) end - context 'sidebar', js: true do + context 'sidebar', :js do it 'changes size when the screen size is smaller' do sidebar_selector = 'aside.right-sidebar.right-sidebar-collapsed' # Resize the window @@ -101,7 +101,7 @@ feature 'Issue Sidebar' do end end - context 'editing issue labels', js: true do + context 'editing issue labels', :js do before do page.within('.block.labels') do find('.edit-link').click @@ -114,7 +114,7 @@ feature 'Issue Sidebar' do end end - context 'creating a new label', js: true do + context 'creating a new label', :js do before do page.within('.block.labels') do click_link 'Create new' diff --git a/spec/features/issues/markdown_toolbar_spec.rb b/spec/features/issues/markdown_toolbar_spec.rb index 634ea111dc1..6869c2c869d 100644 --- a/spec/features/issues/markdown_toolbar_spec.rb +++ b/spec/features/issues/markdown_toolbar_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Issue markdown toolbar', js: true do +feature 'Issue markdown toolbar', :js do let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb index b2724945da4..6d7b1b1cd8f 100644 --- a/spec/features/issues/move_spec.rb +++ b/spec/features/issues/move_spec.rb @@ -37,7 +37,7 @@ feature 'issue move to another project' do visit issue_path(issue) end - scenario 'moving issue to another project', js: true do + scenario 'moving issue to another project', :js do find('.js-move-issue').trigger('click') wait_for_requests all('.js-move-issue-dropdown-item')[0].click @@ -49,7 +49,7 @@ feature 'issue move to another project' do expect(page.current_path).to include project_path(new_project) end - scenario 'searching project dropdown', js: true do + scenario 'searching project dropdown', :js do new_project_search.team << [user, :reporter] find('.js-move-issue').trigger('click') @@ -63,7 +63,7 @@ feature 'issue move to another project' do end end - context 'user does not have permission to move the issue to a project', js: true do + context 'user does not have permission to move the issue to a project', :js do let!(:private_project) { create(:project, :private) } let(:another_project) { create(:project) } background { another_project.team << [user, :guest] } diff --git a/spec/features/issues/spam_issues_spec.rb b/spec/features/issues/spam_issues_spec.rb index 332ce78b138..d25231d624c 100644 --- a/spec/features/issues/spam_issues_spec.rb +++ b/spec/features/issues/spam_issues_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -describe 'New issue', js: true do +describe 'New issue', :js do include StubENV let(:project) { create(:project, :public) } diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb index 8405f1cd48d..29a2d38ae18 100644 --- a/spec/features/issues/todo_spec.rb +++ b/spec/features/issues/todo_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Manually create a todo item from issue', js: true do +feature 'Manually create a todo item from issue', :js do let!(:project) { create(:project) } let!(:issue) { create(:issue, project: project) } let!(:user) { create(:user)} diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb index 7437c469a72..9f5e25ff2cb 100644 --- a/spec/features/issues/user_uses_slash_commands_spec.rb +++ b/spec/features/issues/user_uses_slash_commands_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Issues > User uses quick actions', js: true do +feature 'Issues > User uses quick actions', :js do include QuickActionsHelpers it_behaves_like 'issuable record that supports quick actions in its description and notes', :issue do diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index e2db1442d90..25e99774575 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -270,7 +270,7 @@ describe 'Issues', :js do visit namespace_project_issues_path(user.namespace, project1) end - it 'changes incoming email address token', js: true do + it 'changes incoming email address token', :js do find('.issue-email-modal-btn').click previous_token = find('input#issue_email').value find('.incoming-email-token-reset').trigger('click') @@ -286,7 +286,7 @@ describe 'Issues', :js do end end - describe 'update labels from issue#show', js: true do + describe 'update labels from issue#show', :js do let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } let!(:label) { create(:label, project: project) } @@ -309,7 +309,7 @@ describe 'Issues', :js do let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } context 'by authorized user' do - it 'allows user to select unassigned', js: true do + it 'allows user to select unassigned', :js do visit project_issue_path(project, issue) page.within('.assignee') do @@ -327,7 +327,7 @@ describe 'Issues', :js do expect(issue.reload.assignees).to be_empty end - it 'allows user to select an assignee', js: true do + it 'allows user to select an assignee', :js do issue2 = create(:issue, project: project, author: user) visit project_issue_path(project, issue2) @@ -348,7 +348,7 @@ describe 'Issues', :js do end end - it 'allows user to unselect themselves', js: true do + it 'allows user to unselect themselves', :js do issue2 = create(:issue, project: project, author: user) visit project_issue_path(project, issue2) @@ -377,7 +377,7 @@ describe 'Issues', :js do project.team << [[guest], :guest] end - it 'shows assignee text', js: true do + it 'shows assignee text', :js do sign_out(:user) sign_in(guest) @@ -392,7 +392,7 @@ describe 'Issues', :js do let!(:milestone) { create(:milestone, project: project) } context 'by authorized user' do - it 'allows user to select unassigned', js: true do + it 'allows user to select unassigned', :js do visit project_issue_path(project, issue) page.within('.milestone') do @@ -410,7 +410,7 @@ describe 'Issues', :js do expect(issue.reload.milestone).to be_nil end - it 'allows user to de-select milestone', js: true do + it 'allows user to de-select milestone', :js do visit project_issue_path(project, issue) page.within('.milestone') do @@ -440,7 +440,7 @@ describe 'Issues', :js do issue.save end - it 'shows milestone text', js: true do + it 'shows milestone text', :js do sign_out(:user) sign_in(guest) @@ -473,7 +473,7 @@ describe 'Issues', :js do end end - context 'dropzone upload file', js: true do + context 'dropzone upload file', :js do before do visit new_project_issue_path(project) end @@ -544,7 +544,7 @@ describe 'Issues', :js do end describe 'due date' do - context 'update due on issue#show', js: true do + context 'update due on issue#show', :js do let(:issue) { create(:issue, project: project, author: user, assignees: [user]) } before do @@ -588,8 +588,8 @@ describe 'Issues', :js do end end - describe 'title issue#show', js: true do - it 'updates the title', js: true do + describe 'title issue#show', :js do + it 'updates the title', :js do issue = create(:issue, author: user, assignees: [user], project: project, title: 'new title') visit project_issue_path(project, issue) @@ -603,7 +603,7 @@ describe 'Issues', :js do end end - describe 'confidential issue#show', js: true do + describe 'confidential issue#show', :js do it 'shows confidential sibebar information as confidential and can be turned off' do issue = create(:issue, :confidential, project: project) diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index c9983f0941f..6dfabcc7225 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -197,7 +197,7 @@ feature 'Login' do expect(page).to have_content('The global settings require you to enable Two-Factor Authentication for your account. You need to do this before ') end - it 'allows skipping two-factor configuration', js: true do + it 'allows skipping two-factor configuration', :js do expect(current_path).to eq profile_two_factor_auth_path click_link 'Configure it later' @@ -215,7 +215,7 @@ feature 'Login' do ) end - it 'disallows skipping two-factor configuration', js: true do + it 'disallows skipping two-factor configuration', :js do expect(current_path).to eq profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end @@ -260,7 +260,7 @@ feature 'Login' do 'before ') end - it 'allows skipping two-factor configuration', js: true do + it 'allows skipping two-factor configuration', :js do expect(current_path).to eq profile_two_factor_auth_path click_link 'Configure it later' @@ -279,7 +279,7 @@ feature 'Login' do ) end - it 'disallows skipping two-factor configuration', js: true do + it 'disallows skipping two-factor configuration', :js do expect(current_path).to eq profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end diff --git a/spec/features/merge_requests/assign_issues_spec.rb b/spec/features/merge_requests/assign_issues_spec.rb index 63fa72650ac..d49d145f254 100644 --- a/spec/features/merge_requests/assign_issues_spec.rb +++ b/spec/features/merge_requests/assign_issues_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge request issue assignment', js: true do +feature 'Merge request issue assignment', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue1) { create(:issue, project: project) } diff --git a/spec/features/merge_requests/award_spec.rb b/spec/features/merge_requests/award_spec.rb index e886309133d..a24464f2556 100644 --- a/spec/features/merge_requests/award_spec.rb +++ b/spec/features/merge_requests/award_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge request awards', js: true do +feature 'Merge request awards', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project) } diff --git a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb index 1f5e7b55fb0..fbbfe7942be 100644 --- a/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb +++ b/spec/features/merge_requests/check_if_mergeable_with_unresolved_discussions_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Check if mergeable with unresolved discussions', js: true do +feature 'Check if mergeable with unresolved discussions', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let!(:merge_request) { create(:merge_request_with_diff_notes, source_project: project, author: user) } diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb index 4b1e1b9a8d4..48f370c3ad4 100644 --- a/spec/features/merge_requests/cherry_pick_spec.rb +++ b/spec/features/merge_requests/cherry_pick_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Cherry-pick Merge Requests', js: true do +describe 'Cherry-pick Merge Requests', :js do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: group) } diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index 299b4f5708a..4dd4e40f52c 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Merge Request closing issues message', js: true do +feature 'Merge Request closing issues message', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue_1) { create(:issue, project: project)} diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb index 2d2c674f8fb..b0432ed8fc6 100644 --- a/spec/features/merge_requests/conflicts_spec.rb +++ b/spec/features/merge_requests/conflicts_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Merge request conflict resolution', js: true do +feature 'Merge request conflict resolution', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb index 96e8027a54d..5402d61da54 100644 --- a/spec/features/merge_requests/create_new_mr_spec.rb +++ b/spec/features/merge_requests/create_new_mr_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Create New Merge Request', js: true do +feature 'Create New Merge Request', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb index f9bc3ee6c58..d03ddfece74 100644 --- a/spec/features/merge_requests/created_from_fork_spec.rb +++ b/spec/features/merge_requests/created_from_fork_spec.rb @@ -34,7 +34,7 @@ feature 'Merge request created from fork' do commit_id: merge_request.commit_shas.first) end - scenario 'user can reply to the comment', js: true do + scenario 'user can reply to the comment', :js do visit_merge_request(merge_request) expect(page).to have_content(comment) @@ -57,7 +57,7 @@ feature 'Merge request created from fork' do forked_project.destroy! end - scenario 'user can access merge request', js: true do + scenario 'user can access merge request', :js do visit_merge_request(merge_request) expect(page).to have_content 'Test merge request' @@ -78,7 +78,7 @@ feature 'Merge request created from fork' do create(:ci_build, pipeline: pipeline, name: 'spinach') end - scenario 'user visits a pipelines page', js: true do + scenario 'user visits a pipelines page', :js do visit_merge_request(merge_request) page.within('.merge-request-tabs') { click_link 'Pipelines' } diff --git a/spec/features/merge_requests/deleted_source_branch_spec.rb b/spec/features/merge_requests/deleted_source_branch_spec.rb index 874c6e2ff69..7f69e82af4c 100644 --- a/spec/features/merge_requests/deleted_source_branch_spec.rb +++ b/spec/features/merge_requests/deleted_source_branch_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' # This test serves as a regression test for a bug that caused an error # message to be shown by JavaScript when the source branch was deleted. # Please do not remove "js: true". -describe 'Deleted source branch', js: true do +describe 'Deleted source branch', :js do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } diff --git a/spec/features/merge_requests/diff_notes_avatars_spec.rb b/spec/features/merge_requests/diff_notes_avatars_spec.rb index 4766cdf716f..9aa0672feae 100644 --- a/spec/features/merge_requests/diff_notes_avatars_spec.rb +++ b/spec/features/merge_requests/diff_notes_avatars_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Diff note avatars', js: true do +feature 'Diff note avatars', :js do include NoteInteractionHelpers let(:user) { create(:user) } diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb index fd110e68e84..637e6036384 100644 --- a/spec/features/merge_requests/diff_notes_resolve_spec.rb +++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Diff notes resolve', js: true do +feature 'Diff notes resolve', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user, title: "Bug NS-04") } diff --git a/spec/features/merge_requests/diffs_spec.rb b/spec/features/merge_requests/diffs_spec.rb index ee9bb50a881..80fb7335989 100644 --- a/spec/features/merge_requests/diffs_spec.rb +++ b/spec/features/merge_requests/diffs_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Diffs URL', js: true do +feature 'Diffs URL', :js do include ProjectForksHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb index 7386e78fb13..4538555c168 100644 --- a/spec/features/merge_requests/edit_mr_spec.rb +++ b/spec/features/merge_requests/edit_mr_spec.rb @@ -29,7 +29,7 @@ feature 'Edit Merge Request' do expect(page).to have_content 'Someone edited the merge request the same time you did' end - it 'allows to unselect "Remove source branch"', js: true do + it 'allows to unselect "Remove source branch"', :js do merge_request.update(merge_params: { 'force_remove_source_branch' => '1' }) expect(merge_request.merge_params['force_remove_source_branch']).to be_truthy @@ -42,7 +42,7 @@ feature 'Edit Merge Request' do expect(page).to have_content 'Remove source branch' end - it 'should preserve description textarea height', js: true do + it 'should preserve description textarea height', :js do long_description = %q( Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam ac ornare ligula, ut tempus arcu. Etiam ultricies accumsan dolor vitae faucibus. Donec at elit lacus. Mauris orci ante, aliquam quis lorem eget, convallis faucibus arcu. Aenean at pulvinar lacus. Ut viverra quam massa, molestie ornare tortor dignissim a. Suspendisse tristique pellentesque tellus, id lacinia metus elementum id. Nam tristique, arcu rhoncus faucibus viverra, lacus ipsum sagittis ligula, vitae convallis odio lacus a nibh. Ut tincidunt est purus, ac vestibulum augue maximus in. Suspendisse vel erat et mi ultricies semper. Pellentesque volutpat pellentesque consequat. diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index 166c02a7a7f..8b9ff9be943 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -18,7 +18,7 @@ feature 'Merge Request filtering by Milestone' do sign_in(user) end - scenario 'filters by no Milestone', js: true do + scenario 'filters by no Milestone', :js do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) @@ -32,7 +32,7 @@ feature 'Merge Request filtering by Milestone' do expect(page).to have_css('.merge-request', count: 1) end - context 'filters by upcoming milestone', js: true do + context 'filters by upcoming milestone', :js do it 'does not show merge requests with no expiry' do create(:merge_request, :with_diffs, source_project: project) create(:merge_request, :simple, source_project: project, milestone: milestone) @@ -67,7 +67,7 @@ feature 'Merge Request filtering by Milestone' do end end - scenario 'filters by a specific Milestone', js: true do + scenario 'filters by a specific Milestone', :js do create(:merge_request, :with_diffs, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project) @@ -83,7 +83,7 @@ feature 'Merge Request filtering by Milestone' do milestone.update(name: "rock 'n' roll") end - scenario 'filters by a specific Milestone', js: true do + scenario 'filters by a specific Milestone', :js do create(:merge_request, :with_diffs, source_project: project, milestone: milestone) create(:merge_request, :simple, source_project: project) diff --git a/spec/features/merge_requests/filter_merge_requests_spec.rb b/spec/features/merge_requests/filter_merge_requests_spec.rb index 16703bc1c01..aac295ab940 100644 --- a/spec/features/merge_requests/filter_merge_requests_spec.rb +++ b/spec/features/merge_requests/filter_merge_requests_spec.rb @@ -36,7 +36,7 @@ describe 'Filter merge requests' do expect_mr_list_count(0) end - context 'assignee', js: true do + context 'assignee', :js do it 'updates to current user' do expect_assignee_visual_tokens() end @@ -69,7 +69,7 @@ describe 'Filter merge requests' do expect_mr_list_count(0) end - context 'milestone', js: true do + context 'milestone', :js do it 'updates to current milestone' do expect_milestone_visual_tokens() end @@ -88,7 +88,7 @@ describe 'Filter merge requests' do end end - describe 'for label from mr#index', js: true do + describe 'for label from mr#index', :js do it 'filters by no label' do input_filtered_search('label:none') @@ -137,7 +137,7 @@ describe 'Filter merge requests' do expect_mr_list_count(0) end - context 'assignee and label', js: true do + context 'assignee and label', :js do def expect_assignee_label_visual_tokens wait_for_requests @@ -183,7 +183,7 @@ describe 'Filter merge requests' do visit project_merge_requests_path(project) end - context 'only text', js: true do + context 'only text', :js do it 'filters merge requests by searched text' do input_filtered_search('bug') @@ -199,7 +199,7 @@ describe 'Filter merge requests' do end end - context 'filters and searches', js: true do + context 'filters and searches', :js do it 'filters by text and label' do input_filtered_search('Bug') @@ -289,7 +289,7 @@ describe 'Filter merge requests' do end end - describe 'filter by assignee id', js: true do + describe 'filter by assignee id', :js do it 'filter by current user' do visit project_merge_requests_path(project, assignee_id: user.id) @@ -312,7 +312,7 @@ describe 'Filter merge requests' do end end - describe 'filter by author id', js: true do + describe 'filter by author id', :js do it 'filter by current user' do visit project_merge_requests_path(project, author_id: user.id) diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes.rb index e8ca8a98d70..3c53b51e330 100644 --- a/spec/features/merge_requests/image_diff_notes.rb +++ b/spec/features/merge_requests/image_diff_notes.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'image diff notes', js: true do +feature 'image diff notes', :js do include NoteInteractionHelpers let(:user) { create(:user) } diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb index 08a3bb84aac..82b2b56ef80 100644 --- a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Clicking toggle commit message link', js: true do +feature 'Clicking toggle commit message link', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue_1) { create(:issue, project: project)} diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 59e67420333..91f207bd339 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Only allow merge requests to be merged if the pipeline succeeds', js: true do +feature 'Only allow merge requests to be merged if the pipeline succeeds', :js do let(:merge_request) { create(:merge_request_with_diffs) } let(:project) { merge_request.target_project } @@ -10,7 +10,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t project.team << [merge_request.author, :master] end - context 'project does not have CI enabled', js: true do + context 'project does not have CI enabled', :js do it 'allows MR to be merged' do visit_merge_request(merge_request) @@ -20,7 +20,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t end end - context 'when project has CI enabled', js: true do + context 'when project has CI enabled', :js do given!(:pipeline) do create(:ci_empty_pipeline, project: project, diff --git a/spec/features/merge_requests/pipelines_spec.rb b/spec/features/merge_requests/pipelines_spec.rb index 347ce788b36..a3fcc27cab0 100644 --- a/spec/features/merge_requests/pipelines_spec.rb +++ b/spec/features/merge_requests/pipelines_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Pipelines for Merge Requests', js: true do +feature 'Pipelines for Merge Requests', :js do describe 'pipeline tab' do given(:user) { create(:user) } given(:merge_request) { create(:merge_request) } diff --git a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb index 55a82bdf2b9..25abbb469ab 100644 --- a/spec/features/merge_requests/resolve_outdated_diff_discussions.rb +++ b/spec/features/merge_requests/resolve_outdated_diff_discussions.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Resolve outdated diff discussions', js: true do +feature 'Resolve outdated diff discussions', :js do let(:project) { create(:project, :repository, :public) } let(:merge_request) do diff --git a/spec/features/merge_requests/target_branch_spec.rb b/spec/features/merge_requests/target_branch_spec.rb index 9bbf2610bcb..bce36e05e57 100644 --- a/spec/features/merge_requests/target_branch_spec.rb +++ b/spec/features/merge_requests/target_branch_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Target branch', js: true do +describe 'Target branch', :js do let(:user) { create(:user) } let(:merge_request) { create(:merge_request) } let(:project) { merge_request.project } diff --git a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb index dd989fd49b2..fa3d988b27a 100644 --- a/spec/features/merge_requests/toggle_whitespace_changes_spec.rb +++ b/spec/features/merge_requests/toggle_whitespace_changes_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Toggle Whitespace Changes', js: true do +feature 'Toggle Whitespace Changes', :js do before do sign_in(create(:admin)) merge_request = create(:merge_request) diff --git a/spec/features/merge_requests/toggler_behavior_spec.rb b/spec/features/merge_requests/toggler_behavior_spec.rb index 4e5ec9fbd2d..cd92ad22267 100644 --- a/spec/features/merge_requests/toggler_behavior_spec.rb +++ b/spec/features/merge_requests/toggler_behavior_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'toggler_behavior', js: true do +feature 'toggler_behavior', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:merge_request) { create(:merge_request, source_project: project, author: user) } diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb index 9cb8a357309..1a41fd36a4f 100644 --- a/spec/features/merge_requests/update_merge_requests_spec.rb +++ b/spec/features/merge_requests/update_merge_requests_spec.rb @@ -10,7 +10,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do sign_in(user) end - context 'status', js: true do + context 'status', :js do describe 'close merge request' do before do visit project_merge_requests_path(project) @@ -37,7 +37,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end end - context 'assignee', js: true do + context 'assignee', :js do describe 'set assignee' do before do visit project_merge_requests_path(project) @@ -67,7 +67,7 @@ feature 'Multiple merge requests updating from merge_requests#index' do end end - context 'milestone', js: true do + context 'milestone', :js do let(:milestone) { create(:milestone, project: project) } describe 'set milestone' do diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 95c50df1896..ee0766f1192 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Merge Requests > User uses quick actions', js: true do +feature 'Merge Requests > User uses quick actions', :js do include QuickActionsHelpers it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do diff --git a/spec/features/merge_requests/versions_spec.rb b/spec/features/merge_requests/versions_spec.rb index 8e231fbc281..50f7d721ff3 100644 --- a/spec/features/merge_requests/versions_spec.rb +++ b/spec/features/merge_requests/versions_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Merge Request versions', js: true do +feature 'Merge Request versions', :js do let(:merge_request) { create(:merge_request, importing: true) } let(:project) { merge_request.source_project } let!(:merge_request_diff1) { merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } diff --git a/spec/features/merge_requests/widget_deployments_spec.rb b/spec/features/merge_requests/widget_deployments_spec.rb index c0221525c9f..5658c2c5122 100644 --- a/spec/features/merge_requests/widget_deployments_spec.rb +++ b/spec/features/merge_requests/widget_deployments_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Widget Deployments Header', js: true do +feature 'Widget Deployments Header', :js do describe 'when deployed to an environment' do given(:user) { create(:user) } given(:project) { merge_request.target_project } diff --git a/spec/features/merge_requests/widget_spec.rb b/spec/features/merge_requests/widget_spec.rb index ab1353e3369..2bad3b02250 100644 --- a/spec/features/merge_requests/widget_spec.rb +++ b/spec/features/merge_requests/widget_spec.rb @@ -256,7 +256,7 @@ describe 'Merge request', :js do end end - context 'user can merge into source project but cannot push to fork', js: true do + context 'user can merge into source project but cannot push to fork', :js do let(:fork_project) { create(:project, :public, :repository) } let(:user2) { create(:user) } diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb index aa71c4dbba4..7d5ba3a7328 100644 --- a/spec/features/profiles/keys_spec.rb +++ b/spec/features/profiles/keys_spec.rb @@ -12,7 +12,7 @@ feature 'Profile > SSH Keys' do visit profile_keys_path end - scenario 'auto-populates the title', js: true do + scenario 'auto-populates the title', :js do fill_in('Key', with: attributes_for(:key).fetch(:key)) expect(page).to have_field("Title", with: "dummy@gitlab.com") diff --git a/spec/features/profiles/oauth_applications_spec.rb b/spec/features/profiles/oauth_applications_spec.rb index 45f78444362..8cb240077eb 100644 --- a/spec/features/profiles/oauth_applications_spec.rb +++ b/spec/features/profiles/oauth_applications_spec.rb @@ -7,7 +7,7 @@ describe 'Profile > Applications' do sign_in(user) end - describe 'User manages applications', js: true do + describe 'User manages applications', :js do it 'deletes an application' do create(:oauth_application, owner: user) visit oauth_applications_path diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index f3124bbf29e..a572160dae9 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Profile > Personal Access Tokens', js: true do +describe 'Profile > Personal Access Tokens', :js do let(:user) { create(:user) } def active_personal_access_tokens diff --git a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb index 6a4173d43e1..d5fe5bdffc5 100644 --- a/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb +++ b/spec/features/profiles/user_changes_notified_of_own_activity_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Profile > Notifications > User changes notified_of_own_activity setting', js: true do +feature 'Profile > Notifications > User changes notified_of_own_activity setting', :js do let(:user) { create(:user) } before do diff --git a/spec/features/profiles/user_visits_notifications_tab_spec.rb b/spec/features/profiles/user_visits_notifications_tab_spec.rb index 48c1787c8b7..923ca8b1c80 100644 --- a/spec/features/profiles/user_visits_notifications_tab_spec.rb +++ b/spec/features/profiles/user_visits_notifications_tab_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User visits the notifications tab', js: true do +feature 'User visits the notifications tab', :js do let(:project) { create(:project) } let(:user) { create(:user) } diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 89ae891037e..68c4a647958 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -39,7 +39,7 @@ feature 'list of badges' do end end - scenario 'user changes current ref of build status badge', js: true do + scenario 'user changes current ref of build status badge', :js do page.within('.pipeline-status') do first('.js-project-refs-dropdown').click diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb index 1160f674974..c12e56d2c3f 100644 --- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb +++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', js: true do +feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', :js do include TreeHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/projects/blobs/edit_spec.rb b/spec/features/projects/blobs/edit_spec.rb index 62ac9fd0e95..6c625ed17aa 100644 --- a/spec/features/projects/blobs/edit_spec.rb +++ b/spec/features/projects/blobs/edit_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Editing file blob', js: true do +feature 'Editing file blob', :js do include TreeHelper let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/projects/blobs/shortcuts_blob_spec.rb b/spec/features/projects/blobs/shortcuts_blob_spec.rb index 1e3080fa319..9f1fef80ab5 100644 --- a/spec/features/projects/blobs/shortcuts_blob_spec.rb +++ b/spec/features/projects/blobs/shortcuts_blob_spec.rb @@ -6,7 +6,7 @@ feature 'Blob shortcuts' do let(:path) { project.repository.ls_files(project.repository.root_ref)[0] } let(:sha) { project.repository.commit.sha } - describe 'On a file(blob)', js: true do + describe 'On a file(blob)', :js do def get_absolute_url(path = "") "http://#{page.server.host}:#{page.server.port}#{path}" end diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb index d1f5623554d..941d34dd660 100644 --- a/spec/features/projects/branches_spec.rb +++ b/spec/features/projects/branches_spec.rb @@ -46,7 +46,7 @@ describe 'Branches' do end describe 'Find branches' do - it 'shows filtered branches', js: true do + it 'shows filtered branches', :js do visit project_branches_path(project) fill_in 'branch-search', with: 'fix' @@ -58,7 +58,7 @@ describe 'Branches' do end describe 'Delete unprotected branch' do - it 'removes branch after confirmation', js: true do + it 'removes branch after confirmation', :js do visit project_branches_path(project) fill_in 'branch-search', with: 'fix' diff --git a/spec/features/projects/commit/builds_spec.rb b/spec/features/projects/commit/builds_spec.rb index 740331fe42a..9c57626ea1d 100644 --- a/spec/features/projects/commit/builds_spec.rb +++ b/spec/features/projects/commit/builds_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project commit pipelines', js: true do +feature 'project commit pipelines', :js do given(:project) { create(:project, :repository) } background do diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb index 7086f56bb1b..c11a95732b2 100644 --- a/spec/features/projects/commit/cherry_pick_spec.rb +++ b/spec/features/projects/commit/cherry_pick_spec.rb @@ -64,7 +64,7 @@ describe 'Cherry-pick Commits' do end end - context "I cherry-pick a commit from a different branch", js: true do + context "I cherry-pick a commit from a different branch", :js do it do find('.header-action-buttons a.dropdown-toggle').click find(:css, "a[href='#modal-cherry-pick-commit']").click diff --git a/spec/features/projects/compare_spec.rb b/spec/features/projects/compare_spec.rb index 82d73fe8531..87ffc2a0b90 100644 --- a/spec/features/projects/compare_spec.rb +++ b/spec/features/projects/compare_spec.rb @@ -1,6 +1,6 @@ require "spec_helper" -describe "Compare", js: true do +describe "Compare", :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } diff --git a/spec/features/projects/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/developer_views_empty_project_instructions_spec.rb index fe8567ce348..36809240f76 100644 --- a/spec/features/projects/developer_views_empty_project_instructions_spec.rb +++ b/spec/features/projects/developer_views_empty_project_instructions_spec.rb @@ -17,7 +17,7 @@ feature 'Developer views empty project instructions' do expect_instructions_for('http') end - scenario 'switches to SSH', js: true do + scenario 'switches to SSH', :js do visit_project select_protocol('SSH') @@ -37,7 +37,7 @@ feature 'Developer views empty project instructions' do expect_instructions_for('ssh') end - scenario 'switches to HTTP', js: true do + scenario 'switches to HTTP', :js do visit_project select_protocol('HTTP') diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb index 17f914c9c17..7a372757523 100644 --- a/spec/features/projects/edit_spec.rb +++ b/spec/features/projects/edit_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Project edit', js: true do +feature 'Project edit', :js do let(:admin) { create(:admin) } let(:user) { create(:user) } let(:project) { create(:project) } diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb index af7ad365546..610f566c0cf 100644 --- a/spec/features/projects/environments/environments_spec.rb +++ b/spec/features/projects/environments/environments_spec.rb @@ -145,7 +145,7 @@ feature 'Environments page', :js do expect(page).to have_content(action.name.humanize) end - it 'allows to play a manual action', js: true do + it 'allows to play a manual action', :js do expect(action).to be_manual find('.js-dropdown-play-icon-container').click diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index 57722276d79..e5282b42a4f 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -6,7 +6,7 @@ describe 'Edit Project Settings' do let!(:issue) { create(:issue, project: project) } let(:non_member) { create(:user) } - describe 'project features visibility selectors', js: true do + describe 'project features visibility selectors', :js do before do project.team << [member, :master] sign_in(member) @@ -163,7 +163,7 @@ describe 'Edit Project Settings' do end end - describe 'repository visibility', js: true do + describe 'repository visibility', :js do before do project.team << [member, :master] sign_in(member) diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb index f62a9edd37e..84197e45dcb 100644 --- a/spec/features/projects/files/browse_files_spec.rb +++ b/spec/features/projects/files/browse_files_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'user browses project', js: true do +feature 'user browses project', :js do let(:project) { create(:project, :repository) } let(:user) { create(:user) } diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb index cebb238dda1..3c3a5326538 100644 --- a/spec/features/projects/files/dockerfile_dropdown_spec.rb +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -16,7 +16,7 @@ feature 'User wants to add a Dockerfile file' do expect(page).to have_css('.dockerfile-selector') end - scenario 'user can pick a Dockerfile file from the dropdown', js: true do + scenario 'user can pick a Dockerfile file from the dropdown', :js do find('.js-dockerfile-selector').click wait_for_requests diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb index c7e3f657639..25f7e18ac5c 100644 --- a/spec/features/projects/files/edit_file_soft_wrap_spec.rb +++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User uses soft wrap whilst editing file', js: true do +feature 'User uses soft wrap whilst editing file', :js do before do user = create(:user) project = create(:project, :repository) diff --git a/spec/features/projects/files/find_file_keyboard_spec.rb b/spec/features/projects/files/find_file_keyboard_spec.rb index 7f97fdb8cc9..618725ee781 100644 --- a/spec/features/projects/files/find_file_keyboard_spec.rb +++ b/spec/features/projects/files/find_file_keyboard_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Find file keyboard shortcuts', js: true do +feature 'Find file keyboard shortcuts', :js do let(:user) { create(:user) } let(:project) { create(:project, :repository) } diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb index e2044c9d5aa..81d68c3d67c 100644 --- a/spec/features/projects/files/gitignore_dropdown_spec.rb +++ b/spec/features/projects/files/gitignore_dropdown_spec.rb @@ -13,7 +13,7 @@ feature 'User wants to add a .gitignore file' do expect(page).to have_css('.gitignore-selector') end - scenario 'user can pick a .gitignore file from the dropdown', js: true do + scenario 'user can pick a .gitignore file from the dropdown', :js do find('.js-gitignore-selector').click wait_for_requests within '.gitignore-selector' do diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb index ab242b0b0b5..8e58fa7bd56 100644 --- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb +++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb @@ -13,7 +13,7 @@ feature 'User wants to add a .gitlab-ci.yml file' do expect(page).to have_css('.gitlab-ci-yml-selector') end - scenario 'user can pick a template from the dropdown', js: true do + scenario 'user can pick a template from the dropdown', :js do find('.js-gitlab-ci-yml-selector').click wait_for_requests within '.gitlab-ci-yml-selector' do diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb index 95af263bcac..6c5b1086ec1 100644 --- a/spec/features/projects/files/project_owner_creates_license_file_spec.rb +++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project owner creates a license file', js: true do +feature 'project owner creates a license file', :js do let(:project_master) { create(:user) } let(:project) { create(:project, :repository) } background do diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 7bcab01c739..6c616bf0456 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'project owner sees a link to create a license file in empty project', js: true do +feature 'project owner sees a link to create a license file in empty project', :js do let(:project_master) { create(:user) } let(:project) { create(:project) } background do diff --git a/spec/features/projects/files/template_type_dropdown_spec.rb b/spec/features/projects/files/template_type_dropdown_spec.rb index 48003eeaa87..f95a60e5194 100644 --- a/spec/features/projects/files/template_type_dropdown_spec.rb +++ b/spec/features/projects/files/template_type_dropdown_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Template type dropdown selector', js: true do +feature 'Template type dropdown selector', :js do let(:project) { create(:project, :repository) } let(:user) { create(:user) } diff --git a/spec/features/projects/files/undo_template_spec.rb b/spec/features/projects/files/undo_template_spec.rb index 9bcd5beabb8..64fe350f3dc 100644 --- a/spec/features/projects/files/undo_template_spec.rb +++ b/spec/features/projects/files/undo_template_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Template Undo Button', js: true do +feature 'Template Undo Button', :js do let(:project) { create(:project, :repository) } let(:user) { create(:user) } diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index cff3b1f5743..1c988726ae6 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'GFM autocomplete loading', js: true do +describe 'GFM autocomplete loading', :js do let(:project) { create(:project) } before do diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 62d244ff259..05776c50f9d 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' # It looks up for any sensitive word inside the JSON, so if a sensitive word is found # we''l have to either include it adding the model that includes it to the +safe_list+ # or make sure the attribute is blacklisted in the +import_export.yml+ configuration -feature 'Import/Export - project export integration test', js: true do +feature 'Import/Export - project export integration test', :js do include Select2Helper include ExportFileHelper diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index e5c7781a096..c928459f911 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Import/Export - project import integration test', js: true do +feature 'Import/Export - project import integration test', :js do include Select2Helper let(:user) { create(:user) } diff --git a/spec/features/projects/import_export/namespace_export_file_spec.rb b/spec/features/projects/import_export/namespace_export_file_spec.rb index 691b0e1e4ca..b6a7c3cdcdb 100644 --- a/spec/features/projects/import_export/namespace_export_file_spec.rb +++ b/spec/features/projects/import_export/namespace_export_file_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Import/Export - Namespace export file cleanup', js: true do +feature 'Import/Export - Namespace export file cleanup', :js do let(:export_path) { "#{Dir.tmpdir}/import_file_spec" } let(:config_hash) { YAML.load_file(Gitlab::ImportExport.config_file).deep_stringify_keys } diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index c9b8ef4e37b..62b23121c5a 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'issuable templates', js: true do +feature 'issuable templates', :js do include ProjectForksHelper let(:user) { create(:user) } diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index a4ed589f3de..71702db860c 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -299,7 +299,7 @@ feature 'Jobs' do end shared_examples 'expected variables behavior' do - it 'shows variable key and value after click', js: true do + it 'shows variable key and value after click', :js do expect(page).to have_css('.reveal-variables') expect(page).not_to have_css('.js-build-variable') expect(page).not_to have_css('.js-build-value') diff --git a/spec/features/projects/labels/subscription_spec.rb b/spec/features/projects/labels/subscription_spec.rb index 5716d151250..e8c70dec854 100644 --- a/spec/features/projects/labels/subscription_spec.rb +++ b/spec/features/projects/labels/subscription_spec.rb @@ -13,7 +13,7 @@ feature 'Labels subscription' do sign_in user end - scenario 'users can subscribe/unsubscribe to labels', js: true do + scenario 'users can subscribe/unsubscribe to labels', :js do visit project_labels_path(project) expect(page).to have_content('bug') diff --git a/spec/features/projects/labels/update_prioritization_spec.rb b/spec/features/projects/labels/update_prioritization_spec.rb index 8f85e972027..d063f5c27b5 100644 --- a/spec/features/projects/labels/update_prioritization_spec.rb +++ b/spec/features/projects/labels/update_prioritization_spec.rb @@ -17,7 +17,7 @@ feature 'Prioritize labels' do sign_in user end - scenario 'user can prioritize a group label', js: true do + scenario 'user can prioritize a group label', :js do visit project_labels_path(project) expect(page).to have_content('Star labels to start sorting by priority') @@ -34,7 +34,7 @@ feature 'Prioritize labels' do end end - scenario 'user can unprioritize a group label', js: true do + scenario 'user can unprioritize a group label', :js do create(:label_priority, project: project, label: feature, priority: 1) visit project_labels_path(project) @@ -52,7 +52,7 @@ feature 'Prioritize labels' do end end - scenario 'user can prioritize a project label', js: true do + scenario 'user can prioritize a project label', :js do visit project_labels_path(project) expect(page).to have_content('Star labels to start sorting by priority') @@ -69,7 +69,7 @@ feature 'Prioritize labels' do end end - scenario 'user can unprioritize a project label', js: true do + scenario 'user can unprioritize a project label', :js do create(:label_priority, project: project, label: bug, priority: 1) visit project_labels_path(project) @@ -88,7 +88,7 @@ feature 'Prioritize labels' do end end - scenario 'user can sort prioritized labels and persist across reloads', js: true do + scenario 'user can sort prioritized labels and persist across reloads', :js do create(:label_priority, project: project, label: bug, priority: 1) create(:label_priority, project: project, label: feature, priority: 2) diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb index c8988aa63a7..6d729f2f85f 100644 --- a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb +++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Group requester cannot request access to project', js: true do +feature 'Projects > Members > Group requester cannot request access to project', :js do let(:user) { create(:user) } let(:owner) { create(:user) } let(:group) { create(:group, :public, :access_requestable) } diff --git a/spec/features/projects/members/groups_with_access_list_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb index 9950272af08..b1053982eee 100644 --- a/spec/features/projects/members/groups_with_access_list_spec.rb +++ b/spec/features/projects/members/groups_with_access_list_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Groups with access list', js: true do +feature 'Projects > Members > Groups with access list', :js do let(:user) { create(:user) } let(:group) { create(:group, :public) } let(:project) { create(:project, :public) } diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb index cd621b6b3ce..5f7b4ee0e77 100644 --- a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb +++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Members > Master adds member with expiration date', js: true do +feature 'Projects > Members > Master adds member with expiration date', :js do include Select2Helper include ActiveSupport::Testing::TimeHelpers diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 92486d2bc57..c35b0840248 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -453,7 +453,7 @@ describe 'Pipelines', :js do visit new_project_pipeline_path(project) end - context 'for valid commit', js: true do + context 'for valid commit', :js do before do click_button project.default_branch @@ -501,7 +501,7 @@ describe 'Pipelines', :js do end describe 'find pipelines' do - it 'shows filtered pipelines', js: true do + it 'shows filtered pipelines', :js do click_button project.default_branch page.within '.dropdown-menu' do diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb index 06568817757..15a5cd9990b 100644 --- a/spec/features/projects/project_settings_spec.rb +++ b/spec/features/projects/project_settings_spec.rb @@ -10,7 +10,7 @@ describe 'Edit Project Settings' do sign_in(user) end - describe 'Project settings section', js: true do + describe 'Project settings section', :js do it 'shows errors for invalid project name' do visit edit_project_path(project) fill_in 'project_name_edit', with: 'foo&bar' @@ -125,7 +125,7 @@ describe 'Edit Project Settings' do end end - describe 'Transfer project section', js: true do + describe 'Transfer project section', :js do let!(:project) { create(:project, :repository, namespace: user.namespace, name: 'gitlabhq') } let!(:group) { create(:group) } diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb index f0a23729220..f8695403857 100644 --- a/spec/features/projects/ref_switcher_spec.rb +++ b/spec/features/projects/ref_switcher_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Ref switcher', js: true do +feature 'Ref switcher', :js do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb index d932c4e4d9a..cbdb7973ac8 100644 --- a/spec/features/projects/settings/integration_settings_spec.rb +++ b/spec/features/projects/settings/integration_settings_spec.rb @@ -76,7 +76,7 @@ feature 'Integration settings' do expect(page).to have_content(url) end - scenario 'test existing webhook', js: true do + scenario 'test existing webhook', :js do WebMock.stub_request(:post, hook.url) visit integrations_path diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index 975d204e75e..de8fbb15b9c 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -22,7 +22,7 @@ feature "Pipelines settings" do context 'for master' do given(:role) { :master } - scenario 'be allowed to change', js: true do + scenario 'be allowed to change', :js do fill_in('Test coverage parsing', with: 'coverage_regex') click_on 'Save changes' diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb index 15180d4b498..a4fefb0d0e7 100644 --- a/spec/features/projects/settings/repository_settings_spec.rb +++ b/spec/features/projects/settings/repository_settings_spec.rb @@ -23,7 +23,7 @@ feature 'Repository settings' do context 'for master' do given(:role) { :master } - context 'Deploy Keys', js: true do + context 'Deploy Keys', :js do let(:private_deploy_key) { create(:deploy_key, title: 'private_deploy_key', public: false) } let(:public_deploy_key) { create(:another_deploy_key, title: 'public_deploy_key', public: true) } let(:new_ssh_key) { attributes_for(:key)[:key] } diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb index 37ee6255bd1..1c3b84d0114 100644 --- a/spec/features/projects/settings/visibility_settings_spec.rb +++ b/spec/features/projects/settings/visibility_settings_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Visibility settings', js: true do +feature 'Visibility settings', :js do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace, visibility_level: 20) } diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb index 1bc6fae9e7f..0b94c9eae5d 100644 --- a/spec/features/projects/show_project_spec.rb +++ b/spec/features/projects/show_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Project show page', feature: true do +describe 'Project show page', :feature do context 'when project pending delete' do let(:project) { create(:project, :empty_repo, pending_delete: true) } diff --git a/spec/features/projects/user_browses_files_spec.rb b/spec/features/projects/user_browses_files_spec.rb index b7a0b72db50..f43b11c9485 100644 --- a/spec/features/projects/user_browses_files_spec.rb +++ b/spec/features/projects/user_browses_files_spec.rb @@ -76,7 +76,7 @@ describe 'User browses files' do expect(page).to have_content('LICENSE') end - it 'shows files from a repository with apostroph in its name', js: true do + it 'shows files from a repository with apostroph in its name', :js do first('.js-project-refs-dropdown').click page.within('.project-refs-form') do @@ -91,7 +91,7 @@ describe 'User browses files' do expect(page).not_to have_content('Loading commit data...') end - it 'shows the code with a leading dot in the directory', js: true do + it 'shows the code with a leading dot in the directory', :js do first('.js-project-refs-dropdown').click page.within('.project-refs-form') do @@ -117,7 +117,7 @@ describe 'User browses files' do click_link('.gitignore') end - it 'shows a file content', js: true do + it 'shows a file content', :js do wait_for_requests expect(page).to have_content('*.rbc') end @@ -168,7 +168,7 @@ describe 'User browses files' do visit(tree_path_root_ref) end - it 'shows a preview of a file content', js: true do + it 'shows a preview of a file content', :js do find('.add-to-tree').click click_link('Upload file') drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'logo_sample.svg')) diff --git a/spec/features/projects/user_creates_directory_spec.rb b/spec/features/projects/user_creates_directory_spec.rb index e8f2e4813c5..052cb3188c5 100644 --- a/spec/features/projects/user_creates_directory_spec.rb +++ b/spec/features/projects/user_creates_directory_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User creates a directory', js: true do +feature 'User creates a directory', :js do let(:fork_message) do "You're not allowed to make changes to this project directly. "\ "A fork of this project has been created that you can make changes in, so you can submit a merge request." diff --git a/spec/features/projects/user_creates_files_spec.rb b/spec/features/projects/user_creates_files_spec.rb index 51d918bc85d..cbe70a93942 100644 --- a/spec/features/projects/user_creates_files_spec.rb +++ b/spec/features/projects/user_creates_files_spec.rb @@ -59,7 +59,7 @@ describe 'User creates files' do expect(page).to have_selector('.file-editor') end - it 'creates and commit a new file', js: true do + it 'creates and commit a new file', :js do execute_script("ace.edit('editor').setValue('*.rbca')") fill_in(:file_name, with: 'not_a_file.md') fill_in(:commit_message, with: 'New commit message', visible: true) @@ -74,7 +74,7 @@ describe 'User creates files' do expect(page).to have_content('*.rbca') end - it 'creates and commit a new file with new lines at the end of file', js: true do + it 'creates and commit a new file with new lines at the end of file', :js do execute_script('ace.edit("editor").setValue("Sample\n\n\n")') fill_in(:file_name, with: 'not_a_file.md') fill_in(:commit_message, with: 'New commit message', visible: true) @@ -89,7 +89,7 @@ describe 'User creates files' do expect(evaluate_script('ace.edit("editor").getValue()')).to eq("Sample\n\n\n") end - it 'creates and commit a new file with a directory name', js: true do + it 'creates and commit a new file with a directory name', :js do fill_in(:file_name, with: 'foo/bar/baz.txt') expect(page).to have_selector('.file-editor') @@ -105,7 +105,7 @@ describe 'User creates files' do expect(page).to have_content('*.rbca') end - it 'creates and commit a new file specifying a new branch', js: true do + it 'creates and commit a new file specifying a new branch', :js do expect(page).to have_selector('.file-editor') execute_script("ace.edit('editor').setValue('*.rbca')") @@ -130,7 +130,7 @@ describe 'User creates files' do visit(project2_tree_path_root_ref) end - it 'creates and commit new file in forked project', js: true do + it 'creates and commit new file in forked project', :js do find('.add-to-tree').click click_link('New file') diff --git a/spec/features/projects/user_creates_project_spec.rb b/spec/features/projects/user_creates_project_spec.rb index 1c3791f63ac..4a152572502 100644 --- a/spec/features/projects/user_creates_project_spec.rb +++ b/spec/features/projects/user_creates_project_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'User creates a project', js: true do +feature 'User creates a project', :js do let(:user) { create(:user) } before do diff --git a/spec/features/projects/user_deletes_files_spec.rb b/spec/features/projects/user_deletes_files_spec.rb index 7f48a69d9b7..9e4e92ec076 100644 --- a/spec/features/projects/user_deletes_files_spec.rb +++ b/spec/features/projects/user_deletes_files_spec.rb @@ -21,7 +21,7 @@ describe 'User deletes files' do visit(project_tree_path_root_ref) end - it 'deletes the file', js: true do + it 'deletes the file', :js do click_link('.gitignore') expect(page).to have_content('.gitignore') @@ -41,7 +41,7 @@ describe 'User deletes files' do visit(project2_tree_path_root_ref) end - it 'deletes the file in a forked project', js: true do + it 'deletes the file in a forked project', :js do click_link('.gitignore') expect(page).to have_content('.gitignore') diff --git a/spec/features/projects/user_edits_files_spec.rb b/spec/features/projects/user_edits_files_spec.rb index 2798041fa0c..e8d83a661d4 100644 --- a/spec/features/projects/user_edits_files_spec.rb +++ b/spec/features/projects/user_edits_files_spec.rb @@ -18,7 +18,7 @@ describe 'User edits files' do visit(project_tree_path_root_ref) end - it 'inserts a content of a file', js: true do + it 'inserts a content of a file', :js do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -35,7 +35,7 @@ describe 'User edits files' do expect(page).not_to have_link('edit') end - it 'commits an edited file', js: true do + it 'commits an edited file', :js do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -51,7 +51,7 @@ describe 'User edits files' do expect(page).to have_content('*.rbca') end - it 'commits an edited file to a new branch', js: true do + it 'commits an edited file to a new branch', :js do click_link('.gitignore') find('.js-edit-blob').click @@ -69,7 +69,7 @@ describe 'User edits files' do expect(page).to have_content('*.rbca') end - it 'shows the diff of an edited file', js: true do + it 'shows the diff of an edited file', :js do click_link('.gitignore') find('.js-edit-blob').click find('.file-editor', match: :first) @@ -87,7 +87,7 @@ describe 'User edits files' do visit(project2_tree_path_root_ref) end - it 'inserts a content of a file in a forked project', js: true do + it 'inserts a content of a file in a forked project', :js do click_link('.gitignore') find('.js-edit-blob').click @@ -108,7 +108,7 @@ describe 'User edits files' do expect(evaluate_script('ace.edit("editor").getValue()')).to eq('*.rbca') end - it 'commits an edited file in a forked project', js: true do + it 'commits an edited file in a forked project', :js do click_link('.gitignore') find('.js-edit-blob').click diff --git a/spec/features/projects/user_interacts_with_stars_spec.rb b/spec/features/projects/user_interacts_with_stars_spec.rb index 0ac3f8181fa..d9d2e0ab171 100644 --- a/spec/features/projects/user_interacts_with_stars_spec.rb +++ b/spec/features/projects/user_interacts_with_stars_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe 'User interacts with project stars' do let(:project) { create(:project, :public, :repository) } - context 'when user is signed in', js: true do + context 'when user is signed in', :js do let(:user) { create(:user) } before do diff --git a/spec/features/projects/user_replaces_files_spec.rb b/spec/features/projects/user_replaces_files_spec.rb index a9628198d5b..245b6aa285b 100644 --- a/spec/features/projects/user_replaces_files_spec.rb +++ b/spec/features/projects/user_replaces_files_spec.rb @@ -23,7 +23,7 @@ describe 'User replaces files' do visit(project_tree_path_root_ref) end - it 'replaces an existed file with a new one', js: true do + it 'replaces an existed file with a new one', :js do click_link('.gitignore') expect(page).to have_content('.gitignore') @@ -49,7 +49,7 @@ describe 'User replaces files' do visit(project2_tree_path_root_ref) end - it 'replaces an existed file with a new one in a forked project', js: true do + it 'replaces an existed file with a new one in a forked project', :js do click_link('.gitignore') expect(page).to have_content('.gitignore') diff --git a/spec/features/projects/user_uploads_files_spec.rb b/spec/features/projects/user_uploads_files_spec.rb index 8014c299980..ae51901adc6 100644 --- a/spec/features/projects/user_uploads_files_spec.rb +++ b/spec/features/projects/user_uploads_files_spec.rb @@ -23,7 +23,7 @@ describe 'User uploads files' do visit(project_tree_path_root_ref) end - it 'uploads and commit a new file', js: true do + it 'uploads and commit a new file', :js do find('.add-to-tree').click click_link('Upload file') drop_in_dropzone(File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')) @@ -54,7 +54,7 @@ describe 'User uploads files' do visit(project2_tree_path_root_ref) end - it 'uploads and commit a new file to a forked project', js: true do + it 'uploads and commit a new file to a forked project', :js do find('.add-to-tree').click click_link('Upload file') diff --git a/spec/features/projects/view_on_env_spec.rb b/spec/features/projects/view_on_env_spec.rb index 2a316a0d0db..7f547a4ca1f 100644 --- a/spec/features/projects/view_on_env_spec.rb +++ b/spec/features/projects/view_on_env_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'View on environment', js: true do +describe 'View on environment', :js do let(:branch_name) { 'feature' } let(:file_path) { 'files/ruby/feature.rb' } let(:project) { create(:project, :repository) } diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb index 9a4ccf3c54d..78c350c8ee4 100644 --- a/spec/features/projects/wiki/markdown_preview_spec.rb +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Projects > Wiki > User previews markdown changes', js: true do +feature 'Projects > Wiki > User previews markdown changes', :js do let(:user) { create(:user) } let(:project) { create(:project, namespace: user.namespace) } let(:wiki_content) do diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 4b2c54d54b5..ac62280e4ca 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -57,7 +57,7 @@ feature 'Project' do end end - describe 'remove forked relationship', js: true do + describe 'remove forked relationship', :js do let(:user) { create(:user) } let(:project) { fork_project(create(:project, :public), user, namespace_id: user.namespace) } @@ -126,7 +126,7 @@ feature 'Project' do end end - describe 'removal', js: true do + describe 'removal', :js do let(:user) { create(:user, username: 'test', name: 'test') } let(:project) { create(:project, namespace: user.namespace, name: 'project1') } diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index 8abd4403065..8cc6f17b8d9 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Protected Tags', js: true do +feature 'Protected Tags', :js do let(:user) { create(:user, :admin) } let(:project) { create(:project, :repository) } diff --git a/spec/features/snippets/internal_snippet_spec.rb b/spec/features/snippets/internal_snippet_spec.rb index 3a229612235..3a2768c424f 100644 --- a/spec/features/snippets/internal_snippet_spec.rb +++ b/spec/features/snippets/internal_snippet_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -feature 'Internal Snippets', js: true do +feature 'Internal Snippets', :js do let(:internal_snippet) { create(:personal_snippet, :internal) } describe 'normal user' do diff --git a/spec/features/tags/master_creates_tag_spec.rb b/spec/features/tags/master_creates_tag_spec.rb index 39d79a3327b..1455345bd56 100644 --- a/spec/features/tags/master_creates_tag_spec.rb +++ b/spec/features/tags/master_creates_tag_spec.rb @@ -55,7 +55,7 @@ feature 'Master creates tag' do end end - scenario 'opens dropdown for ref', js: true do + scenario 'opens dropdown for ref', :js do click_link 'New tag' ref_row = find('.form-group:nth-of-type(2) .col-sm-10') page.within ref_row do diff --git a/spec/features/tags/master_deletes_tag_spec.rb b/spec/features/tags/master_deletes_tag_spec.rb index 80750c904b5..f5b3774122b 100644 --- a/spec/features/tags/master_deletes_tag_spec.rb +++ b/spec/features/tags/master_deletes_tag_spec.rb @@ -10,7 +10,7 @@ feature 'Master deletes tag' do visit project_tags_path(project) end - context 'from the tags list page', js: true do + context 'from the tags list page', :js do scenario 'deletes the tag' do expect(page).to have_content 'v1.1.0' @@ -34,7 +34,7 @@ feature 'Master deletes tag' do end end - context 'when pre-receive hook fails', js: true do + context 'when pre-receive hook fails', :js do context 'when Gitaly operation_user_delete_tag feature is enabled' do before do allow_any_instance_of(Gitlab::GitalyClient::OperationService).to receive(:rm_tag) @@ -48,7 +48,7 @@ feature 'Master deletes tag' do end end - context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do before do allow_any_instance_of(Gitlab::Git::HooksService).to receive(:execute) .and_raise(Gitlab::Git::HooksService::PreReceiveError, 'Do not delete tags') diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb index aeb0534b733..485b0b287ad 100644 --- a/spec/features/task_lists_spec.rb +++ b/spec/features/task_lists_spec.rb @@ -63,7 +63,7 @@ feature 'Task Lists' do end describe 'for Issues' do - describe 'multiple tasks', js: true do + describe 'multiple tasks', :js do let!(:issue) { create(:issue, description: markdown, author: user, project: project) } it 'renders' do @@ -103,7 +103,7 @@ feature 'Task Lists' do end end - describe 'single incomplete task', js: true do + describe 'single incomplete task', :js do let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) } it 'renders' do @@ -122,7 +122,7 @@ feature 'Task Lists' do end end - describe 'single complete task', js: true do + describe 'single complete task', :js do let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) } it 'renders' do @@ -141,7 +141,7 @@ feature 'Task Lists' do end end - describe 'nested tasks', js: true do + describe 'nested tasks', :js do let(:issue) { create(:issue, description: nested_tasks_markdown, author: user, project: project) } before do diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb index 47664de469a..548d8372a07 100644 --- a/spec/features/triggers_spec.rb +++ b/spec/features/triggers_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Triggers', js: true do +feature 'Triggers', :js do let(:trigger_title) { 'trigger desc' } let(:user) { create(:user) } let(:user2) { create(:user) } diff --git a/spec/features/uploads/user_uploads_file_to_note_spec.rb b/spec/features/uploads/user_uploads_file_to_note_spec.rb index e1c95590af1..1261ffdc2ee 100644 --- a/spec/features/uploads/user_uploads_file_to_note_spec.rb +++ b/spec/features/uploads/user_uploads_file_to_note_spec.rb @@ -14,20 +14,20 @@ feature 'User uploads file to note' do end context 'before uploading' do - it 'shows "Attach a file" button', js: true do + it 'shows "Attach a file" button', :js do expect(page).to have_button('Attach a file') expect(page).not_to have_selector('.uploading-progress-container', visible: true) end end context 'uploading is in progress' do - it 'shows "Cancel" button on uploading', js: true do + it 'shows "Cancel" button on uploading', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) expect(page).to have_button('Cancel') end - it 'cancels uploading on clicking to "Cancel" button', js: true do + it 'cancels uploading on clicking to "Cancel" button', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) click_button 'Cancel' @@ -37,20 +37,20 @@ feature 'User uploads file to note' do expect(page).not_to have_selector('.uploading-progress-container', visible: true) end - it 'shows "Attaching a file" message on uploading 1 file', js: true do + it 'shows "Attaching a file" message on uploading 1 file', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching a file -') end - it 'shows "Attaching 2 files" message on uploading 2 file', js: true do + it 'shows "Attaching 2 files" message on uploading 2 file', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4'), Rails.root.join('spec', 'fixtures', 'dk.png')], 0, false) expect(page).to have_selector('.attaching-file-message', visible: true, text: 'Attaching 2 files -') end - it 'shows error message, "retry" and "attach a new file" link a if file is too big', js: true do + it 'shows error message, "retry" and "attach a new file" link a if file is too big', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'video_sample.mp4')], 0.01) error_text = 'File is too big (0.06MiB). Max filesize: 0.01MiB.' @@ -63,7 +63,7 @@ feature 'User uploads file to note' do end context 'uploading is complete' do - it 'shows "Attach a file" button on uploading complete', js: true do + it 'shows "Attach a file" button on uploading complete', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')]) wait_for_requests @@ -71,7 +71,7 @@ feature 'User uploads file to note' do expect(page).not_to have_selector('.uploading-progress-container', visible: true) end - scenario 'they see the attached file', js: true do + scenario 'they see the attached file', :js do dropzone_file([Rails.root.join('spec', 'fixtures', 'dk.png')]) click_button 'Comment' wait_for_requests diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb index 13760b4c2fc..8c697e33436 100644 --- a/spec/features/users/snippets_spec.rb +++ b/spec/features/users/snippets_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Snippets tab on a user profile', js: true do +describe 'Snippets tab on a user profile', :js do context 'when the user has snippets' do let(:user) { create(:user) } diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb index 15b89dac572..0252c957c95 100644 --- a/spec/features/users_spec.rb +++ b/spec/features/users_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature 'Users', js: true do +feature 'Users', :js do let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } scenario 'GET /users/sign_in creates a new user account' do diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb index 6794bf4f4ba..5d8e818f7bf 100644 --- a/spec/features/variables_spec.rb +++ b/spec/features/variables_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Project variables', js: true do +describe 'Project variables', :js do let(:user) { create(:user) } let(:project) { create(:project) } let(:variable) { create(:ci_variable, key: 'test_key', value: 'test value') } diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 641971485de..5777b5c4025 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -63,7 +63,7 @@ describe ProjectsHelper do end end - describe "#project_list_cache_key", clean_gitlab_redis_shared_state: true do + describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do let(:project) { create(:project, :repository) } it "includes the route" do diff --git a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb index 59f69d1e4b1..7b5a00c6111 100644 --- a/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_system_uploads_to_new_folder_spec.rb @@ -8,7 +8,7 @@ describe Gitlab::BackgroundMigration::MigrateSystemUploadsToNewFolder do end describe '#perform' do - it 'renames the path of system-uploads', truncate: true do + it 'renames the path of system-uploads', :truncate do upload = create(:upload, model: create(:project), path: 'uploads/system/project/avatar.jpg') migration.perform('uploads/system/', 'uploads/-/system/') diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb index 2c7ef622c51..633e319f46d 100644 --- a/spec/lib/gitlab/checks/force_push_spec.rb +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::Checks::ForcePush do let(:project) { create(:project, :repository) } - context "exit code checking", skip_gitaly_mock: true do + context "exit code checking", :skip_gitaly_mock do it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do allow_any_instance_of(Gitlab::Git::RevList).to receive(:popen).and_return(['normal output', 0]) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb index 90aa4f63dd5..596cc435bd9 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb @@ -229,7 +229,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end - describe '#track_rename', redis: true do + describe '#track_rename', :redis do it 'tracks a rename in redis' do key = 'rename:FakeRenameReservedPathMigrationV1:namespace' @@ -246,7 +246,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase, :trunca end end - describe '#reverts_for_type', redis: true do + describe '#reverts_for_type', :redis do it 'yields for each tracked rename' do subject.track_rename('project', 'old_path', 'new_path') subject.track_rename('project', 'old_path2', 'new_path2') diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb index 32ac0b88a9b..1143182531f 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb @@ -241,7 +241,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces, : end end - describe '#revert_renames', redis: true do + describe '#revert_renames', :redis do it 'renames the routes back to the previous values' do project = create(:project, :repository, path: 'a-project', namespace: namespace) subject.rename_namespace(namespace) diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb index 595e06a9748..8922370b0a0 100644 --- a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb +++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb @@ -115,7 +115,7 @@ describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects, :tr end end - describe '#revert_renames', redis: true do + describe '#revert_renames', :redis do it 'renames the routes back to the previous values' do subject.rename_project(project) diff --git a/spec/lib/gitlab/git/blame_spec.rb b/spec/lib/gitlab/git/blame_spec.rb index 465c2012b05..793228701cf 100644 --- a/spec/lib/gitlab/git/blame_spec.rb +++ b/spec/lib/gitlab/git/blame_spec.rb @@ -73,7 +73,7 @@ describe Gitlab::Git::Blame, seed_helper: true do it_behaves_like 'blaming a file' end - context 'when Gitaly blame feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly blame feature is disabled', :skip_gitaly_mock do it_behaves_like 'blaming a file' end end diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb index f3945e748ab..412a0093d97 100644 --- a/spec/lib/gitlab/git/blob_spec.rb +++ b/spec/lib/gitlab/git/blob_spec.rb @@ -112,7 +112,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it_behaves_like 'finding blobs' end - context 'when project_raw_show Gitaly feature is disabled', skip_gitaly_mock: true do + context 'when project_raw_show Gitaly feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding blobs' end end @@ -150,7 +150,7 @@ describe Gitlab::Git::Blob, seed_helper: true do it_behaves_like 'finding blobs by ID' end - context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do + context 'when the blob_raw Gitaly feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding blobs by ID' end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 3815055139a..9f4e3c49adc 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -261,7 +261,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it_should_behave_like '.where' end - describe '.where without gitaly', skip_gitaly_mock: true do + describe '.where without gitaly', :skip_gitaly_mock do it_should_behave_like '.where' end @@ -336,7 +336,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it_behaves_like 'finding all commits' end - context 'when Gitaly find_all_commits feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly find_all_commits feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding all commits' context 'while applying a sort order based on the `order` option' do @@ -405,7 +405,7 @@ describe Gitlab::Git::Commit, seed_helper: true do it_should_behave_like '#stats' end - describe '#stats with gitaly disabled', skip_gitaly_mock: true do + describe '#stats with gitaly disabled', :skip_gitaly_mock do it_should_behave_like '#stats' end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 5f12125beb2..1ee4acfd193 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -54,7 +54,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#rugged" do - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'raises a storage exception when storage is not available' do broken_repo = described_class.new('broken', 'a/path.git', '') @@ -384,7 +384,7 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - context 'when Gitaly commit_count feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do it_behaves_like 'simple commit counting' end end @@ -418,7 +418,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'check for local branches' end - context 'without gitaly', skip_gitaly_mock: true do + context 'without gitaly', :skip_gitaly_mock do it_behaves_like 'check for local branches' end end @@ -453,7 +453,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like "deleting a branch" end - context "when Gitaly delete_branch is disabled", skip_gitaly_mock: true do + context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do it_behaves_like "deleting a branch" end end @@ -489,7 +489,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'creating a branch' end - context 'when Gitaly create_branch feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'creating a branch' end end @@ -929,7 +929,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'extended commit counting' end - context 'when Gitaly count_commits feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do it_behaves_like 'extended commit counting' end end @@ -996,7 +996,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'finding a branch' end - context 'when Gitaly find_branch feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly find_branch feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding a branch' it 'should reload Rugged::Repository and return master' do @@ -1238,7 +1238,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of refs' end - context 'when Gitaly ref_exists feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of refs' end end @@ -1260,7 +1260,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of tags' end - context 'when Gitaly ref_exists_tags feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of tags' end end @@ -1284,7 +1284,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'checks the existence of branches' end - context 'when Gitaly ref_exists_branches feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do it_behaves_like 'checks the existence of branches' end end @@ -1361,7 +1361,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like 'languages' - context 'with rugged', skip_gitaly_mock: true do + context 'with rugged', :skip_gitaly_mock do it_behaves_like 'languages' end end @@ -1467,7 +1467,7 @@ describe Gitlab::Git::Repository, seed_helper: true do it_behaves_like "user deleting a branch" end - context "when Gitaly user_delete_branch is disabled", skip_gitaly_mock: true do + context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do it_behaves_like "user deleting a branch" end end diff --git a/spec/lib/gitlab/git/tag_spec.rb b/spec/lib/gitlab/git/tag_spec.rb index cc10679ef1e..6c4f538bf01 100644 --- a/spec/lib/gitlab/git/tag_spec.rb +++ b/spec/lib/gitlab/git/tag_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Git::Tag, seed_helper: true do it_behaves_like 'Gitlab::Git::Repository#tags' end - context 'when Gitaly tags feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly tags feature is disabled', :skip_gitaly_mock do it_behaves_like 'Gitlab::Git::Repository#tags' end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index c54327bd2e4..c9643c5da47 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -165,7 +165,7 @@ describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: 4096) end - it 'does not allow keys which are too small', aggregate_failures: true do + it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid expect { pull_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') expect { push_access_check }.to raise_unauthorized('Your SSH key must be at least 4096 bits.') @@ -177,7 +177,7 @@ describe Gitlab::GitAccess do stub_application_setting(rsa_key_restriction: ApplicationSetting::FORBIDDEN_KEY_VALUE) end - it 'does not allow keys which are too small', aggregate_failures: true do + it 'does not allow keys which are too small', :aggregate_failures do expect(actor).not_to be_valid expect { pull_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) expect { push_access_check }.to raise_unauthorized(/Your SSH key type is forbidden/) diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 6f59750b4da..8127b4842b7 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -84,14 +84,14 @@ describe Gitlab::GitalyClient::RefService do end end - describe '#find_ref_name', seed_helper: true do + describe '#find_ref_name', :seed_helper do subject { client.find_ref_name(SeedRepo::Commit::ID, 'refs/heads/master') } it { is_expected.to be_utf8 } it { is_expected.to eq('refs/heads/master') } end - describe '#ref_exists?', seed_helper: true do + describe '#ref_exists?', :seed_helper do it 'finds the master branch ref' do expect(client.ref_exists?('refs/heads/master')).to eq(true) end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 73dd236a5c6..4c1ca4349ea 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } - context 'storage has a tripped circuitbreaker', broken_storage: true do + context 'storage has a tripped circuitbreaker', :broken_storage do let(:repository_storages) { ['broken'] } let(:storages_paths) do Gitlab.config.repositories.storages diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index 139afa22d01..2158b2837e2 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -156,7 +156,7 @@ describe Gitlab::Shell do it_behaves_like '#add_repository' end - context 'without gitaly', skip_gitaly_mock: true do + context 'without gitaly', :skip_gitaly_mock do it_behaves_like '#add_repository' end end @@ -333,7 +333,7 @@ describe Gitlab::Shell do end end - describe '#fetch_remote local', skip_gitaly_mock: true do + describe '#fetch_remote local', :skip_gitaly_mock do it_should_behave_like 'fetch_remote', false end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 4dffe2bd82f..9230d58012f 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -41,7 +41,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_archive feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly workhorse_archive feature is disabled', :skip_gitaly_mock do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) @@ -383,7 +383,7 @@ describe Gitlab::Workhorse do end end - context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly workhorse_raw_show feature is disabled', :skip_gitaly_mock do it 'sets the header correctly' do key, command, params = decode_workhorse_header(subject) diff --git a/spec/migrations/update_upload_paths_to_system_spec.rb b/spec/migrations/update_upload_paths_to_system_spec.rb index 0a45c5ea32d..d4a1553fb0e 100644 --- a/spec/migrations/update_upload_paths_to_system_spec.rb +++ b/spec/migrations/update_upload_paths_to_system_spec.rb @@ -31,7 +31,7 @@ describe UpdateUploadPathsToSystem do end end - describe "#up", truncate: true do + describe "#up", :truncate do it "updates old upload records to the new path" do old_upload = create(:upload, model: create(:project), path: "uploads/project/avatar.jpg") @@ -41,7 +41,7 @@ describe UpdateUploadPathsToSystem do end end - describe "#down", truncate: true do + describe "#down", :truncate do it "updates the new system patsh to the old paths" do new_upload = create(:upload, model: create(:project), path: "uploads/-/system/project/avatar.jpg") diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index a07ce05a865..0a017c068ad 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -488,7 +488,7 @@ describe Member do member.accept_invite!(user) end - it "refreshes user's authorized projects", truncate: true do + it "refreshes user's authorized projects", :truncate do project = member.source expect(user.authorized_projects).not_to include(project) @@ -523,7 +523,7 @@ describe Member do end end - describe "destroying a record", truncate: true do + describe "destroying a record", :truncate do it "refreshes user's authorized projects" do project = create(:project, :private) user = create(:user) diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index b3513c80150..41e2ab20d69 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -30,7 +30,7 @@ describe ProjectGroupLink do end end - describe "destroying a record", truncate: true do + describe "destroying a record", :truncate do it "refreshes group users' authorized projects" do project = create(:project, :private) group = create(:group) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 5764c451b67..5d78aed5b4f 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -40,7 +40,7 @@ describe Repository do it { is_expected.not_to include('feature') } it { is_expected.not_to include('fix') } - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error do broken_repository.branch_names_contains(sample_commit.id) @@ -158,7 +158,7 @@ describe Repository do it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error do broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') @@ -171,7 +171,7 @@ describe Repository do it_behaves_like 'getting last commit for path' end - context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do + context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do it_behaves_like 'getting last commit for path' end end @@ -192,7 +192,7 @@ describe Repository do is_expected.to eq('c1acaa5') end - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error do broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id @@ -205,7 +205,7 @@ describe Repository do it_behaves_like 'getting last commit ID for path' end - context 'when Gitaly feature last_commit_for_path is disabled', skip_gitaly_mock: true do + context 'when Gitaly feature last_commit_for_path is disabled', :skip_gitaly_mock do it_behaves_like 'getting last commit ID for path' end end @@ -255,11 +255,11 @@ describe Repository do it_behaves_like 'finding commits by message' end - context 'when Gitaly commits_by_message feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly commits_by_message feature is disabled', :skip_gitaly_mock do it_behaves_like 'finding commits by message' end - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') } end @@ -589,7 +589,7 @@ describe Repository do expect(results).to match_array([]) end - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error do broken_repository.search_files_by_content('feature', 'master') @@ -626,7 +626,7 @@ describe Repository do expect(results).to match_array([]) end - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') } end @@ -638,7 +638,7 @@ describe Repository do # before the actual test call set(:broken_repository) { create(:project, :broken_storage).repository } - describe 'when storage is broken', broken_storage: true do + describe 'when storage is broken', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error do broken_repository.fetch_ref(broken_repository, source_ref: '1', target_ref: '2') @@ -848,7 +848,7 @@ describe Repository do end end - context 'with Gitaly disabled', skip_gitaly_mock: true do + context 'with Gitaly disabled', :skip_gitaly_mock do context 'when pre hooks were successful' do it 'runs without errors' do hook = double(trigger: [true, nil]) @@ -1101,7 +1101,7 @@ describe Repository do expect(repository.exists?).to eq(false) end - context 'with broken storage', broken_storage: true do + context 'with broken storage', :broken_storage do it 'should raise a storage error' do expect_to_raise_storage_error { broken_repository.exists? } end @@ -1113,7 +1113,7 @@ describe Repository do it_behaves_like 'repo exists check' end - context 'when repository_exists is enabled', skip_gitaly_mock: true do + context 'when repository_exists is enabled', :skip_gitaly_mock do it_behaves_like 'repo exists check' end end @@ -1676,7 +1676,7 @@ describe Repository do it_behaves_like 'adding tag' end - context 'when Gitaly operation_user_add_tag feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly operation_user_add_tag feature is disabled', :skip_gitaly_mock do it_behaves_like 'adding tag' it 'passes commit SHA to pre-receive and update hooks and tag SHA to post-receive hook' do @@ -1735,7 +1735,7 @@ describe Repository do end end - context 'with gitaly disabled', skip_gitaly_mock: true do + context 'with gitaly disabled', :skip_gitaly_mock do it_behaves_like "user deleting a branch" let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature @@ -1794,7 +1794,7 @@ describe Repository do it_behaves_like 'removing tag' end - context 'when Gitaly operation_user_delete_tag feature is disabled', skip_gitaly_mock: true do + context 'when Gitaly operation_user_delete_tag feature is disabled', :skip_gitaly_mock do it_behaves_like 'removing tag' end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index ece6968dde6..1c3c9068f12 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1525,7 +1525,7 @@ describe User do it { is_expected.to eq([private_group]) } end - describe '#authorized_projects', truncate: true do + describe '#authorized_projects', :truncate do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do user = create(:user) @@ -1877,7 +1877,7 @@ describe User do end end - describe '#refresh_authorized_projects', clean_gitlab_redis_shared_state: true do + describe '#refresh_authorized_projects', :clean_gitlab_redis_shared_state do let(:project1) { create(:project) } let(:project2) { create(:project) } let(:user) { create(:user) } diff --git a/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb new file mode 100644 index 00000000000..278662d32ea --- /dev/null +++ b/spec/rubocop/cop/rspec/verbose_include_metadata_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/rspec/verbose_include_metadata' + +describe RuboCop::Cop::RSpec::VerboseIncludeMetadata do + include CopHelper + + subject(:cop) { described_class.new } + + let(:source_file) { 'foo_spec.rb' } + + # Override `CopHelper#inspect_source` to always appear to be in a spec file, + # so that our RSpec-only cop actually runs + def inspect_source(*args) + super(*args, source_file) + end + + shared_examples 'examples with include syntax' do |title| + it "flags violation for #{title} examples that uses verbose include syntax" do + inspect_source(cop, "#{title} 'Test', js: true do; end") + + expect(cop.offenses.size).to eq(1) + offense = cop.offenses.first + + expect(offense.line).to eq(1) + expect(cop.highlights).to eq(["#{title} 'Test', js: true"]) + expect(offense.message).to eq('Use `:js` instead of `js: true`.') + end + + it "doesn't flag violation for #{title} examples that uses compact include syntax", :aggregate_failures do + inspect_source(cop, "#{title} 'Test', :js do; end") + + expect(cop.offenses).to be_empty + end + + it "doesn't flag violation for #{title} examples that uses flag: symbol" do + inspect_source(cop, "#{title} 'Test', flag: :symbol do; end") + + expect(cop.offenses).to be_empty + end + + it "autocorrects #{title} examples that uses verbose syntax into compact syntax" do + autocorrected = autocorrect_source(cop, "#{title} 'Test', js: true do; end", source_file) + + expect(autocorrected).to eql("#{title} 'Test', :js do; end") + end + end + + %w(describe context feature example_group it specify example scenario its).each do |example| + it_behaves_like 'examples with include syntax', example + end +end diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 8282ba7e536..061e0d35590 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -29,7 +29,7 @@ shared_examples 'issuable record that supports quick actions in its description wait_for_requests end - describe "new #{issuable_type}", js: true do + describe "new #{issuable_type}", :js do context 'with commands in the description' do it "creates the #{issuable_type} and interpret commands accordingly" do case issuable_type @@ -53,7 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description end end - describe "note on #{issuable_type}", js: true do + describe "note on #{issuable_type}", :js do before do visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable) end @@ -290,7 +290,7 @@ shared_examples 'issuable record that supports quick actions in its description end end - describe "preview of note on #{issuable_type}", js: true do + describe "preview of note on #{issuable_type}", :js do it 'removes quick actions from note and explains them' do create(:user, username: 'bob') diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb index ab656d619f4..47297de738b 100644 --- a/spec/workers/git_garbage_collect_worker_spec.rb +++ b/spec/workers/git_garbage_collect_worker_spec.rb @@ -104,7 +104,7 @@ describe GitGarbageCollectWorker do it_should_behave_like 'flushing ref caches', true end - context "with Gitaly turned off", skip_gitaly_mock: true do + context "with Gitaly turned off", :skip_gitaly_mock do it_should_behave_like 'flushing ref caches', false end -- cgit v1.2.1 From b352462d01e3c51d09314c282e84afe075d6ad01 Mon Sep 17 00:00:00 2001 From: Annabel Dunstone Gray Date: Sat, 7 Oct 2017 07:52:28 -0700 Subject: Fix dropdown header alignment; empty navbar positioning --- app/assets/stylesheets/framework/dropdowns.scss | 2 +- app/assets/stylesheets/framework/header.scss | 34 ++++++++++++------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss index 9dcf332eee2..5b950ae0ba0 100644 --- a/app/assets/stylesheets/framework/dropdowns.scss +++ b/app/assets/stylesheets/framework/dropdowns.scss @@ -749,7 +749,7 @@ margin-bottom: $dropdown-vertical-offset; } - li { + li:not(.dropdown-bold-header) { display: block; padding: 0 1px; diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index 9b4bfae1144..22945e935ef 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -5,22 +5,6 @@ .navbar-gitlab { @include new-style-dropdown; - &.navbar-empty { - height: $header-height; - background: $white-light; - border-bottom: 1px solid $white-normal; - - .center-logo { - margin: 8px 0; - text-align: center; - - .tanuki-logo, - img { - height: 36px; - } - } - } - &.navbar-gitlab { padding: 0 16px; z-index: 1000; @@ -550,7 +534,7 @@ color: $gl-text-color; left: auto; - .current-user { + li.current-user { padding: 5px 18px; .user-name { @@ -570,3 +554,19 @@ .with-performance-bar .navbar-gitlab { top: $performance-bar-height; } + +.navbar-empty { + height: $header-height; + background: $white-light; + border-bottom: 1px solid $white-normal; + + .center-logo { + margin: 8px 0; + text-align: center; + + .tanuki-logo, + img { + height: 36px; + } + } +} -- cgit v1.2.1 From 4aa2deb4782f1f3b516bfaecdb57203765bbb123 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20D=C3=A1vila?= Date: Sat, 7 Oct 2017 10:47:53 -0500 Subject: Fix error with GPG signature updater when commit was deleted --- app/models/gpg_signature.rb | 2 ++ lib/gitlab/gpg/invalid_gpg_signature_updater.rb | 2 +- spec/models/gpg_signature_spec.rb | 28 +++++++++++++++++++------ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/app/models/gpg_signature.rb b/app/models/gpg_signature.rb index 675e7a2456d..bf88d75246f 100644 --- a/app/models/gpg_signature.rb +++ b/app/models/gpg_signature.rb @@ -60,6 +60,8 @@ class GpgSignature < ActiveRecord::Base end def gpg_commit + return unless commit + Gitlab::Gpg::Commit.new(commit) end end diff --git a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb index b7fb9dde2bc..1991911ef6a 100644 --- a/lib/gitlab/gpg/invalid_gpg_signature_updater.rb +++ b/lib/gitlab/gpg/invalid_gpg_signature_updater.rb @@ -10,7 +10,7 @@ module Gitlab .select(:id, :commit_sha, :project_id) .where('gpg_key_id IS NULL OR verification_status <> ?', GpgSignature.verification_statuses[:verified]) .where(gpg_key_primary_keyid: @gpg_key.keyids) - .find_each { |sig| sig.gpg_commit.update_signature!(sig) } + .find_each { |sig| sig.gpg_commit&.update_signature!(sig) } end end end diff --git a/spec/models/gpg_signature_spec.rb b/spec/models/gpg_signature_spec.rb index db033016c37..0136bb61c07 100644 --- a/spec/models/gpg_signature_spec.rb +++ b/spec/models/gpg_signature_spec.rb @@ -1,6 +1,10 @@ require 'rails_helper' RSpec.describe GpgSignature do + let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' } + let!(:project) { create(:project, :repository, path: 'sample-project') } + let!(:commit) { create(:commit, project: project, sha: commit_sha) } + let(:gpg_signature) { create(:gpg_signature, commit_sha: commit_sha) } let(:gpg_key) { create(:gpg_key) } let(:gpg_key_subkey) { create(:gpg_key_subkey) } @@ -19,11 +23,6 @@ RSpec.describe GpgSignature do describe '#commit' do it 'fetches the commit through the project' do - commit_sha = '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' - project = create :project, :repository - commit = create :commit, project: project - gpg_signature = create :gpg_signature, commit_sha: commit_sha - expect_any_instance_of(Project).to receive(:commit).with(commit_sha).and_return(commit) gpg_signature.commit @@ -44,11 +43,28 @@ RSpec.describe GpgSignature do end it 'clears gpg_key and gpg_key_subkey_id when passing nil' do - gpg_signature = create(:gpg_signature, gpg_key: gpg_key_subkey) gpg_signature.update_attribute(:gpg_key, nil) expect(gpg_signature.gpg_key_id).to be_nil expect(gpg_signature.gpg_key_subkey_id).to be_nil end end + + describe '#gpg_commit' do + context 'when commit does not exist' do + it 'returns nil' do + allow(gpg_signature).to receive(:commit).and_return(nil) + + expect(gpg_signature.gpg_commit).to be_nil + end + end + + context 'when commit exists' do + it 'returns an instance of Gitlab::Gpg::Commit' do + allow(gpg_signature).to receive(:commit).and_return(commit) + + expect(gpg_signature.gpg_commit).to be_an_instance_of(Gitlab::Gpg::Commit) + end + end + end end -- cgit v1.2.1 From 42bc6caee038d0abcb8636182c2c0eac70dae8e8 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Sun, 17 Sep 2017 18:07:29 -0700 Subject: Trim extraneous spaces from DNs --- lib/gitlab/ldap/auth_hash.rb | 4 + lib/gitlab/ldap/person.rb | 41 ++++++++++- spec/lib/gitlab/ldap/auth_hash_spec.rb | 16 +++- spec/lib/gitlab/ldap/person_spec.rb | 130 +++++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 4fbc5fa5262..3123da17fd9 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -3,6 +3,10 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash + def uid + Gitlab::LDAP::Person.normalize_dn(super) + end + private def get_info(key) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 9a6f7827b16..4299d35fabc 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -36,6 +36,12 @@ module Gitlab ] end + def self.normalize_dn(dn) + dn.split(/([,+=])/).map do |part| + normalize_dn_part(part) + end.join('') + end + def initialize(entry, provider) Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry @@ -58,10 +64,43 @@ module Gitlab attribute_value(:email) end - delegate :dn, to: :entry + def dn + self.class.normalize_dn(entry.dn) + end private + def self.normalize_dn_part(part) + cleaned = part.strip + + if cleaned.ends_with?('\\') + # If it ends with an escape character that is not followed by a + # character to be escaped, then this part may be malformed. But let's + # not worry too much about it, and just return it unmodified. + # + # Why? Because the reason we clean DNs is to make our simplistic + # string comparisons work better, even though there are all kinds of + # ways that equivalent DNs can vary as strings. If we run into a + # strange DN, we should just try to work with it. + # + # See https://www.ldap.com/ldap-dns-and-rdns for more. + return part unless part.ends_with?(' ') + + # Ends with an escaped space (which is valid). + cleaned = cleaned + ' ' + end + + # Get rid of blanks. This can happen if a split character is followed by + # whitespace and then another split character. + # + # E.g. this DN: 'uid=john+telephoneNumber= +1 555-555-5555' + # + # Should be returned as: 'uid=john+telephoneNumber=+1 555-555-5555' + cleaned = '' if cleaned.blank? + + cleaned + end + def entry @entry end diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index 8370adf9211..a4bd40705df 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -4,7 +4,7 @@ describe Gitlab::LDAP::AuthHash do let(:auth_hash) do described_class.new( OmniAuth::AuthHash.new( - uid: '123456', + uid: given_uid, provider: 'ldapmain', info: info, extra: { @@ -32,6 +32,8 @@ describe Gitlab::LDAP::AuthHash do end context "without overridden attributes" do + let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' } + it "has the correct username" do expect(auth_hash.username).to eq("123456") end @@ -42,6 +44,8 @@ describe Gitlab::LDAP::AuthHash do end context "with overridden attributes" do + let(:given_uid) { 'uid=John Smith,ou=People,dc=example,dc=com' } + let(:attributes) do { 'username' => %w(mail email), @@ -61,4 +65,14 @@ describe Gitlab::LDAP::AuthHash do expect(auth_hash.name).to eq("John Smith") end end + + describe '#uid' do + context 'when there is extraneous (but valid) whitespace' do + let(:given_uid) { 'uid =John Smith , ou = People, dc= example,dc =com' } + + it 'removes the extraneous whitespace' do + expect(auth_hash.uid).to eq('uid=John Smith,ou=People,dc=example,dc=com') + end + end + end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 087c4d8c92c..74d6979cf61 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -16,6 +16,136 @@ describe Gitlab::LDAP::Person do ) end + describe '.normalize_dn' do + context 'when there is extraneous (but valid) whitespace' do + it 'removes the extraneous whitespace' do + given = 'uid =John Smith , ou = People, dc= example,dc =com' + expected = 'uid=John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + + context 'for a DN with a single RDN' do + it 'removes the extraneous whitespace' do + given = 'uid = John Smith' + expected = 'uid=John Smith' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there are escaped characters' do + it 'removes extraneous whitespace without changing the escaped characters' do + given = 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' + expected = 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'with a multivalued RDN' do + it 'removes extraneous whitespace without modifying the multivalued RDN' do + given = 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' + expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + + context 'with a telephoneNumber with a space after the plus sign' do + # I am not sure whether a space after the telephoneNumber plus sign is valid, + # and I am not sure if this is "proper" behavior under these conditions, and + # I am not sure if it matters to us or anyone else, so rather than dig + # through RFCs, I am only documenting the behavior here. + it 'removes the space after the plus sign in the telephoneNumber' do + given = 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' + expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + end + end + + context 'for a null DN (empty string)' do + it 'returns empty string and does not error' do + given = '' + expected = '' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped leading space in an attribute value' do + it 'does not remove the escaped leading space (and does not error like Net::LDAP::DN.new does)' do + given = 'uid=\\ John Smith,ou=People,dc=example,dc=com' + expected = 'uid=\\ John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped trailing space in an attribute value' do + it 'does not remove the escaped trailing space' do + given = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped leading newline in an attribute value' do + it 'does not remove the escaped leading newline' do + given = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + expected = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped trailing newline in an attribute value' do + it 'does not remove the escaped trailing newline' do + given = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an unescaped leading newline in an attribute value' do + it 'does not remove the unescaped leading newline' do + given = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + expected = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when there is an unescaped trailing newline in an attribute value' do + it 'does not remove the unescaped trailing newline' do + given = 'uid=John Smith\n ,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\n,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'with uppercase characters' do + # We may need to normalize casing at some point. + # I am just making it explicit that we don't at this time. + it 'returns the DN with unmodified casing' do + given = 'UID=John Smith,ou=People,dc=example,dc=com' + expected = 'UID=John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'with a malformed DN' do + context 'when passed a UID instead of a DN' do + it 'returns the UID (with whitespace stripped)' do + given = ' John C. Smith ' + expected = 'John C. Smith' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + + context 'when an equal sign is escaped' do + it 'returns the DN completely unmodified' do + given = 'uid= foo\\=bar' + expected = 'uid= foo\\=bar' + expect(described_class.normalize_dn(given)).to eq(expected) + end + end + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' -- cgit v1.2.1 From abe570cd0b00a6696a0bfa1c4223d9bbbff9b58f Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Sun, 17 Sep 2017 21:28:54 -0700 Subject: Refactor to distinguish between UIDs and DNs --- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/ldap/person.rb | 29 ++++++ spec/lib/gitlab/ldap/person_spec.rb | 170 ++++++++++++++++++++++++++++++++++-- 3 files changed, 192 insertions(+), 9 deletions(-) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 3123da17fd9..da75649d6d5 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -4,7 +4,7 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash def uid - Gitlab::LDAP::Person.normalize_dn(super) + Gitlab::LDAP::Person.normalize_uid_or_dn(super) end private diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 4299d35fabc..5c8924f1472 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -36,6 +36,35 @@ module Gitlab ] end + # Returns the UID or DN in a normalized form + def self.normalize_uid_or_dn(uid_or_dn) + if is_dn?(uid_or_dn) + normalize_dn(uid_or_dn) + else + normalize_uid(uid_or_dn) + end + end + + # Returns true if the string looks like a DN rather than a UID. + # + # An empty string is technically a valid DN (null DN), although we should + # never need to worry about that. + def self.is_dn?(uid_or_dn) + uid_or_dn.blank? || uid_or_dn.include?('=') + end + + # Returns the UID in a normalized form. + # + # 1. Excess spaces are stripped + # 2. The string is downcased (for case-insensitivity) + def self.normalize_uid(uid) + normalize_dn_part(uid) + end + + # Returns the DN in a normalized form. + # + # 1. Excess spaces around attribute names and values are stripped + # 2. The string is downcased (for case-insensitivity) def self.normalize_dn(dn) dn.split(/([,+=])/).map do |part| normalize_dn_part(part) diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 74d6979cf61..bae6a094ca8 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -16,6 +16,146 @@ describe Gitlab::LDAP::Person do ) end + describe '.normalize_uid_or_dn' do + context 'given a DN' do + context 'when there is extraneous (but valid) whitespace' do + it 'removes the extraneous whitespace' do + given = 'uid =John Smith , ou = People, dc= example,dc =com' + expected = 'uid=John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + + context 'for a DN with a single RDN' do + it 'removes the extraneous whitespace' do + given = 'uid = John Smith' + expected = 'uid=John Smith' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there are escaped characters' do + it 'removes extraneous whitespace without changing the escaped characters' do + given = 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' + expected = 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'with a multivalued RDN' do + it 'removes extraneous whitespace without modifying the multivalued RDN' do + given = 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' + expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + + context 'with a telephoneNumber with a space after the plus sign' do + # I am not sure whether a space after the telephoneNumber plus sign is valid, + # and I am not sure if this is "proper" behavior under these conditions, and + # I am not sure if it matters to us or anyone else, so rather than dig + # through RFCs, I am only documenting the behavior here. + it 'removes the space after the plus sign in the telephoneNumber' do + given = 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' + expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + end + end + + context 'for a null DN (empty string)' do + it 'returns empty string and does not error' do + given = '' + expected = '' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped leading space in an attribute value' do + it 'does not remove the escaped leading space (and does not error like Net::LDAP::DN.new does)' do + given = 'uid=\\ John Smith,ou=People,dc=example,dc=com' + expected = 'uid=\\ John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped trailing space in an attribute value' do + it 'does not remove the escaped trailing space' do + given = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped leading newline in an attribute value' do + it 'does not remove the escaped leading newline' do + given = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + expected = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an escaped trailing newline in an attribute value' do + it 'does not remove the escaped trailing newline' do + given = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an unescaped leading newline in an attribute value' do + it 'does not remove the unescaped leading newline' do + given = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + expected = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'when there is an unescaped trailing newline in an attribute value' do + it 'does not remove the unescaped trailing newline' do + given = 'uid=John Smith\n ,ou=People,dc=example,dc=com' + expected = 'uid=John Smith\n,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'with uppercase characters' do + # We may need to normalize casing at some point. + # I am just making it explicit that we don't at this time. + it 'returns the DN with unmodified casing' do + given = 'UID=John Smith,ou=People,dc=example,dc=com' + expected = 'UID=John Smith,ou=People,dc=example,dc=com' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + + context 'with a malformed DN' do + context 'when an equal sign is escaped' do + it 'returns the DN completely unmodified' do + given = 'uid= foo\\=bar' + expected = 'uid= foo\\=bar' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + end + end + + context 'given a UID' do + it 'returns the UID (with whitespace stripped)' do + given = ' John C. Smith ' + expected = 'John C. Smith' + expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + end + end + end + + describe '.normalize_uid' do + it 'returns the UID (with whitespace stripped)' do + given = ' John C. Smith ' + expected = 'John C. Smith' + expect(described_class.normalize_uid(given)).to eq(expected) + end + end + describe '.normalize_dn' do context 'when there is extraneous (but valid) whitespace' do it 'removes the extraneous whitespace' do @@ -128,14 +268,6 @@ describe Gitlab::LDAP::Person do end context 'with a malformed DN' do - context 'when passed a UID instead of a DN' do - it 'returns the UID (with whitespace stripped)' do - given = ' John C. Smith ' - expected = 'John C. Smith' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - context 'when an equal sign is escaped' do it 'returns the DN completely unmodified' do given = 'uid= foo\\=bar' @@ -146,6 +278,28 @@ describe Gitlab::LDAP::Person do end end + describe '.is_dn?' do + context 'given a DN' do + context 'with a single RDN' do + it 'returns true' do + expect(described_class.is_dn?('uid=John Smith')).to be_truthy + end + end + + context 'with multiple RDNs' do + it 'returns true' do + expect(described_class.is_dn?('uid=John Smith,ou=People,dc=example,dc=com')).to be_truthy + end + end + end + + context 'given a UID' do + it 'returns false' do + expect(described_class.is_dn?('John Smith')).to be_falsey + end + end + end + describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' -- cgit v1.2.1 From f1773640bf74125bb09fd5af8e780d2592e922f0 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Sun, 17 Sep 2017 22:28:34 -0700 Subject: Refactor spec --- spec/lib/gitlab/ldap/person_spec.rb | 359 +++++++++++------------------------- 1 file changed, 105 insertions(+), 254 deletions(-) diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index bae6a094ca8..80c24fde16a 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe Gitlab::LDAP::Person do + using RSpec::Parameterized::TableSyntax include LdapHelpers let(:entry) { ldap_user_entry('john.doe') } @@ -18,284 +19,129 @@ describe Gitlab::LDAP::Person do describe '.normalize_uid_or_dn' do context 'given a DN' do - context 'when there is extraneous (but valid) whitespace' do - it 'removes the extraneous whitespace' do - given = 'uid =John Smith , ou = People, dc= example,dc =com' - expected = 'uid=John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - - context 'for a DN with a single RDN' do - it 'removes the extraneous whitespace' do - given = 'uid = John Smith' - expected = 'uid=John Smith' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there are escaped characters' do - it 'removes extraneous whitespace without changing the escaped characters' do - given = 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' - expected = 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'with a multivalued RDN' do - it 'removes extraneous whitespace without modifying the multivalued RDN' do - given = 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' - expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - - context 'with a telephoneNumber with a space after the plus sign' do - # I am not sure whether a space after the telephoneNumber plus sign is valid, - # and I am not sure if this is "proper" behavior under these conditions, and - # I am not sure if it matters to us or anyone else, so rather than dig - # through RFCs, I am only documenting the behavior here. - it 'removes the space after the plus sign in the telephoneNumber' do - given = 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' - expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - end - end - - context 'for a null DN (empty string)' do - it 'returns empty string and does not error' do - given = '' - expected = '' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped leading space in an attribute value' do - it 'does not remove the escaped leading space (and does not error like Net::LDAP::DN.new does)' do - given = 'uid=\\ John Smith,ou=People,dc=example,dc=com' - expected = 'uid=\\ John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped trailing space in an attribute value' do - it 'does not remove the escaped trailing space' do - given = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped leading newline in an attribute value' do - it 'does not remove the escaped leading newline' do - given = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - expected = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped trailing newline in an attribute value' do - it 'does not remove the escaped trailing newline' do - given = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an unescaped leading newline in an attribute value' do - it 'does not remove the unescaped leading newline' do - given = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - expected = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end - - context 'when there is an unescaped trailing newline in an attribute value' do - it 'does not remove the unescaped trailing newline' do - given = 'uid=John Smith\n ,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\n,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) + # Regarding the telephoneNumber test: + # + # I am not sure whether a space after the telephoneNumber plus sign is valid, + # and I am not sure if this is "proper" behavior under these conditions, and + # I am not sure if it matters to us or anyone else, so rather than dig + # through RFCs, I am only documenting the behavior here. + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=John Smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'for a null DN (empty string), returns empty string and does not error' | '' | '' + 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=John Smith\n,ou=People,dc=example,dc=com' + 'does not modify casing' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'UID=John Smith,ou=People,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'for a malformed DN (when an equal sign is escaped), returns the DN completely unmodified' | 'uid= foo\\=bar' | 'uid= foo\\=bar' + end + + with_them do + it 'normalizes the DN' do + assert_generic_test(test_description, described_class.normalize_uid_or_dn(given), expected) end end + end - context 'with uppercase characters' do - # We may need to normalize casing at some point. - # I am just making it explicit that we don't at this time. - it 'returns the DN with unmodified casing' do - given = 'UID=John Smith,ou=People,dc=example,dc=com' - expected = 'UID=John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end + context 'given a UID' do + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | ' John C. Smith ' | 'John C. Smith' + 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' + 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ John Smith' + 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'John Smith\\ ' + 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\nJohn Smith' + 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'John Smith\\\n' + 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\nJohn Smith' + 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'John Smith\n' + 'does not modify casing' | ' John Smith ' | 'John Smith' + 'does not strip non whitespace' | 'John Smith' | 'John Smith' end - context 'with a malformed DN' do - context 'when an equal sign is escaped' do - it 'returns the DN completely unmodified' do - given = 'uid= foo\\=bar' - expected = 'uid= foo\\=bar' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end + with_them do + it 'normalizes the UID' do + assert_generic_test(test_description, described_class.normalize_uid_or_dn(given), expected) end end end - - context 'given a UID' do - it 'returns the UID (with whitespace stripped)' do - given = ' John C. Smith ' - expected = 'John C. Smith' - expect(described_class.normalize_uid_or_dn(given)).to eq(expected) - end - end end describe '.normalize_uid' do - it 'returns the UID (with whitespace stripped)' do - given = ' John C. Smith ' - expected = 'John C. Smith' - expect(described_class.normalize_uid(given)).to eq(expected) - end - end - - describe '.normalize_dn' do - context 'when there is extraneous (but valid) whitespace' do - it 'removes the extraneous whitespace' do - given = 'uid =John Smith , ou = People, dc= example,dc =com' - expected = 'uid=John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - - context 'for a DN with a single RDN' do - it 'removes the extraneous whitespace' do - given = 'uid = John Smith' - expected = 'uid=John Smith' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there are escaped characters' do - it 'removes extraneous whitespace without changing the escaped characters' do - given = 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' - expected = 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end + context 'given a UID' do + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | ' John C. Smith ' | 'John C. Smith' + 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' + 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ John Smith' + 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'John Smith\\ ' + 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\nJohn Smith' + 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'John Smith\\\n' + 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\nJohn Smith' + 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'John Smith\n' + 'does not modify casing' | ' John Smith ' | 'John Smith' + 'does not strip non whitespace' | 'John Smith' | 'John Smith' end - context 'with a multivalued RDN' do - it 'removes extraneous whitespace without modifying the multivalued RDN' do - given = 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' - expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - - context 'with a telephoneNumber with a space after the plus sign' do - # I am not sure whether a space after the telephoneNumber plus sign is valid, - # and I am not sure if this is "proper" behavior under these conditions, and - # I am not sure if it matters to us or anyone else, so rather than dig - # through RFCs, I am only documenting the behavior here. - it 'removes the space after the plus sign in the telephoneNumber' do - given = 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' - expected = 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end + with_them do + it 'normalizes the UID' do + assert_generic_test(test_description, described_class.normalize_uid(given), expected) end end end + end - context 'for a null DN (empty string)' do - it 'returns empty string and does not error' do - given = '' - expected = '' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped leading space in an attribute value' do - it 'does not remove the escaped leading space (and does not error like Net::LDAP::DN.new does)' do - given = 'uid=\\ John Smith,ou=People,dc=example,dc=com' - expected = 'uid=\\ John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped trailing space in an attribute value' do - it 'does not remove the escaped trailing space' do - given = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped leading newline in an attribute value' do - it 'does not remove the escaped leading newline' do - given = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - expected = 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an escaped trailing newline in an attribute value' do - it 'does not remove the escaped trailing newline' do - given = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an unescaped leading newline in an attribute value' do - it 'does not remove the unescaped leading newline' do - given = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - expected = 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'when there is an unescaped trailing newline in an attribute value' do - it 'does not remove the unescaped trailing newline' do - given = 'uid=John Smith\n ,ou=People,dc=example,dc=com' - expected = 'uid=John Smith\n,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'with uppercase characters' do - # We may need to normalize casing at some point. - # I am just making it explicit that we don't at this time. - it 'returns the DN with unmodified casing' do - given = 'UID=John Smith,ou=People,dc=example,dc=com' - expected = 'UID=John Smith,ou=People,dc=example,dc=com' - expect(described_class.normalize_dn(given)).to eq(expected) - end - end - - context 'with a malformed DN' do - context 'when an equal sign is escaped' do - it 'returns the DN completely unmodified' do - given = 'uid= foo\\=bar' - expected = 'uid= foo\\=bar' - expect(described_class.normalize_dn(given)).to eq(expected) - end + describe '.normalize_dn' do + # Regarding the telephoneNumber test: + # + # I am not sure whether a space after the telephoneNumber plus sign is valid, + # and I am not sure if this is "proper" behavior under these conditions, and + # I am not sure if it matters to us or anyone else, so rather than dig + # through RFCs, I am only documenting the behavior here. + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=John Smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'for a null DN (empty string), returns empty string and does not error' | '' | '' + 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' + 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' + 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' + 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=John Smith\n,ou=People,dc=example,dc=com' + 'does not modify casing' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'UID=John Smith,ou=People,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'for a malformed DN (when an equal sign is escaped), returns the DN completely unmodified' | 'uid= foo\\=bar' | 'uid= foo\\=bar' + end + + with_them do + it 'normalizes the DN' do + assert_generic_test(test_description, described_class.normalize_dn(given), expected) end end end describe '.is_dn?' do - context 'given a DN' do - context 'with a single RDN' do - it 'returns true' do - expect(described_class.is_dn?('uid=John Smith')).to be_truthy - end - end - - context 'with multiple RDNs' do - it 'returns true' do - expect(described_class.is_dn?('uid=John Smith,ou=People,dc=example,dc=com')).to be_truthy - end - end + where(:test_description, :given, :expected) do + 'given a DN with a single RDN' | 'uid=John C. Smith' | true + 'given a DN with multiple RDNs' | 'uid=John C. Smith,ou=People,dc=example,dc=com' | true + 'given a UID' | 'John C. Smith' | false + 'given a DN with a single RDN with excess spaces' | ' uid=John C. Smith ' | true + 'given a DN with multiple RDNs with excess spaces' | ' uid=John C. Smith,ou=People,dc=example,dc=com ' | true + 'given a UID with excess spaces' | ' John C. Smith ' | false + 'given a DN with an escaped equal sign' | 'uid=John C. Smith,ou=People\\=' | true + 'given a DN with an equal sign in escaped hex' | 'uid=John C. Smith,ou=People\\3D' | true end - context 'given a UID' do - it 'returns false' do - expect(described_class.is_dn?('John Smith')).to be_falsey + with_them do + it 'returns the expected boolean' do + assert_generic_test(test_description, described_class.is_dn?(given), expected) end end end @@ -327,4 +173,9 @@ describe Gitlab::LDAP::Person do expect(person.email).to eq([user_principal_name]) end end + + def assert_generic_test(test_description, got, expected) + test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" + expect(got).to eq(expected), test_failure_message + end end -- cgit v1.2.1 From 4ae32d9577d63e95c7d924cb72cce2e7b8fbdf47 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Sun, 17 Sep 2017 23:09:36 -0700 Subject: Fix normalize behavior for escaped delimiter chars --- lib/gitlab/ldap/person.rb | 2 +- spec/lib/gitlab/ldap/person_spec.rb | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 5c8924f1472..267514d0fcd 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -66,7 +66,7 @@ module Gitlab # 1. Excess spaces around attribute names and values are stripped # 2. The string is downcased (for case-insensitivity) def self.normalize_dn(dn) - dn.split(/([,+=])/).map do |part| + dn.split(/(? Date: Sun, 17 Sep 2017 23:13:45 -0700 Subject: Downcase normalized LDAP DNs and UIDs --- lib/gitlab/ldap/person.rb | 2 +- spec/lib/gitlab/ldap/auth_hash_spec.rb | 12 +++- spec/lib/gitlab/ldap/person_spec.rb | 116 ++++++++++++++++----------------- 3 files changed, 69 insertions(+), 61 deletions(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 267514d0fcd..d2d6aedba0f 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -100,7 +100,7 @@ module Gitlab private def self.normalize_dn_part(part) - cleaned = part.strip + cleaned = part.strip.downcase if cleaned.ends_with?('\\') # If it ends with an escape character that is not followed by a diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb index a4bd40705df..1785094af10 100644 --- a/spec/lib/gitlab/ldap/auth_hash_spec.rb +++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb @@ -68,10 +68,18 @@ describe Gitlab::LDAP::AuthHash do describe '#uid' do context 'when there is extraneous (but valid) whitespace' do - let(:given_uid) { 'uid =John Smith , ou = People, dc= example,dc =com' } + let(:given_uid) { 'uid =john smith , ou = people, dc= example,dc =com' } it 'removes the extraneous whitespace' do - expect(auth_hash.uid).to eq('uid=John Smith,ou=People,dc=example,dc=com') + expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com') + end + end + + context 'when there are upper case characters' do + let(:given_uid) { 'UID=John Smith,ou=People,dc=example,dc=com' } + + it 'downcases' do + expect(auth_hash.uid).to eq('uid=john smith,ou=people,dc=example,dc=com') end end end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 58e63b52631..c2294e63171 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -26,24 +26,24 @@ describe Gitlab::LDAP::Person do # I am not sure if it matters to us or anyone else, so rather than dig # through RFCs, I am only documenting the behavior here. where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=John Smith,ou=People,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=John Smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' - 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=John Smith\n,ou=People,dc=example,dc=com' - 'does not modify casing' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'UID=John Smith,ou=People,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' + 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' + 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' + 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3D bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=John C. Smith,ou=San Francisco\\, CA' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=John C. Smith,ou=San Francisco\\2C CA' + 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' end with_them do @@ -55,20 +55,20 @@ describe Gitlab::LDAP::Person do context 'given a UID' do where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | ' John C. Smith ' | 'John C. Smith' - 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' - 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ John Smith' - 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'John Smith\\ ' - 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\nJohn Smith' - 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'John Smith\\\n' - 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\nJohn Smith' - 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'John Smith\n' - 'does not modify casing' | ' John Smith ' | 'John Smith' - 'does not strip non whitespace' | 'John Smith' | 'John Smith' + 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' + 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' + 'downcases the whole string' | 'John Smith' | 'john smith' + 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' + 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' + 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' + 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' + 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' + 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' + 'does not strip non whitespace' | 'John Smith' | 'john smith' 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3D bar' - 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'Smith\\, John C.' - 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'Smith\\2C John C.' + 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' + 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' end with_them do @@ -82,20 +82,20 @@ describe Gitlab::LDAP::Person do describe '.normalize_uid' do context 'given a UID' do where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | ' John C. Smith ' | 'John C. Smith' - 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' - 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ John Smith' - 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'John Smith\\ ' - 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\nJohn Smith' - 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'John Smith\\\n' - 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\nJohn Smith' - 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'John Smith\n' - 'does not modify casing' | ' John Smith ' | 'John Smith' - 'does not strip non whitespace' | 'John Smith' | 'John Smith' + 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' + 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' + 'downcases the whole string' | 'John Smith' | 'john smith' + 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' + 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' + 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' + 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' + 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' + 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' + 'does not strip non whitespace' | 'John Smith' | 'john smith' 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3D bar' - 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'Smith\\, John C.' - 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'Smith\\2C John C.' + 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' + 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' end with_them do @@ -114,24 +114,24 @@ describe Gitlab::LDAP::Person do # I am not sure if it matters to us or anyone else, so rather than dig # through RFCs, I am only documenting the behavior here. where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=John Smith,ou=People,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=John Smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=Sebasti\\c3\\a1n\\ C.\\20Smith\\ ,ou=People (aka. \\22humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=John Smith+telephoneNumber=+1 555-555-5555,ou=People,dc=example,dc=com' + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' - 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' - 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' - 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' - 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' - 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=John Smith\n,ou=People,dc=example,dc=com' - 'does not modify casing' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'UID=John Smith,ou=People,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=John Smith,ou=People,dc=example,dc=com' + 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' + 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' + 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' + 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3D bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=John C. Smith,ou=San Francisco\\, CA' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=John C. Smith,ou=San Francisco\\2C CA' + 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' end with_them do -- cgit v1.2.1 From 3e83ba34d18b93dd66b7a910db40c49d0b17b659 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Sun, 17 Sep 2017 23:20:00 -0700 Subject: Dry up spec some more MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit …to stop copy pasting test cases. --- spec/lib/gitlab/ldap/person_spec.rb | 137 ++++++++++++------------------------ 1 file changed, 46 insertions(+), 91 deletions(-) diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index c2294e63171..bbf792462e9 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -17,96 +17,7 @@ describe Gitlab::LDAP::Person do ) end - describe '.normalize_uid_or_dn' do - context 'given a DN' do - # Regarding the telephoneNumber test: - # - # I am not sure whether a space after the telephoneNumber plus sign is valid, - # and I am not sure if this is "proper" behavior under these conditions, and - # I am not sure if it matters to us or anyone else, so rather than dig - # through RFCs, I am only documenting the behavior here. - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' - 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' - 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' - 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' - 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' - end - - with_them do - it 'normalizes the DN' do - assert_generic_test(test_description, described_class.normalize_uid_or_dn(given), expected) - end - end - end - - context 'given a UID' do - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' - 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' - 'downcases the whole string' | 'John Smith' | 'john smith' - 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' - 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' - 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' - 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' - 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' - 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' - 'does not strip non whitespace' | 'John Smith' | 'john smith' - 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' - 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' - end - - with_them do - it 'normalizes the UID' do - assert_generic_test(test_description, described_class.normalize_uid_or_dn(given), expected) - end - end - end - end - - describe '.normalize_uid' do - context 'given a UID' do - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' - 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' - 'downcases the whole string' | 'John Smith' | 'john smith' - 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' - 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' - 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' - 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' - 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' - 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' - 'does not strip non whitespace' | 'John Smith' | 'john smith' - 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' - 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' - end - - with_them do - it 'normalizes the UID' do - assert_generic_test(test_description, described_class.normalize_uid(given), expected) - end - end - end - end - - describe '.normalize_dn' do + shared_examples_for 'normalizes the DN' do # Regarding the telephoneNumber test: # # I am not sure whether a space after the telephoneNumber plus sign is valid, @@ -136,11 +47,55 @@ describe Gitlab::LDAP::Person do with_them do it 'normalizes the DN' do - assert_generic_test(test_description, described_class.normalize_dn(given), expected) + assert_generic_test(test_description, subject, expected) end end end + shared_examples_for 'normalizes the UID' do + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' + 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' + 'downcases the whole string' | 'John Smith' | 'john smith' + 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' + 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' + 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' + 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' + 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' + 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' + 'does not strip non whitespace' | 'John Smith' | 'john smith' + 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' + 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' + 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' + end + + with_them do + it 'normalizes the UID' do + assert_generic_test(test_description, subject, expected) + end + end + end + + describe '.normalize_uid_or_dn' do + subject { described_class.normalize_uid_or_dn(given) } + + it_behaves_like 'normalizes the DN' + it_behaves_like 'normalizes the UID' + end + + describe '.normalize_uid' do + subject { described_class.normalize_uid(given) } + + it_behaves_like 'normalizes the UID' + end + + describe '.normalize_dn' do + subject { described_class.normalize_dn(given) } + + it_behaves_like 'normalizes the DN' + end + describe '.is_dn?' do where(:test_description, :given, :expected) do 'given a DN with a single RDN' | 'uid=John C. Smith' | true -- cgit v1.2.1 From fee3c95d755182edd50168785789c8b954f12927 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 18 Sep 2017 12:30:50 -0700 Subject: Remove redundant `is_` --- lib/gitlab/ldap/person.rb | 4 ++-- spec/lib/gitlab/ldap/person_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index d2d6aedba0f..a4954c3fd71 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -38,7 +38,7 @@ module Gitlab # Returns the UID or DN in a normalized form def self.normalize_uid_or_dn(uid_or_dn) - if is_dn?(uid_or_dn) + if dn?(uid_or_dn) normalize_dn(uid_or_dn) else normalize_uid(uid_or_dn) @@ -49,7 +49,7 @@ module Gitlab # # An empty string is technically a valid DN (null DN), although we should # never need to worry about that. - def self.is_dn?(uid_or_dn) + def self.dn?(uid_or_dn) uid_or_dn.blank? || uid_or_dn.include?('=') end diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index bbf792462e9..c83e2b0898e 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -96,7 +96,7 @@ describe Gitlab::LDAP::Person do it_behaves_like 'normalizes the DN' end - describe '.is_dn?' do + describe '.dn?' do where(:test_description, :given, :expected) do 'given a DN with a single RDN' | 'uid=John C. Smith' | true 'given a DN with multiple RDNs' | 'uid=John C. Smith,ou=People,dc=example,dc=com' | true @@ -110,7 +110,7 @@ describe Gitlab::LDAP::Person do with_them do it 'returns the expected boolean' do - assert_generic_test(test_description, described_class.is_dn?(given), expected) + assert_generic_test(test_description, described_class.dn?(given), expected) end end end -- cgit v1.2.1 From ca5ade22f3d755ad47889e41b77d8e705b6e2ccb Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 18 Sep 2017 16:08:25 -0700 Subject: Fix `dn?` for a UID with an escaped equal sign --- lib/gitlab/ldap/person.rb | 2 +- spec/lib/gitlab/ldap/person_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index a4954c3fd71..ca96a099714 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -50,7 +50,7 @@ module Gitlab # An empty string is technically a valid DN (null DN), although we should # never need to worry about that. def self.dn?(uid_or_dn) - uid_or_dn.blank? || uid_or_dn.include?('=') + uid_or_dn.blank? || !!uid_or_dn.match(/(? Date: Mon, 18 Sep 2017 16:16:14 -0700 Subject: Note invalid DNs --- spec/lib/gitlab/ldap/person_spec.rb | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 663a6ccead1..5f5ac990c55 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -25,24 +25,24 @@ describe Gitlab::LDAP::Person do # I am not sure if it matters to us or anyone else, so rather than dig # through RFCs, I am only documenting the behavior here. where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip the escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' - 'does not strip the escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'does not strip the escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' - 'does not strip the escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' - 'does not strip the unescaped leading newline in an attribute value' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' - 'does not strip the unescaped trailing newline in an attribute value' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'for a null DN (empty string), returns empty string and does not error' | '' | '' + 'does not strip an escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' + 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' + 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' + 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' + 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' end with_them do -- cgit v1.2.1 From 010cd3dea8d8493559e2a6840b882aa4e5cce55c Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 18 Sep 2017 16:40:04 -0700 Subject: Rescue DN normalization attempts --- lib/gitlab/ldap/person.rb | 15 +++++++++++++++ spec/lib/gitlab/ldap/person_spec.rb | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index ca96a099714..5b0d011ee00 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -43,6 +43,11 @@ module Gitlab else normalize_uid(uid_or_dn) end + rescue StandardError => e + Rails.logger.info("Returning original DN \"#{uid_or_dn}\" due to error during normalization attempt: #{e.message}") + Rails.logger.info(e.backtrace.join("\n")) + + uid_or_dn end # Returns true if the string looks like a DN rather than a UID. @@ -59,6 +64,11 @@ module Gitlab # 2. The string is downcased (for case-insensitivity) def self.normalize_uid(uid) normalize_dn_part(uid) + rescue StandardError => e + Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") + Rails.logger.info(e.backtrace.join("\n")) + + uid end # Returns the DN in a normalized form. @@ -69,6 +79,11 @@ module Gitlab dn.split(/(? e + Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") + Rails.logger.info(e.backtrace.join("\n")) + + dn end def initialize(entry, provider) diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 5f5ac990c55..f561fc18f2a 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -82,18 +82,42 @@ describe Gitlab::LDAP::Person do it_behaves_like 'normalizes the DN' it_behaves_like 'normalizes the UID' + + context 'with an exception during normalization' do + let(:given) { described_class } # just something that will cause an exception + + it 'returns the given object unmodified' do + expect(subject).to eq(given) + end + end end describe '.normalize_uid' do subject { described_class.normalize_uid(given) } it_behaves_like 'normalizes the UID' + + context 'with an exception during normalization' do + let(:given) { described_class } # just something that will cause an exception + + it 'returns the given UID unmodified' do + expect(subject).to eq(given) + end + end end describe '.normalize_dn' do subject { described_class.normalize_dn(given) } it_behaves_like 'normalizes the DN' + + context 'with an exception during normalization' do + let(:given) { described_class } # just something that will cause an exception + + it 'returns the given DN unmodified' do + expect(subject).to eq(given) + end + end end describe '.dn?' do -- cgit v1.2.1 From aefc96ca27287cc5d23653606c2cc27114b8fa09 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 19 Sep 2017 10:38:18 -0700 Subject: Rely on LDAP providers giving DNs, not UIDs --- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/ldap/person.rb | 22 ---------------------- spec/lib/gitlab/ldap/person_spec.rb | 35 ----------------------------------- 3 files changed, 1 insertion(+), 58 deletions(-) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index da75649d6d5..3123da17fd9 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -4,7 +4,7 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash def uid - Gitlab::LDAP::Person.normalize_uid_or_dn(super) + Gitlab::LDAP::Person.normalize_dn(super) end private diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 5b0d011ee00..6b1a308d521 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -36,28 +36,6 @@ module Gitlab ] end - # Returns the UID or DN in a normalized form - def self.normalize_uid_or_dn(uid_or_dn) - if dn?(uid_or_dn) - normalize_dn(uid_or_dn) - else - normalize_uid(uid_or_dn) - end - rescue StandardError => e - Rails.logger.info("Returning original DN \"#{uid_or_dn}\" due to error during normalization attempt: #{e.message}") - Rails.logger.info(e.backtrace.join("\n")) - - uid_or_dn - end - - # Returns true if the string looks like a DN rather than a UID. - # - # An empty string is technically a valid DN (null DN), although we should - # never need to worry about that. - def self.dn?(uid_or_dn) - uid_or_dn.blank? || !!uid_or_dn.match(/(? Date: Wed, 20 Sep 2017 10:57:50 -0700 Subject: Extract Net::LDAP::DN class from ruby-net-ldap gem --- lib/gitlab/ldap/dn.rb | 224 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 224 insertions(+) create mode 100644 lib/gitlab/ldap/dn.rb diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb new file mode 100644 index 00000000000..e314b80e578 --- /dev/null +++ b/lib/gitlab/ldap/dn.rb @@ -0,0 +1,224 @@ +# -*- ruby encoding: utf-8 -*- + +## +# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN +# ("Distinguished Name") is a unique identifier for an entry within an LDAP +# directory. It is made up of a number of other attributes strung together, +# to identify the entry in the tree. +# +# Each attribute that makes up a DN needs to have its value escaped so that +# the DN is valid. This class helps take care of that. +# +# A fully escaped DN needs to be unescaped when analysing its contents. This +# class also helps take care of that. +class Net::LDAP::DN + ## + # Initialize a DN, escaping as required. Pass in attributes in name/value + # pairs. If there is a left over argument, it will be appended to the dn + # without escaping (useful for a base string). + # + # Most uses of this class will be to escape a DN, rather than to parse it, + # so storing the dn as an escaped String and parsing parts as required + # with a state machine seems sensible. + def initialize(*args) + buffer = StringIO.new + + args.each_index do |index| + buffer << "=" if index % 2 == 1 + buffer << "," if index % 2 == 0 && index != 0 + + if index < args.length - 1 || index % 2 == 1 + buffer << Net::LDAP::DN.escape(args[index]) + else + buffer << args[index] + end + end + + @dn = buffer.string + end + + ## + # Parse a DN into key value pairs using ASN from + # http://tools.ietf.org/html/rfc2253 section 3. + def each_pair + state = :key + key = StringIO.new + value = StringIO.new + hex_buffer = "" + + @dn.each_char do |char| + case state + when :key then + case char + when 'a'..'z', 'A'..'Z' then + state = :key_normal + key << char + when '0'..'9' then + state = :key_oid + key << char + when ' ' then state = :key + else raise "DN badly formed" + end + when :key_normal then + case char + when '=' then state = :value + when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char + else raise "DN badly formed" + end + when :key_oid then + case char + when '=' then state = :value + when '0'..'9', '.', ' ' then key << char + else raise "DN badly formed" + end + when :value then + case char + when '\\' then state = :value_normal_escape + when '"' then state = :value_quoted + when ' ' then state = :value + when '#' then + state = :value_hexstring + value << char + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else + state = :value_normal + value << char + end + when :value_normal then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else value << char + end + when :value_normal_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal_escape_hex + hex_buffer = char + else state = :value_normal; value << char + end + when :value_normal_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise "DN badly formed" + end + when :value_quoted then + case char + when '\\' then state = :value_quoted_escape + when '"' then state = :value_end + else value << char + end + when :value_quoted_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted_escape_hex + hex_buffer = char + else + state = :value_quoted; + value << char + end + when :value_quoted_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise "DN badly formed" + end + when :value_hexstring then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring_hex + value << char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else raise "DN badly formed" + end + when :value_hexstring_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring + value << char + else raise "DN badly formed" + end + when :value_end then + case char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else raise "DN badly formed" + end + else raise "Fell out of state machine" + end + end + + # Last pair + raise "DN badly formed" unless + [:value, :value_normal, :value_hexstring, :value_end].include? state + + yield key.string.strip, value.string.rstrip + end + + ## + # Returns the DN as an array in the form expected by the constructor. + def to_a + a = [] + self.each_pair { |key, value| a << key << value } + a + end + + ## + # Return the DN as an escaped string. + def to_s + @dn + end + + # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions + # for dn values. All of the following must be escaped in any normal string + # using a single backslash ('\') as escape. + ESCAPES = { + ',' => ',', + '+' => '+', + '"' => '"', + '\\' => '\\', + '<' => '<', + '>' => '>', + ';' => ';', + } + + # Compiled character class regexp using the keys from the above hash, and + # checking for a space or # at the start, or space at the end, of the + # string. + ESCAPE_RE = Regexp.new("(^ |^#| $|[" + + ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "])") + + ## + # Escape a string for use in a DN value + def self.escape(string) + string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } + end + + ## + # Proxy all other requests to the string object, because a DN is mainly + # used within the library as a string + def method_missing(method, *args, &block) + @dn.send(method, *args, &block) + end +end -- cgit v1.2.1 From 2f11db4b005f67fe7687dd15267062556e8431ad Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 11:01:11 -0700 Subject: Adapt DN class for Gitlab --- lib/gitlab/ldap/dn.rb | 402 ++++++++++++++++++++++++++------------------------ 1 file changed, 207 insertions(+), 195 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index e314b80e578..038476b2d2a 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -1,5 +1,13 @@ # -*- ruby encoding: utf-8 -*- +# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN` +# +# For our purposes, this class is used to normalize DNs in order to allow proper +# comparison. +# +# E.g. DNs should be compared case-insensitively (in basically all LDAP +# implementations or setups), therefore we downcase every DN. + ## # Objects of this class represent an LDAP DN ("Distinguished Name"). A DN # ("Distinguished Name") is a unique identifier for an entry within an LDAP @@ -11,214 +19,218 @@ # # A fully escaped DN needs to be unescaped when analysing its contents. This # class also helps take care of that. -class Net::LDAP::DN - ## - # Initialize a DN, escaping as required. Pass in attributes in name/value - # pairs. If there is a left over argument, it will be appended to the dn - # without escaping (useful for a base string). - # - # Most uses of this class will be to escape a DN, rather than to parse it, - # so storing the dn as an escaped String and parsing parts as required - # with a state machine seems sensible. - def initialize(*args) - buffer = StringIO.new +module Gitlab + module LDAP + class DN + ## + # Initialize a DN, escaping as required. Pass in attributes in name/value + # pairs. If there is a left over argument, it will be appended to the dn + # without escaping (useful for a base string). + # + # Most uses of this class will be to escape a DN, rather than to parse it, + # so storing the dn as an escaped String and parsing parts as required + # with a state machine seems sensible. + def initialize(*args) + buffer = StringIO.new - args.each_index do |index| - buffer << "=" if index % 2 == 1 - buffer << "," if index % 2 == 0 && index != 0 + args.each_index do |index| + buffer << "=" if index % 2 == 1 + buffer << "," if index % 2 == 0 && index != 0 - if index < args.length - 1 || index % 2 == 1 - buffer << Net::LDAP::DN.escape(args[index]) - else - buffer << args[index] - end - end + if index < args.length - 1 || index % 2 == 1 + buffer << Net::LDAP::DN.escape(args[index]) + else + buffer << args[index] + end + end - @dn = buffer.string - end + @dn = buffer.string + end - ## - # Parse a DN into key value pairs using ASN from - # http://tools.ietf.org/html/rfc2253 section 3. - def each_pair - state = :key - key = StringIO.new - value = StringIO.new - hex_buffer = "" + ## + # Parse a DN into key value pairs using ASN from + # http://tools.ietf.org/html/rfc2253 section 3. + def each_pair + state = :key + key = StringIO.new + value = StringIO.new + hex_buffer = "" - @dn.each_char do |char| - case state - when :key then - case char - when 'a'..'z', 'A'..'Z' then - state = :key_normal - key << char - when '0'..'9' then - state = :key_oid - key << char - when ' ' then state = :key - else raise "DN badly formed" - end - when :key_normal then - case char - when '=' then state = :value - when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char - else raise "DN badly formed" - end - when :key_oid then - case char - when '=' then state = :value - when '0'..'9', '.', ' ' then key << char - else raise "DN badly formed" - end - when :value then - case char - when '\\' then state = :value_normal_escape - when '"' then state = :value_quoted - when ' ' then state = :value - when '#' then - state = :value_hexstring - value << char - when ',' then - state = :key - yield key.string.strip, value.string.rstrip - key = StringIO.new - value = StringIO.new; - else - state = :value_normal - value << char - end - when :value_normal then - case char - when '\\' then state = :value_normal_escape - when ',' then - state = :key - yield key.string.strip, value.string.rstrip - key = StringIO.new - value = StringIO.new; - else value << char - end - when :value_normal_escape then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_normal_escape_hex - hex_buffer = char - else state = :value_normal; value << char - end - when :value_normal_escape_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_normal - value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" - end - when :value_quoted then - case char - when '\\' then state = :value_quoted_escape - when '"' then state = :value_end - else value << char - end - when :value_quoted_escape then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_quoted_escape_hex - hex_buffer = char - else - state = :value_quoted; - value << char + @dn.each_char do |char| + case state + when :key then + case char + when 'a'..'z', 'A'..'Z' then + state = :key_normal + key << char + when '0'..'9' then + state = :key_oid + key << char + when ' ' then state = :key + else raise "DN badly formed" + end + when :key_normal then + case char + when '=' then state = :value + when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char + else raise "DN badly formed" + end + when :key_oid then + case char + when '=' then state = :value + when '0'..'9', '.', ' ' then key << char + else raise "DN badly formed" + end + when :value then + case char + when '\\' then state = :value_normal_escape + when '"' then state = :value_quoted + when ' ' then state = :value + when '#' then + state = :value_hexstring + value << char + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else + state = :value_normal + value << char + end + when :value_normal then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else value << char + end + when :value_normal_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal_escape_hex + hex_buffer = char + else state = :value_normal; value << char + end + when :value_normal_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise "DN badly formed" + end + when :value_quoted then + case char + when '\\' then state = :value_quoted_escape + when '"' then state = :value_end + else value << char + end + when :value_quoted_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted_escape_hex + hex_buffer = char + else + state = :value_quoted; + value << char + end + when :value_quoted_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise "DN badly formed" + end + when :value_hexstring then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring_hex + value << char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else raise "DN badly formed" + end + when :value_hexstring_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring + value << char + else raise "DN badly formed" + end + when :value_end then + case char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new; + else raise "DN badly formed" + end + else raise "Fell out of state machine" + end end - when :value_quoted_escape_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_quoted - value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" - end - when :value_hexstring then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_hexstring_hex - value << char - when ' ' then state = :value_end - when ',' then - state = :key - yield key.string.strip, value.string.rstrip - key = StringIO.new - value = StringIO.new; - else raise "DN badly formed" - end - when :value_hexstring_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_hexstring - value << char - else raise "DN badly formed" - end - when :value_end then - case char - when ' ' then state = :value_end - when ',' then - state = :key - yield key.string.strip, value.string.rstrip - key = StringIO.new - value = StringIO.new; - else raise "DN badly formed" - end - else raise "Fell out of state machine" - end - end - # Last pair - raise "DN badly formed" unless - [:value, :value_normal, :value_hexstring, :value_end].include? state + # Last pair + raise "DN badly formed" unless + [:value, :value_normal, :value_hexstring, :value_end].include? state - yield key.string.strip, value.string.rstrip - end + yield key.string.strip, value.string.rstrip + end - ## - # Returns the DN as an array in the form expected by the constructor. - def to_a - a = [] - self.each_pair { |key, value| a << key << value } - a - end + ## + # Returns the DN as an array in the form expected by the constructor. + def to_a + a = [] + self.each_pair { |key, value| a << key << value } + a + end - ## - # Return the DN as an escaped string. - def to_s - @dn - end + ## + # Return the DN as an escaped string. + def to_s + @dn + end - # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions - # for dn values. All of the following must be escaped in any normal string - # using a single backslash ('\') as escape. - ESCAPES = { - ',' => ',', - '+' => '+', - '"' => '"', - '\\' => '\\', - '<' => '<', - '>' => '>', - ';' => ';', - } + # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions + # for dn values. All of the following must be escaped in any normal string + # using a single backslash ('\') as escape. + ESCAPES = { + ',' => ',', + '+' => '+', + '"' => '"', + '\\' => '\\', + '<' => '<', + '>' => '>', + ';' => ';', + } - # Compiled character class regexp using the keys from the above hash, and - # checking for a space or # at the start, or space at the end, of the - # string. - ESCAPE_RE = Regexp.new("(^ |^#| $|[" + - ESCAPES.keys.map { |e| Regexp.escape(e) }.join + - "])") + # Compiled character class regexp using the keys from the above hash, and + # checking for a space or # at the start, or space at the end, of the + # string. + ESCAPE_RE = Regexp.new("(^ |^#| $|[" + + ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "])") - ## - # Escape a string for use in a DN value - def self.escape(string) - string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } - end + ## + # Escape a string for use in a DN value + def self.escape(string) + string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } + end - ## - # Proxy all other requests to the string object, because a DN is mainly - # used within the library as a string - def method_missing(method, *args, &block) - @dn.send(method, *args, &block) + ## + # Proxy all other requests to the string object, because a DN is mainly + # used within the library as a string + def method_missing(method, *args, &block) + @dn.send(method, *args, &block) + end + end end end -- cgit v1.2.1 From 91f2492a786bbe697b1f68e7b15090700a4c08a2 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 11:02:25 -0700 Subject: Add `DN#to_s_normalized` --- lib/gitlab/ldap/dn.rb | 8 +++++- spec/lib/gitlab/ldap/dn_spec.rb | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 spec/lib/gitlab/ldap/dn_spec.rb diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 038476b2d2a..0a49d5e4ca8 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -38,7 +38,7 @@ module Gitlab buffer << "," if index % 2 == 0 && index != 0 if index < args.length - 1 || index % 2 == 1 - buffer << Net::LDAP::DN.escape(args[index]) + buffer << self.class.escape(args[index]) else buffer << args[index] end @@ -199,6 +199,12 @@ module Gitlab @dn end + ## + # Return the DN as an escaped and normalized string. + def to_s_normalized + self.class.new(*to_a).to_s + end + # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions # for dn values. All of the following must be escaped in any normal string # using a single backslash ('\') as escape. diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb new file mode 100644 index 00000000000..11711c905a1 --- /dev/null +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::LDAP::DN do + using RSpec::Parameterized::TableSyntax + + describe '#initialize' do + subject { described_class.new(given).to_s_normalized } + + # Regarding the telephoneNumber test: + # + # I am not sure whether a space after the telephoneNumber plus sign is valid, + # and I am not sure if this is "proper" behavior under these conditions, and + # I am not sure if it matters to us or anyone else, so rather than dig + # through RFCs, I am only documenting the behavior here. + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' + 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' + 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' + 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'for a null DN (empty string), returns empty string and does not error' | '' | '' + 'does not strip an escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' + 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' + 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' + 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' + 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' + 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' + end + + with_them do + it 'normalizes the DN' do + assert_generic_test(test_description, subject, expected) + end + end + + context 'when the given DN is malformed' do + let(:given) { 'uid\\=john' } + + it 'raises MalformedDnError' do + expect(subject).to raise_error(MalformedDnError) + end + end + end + + def assert_generic_test(test_description, got, expected) + test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" + expect(got).to eq(expected), test_failure_message + end +end -- cgit v1.2.1 From a0d7a22e7c1e8ae1a61b4ef24ef38180c68782c7 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 11:49:04 -0700 Subject: Always downcase DNs --- lib/gitlab/ldap/dn.rb | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 0a49d5e4ca8..555ef0b80ae 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -37,10 +37,12 @@ module Gitlab buffer << "=" if index % 2 == 1 buffer << "," if index % 2 == 0 && index != 0 + arg = args[index].downcase + if index < args.length - 1 || index % 2 == 1 - buffer << self.class.escape(args[index]) + buffer << self.class.escape(arg) else - buffer << args[index] + buffer << arg end end @@ -60,7 +62,7 @@ module Gitlab case state when :key then case char - when 'a'..'z', 'A'..'Z' then + when 'a'..'z' then state = :key_normal key << char when '0'..'9' then @@ -72,7 +74,7 @@ module Gitlab when :key_normal then case char when '=' then state = :value - when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char + when 'a'..'z', '0'..'9', '-', ' ' then key << char else raise "DN badly formed" end when :key_oid then @@ -110,14 +112,14 @@ module Gitlab end when :value_normal_escape then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_normal_escape_hex hex_buffer = char else state = :value_normal; value << char end when :value_normal_escape_hex then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr else raise "DN badly formed" @@ -130,7 +132,7 @@ module Gitlab end when :value_quoted_escape then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_quoted_escape_hex hex_buffer = char else @@ -139,14 +141,14 @@ module Gitlab end when :value_quoted_escape_hex then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr else raise "DN badly formed" end when :value_hexstring then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_hexstring_hex value << char when ' ' then state = :value_end @@ -159,7 +161,7 @@ module Gitlab end when :value_hexstring_hex then case char - when '0'..'9', 'a'..'f', 'A'..'F' then + when '0'..'9', 'a'..'f' then state = :value_hexstring value << char else raise "DN badly formed" -- cgit v1.2.1 From cb591f86e42a2f3bd4df2980cc4cfed0a0641e71 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 14:21:27 -0700 Subject: Fix to_s_normalize for escaped leading space --- lib/gitlab/ldap/dn.rb | 14 +++----------- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 555ef0b80ae..02a4cbcd13a 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -210,27 +210,19 @@ module Gitlab # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions # for dn values. All of the following must be escaped in any normal string # using a single backslash ('\') as escape. - ESCAPES = { - ',' => ',', - '+' => '+', - '"' => '"', - '\\' => '\\', - '<' => '<', - '>' => '>', - ';' => ';', - } + NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';'] # Compiled character class regexp using the keys from the above hash, and # checking for a space or # at the start, or space at the end, of the # string. ESCAPE_RE = Regexp.new("(^ |^#| $|[" + - ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join + "])") ## # Escape a string for use in a DN value def self.escape(string) - string.gsub(ESCAPE_RE) { |char| "\\" + ESCAPES[char] } + string.gsub(ESCAPE_RE) { |char| "\\" + char } end ## diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 11711c905a1..73124bc4cc4 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -20,7 +20,7 @@ describe Gitlab::LDAP::DN do 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip an escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' -- cgit v1.2.1 From 3fde6f6806dd86e3df4d08d55a63fed19f1dae55 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 14:41:42 -0700 Subject: Fix trailing escaped space --- lib/gitlab/ldap/dn.rb | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 02a4cbcd13a..c23fac2d57a 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -115,6 +115,7 @@ module Gitlab when '0'..'9', 'a'..'f' then state = :value_normal_escape_hex hex_buffer = char + when ' ' then state = :value_normal_escape_space; value << char else state = :value_normal; value << char end when :value_normal_escape_hex then @@ -124,6 +125,16 @@ module Gitlab value << "#{hex_buffer}#{char}".to_i(16).chr else raise "DN badly formed" end + when :value_normal_escape_space then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, value.string # Don't strip trailing escaped space! + key = StringIO.new + value = StringIO.new; + else value << char + end when :value_quoted then case char when '\\' then state = :value_quoted_escape -- cgit v1.2.1 From e65bf3fa63ae45aaf9600cffb50be58eee9023db Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 14:53:50 -0700 Subject: Clarify test --- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 73124bc4cc4..44e30a69d44 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -26,7 +26,7 @@ describe Gitlab::LDAP::DN do 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' -- cgit v1.2.1 From c79879f33a05494f2ae5785a663b874bf8e42655 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:05:25 -0700 Subject: Fix escaped equal signs --- lib/gitlab/ldap/dn.rb | 10 ++++++---- spec/lib/gitlab/ldap/dn_spec.rb | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index c23fac2d57a..554156142cc 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -218,10 +218,12 @@ module Gitlab self.class.new(*to_a).to_s end - # http://tools.ietf.org/html/rfc2253 section 2.4 lists these exceptions - # for dn values. All of the following must be escaped in any normal string - # using a single backslash ('\') as escape. - NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';'] + # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions + # for DN values. All of the following must be escaped in any normal string + # using a single backslash ('\') as escape. The space character is left + # out here because in a "normalized" string, spaces should only be escaped + # if necessary (i.e. leading or trailing space). + NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='] # Compiled character class regexp using the keys from the above hash, and # checking for a space or # at the start, or space at the end, of the diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 44e30a69d44..dafc0037a0d 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -27,8 +27,8 @@ describe Gitlab::LDAP::DN do 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' + 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' + 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' end -- cgit v1.2.1 From f9283b8b18d59d749c803fc0a0b0d8ff1cccac02 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:07:26 -0700 Subject: Reword escaped comma test --- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index dafc0037a0d..f7c91cd0a1f 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -29,8 +29,8 @@ describe Gitlab::LDAP::DN do 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' + 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' end with_them do -- cgit v1.2.1 From 1e7ff892c00eea4e26a653b7a13dee4330b49221 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:07:38 -0700 Subject: Fix escaped hex comma test --- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index f7c91cd0a1f..9c39a193f8b 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -29,8 +29,8 @@ describe Gitlab::LDAP::DN do 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca' end with_them do -- cgit v1.2.1 From f610fea7771f09067c5ee76468d07e217794934e Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:25:30 -0700 Subject: Handle CR and LF characters --- lib/gitlab/ldap/dn.rb | 13 ++++++++++++- spec/lib/gitlab/ldap/dn_spec.rb | 3 +++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 554156142cc..f62d36101c4 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -225,6 +225,12 @@ module Gitlab # if necessary (i.e. leading or trailing space). NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='] + # The following must be represented as escaped hex + HEX_ESCAPES = { + "\n" => '\0a', + "\r" => '\0d' + } + # Compiled character class regexp using the keys from the above hash, and # checking for a space or # at the start, or space at the end, of the # string. @@ -232,10 +238,15 @@ module Gitlab NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join + "])") + HEX_ESCAPE_RE = Regexp.new("([" + + HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "])") + ## # Escape a string for use in a DN value def self.escape(string) - string.gsub(ESCAPE_RE) { |char| "\\" + char } + escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char } + escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } end ## diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 9c39a193f8b..4b21b1b3f7a 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -31,6 +31,9 @@ describe Gitlab::LDAP::DN do 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca' + 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca' + 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca' end with_them do -- cgit v1.2.1 From 7e3eb257babed7317a90e0ad62ca810108e28646 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:29:45 -0700 Subject: Fix for null DN --- lib/gitlab/ldap/dn.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index f62d36101c4..60e2ba96587 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -202,7 +202,7 @@ module Gitlab # Returns the DN as an array in the form expected by the constructor. def to_a a = [] - self.each_pair { |key, value| a << key << value } + self.each_pair { |key, value| a << key << value } unless @dn.empty? a end -- cgit v1.2.1 From 47dff608f4a06c54f243a26fb1412bef70df0844 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:42:35 -0700 Subject: Allow unescaped, non-reserved Unicode characters --- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 4b21b1b3f7a..a39aab91f8b 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -15,7 +15,7 @@ describe Gitlab::LDAP::DN do where(:test_description, :given, :expected) do 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' + 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith \\ ,ou=people (aka. \\"humans\\"),dc=example,dc=com' 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' -- cgit v1.2.1 From 8bd59f3aeb614afb58152b033ba1020edae6c3a7 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 15:55:19 -0700 Subject: Raise UnsupportedDnFormatError on multivalued RDNs --- lib/gitlab/ldap/dn.rb | 2 ++ spec/lib/gitlab/ldap/dn_spec.rb | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 60e2ba96587..234de1fe7eb 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -108,6 +108,7 @@ module Gitlab yield key.string.strip, value.string.rstrip key = StringIO.new value = StringIO.new; + when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") else value << char end when :value_normal_escape then @@ -133,6 +134,7 @@ module Gitlab yield key.string.strip, value.string # Don't strip trailing escaped space! key = StringIO.new value = StringIO.new; + when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") else value << char end when :value_quoted then diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index a39aab91f8b..6b197fa22fd 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -16,8 +16,6 @@ describe Gitlab::LDAP::DN do 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith \\ ,ou=people (aka. \\"humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'for a null DN (empty string), returns empty string and does not error' | '' | '' 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' @@ -42,6 +40,36 @@ describe Gitlab::LDAP::DN do end end + context 'when we do not support the given DN format' do + context 'multivalued RDNs' do + context 'without extraneous whitespace' do + let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' } + + it 'raises UnsupportedDnFormatError' do + expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + end + end + + context 'with extraneous whitespace' do + context 'around the phone number plus sign' do + let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' } + + it 'raises UnsupportedDnFormatError' do + expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + end + end + + context 'not around the phone number plus sign' do + let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' } + + it 'raises UnsupportedDnFormatError' do + expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + end + end + end + end + end + context 'when the given DN is malformed' do let(:given) { 'uid\\=john' } -- cgit v1.2.1 From 66030b03ddef0270a37e3d4eaaa5b871ff695d45 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 16:57:45 -0700 Subject: Test malformed DNs --- lib/gitlab/ldap/dn.rb | 21 ++++---- spec/lib/gitlab/ldap/dn_spec.rb | 106 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 13 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 234de1fe7eb..048b669b13a 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -21,6 +21,9 @@ # class also helps take care of that. module Gitlab module LDAP + MalformedDnError = Class.new(StandardError) + UnsupportedDnFormatError = Class.new(StandardError) + class DN ## # Initialize a DN, escaping as required. Pass in attributes in name/value @@ -69,19 +72,19 @@ module Gitlab state = :key_oid key << char when ' ' then state = :key - else raise "DN badly formed" + else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") end when :key_normal then case char when '=' then state = :value when 'a'..'z', '0'..'9', '-', ' ' then key << char - else raise "DN badly formed" + else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") end when :key_oid then case char when '=' then state = :value when '0'..'9', '.', ' ' then key << char - else raise "DN badly formed" + else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") end when :value then case char @@ -124,7 +127,7 @@ module Gitlab when '0'..'9', 'a'..'f' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" + else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end when :value_normal_escape_space then case char @@ -157,7 +160,7 @@ module Gitlab when '0'..'9', 'a'..'f' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr - else raise "DN badly formed" + else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") end when :value_hexstring then case char @@ -170,14 +173,14 @@ module Gitlab yield key.string.strip, value.string.rstrip key = StringIO.new value = StringIO.new; - else raise "DN badly formed" + else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") end when :value_hexstring_hex then case char when '0'..'9', 'a'..'f' then state = :value_hexstring value << char - else raise "DN badly formed" + else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") end when :value_end then case char @@ -187,14 +190,14 @@ module Gitlab yield key.string.strip, value.string.rstrip key = StringIO.new value = StringIO.new; - else raise "DN badly formed" + else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") end else raise "Fell out of state machine" end end # Last pair - raise "DN badly formed" unless + raise(MalformedDnError, 'DN string ended unexpectedly') unless [:value, :value_normal, :value_hexstring, :value_end].include? state yield key.string.strip, value.string.rstrip diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 6b197fa22fd..d4fbe1c45ea 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -71,16 +71,114 @@ describe Gitlab::LDAP::DN do end context 'when the given DN is malformed' do - let(:given) { 'uid\\=john' } + context 'when ending with a comma' do + let(:given) { 'uid=John Smith,' } - it 'raises MalformedDnError' do - expect(subject).to raise_error(MalformedDnError) + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'when given a BER encoded attribute value with a space in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"x\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"y\"") + end + end + + context 'when given a hex pair with a non-hex character in it, inside double quotes' do + let(:given) { 'uid="Sebasti\\cX\\a1n"' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"x\"") + end + end + + context 'without a name value pair' do + let(:given) { 'John' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'with an open (as opposed to closed) double quote' do + let(:given) { 'cn="James' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid escaped hex code' do + let(:given) { 'cn=J\ames' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') + end + end + + context 'with a value ending with the escape character' do + let(:given) { 'cn=\\' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid OID attribute type name' do + let(:given) { '1.2.d=Value' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN OID attribute type name character "d"') + end + end + + context 'with a period in a non-OID attribute type name' do + let(:given) { 'd1.2=Value' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "."') + end + end + + context 'when starting with non-space, non-alphanumeric character' do + let(:given) { ' -uid=John Smith' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized first character of an RDN attribute type name "-"') + end + end + + context 'when given a UID with an escaped equal sign' do + let(:given) { 'uid\\=john' } + + it 'raises MalformedDnError' do + expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "\\"') + end end end end def assert_generic_test(test_description, got, expected) - test_failure_message = "Failed test description: '#{test_description}'\n\n expected: #{expected}\n got: #{got}" + test_failure_message = "Failed test description: '#{test_description}'\n\n expected: \"#{expected}\"\n got: \"#{got}\"" expect(got).to eq(expected), test_failure_message end end -- cgit v1.2.1 From 1480cf84d883552e952fdac834ad1da242b3dbcf Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 17:00:03 -0700 Subject: Add valid DN tests using OIDs --- spec/lib/gitlab/ldap/dn_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index d4fbe1c45ea..1fd1aef1297 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -32,6 +32,8 @@ describe Gitlab::LDAP::DN do 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca' 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca' 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca' + 'allows attribute type name OIDs' | '0.9.2342.19200300.100.1.25=Example,0.9.2342.19200300.100.1.25=Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' + 'strips extraneous whitespace from attribute type name OIDs' | '0.9.2342.19200300.100.1.25 = Example, 0.9.2342.19200300.100.1.25 = Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' end with_them do -- cgit v1.2.1 From 26054114be8136137c4b3740c82b51f49027eaab Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 17:16:57 -0700 Subject: Fix trailing escaped newline --- lib/gitlab/ldap/dn.rb | 4 ++-- spec/lib/gitlab/ldap/dn_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 048b669b13a..557b5af118e 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -119,7 +119,7 @@ module Gitlab when '0'..'9', 'a'..'f' then state = :value_normal_escape_hex hex_buffer = char - when ' ' then state = :value_normal_escape_space; value << char + when /\s/ then state = :value_normal_escape_whitespace; value << char else state = :value_normal; value << char end when :value_normal_escape_hex then @@ -129,7 +129,7 @@ module Gitlab value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end - when :value_normal_escape_space then + when :value_normal_escape_whitespace then case char when '\\' then state = :value_normal_escape when ',' then diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 1fd1aef1297..8c268645346 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -21,9 +21,9 @@ describe Gitlab::LDAP::DN do 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' - 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com" 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' -- cgit v1.2.1 From fe46c11de81e122433b1b275a1078840b289dfcd Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 17:17:38 -0700 Subject: Fix newline tests --- spec/lib/gitlab/ldap/dn_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 8c268645346..a8687ec95d4 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -20,10 +20,10 @@ describe Gitlab::LDAP::DN do 'for a null DN (empty string), returns empty string and does not error' | '' | '' 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' - 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' - 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' + 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com" + 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" + 'strips an unescaped trailing newline (actually an invalid DN?)' | "uid=John Smith\n,ou=People,dc=example,dc=com" | "uid=john smith,ou=people,dc=example,dc=com" 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' -- cgit v1.2.1 From 45ab20dca91024602e7c73814e8ff89df2000189 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 20 Sep 2017 17:28:57 -0700 Subject: Switch to new DN class for normalizing and parsing DNs --- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/ldap/person.rb | 17 +------------ spec/lib/gitlab/ldap/person_spec.rb | 49 ------------------------------------- spec/lib/gitlab/ldap/user_spec.rb | 16 ++++++------ 4 files changed, 10 insertions(+), 74 deletions(-) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 3123da17fd9..2ea0a51b18f 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -4,7 +4,7 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash def uid - Gitlab::LDAP::Person.normalize_dn(super) + Gitlab::LDAP::DN.new(super).to_s_normalized end private diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 6b1a308d521..44cb6a065c9 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -49,21 +49,6 @@ module Gitlab uid end - # Returns the DN in a normalized form. - # - # 1. Excess spaces around attribute names and values are stripped - # 2. The string is downcased (for case-insensitivity) - def self.normalize_dn(dn) - dn.split(/(? e - Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") - Rails.logger.info(e.backtrace.join("\n")) - - dn - end - def initialize(entry, provider) Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" } @entry = entry @@ -87,7 +72,7 @@ module Gitlab end def dn - self.class.normalize_dn(entry.dn) + DN.new(entry.dn).to_s_normalized end private diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 38458549751..02904f1e351 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -17,41 +17,6 @@ describe Gitlab::LDAP::Person do ) end - shared_examples_for 'normalizes the DN' do - # Regarding the telephoneNumber test: - # - # I am not sure whether a space after the telephoneNumber plus sign is valid, - # and I am not sure if this is "proper" behavior under these conditions, and - # I am not sure if it matters to us or anyone else, so rather than dig - # through RFCs, I am only documenting the behavior here. - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'strips extraneous whitespace without changing escaped characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebasti\\c3\\a1n\\ c.\\20smith\\ ,ou=people (aka. \\22humans\\"),dc=example,dc=com' - 'strips extraneous whitespace without modifying the multivalued RDN' | 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'strips the space after the plus sign in the telephoneNumber' | 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' | 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' - 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip an escaped leading space in an attribute value (and does not error like Net::LDAP::DN.new does)' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' - 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'does not strip an escaped leading newline in an attribute value' | 'uid=\\\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\\\njohn smith,ou=people,dc=example,dc=com' - 'does not strip an escaped trailing newline in an attribute value' | 'uid=John Smith\\\n,ou=People,dc=example,dc=com' | 'uid=john smith\\\n,ou=people,dc=example,dc=com' - 'does not strip an unescaped leading newline (actually an invalid DN)' | 'uid=\nJohn Smith,ou=People,dc=example,dc=com' | 'uid=\njohn smith,ou=people,dc=example,dc=com' - 'does not strip an unescaped trailing newline (actually an invalid DN)' | 'uid=John Smith\n ,ou=People,dc=example,dc=com' | 'uid=john smith\n,ou=people,dc=example,dc=com' - 'does not strip non whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'does not treat escaped equal signs as attribute delimiters' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | 'uid= foo \\3D bar' | 'uid=foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' - 'does not treat escaped hex commas as attribute delimiters' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\2c ca' - end - - with_them do - it 'normalizes the DN' do - assert_generic_test(test_description, subject, expected) - end - end - end - shared_examples_for 'normalizes the UID' do where(:test_description, :given, :expected) do 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' @@ -91,20 +56,6 @@ describe Gitlab::LDAP::Person do end end - describe '.normalize_dn' do - subject { described_class.normalize_dn(given) } - - it_behaves_like 'normalizes the DN' - - context 'with an exception during normalization' do - let(:given) { described_class } # just something that will cause an exception - - it 'returns the given DN unmodified' do - expect(subject).to eq(given) - end - end - end - describe '#name' do it 'uses the configured name attribute and handles values as an array' do name = 'John Doe' diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 6a6e465cea2..9a4705d1cee 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do } end let(:auth_hash) do - OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info) + OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info) end let(:ldap_user_upper_case) { described_class.new(auth_hash_upper_case) } let(:info_upper_case) do @@ -22,12 +22,12 @@ describe Gitlab::LDAP::User do } end let(:auth_hash_upper_case) do - OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info_upper_case) + OmniAuth::AuthHash.new(uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain', info: info_upper_case) end describe '#changed?' do it "marks existing ldap user as changed" do - create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain') expect(ldap_user.changed?).to be_truthy end @@ -37,7 +37,7 @@ describe Gitlab::LDAP::User do end it "does not mark existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com', provider: 'ldapmain') ldap_user.gl_user.user_synced_attributes_metadata(provider: 'ldapmain', email: true) expect(ldap_user.changed?).to be_falsey end @@ -60,7 +60,7 @@ describe Gitlab::LDAP::User do describe 'find or create' do it "finds the user if already existing" do - create(:omniauth_user, extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, extern_uid: 'uid=John Smith,ou=People,dc=example,dc=com', provider: 'ldapmain') expect { ldap_user.save }.not_to change { User.count } end @@ -70,7 +70,7 @@ describe Gitlab::LDAP::User do expect { ldap_user.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' end @@ -79,7 +79,7 @@ describe Gitlab::LDAP::User do expect { ldap_user.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' expect(existing_user.id).to eql ldap_user.gl_user.id end @@ -89,7 +89,7 @@ describe Gitlab::LDAP::User do expect { ldap_user_upper_case.save }.not_to change { User.count } existing_user.reload - expect(existing_user.ldap_identity.extern_uid).to eql 'my-uid' + expect(existing_user.ldap_identity.extern_uid).to eql 'uid=john smith,ou=people,dc=example,dc=com' expect(existing_user.ldap_identity.provider).to eql 'ldapmain' expect(existing_user.id).to eql ldap_user.gl_user.id end -- cgit v1.2.1 From 14ed20d68af935da1a236d010978939a8085aa59 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 21 Sep 2017 09:42:16 -0700 Subject: Resolve Rubocop offenses Disabling some for now since this is based on `Net::LDAP::DN`. --- lib/gitlab/ldap/dn.rb | 42 ++++++++++++++++++++++++----------------- spec/lib/gitlab/ldap/dn_spec.rb | 32 +++++++++++++++---------------- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 557b5af118e..05252c75624 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -37,16 +37,16 @@ module Gitlab buffer = StringIO.new args.each_index do |index| - buffer << "=" if index % 2 == 1 - buffer << "," if index % 2 == 0 && index != 0 + buffer << "=" if index.odd? + buffer << "," if index.even? && index != 0 arg = args[index].downcase - if index < args.length - 1 || index % 2 == 1 - buffer << self.class.escape(arg) - else - buffer << arg - end + buffer << if index < args.length - 1 || index.odd? + self.class.escape(arg) + else + arg + end end @dn = buffer.string @@ -55,6 +55,9 @@ module Gitlab ## # Parse a DN into key value pairs using ASN from # http://tools.ietf.org/html/rfc2253 section 3. + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity def each_pair state = :key key = StringIO.new @@ -98,7 +101,7 @@ module Gitlab state = :key yield key.string.strip, value.string.rstrip key = StringIO.new - value = StringIO.new; + value = StringIO.new else state = :value_normal value << char @@ -110,7 +113,7 @@ module Gitlab state = :key yield key.string.strip, value.string.rstrip key = StringIO.new - value = StringIO.new; + value = StringIO.new when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") else value << char end @@ -119,8 +122,12 @@ module Gitlab when '0'..'9', 'a'..'f' then state = :value_normal_escape_hex hex_buffer = char - when /\s/ then state = :value_normal_escape_whitespace; value << char - else state = :value_normal; value << char + when /\s/ then + state = :value_normal_escape_whitespace + value << char + else + state = :value_normal + value << char end when :value_normal_escape_hex then case char @@ -136,7 +143,7 @@ module Gitlab state = :key yield key.string.strip, value.string # Don't strip trailing escaped space! key = StringIO.new - value = StringIO.new; + value = StringIO.new when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") else value << char end @@ -152,7 +159,7 @@ module Gitlab state = :value_quoted_escape_hex hex_buffer = char else - state = :value_quoted; + state = :value_quoted value << char end when :value_quoted_escape_hex then @@ -172,7 +179,7 @@ module Gitlab state = :key yield key.string.strip, value.string.rstrip key = StringIO.new - value = StringIO.new; + value = StringIO.new else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") end when :value_hexstring_hex then @@ -189,7 +196,7 @@ module Gitlab state = :key yield key.string.strip, value.string.rstrip key = StringIO.new - value = StringIO.new; + value = StringIO.new else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") end else raise "Fell out of state machine" @@ -228,13 +235,13 @@ module Gitlab # using a single backslash ('\') as escape. The space character is left # out here because in a "normalized" string, spaces should only be escaped # if necessary (i.e. leading or trailing space). - NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='] + NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze # The following must be represented as escaped hex HEX_ESCAPES = { "\n" => '\0a', "\r" => '\0d' - } + }.freeze # Compiled character class regexp using the keys from the above hash, and # checking for a space or # at the start, or space at the end, of the @@ -257,6 +264,7 @@ module Gitlab ## # Proxy all other requests to the string object, because a DN is mainly # used within the library as a string + # rubocop:disable GitlabSecurity/PublicSend def method_missing(method, *args, &block) @dn.send(method, *args, &block) end diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index a8687ec95d4..f923c67d922 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' } it 'raises UnsupportedDnFormatError' do - expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) end end @@ -57,7 +57,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' } it 'raises UnsupportedDnFormatError' do - expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) end end @@ -65,7 +65,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' } it 'raises UnsupportedDnFormatError' do - expect{ subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) end end end @@ -77,7 +77,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid=John Smith,' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') end end @@ -85,7 +85,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") end end @@ -93,7 +93,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"x\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"x\"") end end @@ -101,7 +101,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"y\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"y\"") end end @@ -109,7 +109,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid="Sebasti\\cX\\a1n"' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"x\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"x\"") end end @@ -117,7 +117,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'John' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') end end @@ -125,7 +125,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn="James' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') end end @@ -133,7 +133,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn=J\ames' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') end end @@ -141,7 +141,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'cn=\\' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') end end @@ -149,7 +149,7 @@ describe Gitlab::LDAP::DN do let(:given) { '1.2.d=Value' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN OID attribute type name character "d"') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN OID attribute type name character "d"') end end @@ -157,7 +157,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'd1.2=Value' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "."') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "."') end end @@ -165,7 +165,7 @@ describe Gitlab::LDAP::DN do let(:given) { ' -uid=John Smith' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized first character of an RDN attribute type name "-"') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized first character of an RDN attribute type name "-"') end end @@ -173,7 +173,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid\\=john' } it 'raises MalformedDnError' do - expect{ subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "\\"') + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "\\"') end end end -- cgit v1.2.1 From e610332edacd2e389bcaec5931d8bcdccd8a92cc Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 21 Sep 2017 10:32:21 -0700 Subject: Normalize existing persisted DNs --- .../20170921101004_normalize_ldap_extern_uids.rb | 285 +++++++++++++++++++++ 1 file changed, 285 insertions(+) create mode 100644 db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb new file mode 100644 index 00000000000..501ba7c5fe2 --- /dev/null +++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb @@ -0,0 +1,285 @@ +# See http://doc.gitlab.com/ce/development/migration_style_guide.html +# for more information on how to write migrations for GitLab. + +class NormalizeLdapExternUids < ActiveRecord::Migration + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + class Identity < ActiveRecord::Base + self.table_name = 'identities' + end + + # Copied this class to make this migration resilient to future code changes. + # And if the normalize behavior is changed in the future, it must be + # accompanied by another migration. + module Gitlab + module LDAP + MalformedDnError = Class.new(StandardError) + UnsupportedDnFormatError = Class.new(StandardError) + + class DN + ## + # Initialize a DN, escaping as required. Pass in attributes in name/value + # pairs. If there is a left over argument, it will be appended to the dn + # without escaping (useful for a base string). + # + # Most uses of this class will be to escape a DN, rather than to parse it, + # so storing the dn as an escaped String and parsing parts as required + # with a state machine seems sensible. + def initialize(*args) + buffer = StringIO.new + + args.each_index do |index| + buffer << "=" if index.odd? + buffer << "," if index.even? && index != 0 + + arg = args[index].downcase + + buffer << if index < args.length - 1 || index.odd? + self.class.escape(arg) + else + arg + end + end + + @dn = buffer.string + end + + ## + # Parse a DN into key value pairs using ASN from + # http://tools.ietf.org/html/rfc2253 section 3. + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def each_pair + state = :key + key = StringIO.new + value = StringIO.new + hex_buffer = "" + + @dn.each_char do |char| + case state + when :key then + case char + when 'a'..'z' then + state = :key_normal + key << char + when '0'..'9' then + state = :key_oid + key << char + when ' ' then state = :key + else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") + end + when :key_normal then + case char + when '=' then state = :value + when 'a'..'z', '0'..'9', '-', ' ' then key << char + else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") + end + when :key_oid then + case char + when '=' then state = :value + when '0'..'9', '.', ' ' then key << char + else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") + end + when :value then + case char + when '\\' then state = :value_normal_escape + when '"' then state = :value_quoted + when ' ' then state = :value + when '#' then + state = :value_hexstring + value << char + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new + else + state = :value_normal + value << char + end + when :value_normal then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new + when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") + else value << char + end + when :value_normal_escape then + case char + when '0'..'9', 'a'..'f' then + state = :value_normal_escape_hex + hex_buffer = char + when /\s/ then + state = :value_normal_escape_whitespace + value << char + else + state = :value_normal + value << char + end + when :value_normal_escape_hex then + case char + when '0'..'9', 'a'..'f' then + state = :value_normal + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") + end + when :value_normal_escape_whitespace then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, value.string # Don't strip trailing escaped space! + key = StringIO.new + value = StringIO.new + when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") + else value << char + end + when :value_quoted then + case char + when '\\' then state = :value_quoted_escape + when '"' then state = :value_end + else value << char + end + when :value_quoted_escape then + case char + when '0'..'9', 'a'..'f' then + state = :value_quoted_escape_hex + hex_buffer = char + else + state = :value_quoted + value << char + end + when :value_quoted_escape_hex then + case char + when '0'..'9', 'a'..'f' then + state = :value_quoted + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") + end + when :value_hexstring then + case char + when '0'..'9', 'a'..'f' then + state = :value_hexstring_hex + value << char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new + else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") + end + when :value_hexstring_hex then + case char + when '0'..'9', 'a'..'f' then + state = :value_hexstring + value << char + else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") + end + when :value_end then + case char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, value.string.rstrip + key = StringIO.new + value = StringIO.new + else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") + end + else raise "Fell out of state machine" + end + end + + # Last pair + raise(MalformedDnError, 'DN string ended unexpectedly') unless + [:value, :value_normal, :value_hexstring, :value_end].include? state + + yield key.string.strip, value.string.rstrip + end + + ## + # Returns the DN as an array in the form expected by the constructor. + def to_a + a = [] + self.each_pair { |key, value| a << key << value } unless @dn.empty? + a + end + + ## + # Return the DN as an escaped string. + def to_s + @dn + end + + ## + # Return the DN as an escaped and normalized string. + def to_s_normalized + self.class.new(*to_a).to_s + end + + # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions + # for DN values. All of the following must be escaped in any normal string + # using a single backslash ('\') as escape. The space character is left + # out here because in a "normalized" string, spaces should only be escaped + # if necessary (i.e. leading or trailing space). + NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze + + # The following must be represented as escaped hex + HEX_ESCAPES = { + "\n" => '\0a', + "\r" => '\0d' + }.freeze + + # Compiled character class regexp using the keys from the above hash, and + # checking for a space or # at the start, or space at the end, of the + # string. + ESCAPE_RE = Regexp.new("(^ |^#| $|[" + + NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join + + "])") + + HEX_ESCAPE_RE = Regexp.new("([" + + HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "])") + + ## + # Escape a string for use in a DN value + def self.escape(string) + escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char } + escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } + end + + ## + # Proxy all other requests to the string object, because a DN is mainly + # used within the library as a string + # rubocop:disable GitlabSecurity/PublicSend + def method_missing(method, *args, &block) + @dn.send(method, *args, &block) + end + end + end + end + + def up + ldap_identities = Identity.where("provider like 'ldap%'") + ldap_identities.find_each do |identity| + begin + identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_s_normalized + unless identity.save + say "Unable to normalize \"#{identity.extern_uid}\". Skipping." + end + rescue Gitlab::LDAP::MalformedDnError, Gitlab::LDAP::UnsupportedDnFormatError => e + say "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping." + end + end + end + + def down + end +end -- cgit v1.2.1 From b3d61832c37b037f95dad619dd8c0680b6513818 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 2 Oct 2017 13:24:20 -0700 Subject: Move downcasing to normalize method --- lib/gitlab/ldap/dn.rb | 20 ++++++++++---------- spec/lib/gitlab/ldap/dn_spec.rb | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 05252c75624..3766893ff6d 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -40,7 +40,7 @@ module Gitlab buffer << "=" if index.odd? buffer << "," if index.even? && index != 0 - arg = args[index].downcase + arg = args[index] buffer << if index < args.length - 1 || index.odd? self.class.escape(arg) @@ -68,7 +68,7 @@ module Gitlab case state when :key then case char - when 'a'..'z' then + when 'a'..'z', 'A'..'Z' then state = :key_normal key << char when '0'..'9' then @@ -80,7 +80,7 @@ module Gitlab when :key_normal then case char when '=' then state = :value - when 'a'..'z', '0'..'9', '-', ' ' then key << char + when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") end when :key_oid then @@ -119,7 +119,7 @@ module Gitlab end when :value_normal_escape then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal_escape_hex hex_buffer = char when /\s/ then @@ -131,7 +131,7 @@ module Gitlab end when :value_normal_escape_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") @@ -155,7 +155,7 @@ module Gitlab end when :value_quoted_escape then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted_escape_hex hex_buffer = char else @@ -164,14 +164,14 @@ module Gitlab end when :value_quoted_escape_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") end when :value_hexstring then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring_hex value << char when ' ' then state = :value_end @@ -184,7 +184,7 @@ module Gitlab end when :value_hexstring_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring value << char else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") @@ -227,7 +227,7 @@ module Gitlab ## # Return the DN as an escaped and normalized string. def to_s_normalized - self.class.new(*to_a).to_s + self.class.new(*to_a).to_s.downcase end # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index f923c67d922..67a561854aa 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Gitlab::LDAP::DN do using RSpec::Parameterized::TableSyntax - describe '#initialize' do + describe '#to_s_normalized' do subject { described_class.new(given).to_s_normalized } # Regarding the telephoneNumber test: @@ -93,7 +93,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"x\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"X\"") end end @@ -101,7 +101,7 @@ describe Gitlab::LDAP::DN do let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"y\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"Y\"") end end @@ -109,7 +109,7 @@ describe Gitlab::LDAP::DN do let(:given) { 'uid="Sebasti\\cX\\a1n"' } it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"x\"") + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") end end -- cgit v1.2.1 From a6a764f73debb41005fbfe7e902c9f51e2bbcff1 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 2 Oct 2017 14:57:57 -0700 Subject: Refactor initialize method for clarity --- lib/gitlab/ldap/dn.rb | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 3766893ff6d..060f61d1a10 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -34,24 +34,33 @@ module Gitlab # so storing the dn as an escaped String and parsing parts as required # with a state machine seems sensible. def initialize(*args) - buffer = StringIO.new - - args.each_index do |index| - buffer << "=" if index.odd? - buffer << "," if index.even? && index != 0 + if args.length > 1 + initialize_array(args) + else + initialize_string(args[0]) + end + end - arg = args[index] + def initialize_array(args) + buffer = StringIO.new - buffer << if index < args.length - 1 || index.odd? - self.class.escape(arg) - else - arg - end + args.each_with_index do |arg, index| + if index.even? # key + buffer << "," if index > 0 + buffer << arg + else # value + buffer << "=" + buffer << self.class.escape(arg) + end end @dn = buffer.string end + def initialize_string(arg) + @dn = arg.to_s + end + ## # Parse a DN into key value pairs using ASN from # http://tools.ietf.org/html/rfc2253 section 3. -- cgit v1.2.1 From 714f264d62c5d2a45efc4b013f2fca1eb7eff1f1 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Mon, 2 Oct 2017 15:00:50 -0700 Subject: Rename method to `to_normalized_s` --- db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb | 4 ++-- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/ldap/dn.rb | 2 +- lib/gitlab/ldap/person.rb | 2 +- spec/lib/gitlab/ldap/dn_spec.rb | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb index 501ba7c5fe2..aa95fa49e48 100644 --- a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb +++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb @@ -220,7 +220,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration ## # Return the DN as an escaped and normalized string. - def to_s_normalized + def to_normalized_s self.class.new(*to_a).to_s end @@ -270,7 +270,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration ldap_identities = Identity.where("provider like 'ldap%'") ldap_identities.find_each do |identity| begin - identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_s_normalized + identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s unless identity.save say "Unable to normalize \"#{identity.extern_uid}\". Skipping." end diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index 2ea0a51b18f..b173b879f5f 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -4,7 +4,7 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash def uid - Gitlab::LDAP::DN.new(super).to_s_normalized + Gitlab::LDAP::DN.new(super).to_normalized_s end private diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 060f61d1a10..4ecb5566018 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -235,7 +235,7 @@ module Gitlab ## # Return the DN as an escaped and normalized string. - def to_s_normalized + def to_normalized_s self.class.new(*to_a).to_s.downcase end diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 44cb6a065c9..af8aab2444b 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -72,7 +72,7 @@ module Gitlab end def dn - DN.new(entry.dn).to_s_normalized + DN.new(entry.dn).to_normalized_s end private diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 67a561854aa..9a1963e2194 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe Gitlab::LDAP::DN do using RSpec::Parameterized::TableSyntax - describe '#to_s_normalized' do - subject { described_class.new(given).to_s_normalized } + describe '#to_normalized_s' do + subject { described_class.new(given).to_normalized_s } # Regarding the telephoneNumber test: # -- cgit v1.2.1 From 689eea5a43a79cd7fcbb8d579abaf37a81fc267a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 3 Oct 2017 13:55:46 -0700 Subject: Fix space stripping Especially from the last attribute value. --- lib/gitlab/ldap/dn.rb | 44 ++++++++++++++++++++++------------------- spec/lib/gitlab/ldap/dn_spec.rb | 8 +++++++- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 4ecb5566018..751219b7334 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -73,7 +73,7 @@ module Gitlab value = StringIO.new hex_buffer = "" - @dn.each_char do |char| + @dn.each_char.with_index do |char, dn_index| case state when :key then case char @@ -108,7 +108,7 @@ module Gitlab value << char when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else @@ -120,7 +120,7 @@ module Gitlab when '\\' then state = :value_normal_escape when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") @@ -131,9 +131,6 @@ module Gitlab when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal_escape_hex hex_buffer = char - when /\s/ then - state = :value_normal_escape_whitespace - value << char else state = :value_normal value << char @@ -145,17 +142,6 @@ module Gitlab value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end - when :value_normal_escape_whitespace then - case char - when '\\' then state = :value_normal_escape - when ',' then - state = :key - yield key.string.strip, value.string # Don't strip trailing escaped space! - key = StringIO.new - value = StringIO.new - when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") - else value << char - end when :value_quoted then case char when '\\' then state = :value_quoted_escape @@ -186,7 +172,7 @@ module Gitlab when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") @@ -203,7 +189,7 @@ module Gitlab when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") @@ -216,7 +202,25 @@ module Gitlab raise(MalformedDnError, 'DN string ended unexpectedly') unless [:value, :value_normal, :value_hexstring, :value_end].include? state - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) + end + + def rstrip_except_escaped(str, dn_index) + str_ends_with_whitespace = str.match(/\s\z/) + + if str_ends_with_whitespace + dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/) + + if dn_part_ends_with_escaped_whitespace + dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1] + num_chars_to_remove = dn_part_rwhitespace.length - 1 + str = str[0, str.length - num_chars_to_remove] + else + str.rstrip! + end + end + + str end ## diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 9a1963e2194..9a8ef7721ef 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -15,11 +15,17 @@ describe Gitlab::LDAP::DN do where(:test_description, :given, :expected) do 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith\\ , ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith \\ ,ou=people (aka. \\"humans\\"),dc=example,dc=com' + 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith, ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith,ou=people (aka. \\"humans\\"),dc=example,dc=com' 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'for a null DN (empty string), returns empty string and does not error' | '' | '' 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip an escaped leading space in the last attribute value' | 'uid=\\ John Smith' | 'uid=\\ john smith' 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'strips extraneous spaces after an escaped trailing space' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'strips extraneous spaces after an escaped trailing space at the end of the DN' | 'uid=John Smith,ou=People,dc=example,dc=com\\ ' | 'uid=john smith,ou=people,dc=example,dc=com\\ ' + 'properly preserves escaped trailing space after unescaped trailing spaces' | 'uid=John Smith \\ ,ou=People,dc=example,dc=com' | 'uid=john smith \\ ,ou=people,dc=example,dc=com' + 'preserves multiple inner spaces in an attribute value' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'preserves inner spaces after an escaped space' | 'uid=John\\ Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com" 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" -- cgit v1.2.1 From ed07faf2847f5adaebbd65d81d423fd249f9b542 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 3 Oct 2017 13:56:51 -0700 Subject: Remove telephoneNumber format comment Since that behavior changed, and is now under the malformed DN context. --- spec/lib/gitlab/ldap/dn_spec.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 9a8ef7721ef..725a1324109 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -6,12 +6,6 @@ describe Gitlab::LDAP::DN do describe '#to_normalized_s' do subject { described_class.new(given).to_normalized_s } - # Regarding the telephoneNumber test: - # - # I am not sure whether a space after the telephoneNumber plus sign is valid, - # and I am not sure if this is "proper" behavior under these conditions, and - # I am not sure if it matters to us or anyone else, so rather than dig - # through RFCs, I am only documenting the behavior here. where(:test_description, :given, :expected) do 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' -- cgit v1.2.1 From 6b9229466dc84d3d2b4ed002807d28960bfd1a84 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 3 Oct 2017 14:38:55 -0700 Subject: Normalize values, reusing DN normalization code I first attempted to extract logic from the code that normalizes DNs, but I was unsuccessful. This is a hack but it works. --- lib/gitlab/ldap/dn.rb | 6 +++ lib/gitlab/ldap/person.rb | 35 +----------------- spec/lib/gitlab/ldap/dn_spec.rb | 72 ++++++++++++++++++++++++++++++++++++ spec/lib/gitlab/ldap/person_spec.rb | 30 +-------------- spec/support/ldap_shared_examples.rb | 29 +++++++++++++++ 5 files changed, 111 insertions(+), 61 deletions(-) create mode 100644 spec/support/ldap_shared_examples.rb diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 751219b7334..87a7f1c6bc0 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -25,6 +25,12 @@ module Gitlab UnsupportedDnFormatError = Class.new(StandardError) class DN + def self.normalize_value(given_value) + dummy_dn = "placeholder=#{given_value}" + normalized_dn = new(*dummy_dn).to_normalized_s + normalized_dn.sub(/\Aplaceholder=/, '') + end + ## # Initialize a DN, escaping as required. Pass in attributes in name/value # pairs. If there is a left over argument, it will be appended to the dn diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index af8aab2444b..e91e3a176e6 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -41,8 +41,8 @@ module Gitlab # 1. Excess spaces are stripped # 2. The string is downcased (for case-insensitivity) def self.normalize_uid(uid) - normalize_dn_part(uid) - rescue StandardError => e + ::Gitlab::LDAP::DN.normalize_value(uid) + rescue ::Gitlab::LDAP::MalformedDnError, ::Gitlab::LDAP::UnsupportedDnFormatError => e Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") Rails.logger.info(e.backtrace.join("\n")) @@ -77,37 +77,6 @@ module Gitlab private - def self.normalize_dn_part(part) - cleaned = part.strip.downcase - - if cleaned.ends_with?('\\') - # If it ends with an escape character that is not followed by a - # character to be escaped, then this part may be malformed. But let's - # not worry too much about it, and just return it unmodified. - # - # Why? Because the reason we clean DNs is to make our simplistic - # string comparisons work better, even though there are all kinds of - # ways that equivalent DNs can vary as strings. If we run into a - # strange DN, we should just try to work with it. - # - # See https://www.ldap.com/ldap-dns-and-rdns for more. - return part unless part.ends_with?(' ') - - # Ends with an escaped space (which is valid). - cleaned = cleaned + ' ' - end - - # Get rid of blanks. This can happen if a split character is followed by - # whitespace and then another split character. - # - # E.g. this DN: 'uid=john+telephoneNumber= +1 555-555-5555' - # - # Should be returned as: 'uid=john+telephoneNumber=+1 555-555-5555' - cleaned = '' if cleaned.blank? - - cleaned - end - def entry @entry end diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 725a1324109..709eadd7e38 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -3,6 +3,78 @@ require 'spec_helper' describe Gitlab::LDAP::DN do using RSpec::Parameterized::TableSyntax + describe '#normalize_value' do + subject { described_class.normalize_value(given) } + + it_behaves_like 'normalizes a DN attribute value' + + context 'when the given DN is malformed' do + context 'when ending with a comma' do + let(:given) { 'John Smith,' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'when given a BER encoded attribute value with a space in it' do + let(:given) { '#aa aa' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '#aaXaaa' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"X\"") + end + end + + context 'when given a BER encoded attribute value with a non-hex character in it' do + let(:given) { '#aaaYaa' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"Y\"") + end + end + + context 'when given a hex pair with a non-hex character in it, inside double quotes' do + let(:given) { '"Sebasti\\cX\\a1n"' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + end + end + + context 'with an open (as opposed to closed) double quote' do + let(:given) { '"James' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + + context 'with an invalid escaped hex code' do + let(:given) { 'J\ames' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') + end + end + + context 'with a value ending with the escape character' do + let(:given) { 'foo\\' } + + it 'raises MalformedDnError' do + expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + end + end + end + end + describe '#to_normalized_s' do subject { described_class.new(given).to_normalized_s } diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 02904f1e351..743b3fbde2b 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -1,7 +1,6 @@ require 'spec_helper' describe Gitlab::LDAP::Person do - using RSpec::Parameterized::TableSyntax include LdapHelpers let(:entry) { ldap_user_entry('john.doe') } @@ -17,38 +16,13 @@ describe Gitlab::LDAP::Person do ) end - shared_examples_for 'normalizes the UID' do - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | ' John C. Smith ' | 'john c. smith' - 'strips extraneous whitespace without changing escaped characters' | ' Sebasti\\c3\\a1n\\ C.\\20Smith\\ ' | 'sebasti\\c3\\a1n\\ c.\\20smith\\ ' - 'downcases the whole string' | 'John Smith' | 'john smith' - 'does not strip the escaped leading space in an attribute value' | ' \\ John Smith ' | '\\ john smith' - 'does not strip the escaped trailing space in an attribute value' | ' John Smith\\ ' | 'john smith\\ ' - 'does not strip the escaped leading newline in an attribute value' | ' \\\nJohn Smith ' | '\\\njohn smith' - 'does not strip the escaped trailing newline in an attribute value' | ' John Smith\\\n ' | 'john smith\\\n' - 'does not strip the unescaped leading newline in an attribute value' | ' \nJohn Smith ' | '\njohn smith' - 'does not strip the unescaped trailing newline in an attribute value' | ' John Smith\n ' | 'john smith\n' - 'does not strip non whitespace' | 'John Smith' | 'john smith' - 'does not treat escaped equal signs as attribute delimiters' | ' foo \\= bar' | 'foo \\= bar' - 'does not treat escaped hex equal signs as attribute delimiters' | ' foo \\3D bar' | 'foo \\3d bar' - 'does not treat escaped commas as attribute delimiters' | ' Smith\\, John C.' | 'smith\\, john c.' - 'does not treat escaped hex commas as attribute delimiters' | ' Smith\\2C John C.' | 'smith\\2c john c.' - end - - with_them do - it 'normalizes the UID' do - assert_generic_test(test_description, subject, expected) - end - end - end - describe '.normalize_uid' do subject { described_class.normalize_uid(given) } - it_behaves_like 'normalizes the UID' + it_behaves_like 'normalizes a DN attribute value' context 'with an exception during normalization' do - let(:given) { described_class } # just something that will cause an exception + let(:given) { 'John "Smith,' } # just something that will cause an exception it 'returns the given UID unmodified' do expect(subject).to eq(given) diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/ldap_shared_examples.rb new file mode 100644 index 00000000000..3ab8b1d73a1 --- /dev/null +++ b/spec/support/ldap_shared_examples.rb @@ -0,0 +1,29 @@ +shared_examples_for 'normalizes a DN attribute value' do + using RSpec::Parameterized::TableSyntax + + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | ' John Smith ' | 'john smith' + 'unescapes non-reserved, non-special Unicode characters' | 'Sebasti\\c3\\a1n\\ C.\\20Smith' | 'sebastián c. smith' + 'downcases the whole string' | 'JoHn C. Smith' | 'john c. smith' + 'does not strip an escaped leading space in an attribute value' | '\\ John Smith' | '\\ john smith' + 'does not strip an escaped trailing space in an attribute value' | 'John Smith\\ ' | 'john smith\\ ' + 'hex-escapes an escaped leading newline in an attribute value' | "\\\nJohn Smith" | "\\0ajohn smith" + 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "John Smith\\\n" | "john smith\\0a" + 'hex-escapes an unescaped leading newline (actually an invalid DN value?)' | "\nJohn Smith" | "\\0ajohn smith" + 'strips an unescaped trailing newline (actually an invalid DN value?)' | "John Smith\n" | "john smith" + 'does not strip if no extraneous whitespace' | 'John Smith' | 'john smith' + 'does not modify an escaped equal sign in an attribute value' | ' foo \\= bar' | 'foo \\= bar' + 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | ' foo \\3D bar' | 'foo \\= bar' + 'does not modify an escaped comma in an attribute value' | 'San Francisco\\, CA' | 'san francisco\\, ca' + 'converts an escaped hex comma to an escaped comma in an attribute value' | 'San Francisco\\2C CA' | 'san francisco\\, ca' + 'does not modify an escaped hex carriage return character in an attribute value' | 'San Francisco\\,\\0DCA' | 'san francisco\\,\\0dca' + 'does not modify an escaped hex line feed character in an attribute value' | 'San Francisco\\,\\0ACA' | 'san francisco\\,\\0aca' + 'does not modify an escaped hex CRLF in an attribute value' | 'San Francisco\\,\\0D\\0ACA' | 'san francisco\\,\\0d\\0aca' + end + + with_them do + it 'normalizes the DN attribute value' do + assert_generic_test(test_description, subject, expected) + end + end +end -- cgit v1.2.1 From 3d460af091e7241e4bca7c8ae2880ef0fd2f12b3 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 3 Oct 2017 15:15:23 -0700 Subject: Update DN class in migration --- .../20170921101004_normalize_ldap_extern_uids.rb | 101 ++++++++++++--------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb index aa95fa49e48..234e8a3270b 100644 --- a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb +++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb @@ -19,6 +19,12 @@ class NormalizeLdapExternUids < ActiveRecord::Migration UnsupportedDnFormatError = Class.new(StandardError) class DN + def self.normalize_value(given_value) + dummy_dn = "placeholder=#{given_value}" + normalized_dn = new(*dummy_dn).to_normalized_s + normalized_dn.sub(/\Aplaceholder=/, '') + end + ## # Initialize a DN, escaping as required. Pass in attributes in name/value # pairs. If there is a left over argument, it will be appended to the dn @@ -28,22 +34,31 @@ class NormalizeLdapExternUids < ActiveRecord::Migration # so storing the dn as an escaped String and parsing parts as required # with a state machine seems sensible. def initialize(*args) - buffer = StringIO.new - - args.each_index do |index| - buffer << "=" if index.odd? - buffer << "," if index.even? && index != 0 + @dn = if args.length > 1 + initialize_array(args) + else + initialize_string(args[0]) + end + end - arg = args[index].downcase + def initialize_array(args) + buffer = StringIO.new - buffer << if index < args.length - 1 || index.odd? - self.class.escape(arg) - else - arg - end + args.each_with_index do |arg, index| + if index.even? # key + buffer << "," if index > 0 + buffer << arg + else # value + buffer << "=" + buffer << self.class.escape(arg) + end end - @dn = buffer.string + buffer.string + end + + def initialize_string(arg) + arg.to_s end ## @@ -58,11 +73,11 @@ class NormalizeLdapExternUids < ActiveRecord::Migration value = StringIO.new hex_buffer = "" - @dn.each_char do |char| + @dn.each_char.with_index do |char, dn_index| case state when :key then case char - when 'a'..'z' then + when 'a'..'z', 'A'..'Z' then state = :key_normal key << char when '0'..'9' then @@ -74,7 +89,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration when :key_normal then case char when '=' then state = :value - when 'a'..'z', '0'..'9', '-', ' ' then key << char + when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") end when :key_oid then @@ -93,7 +108,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration value << char when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else @@ -105,7 +120,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration when '\\' then state = :value_normal_escape when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") @@ -113,34 +128,20 @@ class NormalizeLdapExternUids < ActiveRecord::Migration end when :value_normal_escape then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal_escape_hex hex_buffer = char - when /\s/ then - state = :value_normal_escape_whitespace - value << char else state = :value_normal value << char end when :value_normal_escape_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end - when :value_normal_escape_whitespace then - case char - when '\\' then state = :value_normal_escape - when ',' then - state = :key - yield key.string.strip, value.string # Don't strip trailing escaped space! - key = StringIO.new - value = StringIO.new - when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") - else value << char - end when :value_quoted then case char when '\\' then state = :value_quoted_escape @@ -149,7 +150,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration end when :value_quoted_escape then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted_escape_hex hex_buffer = char else @@ -158,27 +159,27 @@ class NormalizeLdapExternUids < ActiveRecord::Migration end when :value_quoted_escape_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") end when :value_hexstring then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring_hex value << char when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") end when :value_hexstring_hex then case char - when '0'..'9', 'a'..'f' then + when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring value << char else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") @@ -188,7 +189,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration when ' ' then state = :value_end when ',' then state = :key - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") @@ -201,7 +202,25 @@ class NormalizeLdapExternUids < ActiveRecord::Migration raise(MalformedDnError, 'DN string ended unexpectedly') unless [:value, :value_normal, :value_hexstring, :value_end].include? state - yield key.string.strip, value.string.rstrip + yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) + end + + def rstrip_except_escaped(str, dn_index) + str_ends_with_whitespace = str.match(/\s\z/) + + if str_ends_with_whitespace + dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/) + + if dn_part_ends_with_escaped_whitespace + dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1] + num_chars_to_remove = dn_part_rwhitespace.length - 1 + str = str[0, str.length - num_chars_to_remove] + else + str.rstrip! + end + end + + str end ## @@ -221,7 +240,7 @@ class NormalizeLdapExternUids < ActiveRecord::Migration ## # Return the DN as an escaped and normalized string. def to_normalized_s - self.class.new(*to_a).to_s + self.class.new(*to_a).to_s.downcase end # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions -- cgit v1.2.1 From 1879980f2e206943fd97254ec25b7e7fd835871a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Tue, 3 Oct 2017 15:27:52 -0700 Subject: Move migration to background --- .../20170921101004_normalize_ldap_extern_uids.rb | 293 +------------------- .../normalize_ldap_extern_uids_range.rb | 304 +++++++++++++++++++++ 2 files changed, 313 insertions(+), 284 deletions(-) create mode 100644 lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb diff --git a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb index 234e8a3270b..2230bb0e53c 100644 --- a/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb +++ b/db/post_migrate/20170921101004_normalize_ldap_extern_uids.rb @@ -5,297 +5,22 @@ class NormalizeLdapExternUids < ActiveRecord::Migration include Gitlab::Database::MigrationHelpers DOWNTIME = false + MIGRATION = 'NormalizeLdapExternUidsRange'.freeze + DELAY_INTERVAL = 10.seconds - class Identity < ActiveRecord::Base - self.table_name = 'identities' - end - - # Copied this class to make this migration resilient to future code changes. - # And if the normalize behavior is changed in the future, it must be - # accompanied by another migration. - module Gitlab - module LDAP - MalformedDnError = Class.new(StandardError) - UnsupportedDnFormatError = Class.new(StandardError) - - class DN - def self.normalize_value(given_value) - dummy_dn = "placeholder=#{given_value}" - normalized_dn = new(*dummy_dn).to_normalized_s - normalized_dn.sub(/\Aplaceholder=/, '') - end - - ## - # Initialize a DN, escaping as required. Pass in attributes in name/value - # pairs. If there is a left over argument, it will be appended to the dn - # without escaping (useful for a base string). - # - # Most uses of this class will be to escape a DN, rather than to parse it, - # so storing the dn as an escaped String and parsing parts as required - # with a state machine seems sensible. - def initialize(*args) - @dn = if args.length > 1 - initialize_array(args) - else - initialize_string(args[0]) - end - end - - def initialize_array(args) - buffer = StringIO.new - - args.each_with_index do |arg, index| - if index.even? # key - buffer << "," if index > 0 - buffer << arg - else # value - buffer << "=" - buffer << self.class.escape(arg) - end - end - - buffer.string - end - - def initialize_string(arg) - arg.to_s - end - - ## - # Parse a DN into key value pairs using ASN from - # http://tools.ietf.org/html/rfc2253 section 3. - # rubocop:disable Metrics/AbcSize - # rubocop:disable Metrics/CyclomaticComplexity - # rubocop:disable Metrics/PerceivedComplexity - def each_pair - state = :key - key = StringIO.new - value = StringIO.new - hex_buffer = "" - - @dn.each_char.with_index do |char, dn_index| - case state - when :key then - case char - when 'a'..'z', 'A'..'Z' then - state = :key_normal - key << char - when '0'..'9' then - state = :key_oid - key << char - when ' ' then state = :key - else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") - end - when :key_normal then - case char - when '=' then state = :value - when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") - end - when :key_oid then - case char - when '=' then state = :value - when '0'..'9', '.', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") - end - when :value then - case char - when '\\' then state = :value_normal_escape - when '"' then state = :value_quoted - when ' ' then state = :value - when '#' then - state = :value_hexstring - value << char - when ',' then - state = :key - yield key.string.strip, rstrip_except_escaped(value.string, dn_index) - key = StringIO.new - value = StringIO.new - else - state = :value_normal - value << char - end - when :value_normal then - case char - when '\\' then state = :value_normal_escape - when ',' then - state = :key - yield key.string.strip, rstrip_except_escaped(value.string, dn_index) - key = StringIO.new - value = StringIO.new - when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") - else value << char - end - when :value_normal_escape then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_normal_escape_hex - hex_buffer = char - else - state = :value_normal - value << char - end - when :value_normal_escape_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_normal - value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") - end - when :value_quoted then - case char - when '\\' then state = :value_quoted_escape - when '"' then state = :value_end - else value << char - end - when :value_quoted_escape then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_quoted_escape_hex - hex_buffer = char - else - state = :value_quoted - value << char - end - when :value_quoted_escape_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_quoted - value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") - end - when :value_hexstring then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_hexstring_hex - value << char - when ' ' then state = :value_end - when ',' then - state = :key - yield key.string.strip, rstrip_except_escaped(value.string, dn_index) - key = StringIO.new - value = StringIO.new - else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") - end - when :value_hexstring_hex then - case char - when '0'..'9', 'a'..'f', 'A'..'F' then - state = :value_hexstring - value << char - else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") - end - when :value_end then - case char - when ' ' then state = :value_end - when ',' then - state = :key - yield key.string.strip, rstrip_except_escaped(value.string, dn_index) - key = StringIO.new - value = StringIO.new - else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") - end - else raise "Fell out of state machine" - end - end - - # Last pair - raise(MalformedDnError, 'DN string ended unexpectedly') unless - [:value, :value_normal, :value_hexstring, :value_end].include? state - - yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) - end - - def rstrip_except_escaped(str, dn_index) - str_ends_with_whitespace = str.match(/\s\z/) + disable_ddl_transaction! - if str_ends_with_whitespace - dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/) - - if dn_part_ends_with_escaped_whitespace - dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1] - num_chars_to_remove = dn_part_rwhitespace.length - 1 - str = str[0, str.length - num_chars_to_remove] - else - str.rstrip! - end - end - - str - end - - ## - # Returns the DN as an array in the form expected by the constructor. - def to_a - a = [] - self.each_pair { |key, value| a << key << value } unless @dn.empty? - a - end - - ## - # Return the DN as an escaped string. - def to_s - @dn - end - - ## - # Return the DN as an escaped and normalized string. - def to_normalized_s - self.class.new(*to_a).to_s.downcase - end - - # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions - # for DN values. All of the following must be escaped in any normal string - # using a single backslash ('\') as escape. The space character is left - # out here because in a "normalized" string, spaces should only be escaped - # if necessary (i.e. leading or trailing space). - NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze - - # The following must be represented as escaped hex - HEX_ESCAPES = { - "\n" => '\0a', - "\r" => '\0d' - }.freeze - - # Compiled character class regexp using the keys from the above hash, and - # checking for a space or # at the start, or space at the end, of the - # string. - ESCAPE_RE = Regexp.new("(^ |^#| $|[" + - NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join + - "])") - - HEX_ESCAPE_RE = Regexp.new("([" + - HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join + - "])") - - ## - # Escape a string for use in a DN value - def self.escape(string) - escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char } - escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } - end + class Identity < ActiveRecord::Base + include EachBatch - ## - # Proxy all other requests to the string object, because a DN is mainly - # used within the library as a string - # rubocop:disable GitlabSecurity/PublicSend - def method_missing(method, *args, &block) - @dn.send(method, *args, &block) - end - end - end + self.table_name = 'identities' end def up ldap_identities = Identity.where("provider like 'ldap%'") - ldap_identities.find_each do |identity| - begin - identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s - unless identity.save - say "Unable to normalize \"#{identity.extern_uid}\". Skipping." - end - rescue Gitlab::LDAP::MalformedDnError, Gitlab::LDAP::UnsupportedDnFormatError => e - say "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping." - end + + if ldap_identities.any? + queue_background_migration_jobs_by_range_at_intervals(Identity, MIGRATION, DELAY_INTERVAL) end end diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb new file mode 100644 index 00000000000..7a18feb8a4f --- /dev/null +++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb @@ -0,0 +1,304 @@ +module Gitlab + module BackgroundMigration + class NormalizeLdapExternUidsRange + class Identity < ActiveRecord::Base + self.table_name = 'identities' + end + + # Copied this class to make this migration resilient to future code changes. + # And if the normalize behavior is changed in the future, it must be + # accompanied by another migration. + module Gitlab + module LDAP + MalformedDnError = Class.new(StandardError) + UnsupportedDnFormatError = Class.new(StandardError) + + class DN + def self.normalize_value(given_value) + dummy_dn = "placeholder=#{given_value}" + normalized_dn = new(*dummy_dn).to_normalized_s + normalized_dn.sub(/\Aplaceholder=/, '') + end + + ## + # Initialize a DN, escaping as required. Pass in attributes in name/value + # pairs. If there is a left over argument, it will be appended to the dn + # without escaping (useful for a base string). + # + # Most uses of this class will be to escape a DN, rather than to parse it, + # so storing the dn as an escaped String and parsing parts as required + # with a state machine seems sensible. + def initialize(*args) + if args.length > 1 + initialize_array(args) + else + initialize_string(args[0]) + end + end + + def initialize_array(args) + buffer = StringIO.new + + args.each_with_index do |arg, index| + if index.even? # key + buffer << "," if index > 0 + buffer << arg + else # value + buffer << "=" + buffer << self.class.escape(arg) + end + end + + @dn = buffer.string + end + + def initialize_string(arg) + @dn = arg.to_s + end + + ## + # Parse a DN into key value pairs using ASN from + # http://tools.ietf.org/html/rfc2253 section 3. + # rubocop:disable Metrics/AbcSize + # rubocop:disable Metrics/CyclomaticComplexity + # rubocop:disable Metrics/PerceivedComplexity + def each_pair + state = :key + key = StringIO.new + value = StringIO.new + hex_buffer = "" + + @dn.each_char.with_index do |char, dn_index| + case state + when :key then + case char + when 'a'..'z', 'A'..'Z' then + state = :key_normal + key << char + when '0'..'9' then + state = :key_oid + key << char + when ' ' then state = :key + else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") + end + when :key_normal then + case char + when '=' then state = :value + when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char + else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") + end + when :key_oid then + case char + when '=' then state = :value + when '0'..'9', '.', ' ' then key << char + else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") + end + when :value then + case char + when '\\' then state = :value_normal_escape + when '"' then state = :value_quoted + when ' ' then state = :value + when '#' then + state = :value_hexstring + value << char + when ',' then + state = :key + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) + key = StringIO.new + value = StringIO.new + else + state = :value_normal + value << char + end + when :value_normal then + case char + when '\\' then state = :value_normal_escape + when ',' then + state = :key + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) + key = StringIO.new + value = StringIO.new + when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") + else value << char + end + when :value_normal_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal_escape_hex + hex_buffer = char + else + state = :value_normal + value << char + end + when :value_normal_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_normal + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") + end + when :value_quoted then + case char + when '\\' then state = :value_quoted_escape + when '"' then state = :value_end + else value << char + end + when :value_quoted_escape then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted_escape_hex + hex_buffer = char + else + state = :value_quoted + value << char + end + when :value_quoted_escape_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_quoted + value << "#{hex_buffer}#{char}".to_i(16).chr + else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") + end + when :value_hexstring then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring_hex + value << char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) + key = StringIO.new + value = StringIO.new + else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") + end + when :value_hexstring_hex then + case char + when '0'..'9', 'a'..'f', 'A'..'F' then + state = :value_hexstring + value << char + else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") + end + when :value_end then + case char + when ' ' then state = :value_end + when ',' then + state = :key + yield key.string.strip, rstrip_except_escaped(value.string, dn_index) + key = StringIO.new + value = StringIO.new + else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") + end + else raise "Fell out of state machine" + end + end + + # Last pair + raise(MalformedDnError, 'DN string ended unexpectedly') unless + [:value, :value_normal, :value_hexstring, :value_end].include? state + + yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) + end + + def rstrip_except_escaped(str, dn_index) + str_ends_with_whitespace = str.match(/\s\z/) + + if str_ends_with_whitespace + dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/) + + if dn_part_ends_with_escaped_whitespace + dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1] + num_chars_to_remove = dn_part_rwhitespace.length - 1 + str = str[0, str.length - num_chars_to_remove] + else + str.rstrip! + end + end + + str + end + + ## + # Returns the DN as an array in the form expected by the constructor. + def to_a + a = [] + self.each_pair { |key, value| a << key << value } unless @dn.empty? + a + end + + ## + # Return the DN as an escaped string. + def to_s + @dn + end + + ## + # Return the DN as an escaped and normalized string. + def to_normalized_s + self.class.new(*to_a).to_s.downcase + end + + # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions + # for DN values. All of the following must be escaped in any normal string + # using a single backslash ('\') as escape. The space character is left + # out here because in a "normalized" string, spaces should only be escaped + # if necessary (i.e. leading or trailing space). + NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze + + # The following must be represented as escaped hex + HEX_ESCAPES = { + "\n" => '\0a', + "\r" => '\0d' + }.freeze + + # Compiled character class regexp using the keys from the above hash, and + # checking for a space or # at the start, or space at the end, of the + # string. + ESCAPE_RE = Regexp.new("(^ |^#| $|[" + + NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join + + "])") + + HEX_ESCAPE_RE = Regexp.new("([" + + HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join + + "])") + + ## + # Escape a string for use in a DN value + def self.escape(string) + escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char } + escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } + end + + ## + # Proxy all other requests to the string object, because a DN is mainly + # used within the library as a string + # rubocop:disable GitlabSecurity/PublicSend + def method_missing(method, *args, &block) + @dn.send(method, *args, &block) + end + end + end + end + + def perform(start_id, end_id) + return unless migrate? + + ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id) + ldap_identities.each do |identity| + begin + identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s + unless identity.save + Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping." + end + rescue Gitlab::LDAP::MalformedDnError, Gitlab::LDAP::UnsupportedDnFormatError => e + Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping." + end + end + end + + def migrate? + Identity.table_exists? + end + end + end +end -- cgit v1.2.1 From 9ac732dda24276f953314d6b3bd5c60571ce82bf Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 4 Oct 2017 22:43:07 -0700 Subject: Add migration specs --- .../normalize_ldap_extern_uids_range_spec.rb | 36 ++++++++++++++ spec/migrations/normalize_ldap_extern_uids_spec.rb | 56 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb create mode 100644 spec/migrations/normalize_ldap_extern_uids_spec.rb diff --git a/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb new file mode 100644 index 00000000000..dfbf1bb681a --- /dev/null +++ b/spec/lib/gitlab/background_migration/normalize_ldap_extern_uids_range_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::NormalizeLdapExternUidsRange, :migration, schema: 20170921101004 do + let!(:identities) { table(:identities) } + + before do + # LDAP identities + (1..4).each do |i| + identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i) + end + + # Non-LDAP identity + identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5) + + # Another LDAP identity + identities.create!(id: 6, provider: 'ldapmain', extern_uid: " uid = foo 6, ou = People, dc = example, dc = com ", user_id: 6) + end + + it 'normalizes the LDAP identities in the range' do + described_class.new.perform(1, 3) + expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com") + expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com") + expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com") + expect(identities.find(4).extern_uid).to eq(" uid = foo 4, ou = People, dc = example, dc = com ") + expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ") + expect(identities.find(6).extern_uid).to eq(" uid = foo 6, ou = People, dc = example, dc = com ") + + described_class.new.perform(4, 6) + expect(identities.find(1).extern_uid).to eq("uid=foo 1,ou=people,dc=example,dc=com") + expect(identities.find(2).extern_uid).to eq("uid=foo 2,ou=people,dc=example,dc=com") + expect(identities.find(3).extern_uid).to eq("uid=foo 3,ou=people,dc=example,dc=com") + expect(identities.find(4).extern_uid).to eq("uid=foo 4,ou=people,dc=example,dc=com") + expect(identities.find(5).extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ") + expect(identities.find(6).extern_uid).to eq("uid=foo 6,ou=people,dc=example,dc=com") + end +end diff --git a/spec/migrations/normalize_ldap_extern_uids_spec.rb b/spec/migrations/normalize_ldap_extern_uids_spec.rb new file mode 100644 index 00000000000..262d7742aaf --- /dev/null +++ b/spec/migrations/normalize_ldap_extern_uids_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20170921101004_normalize_ldap_extern_uids') + +describe NormalizeLdapExternUids, :migration, :sidekiq do + let!(:identities) { table(:identities) } + + around do |example| + Timecop.freeze { example.run } + end + + before do + stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_BATCH_SIZE", 2) + stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2) + + # LDAP identities + (1..4).each do |i| + identities.create!(id: i, provider: 'ldapmain', extern_uid: " uid = foo #{i}, ou = People, dc = example, dc = com ", user_id: i) + end + + # Non-LDAP identity + identities.create!(id: 5, provider: 'foo', extern_uid: " uid = foo 5, ou = People, dc = example, dc = com ", user_id: 5) + end + + it 'correctly schedules background migrations' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) + expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(10.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) + expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(20.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) + expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(30.seconds.from_now.to_f) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'migrates the LDAP identities' do + Sidekiq::Testing.inline! do + migrate! + identities.where(id: 1..4).each do |identity| + expect(identity.extern_uid).to eq("uid=foo #{identity.id},ou=people,dc=example,dc=com") + end + end + end + + it 'does not modify non-LDAP identities' do + Sidekiq::Testing.inline! do + migrate! + identity = identities.last + expect(identity.extern_uid).to eq(" uid = foo 5, ou = People, dc = example, dc = com ") + end + end +end -- cgit v1.2.1 From 1c945de93849b43d2bec9407bd0a7c4afd759796 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Wed, 4 Oct 2017 23:04:53 -0700 Subject: Add changelog entry for LDAP normalization --- changelogs/unreleased/mk-normalize-ldap-user-dns.yml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 changelogs/unreleased/mk-normalize-ldap-user-dns.yml diff --git a/changelogs/unreleased/mk-normalize-ldap-user-dns.yml b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml new file mode 100644 index 00000000000..5a128d6acc1 --- /dev/null +++ b/changelogs/unreleased/mk-normalize-ldap-user-dns.yml @@ -0,0 +1,5 @@ +--- +title: Search or compare LDAP DNs case-insensitively and ignore excess whitespace +merge_request: 14697 +author: +type: fixed -- cgit v1.2.1 From 1d1ad7e0b68039100029f20a84fd867fe92cbb32 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 5 Oct 2017 03:27:07 -0700 Subject: Refactor DN error classes --- .../normalize_ldap_extern_uids_range.rb | 29 +++---- lib/gitlab/ldap/dn.rb | 27 +++--- lib/gitlab/ldap/person.rb | 2 +- spec/lib/gitlab/ldap/dn_spec.rb | 96 +++++++++++----------- 4 files changed, 78 insertions(+), 76 deletions(-) diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb index 7a18feb8a4f..cf7eafcff3b 100644 --- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb +++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb @@ -10,10 +10,11 @@ module Gitlab # accompanied by another migration. module Gitlab module LDAP - MalformedDnError = Class.new(StandardError) - UnsupportedDnFormatError = Class.new(StandardError) - class DN + FormatError = Class.new(StandardError) + MalformedError = Class.new(FormatError) + UnsupportedError = Class.new(FormatError) + def self.normalize_value(given_value) dummy_dn = "placeholder=#{given_value}" normalized_dn = new(*dummy_dn).to_normalized_s @@ -79,19 +80,19 @@ module Gitlab state = :key_oid key << char when ' ' then state = :key - else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") + else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") end when :key_normal then case char when '=' then state = :value when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") + else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"") end when :key_oid then case char when '=' then state = :value when '0'..'9', '.', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") + else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"") end when :value then case char @@ -118,7 +119,7 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") + when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported") else value << char end when :value_normal_escape then @@ -135,7 +136,7 @@ module Gitlab when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") + else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end when :value_quoted then case char @@ -157,7 +158,7 @@ module Gitlab when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") + else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") end when :value_hexstring then case char @@ -170,14 +171,14 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") + else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"") end when :value_hexstring_hex then case char when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring value << char - else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") + else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"") end when :value_end then case char @@ -187,14 +188,14 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") + else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"") end else raise "Fell out of state machine" end end # Last pair - raise(MalformedDnError, 'DN string ended unexpectedly') unless + raise(MalformedError, 'DN string ended unexpectedly') unless [:value, :value_normal, :value_hexstring, :value_end].include? state yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) @@ -290,7 +291,7 @@ module Gitlab unless identity.save Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping." end - rescue Gitlab::LDAP::MalformedDnError, Gitlab::LDAP::UnsupportedDnFormatError => e + rescue Gitlab::LDAP::DN::FormatError => e Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping." end end diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index 87a7f1c6bc0..c469420f1f9 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -21,10 +21,11 @@ # class also helps take care of that. module Gitlab module LDAP - MalformedDnError = Class.new(StandardError) - UnsupportedDnFormatError = Class.new(StandardError) - class DN + FormatError = Class.new(StandardError) + MalformedError = Class.new(FormatError) + UnsupportedError = Class.new(FormatError) + def self.normalize_value(given_value) dummy_dn = "placeholder=#{given_value}" normalized_dn = new(*dummy_dn).to_normalized_s @@ -90,19 +91,19 @@ module Gitlab state = :key_oid key << char when ' ' then state = :key - else raise(MalformedDnError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") + else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"") end when :key_normal then case char when '=' then state = :value when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN attribute type name character \"#{char}\"") + else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"") end when :key_oid then case char when '=' then state = :value when '0'..'9', '.', ' ' then key << char - else raise(MalformedDnError, "Unrecognized RDN OID attribute type name character \"#{char}\"") + else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"") end when :value then case char @@ -129,7 +130,7 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - when '+' then raise(UnsupportedDnFormatError, "Multivalued RDNs are not supported") + when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported") else value << char end when :value_normal_escape then @@ -146,7 +147,7 @@ module Gitlab when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_normal value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") + else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"") end when :value_quoted then case char @@ -168,7 +169,7 @@ module Gitlab when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_quoted value << "#{hex_buffer}#{char}".to_i(16).chr - else raise(MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") + else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"") end when :value_hexstring then case char @@ -181,14 +182,14 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - else raise(MalformedDnError, "Expected the first character of a hex pair, but got \"#{char}\"") + else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"") end when :value_hexstring_hex then case char when '0'..'9', 'a'..'f', 'A'..'F' then state = :value_hexstring value << char - else raise(MalformedDnError, "Expected the second character of a hex pair, but got \"#{char}\"") + else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"") end when :value_end then case char @@ -198,14 +199,14 @@ module Gitlab yield key.string.strip, rstrip_except_escaped(value.string, dn_index) key = StringIO.new value = StringIO.new - else raise(MalformedDnError, "Expected the end of an attribute value, but got \"#{char}\"") + else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"") end else raise "Fell out of state machine" end end # Last pair - raise(MalformedDnError, 'DN string ended unexpectedly') unless + raise(MalformedError, 'DN string ended unexpectedly') unless [:value, :value_normal, :value_hexstring, :value_end].include? state yield key.string.strip, rstrip_except_escaped(value.string, @dn.length) diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index e91e3a176e6..81aa352e656 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -42,7 +42,7 @@ module Gitlab # 2. The string is downcased (for case-insensitivity) def self.normalize_uid(uid) ::Gitlab::LDAP::DN.normalize_value(uid) - rescue ::Gitlab::LDAP::MalformedDnError, ::Gitlab::LDAP::UnsupportedDnFormatError => e + rescue ::Gitlab::LDAP::DN::FormatError => e Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") Rails.logger.info(e.backtrace.join("\n")) diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index 709eadd7e38..c300f7160fe 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -12,64 +12,64 @@ describe Gitlab::LDAP::DN do context 'when ending with a comma' do let(:given) { 'John Smith,' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'when given a BER encoded attribute value with a space in it' do let(:given) { '#aa aa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") end end context 'when given a BER encoded attribute value with a non-hex character in it' do let(:given) { '#aaXaaa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"X\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") end end context 'when given a BER encoded attribute value with a non-hex character in it' do let(:given) { '#aaaYaa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"Y\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") end end context 'when given a hex pair with a non-hex character in it, inside double quotes' do let(:given) { '"Sebasti\\cX\\a1n"' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") end end context 'with an open (as opposed to closed) double quote' do let(:given) { '"James' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'with an invalid escaped hex code' do let(:given) { 'J\ames' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') end end context 'with a value ending with the escape character' do let(:given) { 'foo\\' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end end @@ -119,8 +119,8 @@ describe Gitlab::LDAP::DN do context 'without extraneous whitespace' do let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' } - it 'raises UnsupportedDnFormatError' do - expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) end end @@ -128,16 +128,16 @@ describe Gitlab::LDAP::DN do context 'around the phone number plus sign' do let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' } - it 'raises UnsupportedDnFormatError' do - expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) end end context 'not around the phone number plus sign' do let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' } - it 'raises UnsupportedDnFormatError' do - expect { subject }.to raise_error(Gitlab::LDAP::UnsupportedDnFormatError) + it 'raises UnsupportedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError) end end end @@ -148,104 +148,104 @@ describe Gitlab::LDAP::DN do context 'when ending with a comma' do let(:given) { 'uid=John Smith,' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'when given a BER encoded attribute value with a space in it' do let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the end of an attribute value, but got \"a\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"") end end context 'when given a BER encoded attribute value with a non-hex character in it' do let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the first character of a hex pair, but got \"X\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"") end end context 'when given a BER encoded attribute value with a non-hex character in it' do let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair, but got \"Y\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"") end end context 'when given a hex pair with a non-hex character in it, inside double quotes' do let(:given) { 'uid="Sebasti\\cX\\a1n"' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"") end end context 'without a name value pair' do let(:given) { 'John' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'with an open (as opposed to closed) double quote' do let(:given) { 'cn="James' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'with an invalid escaped hex code' do let(:given) { 'cn=J\ames' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Invalid escaped hex code "\am"') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"') end end context 'with a value ending with the escape character' do let(:given) { 'cn=\\' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'DN string ended unexpectedly') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly') end end context 'with an invalid OID attribute type name' do let(:given) { '1.2.d=Value' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN OID attribute type name character "d"') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"') end end context 'with a period in a non-OID attribute type name' do let(:given) { 'd1.2=Value' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "."') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."') end end context 'when starting with non-space, non-alphanumeric character' do let(:given) { ' -uid=John Smith' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized first character of an RDN attribute type name "-"') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"') end end context 'when given a UID with an escaped equal sign' do let(:given) { 'uid\\=john' } - it 'raises MalformedDnError' do - expect { subject }.to raise_error(Gitlab::LDAP::MalformedDnError, 'Unrecognized RDN attribute type name character "\\"') + it 'raises MalformedError' do + expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"') end end end -- cgit v1.2.1 From 8c29a04549d4a956508ef9a92a043a309978fa34 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Thu, 5 Oct 2017 03:47:48 -0700 Subject: Leave bad DNs alone instead of raising errors --- lib/gitlab/ldap/auth_hash.rb | 2 +- lib/gitlab/ldap/person.rb | 11 ++++++++-- spec/lib/gitlab/ldap/dn_spec.rb | 36 +------------------------------- spec/lib/gitlab/ldap/person_spec.rb | 14 +++++++++++++ spec/support/ldap_shared_examples.rb | 40 ++++++++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 38 deletions(-) diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb index b173b879f5f..3123da17fd9 100644 --- a/lib/gitlab/ldap/auth_hash.rb +++ b/lib/gitlab/ldap/auth_hash.rb @@ -4,7 +4,7 @@ module Gitlab module LDAP class AuthHash < Gitlab::OAuth::AuthHash def uid - Gitlab::LDAP::DN.new(super).to_normalized_s + Gitlab::LDAP::Person.normalize_dn(super) end private diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb index 81aa352e656..38d7a9ba2f5 100644 --- a/lib/gitlab/ldap/person.rb +++ b/lib/gitlab/ldap/person.rb @@ -36,6 +36,14 @@ module Gitlab ] end + def self.normalize_dn(dn) + ::Gitlab::LDAP::DN.new(dn).to_normalized_s + rescue ::Gitlab::LDAP::DN::FormatError => e + Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}") + + dn + end + # Returns the UID in a normalized form. # # 1. Excess spaces are stripped @@ -44,7 +52,6 @@ module Gitlab ::Gitlab::LDAP::DN.normalize_value(uid) rescue ::Gitlab::LDAP::DN::FormatError => e Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}") - Rails.logger.info(e.backtrace.join("\n")) uid end @@ -72,7 +79,7 @@ module Gitlab end def dn - DN.new(entry.dn).to_normalized_s + self.class.normalize_dn(entry.dn) end private diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/ldap/dn_spec.rb index c300f7160fe..8e21ecdf9ab 100644 --- a/spec/lib/gitlab/ldap/dn_spec.rb +++ b/spec/lib/gitlab/ldap/dn_spec.rb @@ -78,41 +78,7 @@ describe Gitlab::LDAP::DN do describe '#to_normalized_s' do subject { described_class.new(given).to_normalized_s } - where(:test_description, :given, :expected) do - 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' - 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith, ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith,ou=people (aka. \\"humans\\"),dc=example,dc=com' - 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'for a null DN (empty string), returns empty string and does not error' | '' | '' - 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' - 'does not strip an escaped leading space in the last attribute value' | 'uid=\\ John Smith' | 'uid=\\ john smith' - 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'strips extraneous spaces after an escaped trailing space' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' - 'strips extraneous spaces after an escaped trailing space at the end of the DN' | 'uid=John Smith,ou=People,dc=example,dc=com\\ ' | 'uid=john smith,ou=people,dc=example,dc=com\\ ' - 'properly preserves escaped trailing space after unescaped trailing spaces' | 'uid=John Smith \\ ,ou=People,dc=example,dc=com' | 'uid=john smith \\ ,ou=people,dc=example,dc=com' - 'preserves multiple inner spaces in an attribute value' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'preserves inner spaces after an escaped space' | 'uid=John\\ Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" - 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com" - 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" - 'strips an unescaped trailing newline (actually an invalid DN?)' | "uid=John Smith\n,ou=People,dc=example,dc=com" | "uid=john smith,ou=people,dc=example,dc=com" - 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' - 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' - 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' - 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' - 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca' - 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca' - 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca' - 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca' - 'allows attribute type name OIDs' | '0.9.2342.19200300.100.1.25=Example,0.9.2342.19200300.100.1.25=Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' - 'strips extraneous whitespace from attribute type name OIDs' | '0.9.2342.19200300.100.1.25 = Example, 0.9.2342.19200300.100.1.25 = Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' - end - - with_them do - it 'normalizes the DN' do - assert_generic_test(test_description, subject, expected) - end - end + it_behaves_like 'normalizes a DN' context 'when we do not support the given DN format' do context 'multivalued RDNs' do diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/ldap/person_spec.rb index 743b3fbde2b..d204050ef66 100644 --- a/spec/lib/gitlab/ldap/person_spec.rb +++ b/spec/lib/gitlab/ldap/person_spec.rb @@ -16,6 +16,20 @@ describe Gitlab::LDAP::Person do ) end + describe '.normalize_dn' do + subject { described_class.normalize_dn(given) } + + it_behaves_like 'normalizes a DN' + + context 'with an exception during normalization' do + let(:given) { 'John "Smith,' } # just something that will cause an exception + + it 'returns the given DN unmodified' do + expect(subject).to eq(given) + end + end + end + describe '.normalize_uid' do subject { described_class.normalize_uid(given) } diff --git a/spec/support/ldap_shared_examples.rb b/spec/support/ldap_shared_examples.rb index 3ab8b1d73a1..52c34e78965 100644 --- a/spec/support/ldap_shared_examples.rb +++ b/spec/support/ldap_shared_examples.rb @@ -1,3 +1,43 @@ +shared_examples_for 'normalizes a DN' do + using RSpec::Parameterized::TableSyntax + + where(:test_description, :given, :expected) do + 'strips extraneous whitespace' | 'uid =John Smith , ou = People, dc= example,dc =com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'strips extraneous whitespace for a DN with a single RDN' | 'uid = John Smith' | 'uid=john smith' + 'unescapes non-reserved, non-special Unicode characters' | 'uid = Sebasti\\c3\\a1n\\ C.\\20Smith, ou=People (aka. \\22humans\\") ,dc=example, dc=com' | 'uid=sebastián c. smith,ou=people (aka. \\"humans\\"),dc=example,dc=com' + 'downcases the whole string' | 'UID=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'for a null DN (empty string), returns empty string and does not error' | '' | '' + 'does not strip an escaped leading space in an attribute value' | 'uid=\\ John Smith,ou=People,dc=example,dc=com' | 'uid=\\ john smith,ou=people,dc=example,dc=com' + 'does not strip an escaped leading space in the last attribute value' | 'uid=\\ John Smith' | 'uid=\\ john smith' + 'does not strip an escaped trailing space in an attribute value' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'strips extraneous spaces after an escaped trailing space' | 'uid=John Smith\\ ,ou=People,dc=example,dc=com' | 'uid=john smith\\ ,ou=people,dc=example,dc=com' + 'strips extraneous spaces after an escaped trailing space at the end of the DN' | 'uid=John Smith,ou=People,dc=example,dc=com\\ ' | 'uid=john smith,ou=people,dc=example,dc=com\\ ' + 'properly preserves escaped trailing space after unescaped trailing spaces' | 'uid=John Smith \\ ,ou=People,dc=example,dc=com' | 'uid=john smith \\ ,ou=people,dc=example,dc=com' + 'preserves multiple inner spaces in an attribute value' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'preserves inner spaces after an escaped space' | 'uid=John\\ Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'hex-escapes an escaped leading newline in an attribute value' | "uid=\\\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" + 'hex-escapes and does not strip an escaped trailing newline in an attribute value' | "uid=John Smith\\\n,ou=People,dc=example,dc=com" | "uid=john smith\\0a,ou=people,dc=example,dc=com" + 'hex-escapes an unescaped leading newline (actually an invalid DN?)' | "uid=\nJohn Smith,ou=People,dc=example,dc=com" | "uid=\\0ajohn smith,ou=people,dc=example,dc=com" + 'strips an unescaped trailing newline (actually an invalid DN?)' | "uid=John Smith\n,ou=People,dc=example,dc=com" | "uid=john smith,ou=people,dc=example,dc=com" + 'does not strip if no extraneous whitespace' | 'uid=John Smith,ou=People,dc=example,dc=com' | 'uid=john smith,ou=people,dc=example,dc=com' + 'does not modify an escaped equal sign in an attribute value' | 'uid= foo \\= bar' | 'uid=foo \\= bar' + 'converts an escaped hex equal sign to an escaped equal sign in an attribute value' | 'uid= foo \\3D bar' | 'uid=foo \\= bar' + 'does not modify an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\, CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'converts an escaped hex comma to an escaped comma in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\2C CA' | 'uid=john c. smith,ou=san francisco\\, ca' + 'does not modify an escaped hex carriage return character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0DCA' | 'uid=john c. smith,ou=san francisco\\,\\0dca' + 'does not modify an escaped hex line feed character in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0aca' + 'does not modify an escaped hex CRLF in an attribute value' | 'uid= John C. Smith, ou=San Francisco\\,\\0D\\0ACA' | 'uid=john c. smith,ou=san francisco\\,\\0d\\0aca' + 'allows attribute type name OIDs' | '0.9.2342.19200300.100.1.25=Example,0.9.2342.19200300.100.1.25=Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' + 'strips extraneous whitespace from attribute type name OIDs' | '0.9.2342.19200300.100.1.25 = Example, 0.9.2342.19200300.100.1.25 = Com' | '0.9.2342.19200300.100.1.25=example,0.9.2342.19200300.100.1.25=com' + end + + with_them do + it 'normalizes the DN' do + assert_generic_test(test_description, subject, expected) + end + end +end + shared_examples_for 'normalizes a DN attribute value' do using RSpec::Parameterized::TableSyntax -- cgit v1.2.1 From e0a0c6b04ebcd92640cd9a840c5a45ec39d0d59a Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 6 Oct 2017 10:48:22 -0700 Subject: Make internal methods private --- lib/gitlab/ldap/dn.rb | 42 ++++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index c469420f1f9..ad66f5e96e4 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -48,26 +48,6 @@ module Gitlab end end - def initialize_array(args) - buffer = StringIO.new - - args.each_with_index do |arg, index| - if index.even? # key - buffer << "," if index > 0 - buffer << arg - else # value - buffer << "=" - buffer << self.class.escape(arg) - end - end - - @dn = buffer.string - end - - def initialize_string(arg) - @dn = arg.to_s - end - ## # Parse a DN into key value pairs using ASN from # http://tools.ietf.org/html/rfc2253 section 3. @@ -281,6 +261,28 @@ module Gitlab escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } end + private + + def initialize_array(args) + buffer = StringIO.new + + args.each_with_index do |arg, index| + if index.even? # key + buffer << "," if index > 0 + buffer << arg + else # value + buffer << "=" + buffer << self.class.escape(arg) + end + end + + @dn = buffer.string + end + + def initialize_string(arg) + @dn = arg.to_s + end + ## # Proxy all other requests to the string object, because a DN is mainly # used within the library as a string -- cgit v1.2.1 From 2df7d03586dbe9daa883ccd660d77d7522df29f8 Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 6 Oct 2017 10:53:35 -0700 Subject: Redefine `respond_to?` in light of `method_missing` --- lib/gitlab/ldap/dn.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb index ad66f5e96e4..d6142dc6549 100644 --- a/lib/gitlab/ldap/dn.rb +++ b/lib/gitlab/ldap/dn.rb @@ -290,6 +290,12 @@ module Gitlab def method_missing(method, *args, &block) @dn.send(method, *args, &block) end + + ## + # Redefined to be consistent with redefined `method_missing` behavior + def respond_to?(sym, include_private = false) + @dn.respond_to?(sym, include_private) + end end end end -- cgit v1.2.1 From ac5784368fb23260abd4050a48c4443017c87bfc Mon Sep 17 00:00:00 2001 From: Michael Kozono Date: Fri, 6 Oct 2017 10:57:00 -0700 Subject: Sync up hard coded DN class in migration --- .../normalize_ldap_extern_uids_range.rb | 48 +++++++++++++--------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb index cf7eafcff3b..bc53e6d7f94 100644 --- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb +++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb @@ -37,26 +37,6 @@ module Gitlab end end - def initialize_array(args) - buffer = StringIO.new - - args.each_with_index do |arg, index| - if index.even? # key - buffer << "," if index > 0 - buffer << arg - else # value - buffer << "=" - buffer << self.class.escape(arg) - end - end - - @dn = buffer.string - end - - def initialize_string(arg) - @dn = arg.to_s - end - ## # Parse a DN into key value pairs using ASN from # http://tools.ietf.org/html/rfc2253 section 3. @@ -270,6 +250,28 @@ module Gitlab escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] } end + private + + def initialize_array(args) + buffer = StringIO.new + + args.each_with_index do |arg, index| + if index.even? # key + buffer << "," if index > 0 + buffer << arg + else # value + buffer << "=" + buffer << self.class.escape(arg) + end + end + + @dn = buffer.string + end + + def initialize_string(arg) + @dn = arg.to_s + end + ## # Proxy all other requests to the string object, because a DN is mainly # used within the library as a string @@ -277,6 +279,12 @@ module Gitlab def method_missing(method, *args, &block) @dn.send(method, *args, &block) end + + ## + # Redefined to be consistent with redefined `method_missing` behavior + def respond_to?(sym, include_private = false) + @dn.respond_to?(sym, include_private) + end end end end -- cgit v1.2.1 From 1182fccd3ee894bf971c13a3f3ecf6eff774c1ea Mon Sep 17 00:00:00 2001 From: Stan Hu Date: Sat, 7 Oct 2017 12:10:46 -0700 Subject: Remove executable permissions on images to make docs lint happy [ci skip] --- .../ux_guide/img/illustration-size-large-horizontal.png | Bin doc/development/ux_guide/img/illustration-size-medium.png | Bin .../ux_guide/img/illustrations-border-radius.png | Bin doc/development/ux_guide/img/illustrations-caps-do.png | Bin doc/development/ux_guide/img/illustrations-caps-don't.png | Bin doc/development/ux_guide/img/illustrations-color-grey.png | Bin doc/development/ux_guide/img/illustrations-color-orange.png | Bin doc/development/ux_guide/img/illustrations-color-purple.png | Bin doc/development/ux_guide/img/illustrations-geometric.png | Bin .../ux_guide/img/illustrations-palette-oragne.png | Bin .../ux_guide/img/illustrations-palette-purple.png | Bin 11 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 doc/development/ux_guide/img/illustration-size-large-horizontal.png mode change 100755 => 100644 doc/development/ux_guide/img/illustration-size-medium.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-border-radius.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-caps-do.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-caps-don't.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-color-grey.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-color-orange.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-color-purple.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-geometric.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-palette-oragne.png mode change 100755 => 100644 doc/development/ux_guide/img/illustrations-palette-purple.png diff --git a/doc/development/ux_guide/img/illustration-size-large-horizontal.png b/doc/development/ux_guide/img/illustration-size-large-horizontal.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustration-size-medium.png b/doc/development/ux_guide/img/illustration-size-medium.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-border-radius.png b/doc/development/ux_guide/img/illustrations-border-radius.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-caps-do.png b/doc/development/ux_guide/img/illustrations-caps-do.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-caps-don't.png b/doc/development/ux_guide/img/illustrations-caps-don't.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-color-grey.png b/doc/development/ux_guide/img/illustrations-color-grey.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-color-orange.png b/doc/development/ux_guide/img/illustrations-color-orange.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-color-purple.png b/doc/development/ux_guide/img/illustrations-color-purple.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-geometric.png b/doc/development/ux_guide/img/illustrations-geometric.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-palette-oragne.png b/doc/development/ux_guide/img/illustrations-palette-oragne.png old mode 100755 new mode 100644 diff --git a/doc/development/ux_guide/img/illustrations-palette-purple.png b/doc/development/ux_guide/img/illustrations-palette-purple.png old mode 100755 new mode 100644 -- cgit v1.2.1 From 392e5df796166f0ebe04cca8f6301bcdc676543e Mon Sep 17 00:00:00 2001 From: Bob Van Landuyt Date: Sun, 8 Oct 2017 18:16:04 +0200 Subject: Don't create fork networks for root projects that are deleted --- .../background_migration/create_fork_network_memberships_range.rb | 5 +++++ lib/gitlab/background_migration/populate_fork_networks_range.rb | 5 +++++ .../background_migration/populate_fork_networks_range_spec.rb | 8 ++++++++ 3 files changed, 18 insertions(+) diff --git a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb index 4b468e9cd58..c88eb9783ed 100644 --- a/lib/gitlab/background_migration/create_fork_network_memberships_range.rb +++ b/lib/gitlab/background_migration/create_fork_network_memberships_range.rb @@ -46,6 +46,11 @@ module Gitlab FROM fork_network_members WHERE fork_network_members.project_id = forked_project_links.forked_to_project_id ) + AND EXISTS ( + SELECT true + FROM projects + WHERE forked_project_links.forked_from_project_id = projects.id + ) AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} MISSING_MEMBERS diff --git a/lib/gitlab/background_migration/populate_fork_networks_range.rb b/lib/gitlab/background_migration/populate_fork_networks_range.rb index 6c355ed1e75..2ef3a207dd3 100644 --- a/lib/gitlab/background_migration/populate_fork_networks_range.rb +++ b/lib/gitlab/background_migration/populate_fork_networks_range.rb @@ -20,6 +20,11 @@ module Gitlab FROM fork_networks WHERE forked_project_links.forked_from_project_id = fork_networks.root_project_id ) + AND EXISTS ( + SELECT true + FROM projects + WHERE projects.id = forked_project_links.forked_from_project_id + ) AND forked_project_links.id BETWEEN #{start_id} AND #{end_id} INSERT_NETWORKS diff --git a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb index 3ef1873e615..2c2684a6fc9 100644 --- a/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_fork_networks_range_spec.rb @@ -62,6 +62,14 @@ describe Gitlab::BackgroundMigration::PopulateForkNetworksRange, :migration, sch expect(base2_membership).not_to be_nil end + it 'skips links that had their source project deleted' do + forked_project_links.create(id: 6, forked_from_project_id: 99999, forked_to_project_id: create(:project).id) + + migration.perform(5, 8) + + expect(fork_networks.find_by(root_project_id: 99999)).to be_nil + end + it 'schedules a job for inserting memberships for forks-of-forks' do delay = Gitlab::BackgroundMigration::CreateForkNetworkMembershipsRange::RESCHEDULE_DELAY -- cgit v1.2.1 From b775fe7a8e3571dab4a4f0c332bdd447535ea406 Mon Sep 17 00:00:00 2001 From: Tim Zallmann Date: Fri, 6 Oct 2017 14:44:22 +0200 Subject: Updated Icons + Fix for Collapsed Groups Angle --- app/assets/images/icons.json | 2 +- app/assets/images/icons.svg | 2 +- app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml | 2 +- yarn.lock | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/assets/images/icons.json b/app/assets/images/icons.json index 6b8f85e37fd..c0ed2ffdcb2 100644 --- a/app/assets/images/icons.json +++ b/app/assets/images/icons.json @@ -1 +1 @@ -{"iconCount":135,"spriteSize":58718,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","comment-dots","comment-next","comment","comments","commit","credit-card","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file +{"iconCount":164,"spriteSize":72823,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-right","assignee","bold","book","branch","calendar","cancel","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","dashboard","disk","doc_code","doc_image","doc_text","download","duplicate","earth","eye-slash","eye","file-additions","file-deletion","file-modified","filter","folder","fork","geo-nodes","git-merge","group","history","home","hook","image-comment-dark","import","issue-block","issue-child","issue-close","issue-duplicate","issue-new","issue-open-m","issue-open","issue-parent","issues","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil","pipeline","play","plus-square-o","plus-square","plus","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","talic","task-done","template","thump-down","thump-up","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","user","users","volume-up","warning","work"]} \ No newline at end of file diff --git a/app/assets/images/icons.svg b/app/assets/images/icons.svg index 30cb2109ec2..b9829d0d450 100644 --- a/app/assets/images/icons.svg +++ b/app/assets/images/icons.svg @@ -1 +1 @@ - \ No newline at end of file +cursor_active \ No newline at end of file diff --git a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml index 610ff9001f7..ad0d51d28f9 100644 --- a/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml +++ b/app/views/layouts/nav/breadcrumbs/_collapsed_dropdown.html.haml @@ -4,7 +4,7 @@ %li.dropdown %button.text-expander.has-tooltip.js-breadcrumbs-collapsed-expander{ type: "button", data: { toggle: "dropdown", container: "body" }, "aria-label": button_tooltip, title: button_tooltip } = icon("ellipsis-h") - = sprite_icon("angle-right", css_class: "breadcrumbs-list-angle") + = sprite_icon("angle-right", size: 8, css_class: "breadcrumbs-list-angle") .dropdown-menu %ul - @breadcrumb_dropdown_links[dropdown_location].each_with_index do |link, index| diff --git a/yarn.lock b/yarn.lock index ae86887630b..57644482b32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2713,8 +2713,8 @@ getpass@^0.1.1: assert-plus "^1.0.0" "gitlab-svgs@https://gitlab.com/gitlab-org/gitlab-svgs.git": - version "1.0.2" - resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#e7621d7b028607ae9c69f8b496cd49b42fe607e4" + version "1.0.4" + resolved "https://gitlab.com/gitlab-org/gitlab-svgs.git#46c0a49cd43639948dfcc77a0f94d59deaad1e85" glob-base@^0.3.0: version "0.3.0" -- cgit v1.2.1 From ccf31a13c5ff253256f5d904bf290c5285b2eeab Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 9 Oct 2017 07:45:51 +0000 Subject: Move cycle analytics banner into a vue file --- .../cycle_analytics/components/banner.vue | 55 ++++++++++++++++++++++ .../cycle_analytics/cycle_analytics_bundle.js | 2 + app/views/projects/cycle_analytics/show.html.haml | 15 ++---- spec/javascripts/cycle_analytics/banner_spec.js | 41 ++++++++++++++++ 4 files changed, 101 insertions(+), 12 deletions(-) create mode 100644 app/assets/javascripts/cycle_analytics/components/banner.vue create mode 100644 spec/javascripts/cycle_analytics/banner_spec.js diff --git a/app/assets/javascripts/cycle_analytics/components/banner.vue b/app/assets/javascripts/cycle_analytics/components/banner.vue new file mode 100644 index 00000000000..732697c134e --- /dev/null +++ b/app/assets/javascripts/cycle_analytics/components/banner.vue @@ -0,0 +1,55 @@ + + diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js index 991fcf114da..cdf5e3c0290 100644 --- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js +++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js @@ -3,6 +3,7 @@ import Vue from 'vue'; import Cookies from 'js-cookie'; import Translate from '../vue_shared/translate'; +import banner from './components/banner.vue'; import stageCodeComponent from './components/stage_code_component.vue'; import stagePlanComponent from './components/stage_plan_component.vue'; import stageComponent from './components/stage_component.vue'; @@ -44,6 +45,7 @@ $(() => { }, }, components: { + banner, 'stage-issue-component': stageComponent, 'stage-plan-component': stagePlanComponent, 'stage-code-component': stageCodeComponent, diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml index c06e9f323af..71d30da14a9 100644 --- a/app/views/projects/cycle_analytics/show.html.haml +++ b/app/views/projects/cycle_analytics/show.html.haml @@ -6,18 +6,9 @@ #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } } - if @cycle_analytics_no_data - .landing.content-block{ "v-if" => "!isOverviewDialogDismissed" } - %button.dismiss-button{ type: 'button', 'aria-label': 'Dismiss Cycle Analytics introduction box', "@click" => "dismissOverviewDialog()" } - = icon("times") - .svg-container - = custom_icon('icon_cycle_analytics_splash') - .inner-content - %h4 - {{ __('Introducing Cycle Analytics') }} - %p - {{ __('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.') }} - %p - = link_to _('Read more'), help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn' + %banner{ "v-if" => "!isOverviewDialogDismissed", + "documentation-link": help_page_path('user/project/cycle_analytics'), + "v-on:dismiss-overview-dialog" => "dismissOverviewDialog()" } = icon("spinner spin", "v-show" => "isLoading") .wrapper{ "v-show" => "!isLoading && !hasError" } .panel.panel-default diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js new file mode 100644 index 00000000000..fb6b7fee168 --- /dev/null +++ b/spec/javascripts/cycle_analytics/banner_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import banner from '~/cycle_analytics/components/banner.vue'; +import mountComponent from '../helpers/vue_mount_component_helper'; + +describe('Cycle analytics banner', () => { + let vm; + + beforeEach(() => { + const Component = Vue.extend(banner); + vm = mountComponent(Component, { + documentationLink: 'path', + }); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render cycle analytics information', () => { + expect( + vm.$el.querySelector('h4').textContent.trim(), + ).toEqual('Introducing Cycle Analytics'); + expect( + vm.$el.querySelector('p').textContent.trim(), + ).toContain('Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.'); + expect( + vm.$el.querySelector('a').textContent.trim(), + ).toEqual('Read more'); + expect( + vm.$el.querySelector('a').getAttribute('href'), + ).toEqual('path'); + }); + + it('should emit an event when close button is clicked', () => { + spyOn(vm, '$emit'); + + vm.$el.querySelector('.js-ca-dismiss-button').click(); + + expect(vm.$emit).toHaveBeenCalled(); + }); +}); -- cgit v1.2.1 From 7b8148220c0c7ad17d24c9ddf1e669ee73f086bf Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 9 Oct 2017 09:04:13 +0000 Subject: Remove AjaxLoadingSpinner and CreateLabelDropdown from global namespace --- app/assets/javascripts/ajax_loading_spinner.js | 5 +--- .../boards/components/new_list_dropdown.js | 21 ++++++++-------- app/assets/javascripts/create_label.js | 29 ++++++++++------------ app/assets/javascripts/dispatcher.js | 3 ++- app/assets/javascripts/labels_select.js | 3 ++- app/assets/javascripts/main.js | 2 -- spec/javascripts/ajax_loading_spinner_spec.js | 4 +-- 7 files changed, 31 insertions(+), 36 deletions(-) diff --git a/app/assets/javascripts/ajax_loading_spinner.js b/app/assets/javascripts/ajax_loading_spinner.js index 8f5e2e545ec..2bc77859c26 100644 --- a/app/assets/javascripts/ajax_loading_spinner.js +++ b/app/assets/javascripts/ajax_loading_spinner.js @@ -1,4 +1,4 @@ -class AjaxLoadingSpinner { +export default class AjaxLoadingSpinner { static init() { const $elements = $('.js-ajax-loading-spinner'); @@ -30,6 +30,3 @@ class AjaxLoadingSpinner { classList.toggle('fa-spin'); } } - -window.gl = window.gl || {}; -gl.AjaxLoadingSpinner = AjaxLoadingSpinner; diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js index d7f203b3f96..c19c989680d 100644 --- a/app/assets/javascripts/boards/components/new_list_dropdown.js +++ b/app/assets/javascripts/boards/components/new_list_dropdown.js @@ -1,6 +1,7 @@ -/* eslint-disable comma-dangle, func-names, no-new, space-before-function-paren, one-var, +/* eslint-disable func-names, no-new, space-before-function-paren, one-var, promise/catch-or-return */ import _ from 'underscore'; +import CreateLabelDropdown from '../../create_label'; window.gl = window.gl || {}; window.gl.issueBoards = window.gl.issueBoards || {}; @@ -15,15 +16,15 @@ $(document).off('created.label').on('created.label', (e, label) => { label: { id: label.id, title: label.title, - color: label.color - } + color: label.color, + }, }); }); gl.issueBoards.newListDropdownInit = () => { $('.js-new-board-list').each(function () { const $this = $(this); - new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); + new CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path')); $this.glDropdown({ data(term, callback) { @@ -38,17 +39,17 @@ gl.issueBoards.newListDropdownInit = () => { const $a = $('', { class: (active ? `is-active js-board-list-${active.id}` : ''), text: label.title, - href: '#' + href: '#', }); const $labelColor = $('', { class: 'dropdown-label-box', - style: `background-color: ${label.color}` + style: `background-color: ${label.color}`, }); return $li.append($a.prepend($labelColor)); }, search: { - fields: ['title'] + fields: ['title'], }, filterable: true, selectable: true, @@ -66,13 +67,13 @@ gl.issueBoards.newListDropdownInit = () => { label: { id: label.id, title: label.title, - color: label.color - } + color: label.color, + }, }); Store.state.lists = _.sortBy(Store.state.lists, 'position'); } - } + }, }); }); }; diff --git a/app/assets/javascripts/create_label.js b/app/assets/javascripts/create_label.js index 907b468e576..3bed0678350 100644 --- a/app/assets/javascripts/create_label.js +++ b/app/assets/javascripts/create_label.js @@ -1,8 +1,8 @@ -/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, comma-dangle, prefer-template, quotes, no-param-reassign, wrap-iife, max-len */ +/* eslint-disable func-names, prefer-arrow-callback */ import Api from './api'; -class CreateLabelDropdown { - constructor ($el, namespacePath, projectPath) { +export default class CreateLabelDropdown { + constructor($el, namespacePath, projectPath) { this.$el = $el; this.namespacePath = namespacePath; this.projectPath = projectPath; @@ -22,7 +22,7 @@ class CreateLabelDropdown { this.addBinding(); } - cleanBinding () { + cleanBinding() { this.$colorSuggestions.off('click'); this.$newLabelField.off('keyup change'); this.$newColorField.off('keyup change'); @@ -31,7 +31,7 @@ class CreateLabelDropdown { this.$newLabelCreateButton.off('click'); } - addBinding () { + addBinding() { const self = this; this.$colorSuggestions.on('click', function (e) { @@ -44,7 +44,7 @@ class CreateLabelDropdown { this.$dropdownBack.on('click', this.resetForm.bind(this)); - this.$cancelButton.on('click', function(e) { + this.$cancelButton.on('click', function (e) { e.preventDefault(); e.stopPropagation(); @@ -55,7 +55,7 @@ class CreateLabelDropdown { this.$newLabelCreateButton.on('click', this.saveLabel.bind(this)); } - addColorValue (e, $this) { + addColorValue(e, $this) { e.preventDefault(); e.stopPropagation(); @@ -66,7 +66,7 @@ class CreateLabelDropdown { .addClass('is-active'); } - enableLabelCreateButton () { + enableLabelCreateButton() { if (this.$newLabelField.val() !== '' && this.$newColorField.val() !== '') { this.$newLabelError.hide(); this.$newLabelCreateButton.enable(); @@ -75,7 +75,7 @@ class CreateLabelDropdown { } } - resetForm () { + resetForm() { this.$newLabelField .val('') .trigger('change'); @@ -90,13 +90,13 @@ class CreateLabelDropdown { .removeClass('is-active'); } - saveLabel (e) { + saveLabel(e) { e.preventDefault(); e.stopPropagation(); Api.newLabel(this.namespacePath, this.projectPath, { title: this.$newLabelField.val(), - color: this.$newColorField.val() + color: this.$newColorField.val(), }, (label) => { this.$newLabelCreateButton.enable(); @@ -107,8 +107,8 @@ class CreateLabelDropdown { errors = label.message; } else { errors = Object.keys(label.message).map(key => - `${gl.text.humanize(key)} ${label.message[key].join(', ')}` - ).join("
"); + `${gl.text.humanize(key)} ${label.message[key].join(', ')}`, + ).join('
'); } this.$newLabelError @@ -122,6 +122,3 @@ class CreateLabelDropdown { }); } } - -window.gl = window.gl || {}; -gl.CreateLabelDropdown = CreateLabelDropdown; diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 33271c25146..6a4dce20f24 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -76,6 +76,7 @@ import GpgBadges from './gpg_badges'; import UserFeatureHelper from './helpers/user_feature_helper'; import initChangesDropdown from './init_changes_dropdown'; import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; +import AjaxLoadingSpinner from './ajax_loading_spinner'; (function() { var Dispatcher; @@ -237,7 +238,7 @@ import { ajaxGet, convertPermissionToBoolean } from './lib/utils/common_utils'; new NewBranchForm($('.js-create-branch-form'), JSON.parse(document.getElementById('availableRefs').innerHTML)); break; case 'projects:branches:index': - gl.AjaxLoadingSpinner.init(); + AjaxLoadingSpinner.init(); new DeleteModal(); break; case 'projects:issues:new': diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js index 2538d9c2093..d479f7ed682 100644 --- a/app/assets/javascripts/labels_select.js +++ b/app/assets/javascripts/labels_select.js @@ -4,6 +4,7 @@ import _ from 'underscore'; import IssuableBulkUpdateActions from './issuable_bulk_update_actions'; import DropdownUtils from './filtered_search/dropdown_utils'; +import CreateLabelDropdown from './create_label'; (function() { this.LabelsSelect = (function() { @@ -61,7 +62,7 @@ import DropdownUtils from './filtered_search/dropdown_utils'; $sidebarLabelTooltip.tooltip(); if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) { - new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); + new CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath); } saveLabelData = function() { diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 5858c2b6fd8..64d7c80e689 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -57,7 +57,6 @@ import './u2f/util'; import './abuse_reports'; import './activities'; import './admin'; -import './ajax_loading_spinner'; import './api'; import './aside'; import './autosave'; @@ -74,7 +73,6 @@ import './compare_autocomplete'; import './confirm_danger_modal'; import './copy_as_gfm'; import './copy_to_clipboard'; -import './create_label'; import './diff'; import './dropzone_input'; import './due_date_select'; diff --git a/spec/javascripts/ajax_loading_spinner_spec.js b/spec/javascripts/ajax_loading_spinner_spec.js index 46e072a8ebb..c93b7cc6cac 100644 --- a/spec/javascripts/ajax_loading_spinner_spec.js +++ b/spec/javascripts/ajax_loading_spinner_spec.js @@ -1,6 +1,6 @@ import 'jquery'; import 'jquery-ujs'; -import '~/ajax_loading_spinner'; +import AjaxLoadingSpinner from '~/ajax_loading_spinner'; describe('Ajax Loading Spinner', () => { const fixtureTemplate = 'static/ajax_loading_spinner.html.raw'; @@ -8,7 +8,7 @@ describe('Ajax Loading Spinner', () => { beforeEach(() => { loadFixtures(fixtureTemplate); - gl.AjaxLoadingSpinner.init(); + AjaxLoadingSpinner.init(); }); it('change current icon with spinner icon and disable link while waiting ajax response', (done) => { -- cgit v1.2.1 From 09dbbc27070cbd6151ec87fa82ae3c77c78492e1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Mon, 9 Oct 2017 09:06:09 +0000 Subject: Resolve "Simple documentation update - backup to restore in restore section" --- doc/raketasks/backup_restore.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md index ae69d7f92f2..e4c09b2b507 100644 --- a/doc/raketasks/backup_restore.md +++ b/doc/raketasks/backup_restore.md @@ -370,7 +370,7 @@ This is recommended to reduce cron spam. ## Restore -GitLab provides a simple command line interface to backup your whole installation, +GitLab provides a simple command line interface to restore your whole installation, and is flexible enough to fit your needs. The [restore prerequisites section](#restore-prerequisites) includes crucial @@ -445,6 +445,14 @@ Restoring repositories: Deleting tmp directories...[DONE] ``` +Next, restore `/home/git/gitlab/.secret` if necessary as mentioned above. + +Restart GitLab: + +```shell +sudo service gitlab restart +``` + ### Restore for Omnibus installations This procedure assumes that: @@ -480,10 +488,12 @@ restore: sudo gitlab-rake gitlab:backup:restore BACKUP=1493107454_2017_04_25_9.1.0 ``` +Next, restore `/etc/gitlab/gitlab-secrets.json` if necessary as mentioned above. + Restart and check GitLab: ```shell -sudo gitlab-ctl start +sudo gitlab-ctl restart sudo gitlab-rake gitlab:check SANITIZE=true ``` -- cgit v1.2.1 From 75212feefd3e8fba3e59e7c070c6dd0d7d2dd96e Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 9 Oct 2017 11:28:01 +0200 Subject: Move i18n/introduction to i18n/index --- doc/development/README.md | 4 +- doc/development/i18n/index.md | 74 ++++++++++++++++++++++++++++++++++++ doc/development/i18n/introduction.md | 74 ------------------------------------ 3 files changed, 76 insertions(+), 76 deletions(-) create mode 100644 doc/development/i18n/index.md delete mode 100644 doc/development/i18n/introduction.md diff --git a/doc/development/README.md b/doc/development/README.md index a976da5dbd4..b648c7ce086 100644 --- a/doc/development/README.md +++ b/doc/development/README.md @@ -64,9 +64,9 @@ - [Hash Indexes](hash_indexes.md) - [Swapping Tables](swapping_tables.md) -## i18n +## Internationalization (i18n) -- [Introduction](i18n/introduction.md) +- [Introduction](i18n/index.md) - [Externalization](i18n/externalization.md) - [Translation](i18n/translation.md) diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md new file mode 100644 index 00000000000..e8182ace9c3 --- /dev/null +++ b/doc/development/i18n/index.md @@ -0,0 +1,74 @@ +# Translate GitLab to your language + +The text in GitLab's user interface is in American English by default. +Each string can be translated to other languages. +As each string is translated, it is added to the languages translation file, +and will be available in future releases of GitLab. + +Contributions to translations are always needed. +Many strings are not yet available for translation because they have not been externalized. +Helping externalize strings benefits all languages. +Some translations are incomplete or inconsistent. +Translating strings will help complete and improve each language. + +## How to contribute + +### Externalize strings + +Before a string can be translated, it must be externalized. +This is the process where English strings in the GitLab source code are wrapped in a function that +retrieves the translated string for the user's language. + +As new features are added and existing features are updated, the surrounding strings are being +externalized, however, there are many parts of GitLab that still need more work to externalize all +strings. + +See [Externalization for GitLab](externalization.md). + +### Translate strings + +The translation process is managed at [translate.gitlab.com](https://translate.gitlab.com) +using [Crowdin](https://crowdin.com/). +You will need to create an account before you can submit translations. +Once you are signed in, select the language you wish to contribute translations to. + +Voting for translations is also valuable, helping to confirm good and flag inaccurate translations. + +See [Translation guidelines](translation.md). + +### Proof reading + +Proof reading helps ensure the accuracy and consistency of translations. +All translations are proof read before being accepted. +If a translations requires changes, you will be notified with a comment explaining why. + +Community assistance proof reading translations is encouraged and appreciated. +Requests to become a proof reader will be considered on the merits of previous translations. + +- Bulgarian +- Chinese Simplified + - [Huang Tao](https://crowdin.com/profile/htve) +- Chinese Traditional + - [Huang Tao](https://crowdin.com/profile/htve) +- Chinese Traditional, Hong Kong + - [Huang Tao](https://crowdin.com/profile/htve) +- Dutch +- Esperanto +- French +- German +- Italian +- Japanese +- Korean + - [Huang Tao](https://crowdin.com/profile/htve) +- Portuguese, Brazilian +- Russian + - [Alexy Lustin](https://crowdin.com/profile/lustin) + - [Nikita Grylov](https://crowdin.com/profile/nixel2007) +- Spanish +- Ukrainian + +If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues). + +### Release + +Translations are typically included in the next major or minor release. diff --git a/doc/development/i18n/introduction.md b/doc/development/i18n/introduction.md deleted file mode 100644 index e8182ace9c3..00000000000 --- a/doc/development/i18n/introduction.md +++ /dev/null @@ -1,74 +0,0 @@ -# Translate GitLab to your language - -The text in GitLab's user interface is in American English by default. -Each string can be translated to other languages. -As each string is translated, it is added to the languages translation file, -and will be available in future releases of GitLab. - -Contributions to translations are always needed. -Many strings are not yet available for translation because they have not been externalized. -Helping externalize strings benefits all languages. -Some translations are incomplete or inconsistent. -Translating strings will help complete and improve each language. - -## How to contribute - -### Externalize strings - -Before a string can be translated, it must be externalized. -This is the process where English strings in the GitLab source code are wrapped in a function that -retrieves the translated string for the user's language. - -As new features are added and existing features are updated, the surrounding strings are being -externalized, however, there are many parts of GitLab that still need more work to externalize all -strings. - -See [Externalization for GitLab](externalization.md). - -### Translate strings - -The translation process is managed at [translate.gitlab.com](https://translate.gitlab.com) -using [Crowdin](https://crowdin.com/). -You will need to create an account before you can submit translations. -Once you are signed in, select the language you wish to contribute translations to. - -Voting for translations is also valuable, helping to confirm good and flag inaccurate translations. - -See [Translation guidelines](translation.md). - -### Proof reading - -Proof reading helps ensure the accuracy and consistency of translations. -All translations are proof read before being accepted. -If a translations requires changes, you will be notified with a comment explaining why. - -Community assistance proof reading translations is encouraged and appreciated. -Requests to become a proof reader will be considered on the merits of previous translations. - -- Bulgarian -- Chinese Simplified - - [Huang Tao](https://crowdin.com/profile/htve) -- Chinese Traditional - - [Huang Tao](https://crowdin.com/profile/htve) -- Chinese Traditional, Hong Kong - - [Huang Tao](https://crowdin.com/profile/htve) -- Dutch -- Esperanto -- French -- German -- Italian -- Japanese -- Korean - - [Huang Tao](https://crowdin.com/profile/htve) -- Portuguese, Brazilian -- Russian - - [Alexy Lustin](https://crowdin.com/profile/lustin) - - [Nikita Grylov](https://crowdin.com/profile/nixel2007) -- Spanish -- Ukrainian - -If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues). - -### Release - -Translations are typically included in the next major or minor release. -- cgit v1.2.1 From 5a25fc1ed80ef913b9abd30c7decdbcd2dab6676 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 9 Oct 2017 11:36:07 +0200 Subject: Update i18n docs --- doc/development/i18n/externalization.md | 17 ++++++++--------- doc/development/i18n/index.md | 4 +++- doc/development/i18n/translation.md | 4 ++++ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md index bd0ef39ca62..167260b6e0e 100644 --- a/doc/development/i18n/externalization.md +++ b/doc/development/i18n/externalization.md @@ -2,22 +2,21 @@ > [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10669) in GitLab 9.2. -For working with internationalization (i18n) we use -[GNU gettext](https://www.gnu.org/software/gettext/) given it's the most used -tool for this task and we have a lot of applications that will help us to work -with it. +For working with internationalization (i18n), +[GNU gettext](https://www.gnu.org/software/gettext/) is used given it's the most +used tool for this task and there are a lot of applications that will help us to +work with it. ## Setting up GitLab Development Kit (GDK) -In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) project we must download and -configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit), we can do it by following this [guide](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md). +In order to be able to work on the [GitLab Community Edition](https://gitlab.com/gitlab-org/gitlab-ce) +project you must download and configure it through [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit/blob/master/doc/set-up-gdk.md). -Once we have the GitLab project ready we can start working on the -translation of the project. +Once you have the GitLab project ready, you can start working on the translation. ## Tools -We use a couple of gems: +The following tools are used: 1. [`gettext_i18n_rails`](https://github.com/grosser/gettext_i18n_rails): this gem allow us to translate content from models, views and controllers. Also diff --git a/doc/development/i18n/index.md b/doc/development/i18n/index.md index e8182ace9c3..4cb2624c098 100644 --- a/doc/development/i18n/index.md +++ b/doc/development/i18n/index.md @@ -13,6 +13,8 @@ Translating strings will help complete and improve each language. ## How to contribute +There are many ways you can contribute in translating GitLab. + ### Externalize strings Before a string can be translated, it must be externalized. @@ -69,6 +71,6 @@ Requests to become a proof reader will be considered on the merits of previous t If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues). -### Release +## Release Translations are typically included in the next major or minor release. diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md index 8687e47ee1c..79707aaf671 100644 --- a/doc/development/i18n/translation.md +++ b/doc/development/i18n/translation.md @@ -4,6 +4,8 @@ For managing the translation process we use [Crowdin](https://crowdin.com). ## Using Crowdin +The first step is to get familiar with Crowdin. + ### Sign In To contribute translations at [translate.gitlab.com](https://translate.gitlab.com) @@ -37,6 +39,8 @@ Remember to **Save** each translation. ## Translation Guidelines +Be sure to check the following guidelines before you translate any strings. + ### Technical terms Technical terms should be treated like proper nouns and not be translated. -- cgit v1.2.1 From 0e9c88625b2daa3d4699ab590cd1e19c06254456 Mon Sep 17 00:00:00 2001 From: Achilleas Pipinellis Date: Mon, 9 Oct 2017 11:39:13 +0200 Subject: Fix link to new i18n index page --- doc/development/i18n_guide.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md index 6eeffcc0f35..f6e949b5fd8 100644 --- a/doc/development/i18n_guide.md +++ b/doc/development/i18n_guide.md @@ -1 +1 @@ -This document was moved to [a new location](externalization.md). +This document was moved to [a new location](i18n/index.md). -- cgit v1.2.1 From 2597d8d2e0956644619acbac61343635cb7bd459 Mon Sep 17 00:00:00 2001 From: Filipa Lacerda Date: Mon, 9 Oct 2017 10:46:40 +0000 Subject: Removes CommitsList from global namespace --- app/assets/javascripts/commits.js | 51 +++++++++-------- app/assets/javascripts/dispatcher.js | 2 +- spec/javascripts/commits_spec.js | 108 +++++++++++++++++------------------ 3 files changed, 79 insertions(+), 82 deletions(-) diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js index 047544b1762..ae6b8902032 100644 --- a/app/assets/javascripts/commits.js +++ b/app/assets/javascripts/commits.js @@ -1,17 +1,19 @@ -/* eslint-disable func-names, space-before-function-paren, wrap-iife, quotes, consistent-return, no-return-assign, no-param-reassign, one-var, no-var, one-var-declaration-per-line, no-unused-vars, prefer-template, object-shorthand, comma-dangle, max-len, prefer-arrow-callback */ +/* eslint-disable func-names, wrap-iife, consistent-return, + no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars, + prefer-template, object-shorthand, prefer-arrow-callback */ /* global Pager */ -window.CommitsList = (function() { - var CommitsList = {}; +export default (function () { + const CommitsList = {}; CommitsList.timer = null; - CommitsList.init = function(limit) { + CommitsList.init = function (limit) { this.$contentList = $('.content_list'); - $("body").on("click", ".day-commits-table li.commit", function(e) { - if (e.target.nodeName !== "A") { - location.href = $(this).attr("url"); + $('body').on('click', '.day-commits-table li.commit', function (e) { + if (e.target.nodeName !== 'A') { + location.href = $(this).attr('url'); e.stopPropagation(); return false; } @@ -19,48 +21,47 @@ window.CommitsList = (function() { Pager.init(parseInt(limit, 10), false, false, this.processCommits); - this.content = $("#commits-list"); - this.searchField = $("#commits-search"); + this.content = $('#commits-list'); + this.searchField = $('#commits-search'); this.lastSearch = this.searchField.val(); return this.initSearch(); }; - CommitsList.initSearch = function() { + CommitsList.initSearch = function () { this.timer = null; - return this.searchField.keyup((function(_this) { - return function() { + return this.searchField.keyup((function (_this) { + return function () { clearTimeout(_this.timer); return _this.timer = setTimeout(_this.filterResults, 500); }; })(this)); }; - CommitsList.filterResults = function() { - var commitsUrl, form, search; - form = $(".commits-search-form"); - search = CommitsList.searchField.val(); + CommitsList.filterResults = function () { + const form = $('.commits-search-form'); + const search = CommitsList.searchField.val(); if (search === CommitsList.lastSearch) return; - commitsUrl = form.attr("action") + '?' + form.serialize(); + const commitsUrl = form.attr('action') + '?' + form.serialize(); CommitsList.content.fadeTo('fast', 0.5); return $.ajax({ - type: "GET", - url: form.attr("action"), + type: 'GET', + url: form.attr('action'), data: form.serialize(), - complete: function() { + complete: function () { return CommitsList.content.fadeTo('fast', 1.0); }, - success: function(data) { + success: function (data) { CommitsList.lastSearch = search; CommitsList.content.html(data.html); return history.replaceState({ - page: commitsUrl + page: commitsUrl, // Change url so if user reload a page - search results are saved }, document.title, commitsUrl); }, - error: function() { + error: function () { CommitsList.lastSearch = null; }, - dataType: "json" + dataType: 'json', }); }; @@ -81,7 +82,7 @@ window.CommitsList = (function() { commitsCount = $commitsHeadersLast.nextUntil('li.js-commit-header').find('li.commit').length; // Remove duplicate of commits header. - processedData = $processedData.not(`li.js-commit-header[data-day="${loadedShownDayFirst}"]`); + processedData = $processedData.not(`li.js-commit-header[data-day='${loadedShownDayFirst}']`); // Update commits count in the previous commits header. commitsCount += Number($(processedData).nextUntil('li.js-commit-header').first().find('li.commit').length); diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index c3349c382ad..1edd460f380 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -7,7 +7,6 @@ /* global IssuableForm */ /* global LabelsSelect */ /* global MilestoneSelect */ -/* global CommitsList */ /* global NewBranchForm */ /* global NotificationsForm */ /* global NotificationsDropdown */ @@ -35,6 +34,7 @@ /* global Sidebar */ /* global ShortcutsWiki */ +import CommitsList from './commits'; import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; import DeleteModal from './branches/branches_delete_modal'; diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index ace95000468..e5a5e3293b9 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -1,77 +1,73 @@ -/* global CommitsList */ - import 'vendor/jquery.endless-scroll'; import '~/pager'; -import '~/commits'; - -(() => { - describe('Commits List', () => { - beforeEach(() => { - setFixtures(` -
- -
-
    - `); - }); +import CommitsList from '~/commits'; - it('should be defined', () => { - expect(CommitsList).toBeDefined(); - }); +describe('Commits List', () => { + beforeEach(() => { + setFixtures(` +
    + +
    +
      + `); + }); - describe('processCommits', () => { - it('should join commit headers', () => { - CommitsList.$contentList = $(` -
      -
    1. - 20 Sep, 2016 - 1 commit -
    2. -
    3. -
      - `); + it('should be defined', () => { + expect(CommitsList).toBeDefined(); + }); - const data = ` + describe('processCommits', () => { + it('should join commit headers', () => { + CommitsList.$contentList = $(` +
    4. 20 Sep, 2016 1 commit
    5. - `; +
      + `); - // The last commit header should be removed - // since the previous one has the same data-day value. - expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); - }); + const data = ` +
    6. + 20 Sep, 2016 + 1 commit +
    7. +
    8. + `; + + // The last commit header should be removed + // since the previous one has the same data-day value. + expect(CommitsList.processCommits(data).find('li.commit-header').length).toBe(0); }); + }); - describe('on entering input', () => { - let ajaxSpy; + describe('on entering input', () => { + let ajaxSpy; - beforeEach(() => { - CommitsList.init(25); - CommitsList.searchField.val(''); + beforeEach(() => { + CommitsList.init(25); + CommitsList.searchField.val(''); - spyOn(history, 'replaceState').and.stub(); - ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { - req.success({ - data: '
    9. Result
    10. ', - }); + spyOn(history, 'replaceState').and.stub(); + ajaxSpy = spyOn(jQuery, 'ajax').and.callFake((req) => { + req.success({ + data: '
    11. Result
    12. ', }); }); + }); - it('should save the last search string', () => { - CommitsList.searchField.val('GitLab'); - CommitsList.filterResults(); - expect(ajaxSpy).toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual('GitLab'); - }); + it('should save the last search string', () => { + CommitsList.searchField.val('GitLab'); + CommitsList.filterResults(); + expect(ajaxSpy).toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual('GitLab'); + }); - it('should not make ajax call if the input does not change', () => { - CommitsList.filterResults(); - expect(ajaxSpy).not.toHaveBeenCalled(); - expect(CommitsList.lastSearch).toEqual(''); - }); + it('should not make ajax call if the input does not change', () => { + CommitsList.filterResults(); + expect(ajaxSpy).not.toHaveBeenCalled(); + expect(CommitsList.lastSearch).toEqual(''); }); }); -})(); +}); -- cgit v1.2.1