diff options
author | Phil Hughes <me@iamphill.com> | 2016-08-03 09:08:24 +0100 |
---|---|---|
committer | Phil Hughes <me@iamphill.com> | 2016-08-03 09:08:24 +0100 |
commit | a45071d0ea18d9bb8a5209ef97e4858dda08cd33 (patch) | |
tree | d9f8e538c5c0e6ef2e25acf2520f31a419b34ec8 /spec | |
parent | e5b64f20c730bd6e18af694b2c1503020ba1db51 (diff) | |
parent | e63729d9e70a661fb3fb8cb558716f6a44a52798 (diff) | |
download | gitlab-ce-a45071d0ea18d9bb8a5209ef97e4858dda08cd33.tar.gz |
Merge branch 'master' into ref-switcher-enter-submit
Diffstat (limited to 'spec')
199 files changed, 7144 insertions, 4444 deletions
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 60c654f622d..ed0b7f9e240 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -163,4 +163,17 @@ describe AutocompleteController do expect(body.collect { |u| u['id'] }).not_to include(99999) end end + + context 'skip_users parameter included' do + before { sign_in(user) } + + it 'skips the user IDs passed' do + get(:users, skip_users: [user, user2].map(&:id)) + + other_user_ids = [non_member, project.owner, project.creator].map(&:id) + response_user_ids = JSON.parse(response.body).map { |user| user['id'] } + + expect(response_user_ids).to contain_exactly(*other_user_ids) + end + end end diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb index 267d511c2db..33c75e7584f 100644 --- a/spec/controllers/help_controller_spec.rb +++ b/spec/controllers/help_controller_spec.rb @@ -36,7 +36,7 @@ describe HelpController do context 'when requested file exists' do it 'renders the raw file' do get :show, - path: 'workflow/protected_branches/protected_branches1', + path: 'user/project/img/labels_filter', format: :png expect(response).to be_success expect(response.content_type).to eq 'image/png' @@ -63,4 +63,13 @@ describe HelpController do end end end + + describe 'GET #ui' do + context 'for UI Development Kit' do + it 'renders found' do + get :ui + expect(response).to have_http_status(200) + end + end + end end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb index 3001d32e719..df902da86f8 100644 --- a/spec/controllers/projects/commit_controller_spec.rb +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -24,15 +24,6 @@ describe Projects::CommitController do get :show, params.merge(extra_params) end - let(:project) { create(:project) } - - before do - user = create(:user) - project.team << [user, :master] - - sign_in(user) - end - context 'with valid id' do it 'responds with 200' do go(id: commit.id) diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb new file mode 100644 index 00000000000..768105cae95 --- /dev/null +++ b/spec/controllers/projects/environments_controller_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Projects::EnvironmentsController do + let(:environment) { create(:environment) } + let(:project) { environment.project } + let(:user) { create(:user) } + + before do + project.team << [user, :master] + + sign_in(user) + end + + describe 'GET show' do + context 'with valid id' do + it 'responds with a status code 200' do + get :show, environment_params + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with a status code 404' do + params = environment_params + params[:id] = 12345 + get :show, params + + expect(response).to have_http_status(404) + end + end + end + + describe 'GET edit' do + it 'responds with a status code 200' do + get :edit, environment_params + + expect(response).to be_ok + end + end + + describe 'PATCH #update' do + it 'responds with a 302' do + patch_params = environment_params.merge(environment: { external_url: 'https://git.gitlab.com' }) + patch :update, patch_params + + expect(response).to have_http_status(302) + end + end + + def environment_params + { + namespace_id: project.namespace, + project_id: project, + id: environment.id + } + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 7cf09fa4a4a..ec820de3d09 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -6,37 +6,65 @@ describe Projects::IssuesController do let(:issue) { create(:issue, project: project) } describe "GET #index" do - before do - sign_in(user) - project.team << [user, :developer] - end + context 'external issue tracker' do + it 'redirects to the external issue tracker' do + external = double(issues_url: 'https://example.com/issues') + allow(project).to receive(:external_issue_tracker).and_return(external) + controller.instance_variable_set(:@project, project) - it "returns index" do - get :index, namespace_id: project.namespace.path, project_id: project.path + get :index, namespace_id: project.namespace.path, project_id: project - expect(response).to have_http_status(200) + expect(response).to redirect_to('https://example.com/issues') + end end - it "return 301 if request path doesn't match project path" do - get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + context 'internal issue tracker' do + before do + sign_in(user) + project.team << [user, :developer] + end - expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) - end + it "returns index" do + get :index, namespace_id: project.namespace.path, project_id: project.path + + expect(response).to have_http_status(200) + end - it "returns 404 when issues are disabled" do - project.issues_enabled = false - project.save + it "return 301 if request path doesn't match project path" do + get :index, namespace_id: project.namespace.path, project_id: project.path.upcase + + expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project)) + end + + it "returns 404 when issues are disabled" do + project.issues_enabled = false + project.save + + get :index, namespace_id: project.namespace.path, project_id: project.path + expect(response).to have_http_status(404) + end - get :index, namespace_id: project.namespace.path, project_id: project.path - expect(response).to have_http_status(404) + it "returns 404 when external issue tracker is enabled" do + controller.instance_variable_set(:@project, project) + allow(project).to receive(:default_issues_tracker?).and_return(false) + + get :index, namespace_id: project.namespace.path, project_id: project.path + expect(response).to have_http_status(404) + end end + end + + describe 'GET #new' do + context 'external issue tracker' do + it 'redirects to the external issue tracker' do + external = double(new_issue_path: 'https://example.com/issues/new') + allow(project).to receive(:external_issue_tracker).and_return(external) + controller.instance_variable_set(:@project, project) - it "returns 404 when external issue tracker is enabled" do - controller.instance_variable_set(:@project, project) - allow(project).to receive(:default_issues_tracker?).and_return(false) + get :new, namespace_id: project.namespace.path, project_id: project - get :index, namespace_id: project.namespace.path, project_id: project.path - expect(response).to have_http_status(404) + expect(response).to redirect_to('https://example.com/issues/new') + end end end @@ -243,6 +271,37 @@ describe Projects::IssuesController do end end + describe 'POST #create' do + context 'Akismet is enabled' do + before do + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) + end + + def post_spam_issue + sign_in(user) + spam_project = create(:empty_project, :public) + post :create, { + namespace_id: spam_project.namespace.to_param, + project_id: spam_project.to_param, + issue: { title: 'Spam Title', description: 'Spam lives here' } + } + end + + it 'rejects an issue recognized as spam' do + expect{ post_spam_issue }.not_to change(Issue, :count) + expect(response).to render_template(:new) + end + + it 'creates a spam log' do + post_spam_issue + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('Spam Title') + end + end + end + describe "DELETE #destroy" do context "when the user is a developer" do before { sign_in(user) } diff --git a/spec/controllers/projects/tags_controller_spec.rb b/spec/controllers/projects/tags_controller_spec.rb new file mode 100644 index 00000000000..a6995145cc1 --- /dev/null +++ b/spec/controllers/projects/tags_controller_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Projects::TagsController do + let(:project) { create(:project, :public) } + let!(:release) { create(:release, project: project) } + let!(:invalid_release) { create(:release, project: project, tag: 'does-not-exist') } + + describe 'GET index' do + before { get :index, namespace_id: project.namespace.to_param, project_id: project.to_param } + + it 'returns the tags for the page' do + expect(assigns(:tags).map(&:name)).to eq(['v1.1.0', 'v1.0.0']) + end + + it 'returns releases matching those tags' do + expect(assigns(:releases)).to include(release) + expect(assigns(:releases)).not_to include(invalid_release) + end + end +end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index 0893ee89f6a..71d0e4be834 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -14,9 +14,9 @@ describe Projects::UploadsController do context "without params['file']" do it "returns an error" do - post :create, + post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, + project_id: project.to_param, format: :json expect(response).to have_http_status(422) end @@ -34,23 +34,21 @@ describe Projects::UploadsController do it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"rails_sample\"' expect(response.body).to match "\"url\":\"/uploads" - expect(response.body).to match '\"is_image\":true' end end context 'with valid non-image file' do before do - post :create, + post :create, namespace_id: project.namespace.to_param, - project_id: project.to_param, - file: txt, + project_id: project.to_param, + file: txt, format: :json end it 'returns a content with original filename, new link, and correct type.' do expect(response.body).to match '\"alt\":\"doc_sample.txt\"' expect(response.body).to match "\"url\":\"/uploads" - expect(response.body).to match '\"is_image\":false' end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index 5fb671df570..1b32d560b16 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -3,6 +3,8 @@ include ActionDispatch::TestProcess FactoryGirl.define do factory :ci_build, class: Ci::Build do name 'test' + stage 'test' + stage_idx 0 ref 'master' tag false created_at 'Di 29. Okt 09:50:00 CET 2013' @@ -43,6 +45,11 @@ FactoryGirl.define do status 'pending' end + trait :manual do + status 'skipped' + self.when 'manual' + end + trait :allowed_to_fail do allow_failure true end @@ -83,5 +90,21 @@ FactoryGirl.define do build.save! end end + + trait :artifacts_expired do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + 'application/x-gzip') + + build.artifacts_expire_at = 1.minute.ago + + build.save! + end + end end end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index 6d47d05f8ad..b8d8fab0e0b 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -5,7 +5,8 @@ FactoryGirl.define do variables do { - TRIGGER_KEY: 'TRIGGER_VALUE' + TRIGGER_KEY_1: 'TRIGGER_VALUE_1', + TRIGGER_KEY_2: 'TRIGGER_VALUE_2' } end end diff --git a/spec/factories/environments.rb b/spec/factories/environments.rb index 07265c26ca3..846cccfc7fa 100644 --- a/spec/factories/environments.rb +++ b/spec/factories/environments.rb @@ -3,5 +3,6 @@ FactoryGirl.define do sequence(:name) { |n| "environment#{n}" } project factory: :empty_project + sequence(:external_url) { |n| "https://env#{n}.example.gitlab.com" } end end diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb index e72aa9479b7..2c0a2dd94ca 100644 --- a/spec/factories/issues.rb +++ b/spec/factories/issues.rb @@ -18,5 +18,15 @@ FactoryGirl.define do factory :closed_issue, traits: [:closed] factory :reopened_issue, traits: [:reopened] + + factory :labeled_issue do + transient do + labels [] + end + + after(:create) do |issue, evaluator| + issue.update_attributes(labels: evaluator.labels) + end + end end end diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb index 28ed8078157..5575852c2d7 100644 --- a/spec/factories/protected_branches.rb +++ b/spec/factories/protected_branches.rb @@ -2,5 +2,28 @@ FactoryGirl.define do factory :protected_branch do name project + + after(:create) do |protected_branch| + protected_branch.create_push_access_level!(access_level: Gitlab::Access::MASTER) + protected_branch.create_merge_access_level!(access_level: Gitlab::Access::MASTER) + end + + trait :developers_can_push do + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end + end + + trait :developers_can_merge do + after(:create) do |protected_branch| + protected_branch.merge_access_level.update!(access_level: Gitlab::Access::DEVELOPER) + end + end + + trait :no_one_can_push do + after(:create) do |protected_branch| + protected_branch.push_access_level.update!(access_level: Gitlab::Access::NO_ACCESS) + end + end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index cab3dc1d167..0cfeb2e57d8 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -199,9 +199,13 @@ describe "Builds" do click_link 'Retry' end - it { expect(page.status_code).to eq(200) } - it { expect(page).to have_content 'pending' } - it { expect(page).to have_content 'Cancel' } + it 'shows the right status and buttons' do + expect(page).to have_http_status(200) + expect(page).to have_content 'pending' + page.within('aside.right-sidebar') do + expect(page).to have_content 'Cancel' + end + end end context "Build from other project" do @@ -212,7 +216,25 @@ describe "Builds" do page.driver.post(retry_namespace_project_build_path(@project.namespace, @project, @build2)) end - it { expect(page.status_code).to eq(404) } + it { expect(page).to have_http_status(404) } + end + + context "Build that current user is not allowed to retry" do + before do + @build.run! + @build.cancel! + @project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) + + logout_direct + login_with(create(:user)) + visit namespace_project_build_path(@project.namespace, @project, @build) + end + + it 'does not show the Retry button' do + page.within('aside.right-sidebar') do + expect(page).not_to have_content 'Retry' + end + end end end diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb index 7fb28f4174b..fcd41b38413 100644 --- a/spec/features/environments_spec.rb +++ b/spec/features/environments_spec.rb @@ -13,6 +13,7 @@ feature 'Environments', feature: true do describe 'when showing environments' do given!(:environment) { } given!(:deployment) { } + given!(:manual) { } before do visit namespace_project_environments_path(project.namespace, project) @@ -43,6 +44,24 @@ feature 'Environments', feature: true do scenario 'does show deployment SHA' do expect(page).to have_link(deployment.short_sha) end + + context 'with build and manual actions' do + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } + given(:deployment) { create(:deployment, environment: environment, deployable: build) } + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end + end end end @@ -54,6 +73,7 @@ feature 'Environments', feature: true do describe 'when showing the environment' do given(:environment) { create(:environment, project: project) } given!(:deployment) { } + given!(:manual) { } before do visit namespace_project_environment_path(project.namespace, project, environment) @@ -72,20 +92,36 @@ feature 'Environments', feature: true do expect(page).to have_link(deployment.short_sha) end - scenario 'does not show a retry button for deployment without build' do - expect(page).not_to have_link('Retry') + scenario 'does not show a re-deploy button for deployment without build' do + expect(page).not_to have_link('Re-deploy') end context 'with build' do - given(:build) { create(:ci_build, project: project) } + given(:pipeline) { create(:ci_pipeline, project: project) } + given(:build) { create(:ci_build, pipeline: pipeline) } given(:deployment) { create(:deployment, environment: environment, deployable: build) } scenario 'does show build name' do expect(page).to have_link("#{build.name} (##{build.id})") end - scenario 'does show retry button' do - expect(page).to have_link('Retry') + scenario 'does show re-deploy button' do + expect(page).to have_link('Re-deploy') + end + + context 'with manual action' do + given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') } + + scenario 'does show a play button' do + expect(page).to have_link(manual.name.humanize) + end + + scenario 'does allow to play manual action' do + expect(manual).to be_skipped + expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count } + expect(page).to have_content(manual.name) + expect(manual.reload).to be_pending + end end end end @@ -104,7 +140,7 @@ feature 'Environments', feature: true do context 'for valid name' do before do fill_in('Name', with: 'production') - click_on 'Create environment' + click_on 'Save' end scenario 'does create a new pipeline' do @@ -115,7 +151,7 @@ feature 'Environments', feature: true do context 'for invalid name' do before do fill_in('Name', with: 'name with spaces') - click_on 'Create environment' + click_on 'Save' end scenario 'does show errors' do diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb index d1a6a98ab72..b3baa2ab57c 100644 --- a/spec/features/groups/members/user_requests_access_spec.rb +++ b/spec/features/groups/members/user_requests_access_spec.rb @@ -12,6 +12,13 @@ feature 'Groups > Members > User requests access', feature: true do visit group_path(group) end + scenario 'request access feature is disabled' do + group.update_attributes(request_access_enabled: false) + visit group_path(group) + + expect(page).not_to have_content 'Request Access' + end + scenario 'user can request access to a group' do perform_enqueued_jobs { click_link 'Request Access' } diff --git a/spec/features/issuables/default_sort_order_spec.rb b/spec/features/issuables/default_sort_order_spec.rb new file mode 100644 index 00000000000..0d495cd04aa --- /dev/null +++ b/spec/features/issuables/default_sort_order_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' + +describe 'Projects > Issuables > Default sort order', feature: true do + let(:project) { create(:empty_project, :public) } + + let(:first_created_issuable) { issuables.order_created_asc.first } + let(:last_created_issuable) { issuables.order_created_desc.first } + + let(:first_updated_issuable) { issuables.order_updated_asc.first } + let(:last_updated_issuable) { issuables.order_updated_desc.first } + + context 'for merge requests' do + include MergeRequestHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + source_branch: "#{issuable_type}_#{i}", + source_project: project }.merge(ts) + end + + MergeRequest.all + end + + context 'in the "merge requests" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests project + + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / open" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + + context 'in the "merge requests / merged" tab', js: true do + let(:issuable_type) { :merged_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'merged') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / closed" tab', js: true do + let(:issuable_type) { :closed_merge_request } + + it 'is "last updated"' do + visit_merge_requests_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_merge_request).to include(last_updated_issuable.title) + expect(last_merge_request).to include(first_updated_issuable.title) + end + end + + context 'in the "merge requests / all" tab', js: true do + let(:issuable_type) { :merge_request } + + it 'is "last created"' do + visit_merge_requests_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_merge_request).to include(last_created_issuable.title) + expect(last_merge_request).to include(first_created_issuable.title) + end + end + end + + context 'for issues' do + include IssueHelpers + + let!(:issuables) do + timestamps = [{ created_at: 3.minutes.ago, updated_at: 20.seconds.ago }, + { created_at: 2.minutes.ago, updated_at: 30.seconds.ago }, + { created_at: 4.minutes.ago, updated_at: 10.seconds.ago }] + + timestamps.each_with_index do |ts, i| + create issuable_type, { title: "#{issuable_type}_#{i}", + project: project }.merge(ts) + end + + Issue.all + end + + context 'in the "issues" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues project + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / open" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'open') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + + context 'in the "issues / closed" tab', js: true do + let(:issuable_type) { :closed_issue } + + it 'is "last updated"' do + visit_issues_with_state(project, 'closed') + + expect(selected_sort_order).to eq('last updated') + expect(first_issue).to include(last_updated_issuable.title) + expect(last_issue).to include(first_updated_issuable.title) + end + end + + context 'in the "issues / all" tab', js: true do + let(:issuable_type) { :issue } + + it 'is "last created"' do + visit_issues_with_state(project, 'all') + + expect(selected_sort_order).to eq('last created') + expect(first_issue).to include(last_created_issuable.title) + expect(last_issue).to include(first_created_issuable.title) + end + end + end + + def selected_sort_order + find('.pull-right .dropdown button').text.downcase + end + + def visit_merge_requests_with_state(project, state) + visit_merge_requests project + visit_issuables_with_state state + end + + def visit_issues_with_state(project, state) + visit_issues project + visit_issuables_with_state state + end + + def visit_issuables_with_state(state) + within('.issues-state-filters') { find("span", text: state.titleize).click } + end +end diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb index 5ea02b8d39c..cb117d2476f 100644 --- a/spec/features/issues/filter_by_labels_spec.rb +++ b/spec/features/issues/filter_by_labels_spec.rb @@ -205,7 +205,7 @@ feature 'Issue filtering by Labels', feature: true do page.within '.labels-filter' do click_button 'Label' wait_for_ajax - fill_in 'label-name', with: 'bug' + find('.dropdown-input input').set 'bug' page.within '.dropdown-content' do expect(page).not_to have_content 'enhancement' diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index d51c9abea19..9c92b52898c 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Issues', feature: true do + include IssueHelpers include SortingHelper let(:project) { create(:project) } @@ -186,15 +187,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by most recently updated' do @@ -350,8 +351,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('foo') - expect(last_issue).to include('bar') + expect(first_issue).to include('bar') + expect(last_issue).to include('foo') expect(page).not_to have_content 'baz' end end @@ -524,6 +525,35 @@ describe 'Issues', feature: true do end end + describe 'new issue by email' do + shared_examples 'show the email in the modal' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + + visit namespace_project_issues_path(project.namespace, project) + click_button('Email a new issue') + end + + it 'click the button to show modal for the new email' do + page.within '#issue-email-modal' do + email = project.new_issue_address(@user) + + expect(page).to have_selector("input[value='#{email}']") + end + end + end + + context 'with existing issues' do + let!(:issue) { create(:issue, project: project, author: @user) } + + it_behaves_like 'show the email in the modal' + end + + context 'without existing issues' do + it_behaves_like 'show the email in the modal' + end + end + describe 'due date' do context 'update due on issue#show', js: true do let(:issue) { create(:issue, project: project, author: @user, assignee: @user) } @@ -561,14 +591,6 @@ describe 'Issues', feature: true do end end - def first_issue - page.all('ul.issues-list > li').first.text - end - - def last_issue - page.all('ul.issues-list > li').last.text - end - def drop_in_dropzone(file_path) # Generate a fake input selector page.execute_script <<-JS diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index 09ccc77c101..32159559c37 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -236,6 +236,14 @@ describe 'GitLab Markdown', feature: true do it 'includes TaskListFilter' do expect(doc).to parse_task_lists end + + it 'includes InlineDiffFilter' do + expect(doc).to parse_inline_diffs + end + + it 'includes VideoLinkFilter' do + expect(doc).to parse_video_links + end end context 'wiki pipeline' do @@ -293,6 +301,10 @@ describe 'GitLab Markdown', feature: true do it 'includes InlineDiffFilter' do expect(doc).to parse_inline_diffs end + + it 'includes VideoLinkFilter' do + expect(doc).to parse_video_links + end end # Fake a `current_user` helper diff --git a/spec/features/merge_requests/user_lists_merge_requests_spec.rb b/spec/features/merge_requests/user_lists_merge_requests_spec.rb index 1c130057c56..cabb8e455f9 100644 --- a/spec/features/merge_requests/user_lists_merge_requests_spec.rb +++ b/spec/features/merge_requests/user_lists_merge_requests_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe 'Projects > Merge requests > User lists merge requests', feature: true do + include MergeRequestHelpers include SortingHelper let(:project) { create(:project, :public) } @@ -23,10 +24,12 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true milestone: create(:milestone, due_date: '2013-12-12'), created_at: 2.minutes.ago, updated_at: 2.minutes.ago) + # lfs in itself is not a great choice for the title if one wants to match the whole body content later on + # just think about the scenario when faker generates 'Chester Runolfsson' as the user's name create(:merge_request, - title: 'lfs', + title: 'merge_lfs', source_project: project, - source_branch: 'lfs', + source_branch: 'merge_lfs', created_at: 3.minutes.ago, updated_at: 10.seconds.ago) end @@ -35,7 +38,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true visit_merge_requests(project, assignee_id: IssuableFinder::NONE) expect(current_path).to eq(namespace_project_merge_requests_path(project.namespace, project)) - expect(page).to have_content 'lfs' + expect(page).to have_content 'merge_lfs' expect(page).not_to have_content 'fix' expect(page).not_to have_content 'markdown' expect(count_merge_requests).to eq(1) @@ -44,7 +47,7 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'filters on a specific assignee' do visit_merge_requests(project, assignee_id: user.id) - expect(page).not_to have_content 'lfs' + expect(page).not_to have_content 'merge_lfs' expect(page).to have_content 'fix' expect(page).to have_content 'markdown' expect(count_merge_requests).to eq(2) @@ -53,23 +56,23 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true it 'sorts by newest' do visit_merge_requests(project, sort: sort_value_recently_created) - expect(first_merge_request).to include('lfs') - expect(last_merge_request).to include('fix') + expect(first_merge_request).to include('fix') + expect(last_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end it 'sorts by oldest' do visit_merge_requests(project, sort: sort_value_oldest_created) - expect(first_merge_request).to include('fix') - expect(last_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') + expect(last_merge_request).to include('fix') expect(count_merge_requests).to eq(3) end it 'sorts by last updated' do visit_merge_requests(project, sort: sort_value_recently_updated) - expect(first_merge_request).to include('lfs') + expect(first_merge_request).to include('merge_lfs') expect(count_merge_requests).to eq(3) end @@ -143,18 +146,6 @@ describe 'Projects > Merge requests > User lists merge requests', feature: true end end - def visit_merge_requests(project, opts = {}) - visit namespace_project_merge_requests_path(project.namespace, project, opts) - end - - def first_merge_request - page.all('ul.mr-list > li').first.text - end - - def last_merge_request - page.all('ul.mr-list > li').last.text - end - def count_merge_requests page.all('ul.mr-list > li').count end diff --git a/spec/features/pipelines_settings_spec.rb b/spec/features/pipelines_settings_spec.rb new file mode 100644 index 00000000000..dcc364a3d01 --- /dev/null +++ b/spec/features/pipelines_settings_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +feature "Pipelines settings", feature: true do + include GitlabRoutingHelper + + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:role) { :developer } + + background do + login_as(user) + project.team << [user, role] + visit namespace_project_pipelines_settings_path(project.namespace, project) + end + + context 'for developer' do + given(:role) { :developer } + + scenario 'to be disallowed to view' do + expect(page.status_code).to eq(404) + end + end + + context 'for master' do + given(:role) { :master } + + scenario 'be allowed to change' do + fill_in('Test coverage parsing', with: 'coverage_regex') + click_on 'Save changes' + + expect(page.status_code).to eq(200) + expect(page).to have_field('Test coverage parsing', with: 'coverage_regex') + end + end +end diff --git a/spec/features/pipelines_spec.rb b/spec/features/pipelines_spec.rb index e7ee0aaea3c..377a9aba60d 100644 --- a/spec/features/pipelines_spec.rb +++ b/spec/features/pipelines_spec.rb @@ -62,6 +62,20 @@ describe "Pipelines" do end end + context 'with manual actions' do + let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'manual build', stage: 'test', commands: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).to have_link('Manual build') } + + context 'when playing' do + before { click_link('Manual build') } + + it { expect(manual.reload).to be_pending } + end + end + context 'for generic statuses' do context 'when running' do let!(:running) { create(:generic_commit_status, status: 'running', pipeline: pipeline, stage: 'test') } @@ -102,9 +116,19 @@ describe "Pipelines" do it { expect(page).to have_link(with_artifacts.name) } end + context 'with artifacts expired' do + let!(:with_artifacts_expired) { create(:ci_build, :artifacts_expired, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + + before { visit namespace_project_pipelines_path(project.namespace, project) } + + it { expect(page).not_to have_selector('.build-artifacts') } + end + context 'without artifacts' do let!(:without_artifacts) { create(:ci_build, :success, pipeline: pipeline, name: 'rspec', stage: 'test') } + before { visit namespace_project_pipelines_path(project.namespace, project) } + it { expect(page).not_to have_selector('.build-artifacts') } end end @@ -117,6 +141,7 @@ describe "Pipelines" do @success = create(:ci_build, :success, pipeline: pipeline, stage: 'build', name: 'build') @failed = create(:ci_build, :failed, pipeline: pipeline, stage: 'test', name: 'test', commands: 'test') @running = create(:ci_build, :running, pipeline: pipeline, stage: 'deploy', name: 'deploy') + @manual = create(:ci_build, :manual, pipeline: pipeline, stage: 'deploy', name: 'manual build') @external = create(:generic_commit_status, status: 'success', pipeline: pipeline, name: 'jenkins', stage: 'external') end @@ -131,6 +156,7 @@ describe "Pipelines" do expect(page).to have_content(@external.id) expect(page).to have_content('Retry failed') expect(page).to have_content('Cancel running') + expect(page).to have_link('Play') end context 'retrying builds' do @@ -154,6 +180,12 @@ describe "Pipelines" do it { expect(page).to have_selector('.ci-canceled') } end end + + context 'playing manual build' do + before { click_link('Play') } + + it { expect(@manual.reload).to be_pending } + end end describe 'POST /:project/pipelines' do diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index 01e90618a98..75166bca119 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -6,7 +6,7 @@ feature 'list of badges' do project = create(:project) project.team << [user, :master] login_as(user) - visit namespace_project_badges_path(project.namespace, project) + visit namespace_project_pipelines_settings_path(project.namespace, project) end scenario 'user displays list of badges' do diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb new file mode 100644 index 00000000000..79abba21854 --- /dev/null +++ b/spec/features/projects/branches_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +describe 'Branches', feature: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + + before do + login_as :user + project.team << [@user, :developer] + end + + describe 'Initial branches page' do + it 'shows all the branches' do + visit namespace_project_branches_path(project.namespace, project) + + repository.branches { |branch| expect(page).to have_content("#{branch.name}") } + expect(page).to have_content("Protected branches can be managed in project settings") + end + end + + describe 'Find branches' do + it 'shows filtered branches', js: true do + visit namespace_project_branches_path(project.namespace, project, project.id) + + fill_in 'branch-search', with: 'fix' + find('#branch-search').native.send_keys(:enter) + + expect(page).to have_content('fix') + expect(find('.all-branches')).to have_selector('li', count: 1) + end + end +end diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index bc3bf53fe9d..7835e1678ad 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -8,6 +8,7 @@ feature 'project import', feature: true, js: true do let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } let(:export_path) { "#{Dir::tmpdir}/import_file_spec" } let(:project) { Project.last } + let(:project_hook) { Gitlab::Git::Hook.new('post-receive', project.repository.path) } background do allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) @@ -37,7 +38,7 @@ feature 'project import', feature: true, js: true do expect(project).not_to be_nil expect(project.issues).not_to be_empty expect(project.merge_requests).not_to be_empty - expect(project.repo_exists?).to be true + expect(project_hook).to exist expect(wiki_exists?).to be true expect(project.import_status).to eq('finished') end @@ -59,6 +60,21 @@ feature 'project import', feature: true, js: true do end end + scenario 'project with no name' do + create(:project, namespace_id: 2) + + visit new_project_path + + select2('2', from: '#project_namespace_id') + + # click on disabled element + find(:link, 'GitLab export').trigger('click') + + page.within('.flash-container') do + expect(page).to have_content('Please enter path and name') + end + end + def wiki_exists? wiki = ProjectWiki.new(project) File.exist?(wiki.repository.path_to_repo) && !wiki.repository.empty? diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb index f2fe3ef364d..56ede8eb5be 100644 --- a/spec/features/projects/members/user_requests_access_spec.rb +++ b/spec/features/projects/members/user_requests_access_spec.rb @@ -11,6 +11,13 @@ feature 'Projects > Members > User requests access', feature: true do visit namespace_project_path(project.namespace, project) end + scenario 'request access feature is disabled' do + project.update_attributes(request_access_enabled: false) + visit namespace_project_path(project.namespace, project) + + expect(page).not_to have_content 'Request Access' + end + scenario 'user can request access to a project' do perform_enqueued_jobs { click_link 'Request Access' } diff --git a/spec/features/projects/project_settings_spec.rb b/spec/features/projects/project_settings_spec.rb new file mode 100644 index 00000000000..3de25d7af7d --- /dev/null +++ b/spec/features/projects/project_settings_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe 'Edit Project Settings', feature: true do + let(:user) { create(:user) } + let(:project) { create(:empty_project, path: 'gitlab', name: 'sample') } + + before do + login_as(user) + project.team << [user, :master] + end + + describe 'Project settings', js: true do + it 'shows errors for invalid project name' do + visit edit_namespace_project_path(project.namespace, project) + + fill_in 'project_name_edit', with: 'foo&bar' + + click_button 'Save changes' + + expect(page).to have_field 'project_name_edit', with: 'foo&bar' + expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'." + expect(page).to have_button 'Save changes' + end + end + + describe 'Rename repository' do + it 'shows errors for invalid project path/name' do + visit edit_namespace_project_path(project.namespace, project) + + fill_in 'Project name', with: 'foo&bar' + fill_in 'Path', with: 'foo&bar' + + click_button 'Rename project' + + expect(page).to have_field 'Project name', with: 'foo&bar' + expect(page).to have_field 'Path', with: 'foo&bar' + expect(page).to have_content "Name can contain only letters, digits, '_', '.', dash and space. It must start with letter, digit or '_'." + expect(page).to have_content "Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-', end in '.git' or end in '.atom'" + end + end +end diff --git a/spec/features/projects/slack_service/slack_service_spec.rb b/spec/features/projects/slack_service/slack_service_spec.rb new file mode 100644 index 00000000000..16541f51d98 --- /dev/null +++ b/spec/features/projects/slack_service/slack_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +feature 'Projects > Slack service > Setup events', feature: true do + let(:user) { create(:user) } + let(:service) { SlackService.new } + let(:project) { create(:project, slack_service: service) } + + background do + service.fields + service.update_attributes(push_channel: 1, issue_channel: 2, merge_request_channel: 3, note_channel: 4, tag_push_channel: 5, build_channel: 6, wiki_page_channel: 7) + project.team << [user, :master] + login_as(user) + end + + scenario 'user can filter events by channel' do + visit edit_namespace_project_service_path(project.namespace, project, service) + + expect(page.find_field("service_push_channel").value).to have_content '1' + expect(page.find_field("service_issue_channel").value).to have_content '2' + expect(page.find_field("service_merge_request_channel").value).to have_content '3' + expect(page.find_field("service_note_channel").value).to have_content '4' + expect(page.find_field("service_tag_push_channel").value).to have_content '5' + expect(page.find_field("service_build_channel").value).to have_content '6' + expect(page.find_field("service_wiki_page_channel").value).to have_content '7' + end +end diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb new file mode 100644 index 00000000000..a1c386ddc18 --- /dev/null +++ b/spec/features/projects/wiki/markdown_preview_spec.rb @@ -0,0 +1,140 @@ +require 'spec_helper' + +feature 'Projects > Wiki > User previews markdown changes', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:wiki_content) do + <<-HEREDOC +[regular link](regular) +[relative link 1](../relative) +[relative link 2](./relative) +[relative link 3](./e/f/relative) + HEREDOC + end + + background do + project.team << [user, :master] + login_as(user) + + visit namespace_project_path(project.namespace, project) + click_link 'Wiki' + WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute + end + + context "while creating a new wiki page" do + context "when there are no spaces or hyphens in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a/b/c/d' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>") + end + end + + context "when there are spaces in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a page/b page/c page/d page' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + + context "when there are hyphens in the page name" do + it "rewrites relative links as expected" do + click_link 'New Page' + fill_in :new_wiki_path, with: 'a-page/b-page/c-page/d-page' + click_button 'Create Page' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + end + + context "while editing a wiki page" do + def create_wiki_page(path) + click_link 'New Page' + fill_in :new_wiki_path, with: path + click_button 'Create Page' + fill_in :wiki_content, with: 'content' + click_on "Create page" + end + + context "when there are no spaces or hyphens in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a/b/c/d' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a/b/c/e/f/relative\">relative link 3</a>") + end + end + + context "when there are spaces in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a page/b page/c page/d page' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + + context "when there are hyphens in the page name" do + it "rewrites relative links as expected" do + create_wiki_page 'a-page/b-page/c-page/d-page' + click_link 'Edit' + + fill_in :wiki_content, with: wiki_content + click_on "Preview" + + expect(page).to have_content("regular link") + + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/regular\">regular link</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/relative\">relative link 1</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/relative\">relative link 2</a>") + expect(page.html).to include("<a href=\"/#{project.path_with_namespace}/wikis/a-page/b-page/c-page/e/f/relative\">relative link 3</a>") + end + end + end +end diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb index 7e6eef65873..7afd83b7250 100644 --- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb +++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb @@ -30,18 +30,48 @@ feature 'Projects > Wiki > User creates wiki page', feature: true do WikiPages::CreateService.new(project, user, title: 'home', content: 'Home page').execute end - scenario 'via the "new wiki page" page', js: true do - click_link 'New Page' + context 'via the "new wiki page" page' do + scenario 'when the wiki page has a single word name', js: true do + click_link 'New Page' - fill_in :new_wiki_path, with: 'foo' - click_button 'Create Page' + fill_in :new_wiki_path, with: 'foo' + click_button 'Create Page' - fill_in :wiki_content, with: 'My awesome wiki!' - click_button 'Create page' + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' - expect(page).to have_content('Foo') - expect(page).to have_content("last edited by #{user.name}") - expect(page).to have_content('My awesome wiki!') + expect(page).to have_content('Foo') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'when the wiki page has spaces in the name', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'Spaces in the name' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Spaces in the name') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end + + scenario 'when the wiki page has hyphens in the name', js: true do + click_link 'New Page' + + fill_in :new_wiki_path, with: 'hyphens-in-the-name' + click_button 'Create Page' + + fill_in :wiki_content, with: 'My awesome wiki!' + click_button 'Create page' + + expect(page).to have_content('Hyphens in the name') + expect(page).to have_content("last edited by #{user.name}") + expect(page).to have_content('My awesome wiki!') + end end end end diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb index d94dee0c797..57734b33a44 100644 --- a/spec/features/protected_branches_spec.rb +++ b/spec/features/protected_branches_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Projected Branches', feature: true, js: true do + include WaitForAjax + let(:user) { create(:user, :admin) } let(:project) { create(:project) } @@ -81,4 +83,68 @@ feature 'Projected Branches', feature: true, js: true do end end end + + describe "access control" do + ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can push to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-push").click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) + end + + it "allows updating protected branches so that #{access_type_name} can push to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-push").click + within('.dropdown-menu.push') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.push_access_level.access_level).to eq(access_type_id) + end + end + + ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)| + it "allows creating protected branches that #{access_type_name} can merge to" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + within('.new_protected_branch') do + find(".allowed-to-merge").click + within(".dropdown.open .dropdown-menu") { click_on access_type_name } + end + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) + end + + it "allows updating protected branches so that #{access_type_name} can merge to them" do + visit namespace_project_protected_branches_path(project.namespace, project) + set_protected_branch_name('master') + click_on "Protect" + + expect(ProtectedBranch.count).to eq(1) + + within(".protected-branches-list") do + find(".allowed-to-merge").click + within('.dropdown-menu.merge') { click_on access_type_name } + end + + wait_for_ajax + expect(ProtectedBranch.last.merge_access_level.access_level).to eq(access_type_id) + end + end + end end diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb index d0a301038c4..09f70cd3b00 100644 --- a/spec/features/search_spec.rb +++ b/spec/features/search_spec.rb @@ -28,6 +28,26 @@ describe "Search", feature: true do end context 'search for comments' do + context 'when comment belongs to a invalid commit' do + let(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'Bug here') } + + before { note.update_attributes(commit_id: 12345678) } + + it 'finds comment' do + visit namespace_project_path(project.namespace, project) + + page.within '.search' do + fill_in 'search', with: note.note + click_button 'Go' + end + + click_link 'Comments' + + expect(page).to have_text("Commit deleted") + expect(page).to have_text("12345678") + end + end + it 'finds a snippet' do snippet = create(:project_snippet, :private, project: project, author: user, title: 'Some title') note = create(:note, diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb new file mode 100644 index 00000000000..6fce11de30f --- /dev/null +++ b/spec/finders/branches_finder_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe BranchesFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:repository) { project.repository } + + describe '#execute' do + context 'sort only' do + it 'sorts by name' do + branches_finder = described_class.new(repository, {}) + + result = branches_finder.execute + + expect(result.first.name).to eq("'test'") + end + + it 'sorts by recently_updated' do + branches_finder = described_class.new(repository, { sort: 'updated_desc' }) + + result = branches_finder.execute + + recently_updated_branch = repository.branches.max do |a, b| + repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date + end + + expect(result.first.name).to eq(recently_updated_branch.name) + end + + it 'sorts by last_updated' do + branches_finder = described_class.new(repository, { sort: 'updated_asc' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + end + end + + context 'filter only' do + it 'filters branches by name' do + branches_finder = described_class.new(repository, { search: 'fix' }) + + result = branches_finder.execute + + expect(result.first.name).to eq('fix') + expect(result.count).to eq(1) + end + + it 'does not find any branch with that name' do + branches_finder = described_class.new(repository, { search: 'random' }) + + result = branches_finder.execute + + expect(result.count).to eq(0) + end + end + + context 'filter and sort' do + it 'filters branches by name and sorts by recently_updated' do + params = { sort: 'updated_desc', search: 'feature' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature_conflict') + expect(result.count).to eq(2) + end + + it 'filters branches by name and sorts by last_updated' do + params = { sort: 'updated_asc', search: 'feature' } + branches_finder = described_class.new(repository, params) + + result = branches_finder.execute + + expect(result.first.name).to eq('feature') + expect(result.count).to eq(2) + end + end + end +end diff --git a/spec/fixtures/domain_blacklist.txt b/spec/fixtures/domain_blacklist.txt new file mode 100644 index 00000000000..baeb11eda9a --- /dev/null +++ b/spec/fixtures/domain_blacklist.txt @@ -0,0 +1,3 @@ +example.com +test.com +foo.bar
\ No newline at end of file diff --git a/spec/fixtures/emails/valid_new_issue.eml b/spec/fixtures/emails/valid_new_issue.eml new file mode 100644 index 00000000000..3cf53a656a5 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue.eml @@ -0,0 +1,23 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 + +The reply by email functionality should be extended to allow creating a new issue by email. + +* Allow an admin to specify which project the issue should be created under by checking the sender domain. +* Possibly allow the use of regular expression matches within the subject/body to specify which project the issue should be created under. diff --git a/spec/fixtures/emails/valid_new_issue_empty.eml b/spec/fixtures/emails/valid_new_issue_empty.eml new file mode 100644 index 00000000000..fc1d52a3f42 --- /dev/null +++ b/spec/fixtures/emails/valid_new_issue_empty.eml @@ -0,0 +1,18 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+auth_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_authentication_token.eml b/spec/fixtures/emails/wrong_authentication_token.eml new file mode 100644 index 00000000000..0994c2f7775 --- /dev/null +++ b/spec/fixtures/emails/wrong_authentication_token.eml @@ -0,0 +1,18 @@ +Return-Path: <jake@adventuretime.ooo> +Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400 +Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400 +Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <incoming+gitlabhq/gitlabhq@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700 +Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700 +Date: Thu, 13 Jun 2013 17:03:48 -0400 +From: Jake the Dog <jake@adventuretime.ooo> +To: incoming+gitlabhq/gitlabhq+bad_token@appmail.adventuretime.ooo +Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com> +Subject: New Issue by email +Mime-Version: 1.0 +Content-Type: text/plain; + charset=ISO-8859-1 +Content-Transfer-Encoding: 7bit +X-Sieve: CMU Sieve 2.2 +X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu, + 13 Jun 2013 14:03:48 -0700 (PDT) +X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1 diff --git a/spec/fixtures/emails/wrong_reply_key.eml b/spec/fixtures/emails/wrong_mail_key.eml index 491e078fb5b..491e078fb5b 100644 --- a/spec/fixtures/emails/wrong_reply_key.eml +++ b/spec/fixtures/emails/wrong_mail_key.eml diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index c75d28d9801..f3e7c2d1a9f 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -256,3 +256,7 @@ However the wrapping tags can not be mixed as such - - [+ additions +} - {- delletions -] - [- delletions -} + +### Videos + +![My Video](/assets/videos/gitlab-demo.mp4) diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml deleted file mode 100644 index 37066c8e930..00000000000 --- a/spec/fixtures/parallel_diff_result.yml +++ /dev/null @@ -1,800 +0,0 @@ ---- -- :left: - :type: match - :number: 6 - :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: match - :number: 6 - :text: "@@ -6,12 +6,18 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 6 - :text: |2 - <span id="LC6" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 6 - :new_line: 6 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 6 - :text: |2 - <span id="LC6" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 6 - :new_line: 6 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 7 - :text: |2 - <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 7 - :new_line: 7 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 7 - :text: |2 - <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 7 - :new_line: 7 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 8 - :text: |2 - <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 8 - :new_line: 8 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 8 - :text: |2 - <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 8 - :new_line: 8 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 9 - :text: | - -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 9 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 9 - :text: | - +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 9 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 10 - :text: |2 - <span id="LC10" class="line"> <span class="k">end</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 10 - :new_line: 10 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 10 - :text: |2 - <span id="LC10" class="line"> <span class="k">end</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 10 - :new_line: 10 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 11 - :text: |2 - <span id="LC11" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 11 - :new_line: 11 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 11 - :text: |2 - <span id="LC11" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 11 - :new_line: 11 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 12 - :text: |2 - <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 12 - :new_line: 12 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 12 - :text: |2 - <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 12 - :new_line: 12 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 13 - :text: | - -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 13 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 13 - :text: | - +<span id="LC13" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 13 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: old - :number: 14 - :text: | - -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 14 - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 14 - :text: | - +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 14 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 15 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 15 - :text: | - +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 15 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 16 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 16 - :text: | - +<span id="LC16" class="line"> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 16 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 17 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 17 - :text: | - +<span id="LC17" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 17 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 18 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 18 - :text: | - +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 18 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 19 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 19 - :text: | - +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 19 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 20 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 20 - :text: | - +<span id="LC20" class="line"> <span class="p">}</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 20 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 15 - :text: |2 - <span id="LC21" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 15 - :new_line: 21 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 21 - :text: |2 - <span id="LC21" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 15 - :new_line: 21 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 16 - :text: |2 - <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 16 - :new_line: 22 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 22 - :text: |2 - <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 16 - :new_line: 22 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 17 - :text: |2 - <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 17 - :new_line: 23 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 23 - :text: |2 - <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 17 - :new_line: 23 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: match - :number: 19 - :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: match - :number: 25 - :text: "@@ -19,6 +25,7 @@ module Popen" - :line_code: - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 19 - :text: |2 - <span id="LC25" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 19 - :new_line: 25 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 25 - :text: |2 - <span id="LC25" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 19 - :new_line: 25 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 20 - :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 20 - :new_line: 26 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 26 - :text: |2 - <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 20 - :new_line: 26 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 21 - :text: |2 - <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 21 - :new_line: 27 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 27 - :text: |2 - <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 21 - :new_line: 27 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: - :text: '' - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 28 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: new - :number: 28 - :text: | - +<span id="LC28" class="line"></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: - :new_line: 28 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 22 - :text: |2 - <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 22 - :new_line: 29 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 29 - :text: |2 - <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 22 - :new_line: 29 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 23 - :text: |2 - <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 23 - :new_line: 30 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 30 - :text: |2 - <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 23 - :new_line: 30 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d -- :left: - :type: - :number: 24 - :text: |2 - <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 24 - :new_line: 31 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d - :right: - :type: - :number: 31 - :text: |2 - <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> - :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 - :position: !ruby/object:Gitlab::Diff::Position - attributes: - :old_path: files/ruby/popen.rb - :new_path: files/ruby/popen.rb - :old_line: 24 - :new_line: 31 - :base_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :start_sha: 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9 - :head_sha: 570e7b2abdd848b95f2f578043fc23bd6f6fd24d diff --git a/spec/fixtures/video_sample.mp4 b/spec/fixtures/video_sample.mp4 Binary files differnew file mode 100644 index 00000000000..acd45190998 --- /dev/null +++ b/spec/fixtures/video_sample.mp4 diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index bd0108f9938..b2d6d59b1ee 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe BlobHelper do + include TreeHelper + let(:blob_name) { 'test.lisp' } let(:no_context_content) { ":type \"assem\"))" } let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } @@ -65,4 +67,20 @@ describe BlobHelper do expect(sanitize_svg(blob).data).to eq(expected) end end + + describe "#edit_blob_link" do + let(:project) { create(:project) } + + before do + allow(self).to receive(:current_user).and_return(double) + end + + it 'verifies blob is text' do + expect(self).not_to receive(:blob_text_viewable?) + + button = edit_blob_link(project, 'refs/heads/master', 'README.md') + + expect(button).to start_with('<button') + end + end end diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb index 45199d0f09d..637b02d9388 100644 --- a/spec/helpers/ci_status_helper_spec.rb +++ b/spec/helpers/ci_status_helper_spec.rb @@ -7,7 +7,13 @@ describe CiStatusHelper do let(:failed_commit) { double("Ci::Pipeline", status: 'failed') } describe 'ci_icon_for_status' do - it { expect(helper.ci_icon_for_status(success_commit.status)).to include('fa-check') } - it { expect(helper.ci_icon_for_status(failed_commit.status)).to include('fa-close') } + it 'renders to correct svg on success' do + expect(helper).to receive(:render).with('shared/icons/icon_status_success.svg', anything) + helper.ci_icon_for_status(success_commit.status) + end + it 'renders the correct svg on failure' do + expect(helper).to receive(:render).with('shared/icons/icon_status_failed.svg', anything) + helper.ci_icon_for_status(failed_commit.status) + end end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index 831ae7fb69c..9ee46dd2508 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -5,52 +5,6 @@ describe IssuesHelper do let(:issue) { create :issue, project: project } let(:ext_project) { create :redmine_project } - describe "url_for_project_issues" do - let(:project_url) { ext_project.external_issue_tracker.project_url } - let(:ext_expected) { project_url.gsub(':project_id', ext_project.id.to_s) } - let(:int_expected) { polymorphic_path([@project.namespace, project]) } - - it "should return internal path if used internal tracker" do - @project = project - expect(url_for_project_issues).to match(int_expected) - end - - it "should return path to external tracker" do - @project = ext_project - - expect(url_for_project_issues).to match(ext_expected) - end - - it "should return empty string if project nil" do - @project = nil - - expect(url_for_project_issues).to eq "" - end - - it 'returns an empty string if project_url is invalid' do - expect(project).to receive_message_chain('issues_tracker.project_url') { 'javascript:alert("foo");' } - - expect(url_for_project_issues(project)).to eq '' - end - - it 'returns an empty string if project_path is invalid' do - expect(project).to receive_message_chain('issues_tracker.project_path') { 'javascript:alert("foo");' } - - expect(url_for_project_issues(project, only_path: true)).to eq '' - end - - describe "when external tracker was enabled and then config removed" do - before do - @project = ext_project - allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) - end - - it "should return path to external tracker" do - expect(url_for_project_issues).to match(ext_expected) - end - end - end - describe "url_for_issue" do let(:issues_url) { ext_project.external_issue_tracker.issues_url} let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) } @@ -97,52 +51,6 @@ describe IssuesHelper do end end - describe 'url_for_new_issue' do - let(:issues_url) { ext_project.external_issue_tracker.new_issue_url } - let(:ext_expected) { issues_url.gsub(':project_id', ext_project.id.to_s) } - let(:int_expected) { new_namespace_project_issue_path(project.namespace, project) } - - it "should return internal path if used internal tracker" do - @project = project - expect(url_for_new_issue).to match(int_expected) - end - - it "should return path to external tracker" do - @project = ext_project - - expect(url_for_new_issue).to match(ext_expected) - end - - it "should return empty string if project nil" do - @project = nil - - expect(url_for_new_issue).to eq "" - end - - it 'returns an empty string if issue_url is invalid' do - expect(project).to receive_message_chain('issues_tracker.new_issue_url') { 'javascript:alert("foo");' } - - expect(url_for_new_issue(project)).to eq '' - end - - it 'returns an empty string if issue_path is invalid' do - expect(project).to receive_message_chain('issues_tracker.new_issue_path') { 'javascript:alert("foo");' } - - expect(url_for_new_issue(project, only_path: true)).to eq '' - end - - describe "when external tracker was enabled and then config removed" do - before do - @project = ext_project - allow(Gitlab.config).to receive(:issues_tracker).and_return(nil) - end - - it "should return internal path" do - expect(url_for_new_issue).to match(ext_expected) - end - end - end - describe "merge_requests_sentence" do subject { merge_requests_sentence(merge_requests)} let(:merge_requests) do diff --git a/spec/helpers/notes_helper_spec.rb b/spec/helpers/notes_helper_spec.rb index 08a93503258..af371248ae9 100644 --- a/spec/helpers/notes_helper_spec.rb +++ b/spec/helpers/notes_helper_spec.rb @@ -1,37 +1,30 @@ require "spec_helper" describe NotesHelper do - describe "#notes_max_access_for_users" do - let(:owner) { create(:owner) } - let(:group) { create(:group) } - let(:project) { create(:empty_project, namespace: group) } - let(:master) { create(:user) } - let(:reporter) { create(:user) } - let(:guest) { create(:user) } - - let(:owner_note) { create(:note, author: owner, project: project) } - let(:master_note) { create(:note, author: master, project: project) } - let(:reporter_note) { create(:note, author: reporter, project: project) } - let!(:notes) { [owner_note, master_note, reporter_note] } - - before do - group.add_owner(owner) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] - end + let(:owner) { create(:owner) } + let(:group) { create(:group) } + let(:project) { create(:empty_project, namespace: group) } + let(:master) { create(:user) } + let(:reporter) { create(:user) } + let(:guest) { create(:user) } - it 'return human access levels' do - original_method = project.team.method(:human_max_access) - expect_any_instance_of(ProjectTeam).to receive(:human_max_access).exactly(3).times do |*args| - original_method.call(args[1]) - end + let(:owner_note) { create(:note, author: owner, project: project) } + let(:master_note) { create(:note, author: master, project: project) } + let(:reporter_note) { create(:note, author: reporter, project: project) } + let!(:notes) { [owner_note, master_note, reporter_note] } + before do + group.add_owner(owner) + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [guest, :guest] + end + + describe "#notes_max_access_for_users" do + it 'return human access levels' do expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') expect(helper.note_max_access_for_user(master_note)).to eq('Master') expect(helper.note_max_access_for_user(reporter_note)).to eq('Reporter') - # Call it again to ensure value is cached - expect(helper.note_max_access_for_user(owner_note)).to eq('Owner') end it 'handles access in different projects' do @@ -43,4 +36,16 @@ describe NotesHelper do expect(helper.note_max_access_for_user(other_note)).to eq('Reporter') end end + + describe '#preload_max_access_for_authors' do + it 'loads multiple users' do + expected_access = { + owner.id => Gitlab::Access::OWNER, + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER + } + + expect(helper.preload_max_access_for_authors(notes, project)).to eq(expected_access) + end + end end diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb index 3f62527c5bb..bf3ed5c094c 100644 --- a/spec/helpers/time_helper_spec.rb +++ b/spec/helpers/time_helper_spec.rb @@ -1,36 +1,34 @@ require 'spec_helper' describe TimeHelper do - describe "#duration_in_words" do + describe "#time_interval_in_words" do it "returns minutes and seconds" do intervals_in_words = { 100 => "1 minute 40 seconds", + 100.32 => "1 minute 40 seconds", 121 => "2 minutes 1 second", 3721 => "62 minutes 1 second", 0 => "0 seconds" } intervals_in_words.each do |interval, expectation| - expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation) + expect(time_interval_in_words(interval)).to eq(expectation) end end - - it "calculates interval from now if there is no finished_at" do - expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds") - end end - describe "#time_interval_in_words" do + describe "#duration_in_numbers" do it "returns minutes and seconds" do - intervals_in_words = { - 100 => "1 minute 40 seconds", - 121 => "2 minutes 1 second", - 3721 => "62 minutes 1 second", - 0 => "0 seconds" + duration_in_numbers = { + [100, 0] => "01:40", + [121, 0] => "02:01", + [3721, 0] => "01:02:01", + [0, 0] => "00:00", + [nil, Time.now.to_i - 42] => "00:42" } - intervals_in_words.each do |interval, expectation| - expect(time_interval_in_words(interval)).to eq(expectation) + duration_in_numbers.each do |interval, expectation| + expect(duration_in_numbers(*interval)).to eq(expectation) end end end diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index 5178bd130f4..baab30f482f 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -1,41 +1,58 @@ require 'spec_helper' +require_relative '../../config/initializers/6_validations.rb' describe '6_validations', lib: true do + before :all do + FileUtils.mkdir_p('tmp/tests/paths/a/b/c/d') + FileUtils.mkdir_p('tmp/tests/paths/a/b/c2') + FileUtils.mkdir_p('tmp/tests/paths/a/b/d') + end + + after :all do + FileUtils.rm_rf('tmp/tests/paths') + end + context 'with correct settings' do before do - mock_storages('foo' => '/a/b/c', 'bar' => 'a/b/d') + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/d') end it 'passes through' do - expect { load_validations }.not_to raise_error + expect { validate_storages }.not_to raise_error end end context 'with invalid storage names' do before do - mock_storages('name with spaces' => '/a/b/c') + mock_storages('name with spaces' => 'tmp/tests/paths/a/b/c') end it 'throws an error' do - expect { load_validations }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') + expect { validate_storages }.to raise_error('"name with spaces" is not a valid storage name. Please fix this in your gitlab.yml before starting GitLab.') end end context 'with nested storage paths' do before do - mock_storages('foo' => '/a/b/c', 'bar' => '/a/b/c/d') + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c/d') end it 'throws an error' do - expect { load_validations }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') + expect { validate_storages }.to raise_error('bar is a nested path of foo. Nested paths are not supported for repository storages. Please fix this in your gitlab.yml before starting GitLab.') end end - def mock_storages(storages) - allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) + context 'with similar but un-nested storage paths' do + before do + mock_storages('foo' => 'tmp/tests/paths/a/b/c', 'bar' => 'tmp/tests/paths/a/b/c2') + end + + it 'passes through' do + expect { validate_storages }.not_to raise_error + end end - def load_validations - load File.join(__dir__, '../../config/initializers/6_validations.rb') + def mock_storages(storages) + allow(Gitlab.config.repositories).to receive(:storages).and_return(storages) end end diff --git a/spec/initializers/trusted_proxies_spec.rb b/spec/initializers/trusted_proxies_spec.rb index 14c8df954a6..290e47763eb 100644 --- a/spec/initializers/trusted_proxies_spec.rb +++ b/spec/initializers/trusted_proxies_spec.rb @@ -17,6 +17,12 @@ describe 'trusted_proxies', lib: true do expect(request.remote_ip).to eq('10.1.5.89') expect(request.ip).to eq('10.1.5.89') end + + it 'filters out bad values' do + request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 10.1.5.89') + expect(request.remote_ip).to eq('10.1.5.89') + expect(request.ip).to eq('10.1.5.89') + end end context 'with private IP ranges added' do @@ -41,6 +47,12 @@ describe 'trusted_proxies', lib: true do expect(request.remote_ip).to eq('1.1.1.1') expect(request.ip).to eq('1.1.1.1') end + + it 'handles invalid ip addresses' do + request = stub_request('HTTP_X_FORWARDED_FOR' => '(null), 1.1.1.1:12345, 1.1.1.1') + expect(request.remote_ip).to eq('1.1.1.1') + expect(request.ip).to eq('1.1.1.1') + end end def stub_request(headers = {}) diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js new file mode 100644 index 00000000000..b48026c3b77 --- /dev/null +++ b/spec/javascripts/application_spec.js @@ -0,0 +1,32 @@ + +/*= require lib/utils/common_utils */ + +(function() { + describe('Application', function() { + return describe('disable buttons', function() { + fixture.preload('application.html'); + beforeEach(function() { + return fixture.load('application.html'); + }); + it('should prevent default action for disabled buttons', function() { + var $button, isClicked; + gl.utils.preventDisabledButtons(); + isClicked = false; + $button = $('#test-button'); + $button.click(function() { + return isClicked = true; + }); + $button.trigger('click'); + return expect(isClicked).toBe(false); + }); + return it('should be on the same page if a disabled link clicked', function() { + var locationBeforeLinkClick; + locationBeforeLinkClick = window.location.href; + gl.utils.preventDisabledButtons(); + $('#test-link').click(); + return expect(window.location.href).toBe(locationBeforeLinkClick); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/application_spec.js.coffee b/spec/javascripts/application_spec.js.coffee deleted file mode 100644 index 4b6a2bb5440..00000000000 --- a/spec/javascripts/application_spec.js.coffee +++ /dev/null @@ -1,30 +0,0 @@ -#= require lib/utils/common_utils - -describe 'Application', -> - describe 'disable buttons', -> - fixture.preload('application.html') - - beforeEach -> - fixture.load('application.html') - - it 'should prevent default action for disabled buttons', -> - - gl.utils.preventDisabledButtons() - - isClicked = false - $button = $ '#test-button' - - $button.click -> isClicked = true - $button.trigger 'click' - - expect(isClicked).toBe false - - - it 'should be on the same page if a disabled link clicked', -> - - locationBeforeLinkClick = window.location.href - gl.utils.preventDisabledButtons() - - $('#test-link').click() - - expect(window.location.href).toBe locationBeforeLinkClick diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js new file mode 100644 index 00000000000..3ddc163033e --- /dev/null +++ b/spec/javascripts/awards_handler_spec.js @@ -0,0 +1,187 @@ + +/*= require awards_handler */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + + +/*= require ./fixtures/emoji_menu */ + +(function() { + var awardsHandler, lazyAssert; + + awardsHandler = null; + + window.gl || (window.gl = {}); + + window.gon || (window.gon = {}); + + gl.emojiAliases = function() { + return { + '+1': 'thumbsup', + '-1': 'thumbsdown' + }; + }; + + gon.award_menu_url = '/emojis'; + + lazyAssert = function(done, assertFn) { + return setTimeout(function() { + assertFn(); + return done(); + }, 333); + }; + + describe('AwardsHandler', function() { + fixture.preload('awards_handler.html'); + beforeEach(function() { + fixture.load('awards_handler.html'); + awardsHandler = new AwardsHandler; + spyOn(awardsHandler, 'postEmoji').and.callFake((function(_this) { + return function(url, emoji, cb) { + return cb(); + }; + })(this)); + return spyOn(jQuery, 'get').and.callFake(function(req, cb) { + return cb(window.emojiMenu); + }); + }); + describe('::showEmojiMenu', function() { + it('should show emoji menu when Add emoji button clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(true); + expect($emojiMenu.find('#emoji_search').length).toBe(1); + return expect($('.js-awards-block.current').length).toBe(1); + }); + }); + it('should also show emoji menu for the smiley icon in notes', function(done) { + $('.note-action-button').click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + return expect($emojiMenu.length).toBe(1); + }); + }); + return it('should remove emoji menu when body is clicked', function(done) { + $('.js-add-award').eq(0).click(); + return lazyAssert(done, function() { + var $emojiMenu; + $emojiMenu = $('.emoji-menu'); + $('body').click(); + expect($emojiMenu.length).toBe(1); + expect($emojiMenu.hasClass('is-visible')).toBe(false); + return expect($('.js-awards-block.current').length).toBe(0); + }); + }); + }); + describe('::addAwardToEmojiBar', function() { + it('should add emoji to votes block', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + expect($emojiButton.length).toBe(1); + expect($emojiButton.next('.js-counter').text()).toBe('1'); + return expect($votesBlock.hasClass('hidden')).toBe(false); + }); + it('should remove the emoji when we click again', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + return expect($emojiButton.length).toBe(0); + }); + return it('should decrement the emoji counter', function() { + var $emojiButton, $votesBlock; + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + $emojiButton = $votesBlock.find('[data-emoji=heart]'); + $emojiButton.next('.js-counter').text(5); + awardsHandler.addAwardToEmojiBar($votesBlock, 'heart', false); + expect($emojiButton.length).toBe(1); + return expect($emojiButton.next('.js-counter').text()).toBe('4'); + }); + }); + describe('::getAwardUrl', function() { + return it('should return the url for request', function() { + return expect(awardsHandler.getAwardUrl()).toBe('/gitlab-org/gitlab-test/issues/8/toggle_award_emoji'); + }); + }); + describe('::addAward and ::checkMutuality', function() { + return it('should handle :+1: and :-1: mutuality', function() { + var $thumbsDownEmoji, $thumbsUpEmoji, $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent(); + $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsup', false); + expect($thumbsUpEmoji.hasClass('active')).toBe(true); + expect($thumbsDownEmoji.hasClass('active')).toBe(false); + $thumbsUpEmoji.tooltip(); + $thumbsDownEmoji.tooltip(); + awardsHandler.addAward($votesBlock, awardUrl, 'thumbsdown', true); + expect($thumbsUpEmoji.hasClass('active')).toBe(false); + return expect($thumbsDownEmoji.hasClass('active')).toBe(true); + }); + }); + describe('::removeEmoji', function() { + return it('should remove emoji', function() { + var $votesBlock, awardUrl; + awardUrl = awardsHandler.getAwardUrl(); + $votesBlock = $('.js-awards-block').eq(0); + awardsHandler.addAward($votesBlock, awardUrl, 'fire', false); + expect($votesBlock.find('[data-emoji=fire]').length).toBe(1); + awardsHandler.removeEmoji($votesBlock.find('[data-emoji=fire]').closest('button')); + return expect($votesBlock.find('[data-emoji=fire]').length).toBe(0); + }); + }); + describe('search', function() { + return it('should filter the emoji', function() { + $('.js-add-award').eq(0).click(); + expect($('[data-emoji=angel]').is(':visible')).toBe(true); + expect($('[data-emoji=anger]').is(':visible')).toBe(true); + $('#emoji_search').val('ali').trigger('keyup'); + expect($('[data-emoji=angel]').is(':visible')).toBe(false); + expect($('[data-emoji=anger]').is(':visible')).toBe(false); + return expect($('[data-emoji=alien]').is(':visible')).toBe(true); + }); + }); + return describe('emoji menu', function() { + var openEmojiMenuAndAddEmoji, selector; + selector = '[data-emoji=sunglasses]'; + openEmojiMenuAndAddEmoji = function() { + var $block, $emoji, $menu; + $('.js-add-award').eq(0).click(); + $menu = $('.emoji-menu'); + $block = $('.js-awards-block'); + $emoji = $menu.find(".emoji-menu-list-item " + selector); + expect($emoji.length).toBe(1); + expect($block.find(selector).length).toBe(0); + $emoji.click(); + expect($menu.hasClass('.is-visible')).toBe(false); + return expect($block.find(selector).length).toBe(1); + }; + it('should add selected emoji to awards block', function() { + return openEmojiMenuAndAddEmoji(); + }); + return it('should remove already selected emoji', function() { + var $block, $emoji; + openEmojiMenuAndAddEmoji(); + $('.js-add-award').eq(0).click(); + $block = $('.js-awards-block'); + $emoji = $('.emoji-menu').find(".emoji-menu-list-item " + selector); + $emoji.click(); + return expect($block.find(selector).length).toBe(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/awards_handler_spec.js.coffee b/spec/javascripts/awards_handler_spec.js.coffee deleted file mode 100644 index d7f9c6fc076..00000000000 --- a/spec/javascripts/awards_handler_spec.js.coffee +++ /dev/null @@ -1,200 +0,0 @@ -#= require awards_handler -#= require jquery -#= require jquery.cookie -#= require ./fixtures/emoji_menu - -awardsHandler = null -window.gl or= {} -window.gon or= {} -gl.emojiAliases = -> return { '+1': 'thumbsup', '-1': 'thumbsdown' } -gon.award_menu_url = '/emojis' - - -lazyAssert = (done, assertFn) -> - - setTimeout -> # Maybe jasmine.clock here? - assertFn() - done() - , 333 - - -describe 'AwardsHandler', -> - - fixture.preload 'awards_handler.html' - - beforeEach -> - fixture.load 'awards_handler.html' - awardsHandler = new AwardsHandler - spyOn(awardsHandler, 'postEmoji').and.callFake (url, emoji, cb) => cb() - spyOn(jQuery, 'get').and.callFake (req, cb) -> cb window.emojiMenu - - - describe '::showEmojiMenu', -> - - it 'should show emoji menu when Add emoji button clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe yes - expect($emojiMenu.find('#emoji_search').length).toBe 1 - expect($('.js-awards-block.current').length).toBe 1 - - - it 'should also show emoji menu for the smiley icon in notes', (done) -> - - $('.note-action-button').click() - - lazyAssert done, -> - $emojiMenu = $ '.emoji-menu' - expect($emojiMenu.length).toBe 1 - - - it 'should remove emoji menu when body is clicked', (done) -> - - $('.js-add-award').eq(0).click() - - lazyAssert done, -> - $emojiMenu = $('.emoji-menu') - $('body').click() - expect($emojiMenu.length).toBe 1 - expect($emojiMenu.hasClass('is-visible')).toBe no - expect($('.js-awards-block.current').length).toBe 0 - - - describe '::addAwardToEmojiBar', -> - - it 'should add emoji to votes block', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '1' - expect($votesBlock.hasClass('hidden')).toBe no - - - it 'should remove the emoji when we click again', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - $emojiButton = $votesBlock.find '[data-emoji=heart]' - - expect($emojiButton.length).toBe 0 - - - it 'should decrement the emoji counter', -> - - $votesBlock = $('.js-awards-block').eq 0 - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - $emojiButton = $votesBlock.find '[data-emoji=heart]' - $emojiButton.next('.js-counter').text 5 - - awardsHandler.addAwardToEmojiBar $votesBlock, 'heart', no - - expect($emojiButton.length).toBe 1 - expect($emojiButton.next('.js-counter').text()).toBe '4' - - - describe '::getAwardUrl', -> - - it 'should return the url for request', -> - - expect(awardsHandler.getAwardUrl()).toBe '/gitlab-org/gitlab-test/issues/8/toggle_award_emoji' - - - describe '::addAward and ::checkMutuality', -> - - it 'should handle :+1: and :-1: mutuality', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - $thumbsUpEmoji = $votesBlock.find('[data-emoji=thumbsup]').parent() - $thumbsDownEmoji = $votesBlock.find('[data-emoji=thumbsdown]').parent() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsup', no - - expect($thumbsUpEmoji.hasClass('active')).toBe yes - expect($thumbsDownEmoji.hasClass('active')).toBe no - - $thumbsUpEmoji.tooltip() - $thumbsDownEmoji.tooltip() - - awardsHandler.addAward $votesBlock, awardUrl, 'thumbsdown', yes - - expect($thumbsUpEmoji.hasClass('active')).toBe no - expect($thumbsDownEmoji.hasClass('active')).toBe yes - - - describe '::removeEmoji', -> - - it 'should remove emoji', -> - - awardUrl = awardsHandler.getAwardUrl() - $votesBlock = $('.js-awards-block').eq 0 - - awardsHandler.addAward $votesBlock, awardUrl, 'fire', no - expect($votesBlock.find('[data-emoji=fire]').length).toBe 1 - - awardsHandler.removeEmoji $votesBlock.find('[data-emoji=fire]').closest('button') - expect($votesBlock.find('[data-emoji=fire]').length).toBe 0 - - - describe 'search', -> - - it 'should filter the emoji', -> - - $('.js-add-award').eq(0).click() - - expect($('[data-emoji=angel]').is(':visible')).toBe yes - expect($('[data-emoji=anger]').is(':visible')).toBe yes - - $('#emoji_search').val('ali').trigger 'keyup' - - expect($('[data-emoji=angel]').is(':visible')).toBe no - expect($('[data-emoji=anger]').is(':visible')).toBe no - expect($('[data-emoji=alien]').is(':visible')).toBe yes - - - describe 'emoji menu', -> - - selector = '[data-emoji=sunglasses]' - - openEmojiMenuAndAddEmoji = -> - - $('.js-add-award').eq(0).click() - - $menu = $ '.emoji-menu' - $block = $ '.js-awards-block' - $emoji = $menu.find ".emoji-menu-list-item #{selector}" - - expect($emoji.length).toBe 1 - expect($block.find(selector).length).toBe 0 - - $emoji.click() - - expect($menu.hasClass('.is-visible')).toBe no - expect($block.find(selector).length).toBe 1 - - - it 'should add selected emoji to awards block', -> - - openEmojiMenuAndAddEmoji() - - - it 'should remove already selected emoji', -> - - openEmojiMenuAndAddEmoji() - $('.js-add-award').eq(0).click() - - $block = $ '.js-awards-block' - $emoji = $('.emoji-menu').find ".emoji-menu-list-item #{selector}" - - $emoji.click() - expect($block.find(selector).length).toBe 0 diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js new file mode 100644 index 00000000000..78795f7654a --- /dev/null +++ b/spec/javascripts/behaviors/autosize_spec.js @@ -0,0 +1,21 @@ + +/*= require behaviors/autosize */ + +(function() { + describe('Autosize behavior', function() { + var load; + beforeEach(function() { + return fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>'); + }); + it('does not overwrite the resize property', function() { + load(); + return expect($('textarea')).toHaveCss({ + resize: 'vertical' + }); + }); + return load = function() { + return $(document).trigger('page:load'); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee deleted file mode 100644 index 7fc1d19c35f..00000000000 --- a/spec/javascripts/behaviors/autosize_spec.js.coffee +++ /dev/null @@ -1,11 +0,0 @@ -#= require behaviors/autosize - -describe 'Autosize behavior', -> - beforeEach -> - fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>') - - it 'does not overwrite the resize property', -> - load() - expect($('textarea')).toHaveCss(resize: 'vertical') - - load = -> $(document).trigger('page:load') diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js new file mode 100644 index 00000000000..4c52ecd903d --- /dev/null +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -0,0 +1,93 @@ + +/*= require behaviors/quick_submit */ + +(function() { + describe('Quick Submit behavior', function() { + var keydownEvent; + fixture.preload('behaviors/quick_submit.html'); + beforeEach(function() { + fixture.load('behaviors/quick_submit.html'); + $('form').submit(function(e) { + return e.preventDefault(); + }); + return this.spies = { + submit: spyOnEvent('form', 'submit') + }; + }); + it('does not respond to other keyCodes', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + keyCode: 32 + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to Enter alone', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: false, + metaKey: false + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('does not respond to repeated events', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + repeat: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + it('disables submit buttons', function() { + $('textarea').trigger(keydownEvent()); + expect($('input[type=submit]')).toBeDisabled(); + return expect($('button[type=submit]')).toBeDisabled(); + }); + if (navigator.userAgent.match(/Macintosh/)) { + it('responds to Meta+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + ctrlKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } else { + it('responds to Ctrl+Enter', function() { + $('input.quick-submit-input').trigger(keydownEvent()); + return expect(this.spies.submit).toHaveBeenTriggered(); + }); + it('excludes other modifier keys', function() { + $('input.quick-submit-input').trigger(keydownEvent({ + altKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + metaKey: true + })); + $('input.quick-submit-input').trigger(keydownEvent({ + shiftKey: true + })); + return expect(this.spies.submit).not.toHaveBeenTriggered(); + }); + } + return keydownEvent = function(options) { + var defaults; + if (navigator.userAgent.match(/Macintosh/)) { + defaults = { + keyCode: 13, + metaKey: true + }; + } else { + defaults = { + keyCode: 13, + ctrlKey: true + }; + } + return $.Event('keydown', $.extend({}, defaults, options)); + }; + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/quick_submit_spec.js.coffee b/spec/javascripts/behaviors/quick_submit_spec.js.coffee deleted file mode 100644 index d3b003a328a..00000000000 --- a/spec/javascripts/behaviors/quick_submit_spec.js.coffee +++ /dev/null @@ -1,70 +0,0 @@ -#= require behaviors/quick_submit - -describe 'Quick Submit behavior', -> - fixture.preload('behaviors/quick_submit.html') - - beforeEach -> - fixture.load('behaviors/quick_submit.html') - - # Prevent a form submit from moving us off the testing page - $('form').submit (e) -> e.preventDefault() - - @spies = { - submit: spyOnEvent('form', 'submit') - } - - it 'does not respond to other keyCodes', -> - $('input.quick-submit-input').trigger(keydownEvent(keyCode: 32)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to Enter alone', -> - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: false, metaKey: false)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'does not respond to repeated events', -> - $('input.quick-submit-input').trigger(keydownEvent(repeat: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - it 'disables submit buttons', -> - $('textarea').trigger(keydownEvent()) - - expect($('input[type=submit]')).toBeDisabled() - expect($('button[type=submit]')).toBeDisabled() - - # We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll - # only run the tests that apply to the current platform - if navigator.userAgent.match(/Macintosh/) - it 'responds to Meta+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(ctrlKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - else - it 'responds to Ctrl+Enter', -> - $('input.quick-submit-input').trigger(keydownEvent()) - - expect(@spies.submit).toHaveBeenTriggered() - - it 'excludes other modifier keys', -> - $('input.quick-submit-input').trigger(keydownEvent(altKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(metaKey: true)) - $('input.quick-submit-input').trigger(keydownEvent(shiftKey: true)) - - expect(@spies.submit).not.toHaveBeenTriggered() - - keydownEvent = (options) -> - if navigator.userAgent.match(/Macintosh/) - defaults = { keyCode: 13, metaKey: true } - else - defaults = { keyCode: 13, ctrlKey: true } - - $.Event('keydown', $.extend({}, defaults, options)) diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js new file mode 100644 index 00000000000..724c3baf989 --- /dev/null +++ b/spec/javascripts/behaviors/requires_input_spec.js @@ -0,0 +1,44 @@ + +/*= require behaviors/requires_input */ + +(function() { + describe('requiresInput', function() { + fixture.preload('behaviors/requires_input.html'); + beforeEach(function() { + return fixture.load('behaviors/requires_input.html'); + }); + it('disables submit when any field is required', function() { + $('.js-requires-input').requiresInput(); + return expect($('.submit')).toBeDisabled(); + }); + it('enables submit when no field is required', function() { + $('*[required=required]').removeAttr('required'); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields are pre-filled', function() { + $('*[required=required]').remove(); + $('.js-requires-input').requiresInput(); + return expect($('.submit')).not.toBeDisabled(); + }); + it('enables submit when all required fields receive input', function() { + $('.js-requires-input').requiresInput(); + $('#required1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#optional1').val('input1').change(); + expect($('.submit')).toBeDisabled(); + $('#required2').val('input2').change(); + $('#required3').val('input3').change(); + $('#required4').val('input4').change(); + $('#required5').val('1').change(); + return expect($('.submit')).not.toBeDisabled(); + }); + return it('is called on page:load event', function() { + var spy; + spy = spyOn($.fn, 'requiresInput'); + $(document).trigger('page:load'); + return expect(spy).toHaveBeenCalled(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/behaviors/requires_input_spec.js.coffee b/spec/javascripts/behaviors/requires_input_spec.js.coffee deleted file mode 100644 index 61a17632173..00000000000 --- a/spec/javascripts/behaviors/requires_input_spec.js.coffee +++ /dev/null @@ -1,49 +0,0 @@ -#= require behaviors/requires_input - -describe 'requiresInput', -> - fixture.preload('behaviors/requires_input.html') - - beforeEach -> - fixture.load('behaviors/requires_input.html') - - it 'disables submit when any field is required', -> - $('.js-requires-input').requiresInput() - - expect($('.submit')).toBeDisabled() - - it 'enables submit when no field is required', -> - $('*[required=required]').removeAttr('required') - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields are pre-filled', -> - $('*[required=required]').remove() - - $('.js-requires-input').requiresInput() - - expect($('.submit')).not.toBeDisabled() - - it 'enables submit when all required fields receive input', -> - $('.js-requires-input').requiresInput() - - $('#required1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#optional1').val('input1').change() - expect($('.submit')).toBeDisabled() - - $('#required2').val('input2').change() - $('#required3').val('input3').change() - $('#required4').val('input4').change() - $('#required5').val('1').change() - - expect($('.submit')).not.toBeDisabled() - - it 'is called on page:load event', -> - spy = spyOn($.fn, 'requiresInput') - - $(document).trigger('page:load') - - expect(spy).toHaveBeenCalled() diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js new file mode 100644 index 00000000000..eced2f6575d --- /dev/null +++ b/spec/javascripts/extensions/array_spec.js @@ -0,0 +1,22 @@ + +/*= require extensions/array */ + +(function() { + describe('Array extensions', function() { + describe('first', function() { + return it('returns the first item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.first()).toBe(0); + }); + }); + return describe('last', function() { + return it('returns the last item', function() { + var arr; + arr = [0, 1, 2, 3, 4, 5]; + return expect(arr.last()).toBe(5); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/array_spec.js.coffee b/spec/javascripts/extensions/array_spec.js.coffee deleted file mode 100644 index 4ceac619422..00000000000 --- a/spec/javascripts/extensions/array_spec.js.coffee +++ /dev/null @@ -1,12 +0,0 @@ -#= require extensions/array - -describe 'Array extensions', -> - describe 'first', -> - it 'returns the first item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.first()).toBe(0) - - describe 'last', -> - it 'returns the last item', -> - arr = [0, 1, 2, 3, 4, 5] - expect(arr.last()).toBe(5) diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js new file mode 100644 index 00000000000..b644344b95a --- /dev/null +++ b/spec/javascripts/extensions/jquery_spec.js @@ -0,0 +1,42 @@ + +/*= require extensions/jquery */ + +(function() { + describe('jQuery extensions', function() { + describe('disable', function() { + beforeEach(function() { + return fixture.set('<input type="text" />'); + }); + it('adds the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveAttr('disabled', 'disabled'); + }); + return it('adds the disabled class', function() { + var $input; + $input = $('input').first(); + $input.disable(); + return expect($input).toHaveClass('disabled'); + }); + }); + return describe('enable', function() { + beforeEach(function() { + return fixture.set('<input type="text" disabled="disabled" class="disabled" />'); + }); + it('removes the disabled attribute', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveAttr('disabled'); + }); + return it('removes the disabled class', function() { + var $input; + $input = $('input').first(); + $input.enable(); + return expect($input).not.toHaveClass('disabled'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/extensions/jquery_spec.js.coffee b/spec/javascripts/extensions/jquery_spec.js.coffee deleted file mode 100644 index b10e16b7d01..00000000000 --- a/spec/javascripts/extensions/jquery_spec.js.coffee +++ /dev/null @@ -1,34 +0,0 @@ -#= require extensions/jquery - -describe 'jQuery extensions', -> - describe 'disable', -> - beforeEach -> - fixture.set '<input type="text" />' - - it 'adds the disabled attribute', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveAttr('disabled', 'disabled') - - it 'adds the disabled class', -> - $input = $('input').first() - - $input.disable() - expect($input).toHaveClass('disabled') - - describe 'enable', -> - beforeEach -> - fixture.set '<input type="text" disabled="disabled" class="disabled" />' - - it 'removes the disabled attribute', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveAttr('disabled') - - it 'removes the disabled class', -> - $input = $('input').first() - - $input.enable() - expect($input).not.toHaveClass('disabled') diff --git a/spec/javascripts/fixtures/emoji_menu.coffee b/spec/javascripts/fixtures/emoji_menu.coffee deleted file mode 100644 index ce1a41390d2..00000000000 --- a/spec/javascripts/fixtures/emoji_menu.coffee +++ /dev/null @@ -1,957 +0,0 @@ -window.emojiMenu = """ - <div class='emoji-menu'> - <input type="text" name="emoji_search" id="emoji_search" value="" class="emoji-search search-input form-control" /> - <div class='emoji-menu-content'> - <h5 class='emoji-menu-title'> - Emoticons - </h5> - <ul class='clearfix emoji-menu-list'> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47D" title="alien" data-aliases="" data-emoji="alien" data-unicode-name="1F47D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47C" title="angel" data-aliases="" data-emoji="angel" data-unicode-name="1F47C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A2" title="anger" data-aliases="" data-emoji="anger" data-unicode-name="1F4A2"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F620" title="angry" data-aliases="" data-emoji="angry" data-unicode-name="1F620"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F627" title="anguished" data-aliases="" data-emoji="anguished" data-unicode-name="1F627"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F632" title="astonished" data-aliases="" data-emoji="astonished" data-unicode-name="1F632"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45F" title="athletic_shoe" data-aliases="" data-emoji="athletic_shoe" data-unicode-name="1F45F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F476" title="baby" data-aliases="" data-emoji="baby" data-unicode-name="1F476"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F459" title="bikini" data-aliases="" data-emoji="bikini" data-unicode-name="1F459"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F499" title="blue_heart" data-aliases="" data-emoji="blue_heart" data-unicode-name="1F499"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60A" title="blush" data-aliases="" data-emoji="blush" data-unicode-name="1F60A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A5" title="boom" data-aliases="" data-emoji="boom" data-unicode-name="1F4A5"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F462" title="boot" data-aliases="" data-emoji="boot" data-unicode-name="1F462"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F647" title="bow" data-aliases="" data-emoji="bow" data-unicode-name="1F647"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F466" title="boy" data-aliases="" data-emoji="boy" data-unicode-name="1F466"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F470" title="bride_with_veil" data-aliases="" data-emoji="bride_with_veil" data-unicode-name="1F470"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4BC" title="briefcase" data-aliases="" data-emoji="briefcase" data-unicode-name="1F4BC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F494" title="broken_heart" data-aliases="" data-emoji="broken_heart" data-unicode-name="1F494"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F464" title="bust_in_silhouette" data-aliases="" data-emoji="bust_in_silhouette" data-unicode-name="1F464"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F465" title="busts_in_silhouette" data-aliases="" data-emoji="busts_in_silhouette" data-unicode-name="1F465"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44F" title="clap" data-aliases="" data-emoji="clap" data-unicode-name="1F44F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F302" title="closed_umbrella" data-aliases="" data-emoji="closed_umbrella" data-unicode-name="1F302"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F630" title="cold_sweat" data-aliases="" data-emoji="cold_sweat" data-unicode-name="1F630"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F616" title="confounded" data-aliases="" data-emoji="confounded" data-unicode-name="1F616"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F615" title="confused" data-aliases="" data-emoji="confused" data-unicode-name="1F615"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F477" title="construction_worker" data-aliases="" data-emoji="construction_worker" data-unicode-name="1F477"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46E" title="cop" data-aliases="" data-emoji="cop" data-unicode-name="1F46E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46B" title="couple" data-aliases="" data-emoji="couple" data-unicode-name="1F46B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F491" title="couple_with_heart" data-aliases="" data-emoji="couple_with_heart" data-unicode-name="1F491"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48F" title="couplekiss" data-aliases="" data-emoji="couplekiss" data-unicode-name="1F48F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F451" title="crown" data-aliases="" data-emoji="crown" data-unicode-name="1F451"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F622" title="cry" data-aliases="" data-emoji="cry" data-unicode-name="1F622"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63F" title="crying_cat_face" data-aliases="" data-emoji="crying_cat_face" data-unicode-name="1F63F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F498" title="cupid" data-aliases="" data-emoji="cupid" data-unicode-name="1F498"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F483" title="dancer" data-aliases="" data-emoji="dancer" data-unicode-name="1F483"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46F" title="dancers" data-aliases="" data-emoji="dancers" data-unicode-name="1F46F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A8" title="dash" data-aliases="" data-emoji="dash" data-unicode-name="1F4A8"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61E" title="disappointed" data-aliases="" data-emoji="disappointed" data-unicode-name="1F61E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F625" title="disappointed_relieved" data-aliases="" data-emoji="disappointed_relieved" data-unicode-name="1F625"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AB" title="dizzy" data-aliases="" data-emoji="dizzy" data-unicode-name="1F4AB"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F635" title="dizzy_face" data-aliases="" data-emoji="dizzy_face" data-unicode-name="1F635"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F457" title="dress" data-aliases="" data-emoji="dress" data-unicode-name="1F457"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A7" title="droplet" data-aliases="" data-emoji="droplet" data-unicode-name="1F4A7"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F442" title="ear" data-aliases="" data-emoji="ear" data-unicode-name="1F442"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F611" title="expressionless" data-aliases="" data-emoji="expressionless" data-unicode-name="1F611"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F453" title="eyeglasses" data-aliases="" data-emoji="eyeglasses" data-unicode-name="1F453"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F440" title="eyes" data-aliases="" data-emoji="eyes" data-unicode-name="1F440"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46A" title="family" data-aliases="" data-emoji="family" data-unicode-name="1F46A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F628" title="fearful" data-aliases="" data-emoji="fearful" data-unicode-name="1F628"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F525" title="fire" data-aliases=":flame:" data-emoji="fire" data-unicode-name="1F525"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270A" title="fist" data-aliases="" data-emoji="fist" data-unicode-name="270A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F633" title="flushed" data-aliases="" data-emoji="flushed" data-unicode-name="1F633"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F463" title="footprints" data-aliases="" data-emoji="footprints" data-unicode-name="1F463"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F626" title="frowning" data-aliases=":anguished:" data-emoji="frowning" data-unicode-name="1F626"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48E" title="gem" data-aliases="" data-emoji="gem" data-unicode-name="1F48E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F467" title="girl" data-aliases="" data-emoji="girl" data-unicode-name="1F467"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49A" title="green_heart" data-aliases="" data-emoji="green_heart" data-unicode-name="1F49A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62C" title="grimacing" data-aliases="" data-emoji="grimacing" data-unicode-name="1F62C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F601" title="grin" data-aliases="" data-emoji="grin" data-unicode-name="1F601"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F600" title="grinning" data-aliases="" data-emoji="grinning" data-unicode-name="1F600"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F482" title="guardsman" data-aliases="" data-emoji="guardsman" data-unicode-name="1F482"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F487" title="haircut" data-aliases="" data-emoji="haircut" data-unicode-name="1F487"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45C" title="handbag" data-aliases="" data-emoji="handbag" data-unicode-name="1F45C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F649" title="hear_no_evil" data-aliases="" data-emoji="hear_no_evil" data-unicode-name="1F649"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2764" title="heart" data-aliases="" data-emoji="heart" data-unicode-name="2764"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60D" title="heart_eyes" data-aliases="" data-emoji="heart_eyes" data-unicode-name="1F60D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63B" title="heart_eyes_cat" data-aliases="" data-emoji="heart_eyes_cat" data-unicode-name="1F63B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F493" title="heartbeat" data-aliases="" data-emoji="heartbeat" data-unicode-name="1F493"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F497" title="heartpulse" data-aliases="" data-emoji="heartpulse" data-unicode-name="1F497"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F460" title="high_heel" data-aliases="" data-emoji="high_heel" data-unicode-name="1F460"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62F" title="hushed" data-aliases="" data-emoji="hushed" data-unicode-name="1F62F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47F" title="imp" data-aliases="" data-emoji="imp" data-unicode-name="1F47F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F481" title="information_desk_person" data-aliases="" data-emoji="information_desk_person" data-unicode-name="1F481"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F607" title="innocent" data-aliases="" data-emoji="innocent" data-unicode-name="1F607"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F47A" title="japanese_goblin" data-aliases="" data-emoji="japanese_goblin" data-unicode-name="1F47A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F479" title="japanese_ogre" data-aliases="" data-emoji="japanese_ogre" data-unicode-name="1F479"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F456" title="jeans" data-aliases="" data-emoji="jeans" data-unicode-name="1F456"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F602" title="joy" data-aliases="" data-emoji="joy" data-unicode-name="1F602"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F639" title="joy_cat" data-aliases="" data-emoji="joy_cat" data-unicode-name="1F639"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F458" title="kimono" data-aliases="" data-emoji="kimono" data-unicode-name="1F458"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48B" title="kiss" data-aliases="" data-emoji="kiss" data-unicode-name="1F48B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F617" title="kissing" data-aliases="" data-emoji="kissing" data-unicode-name="1F617"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63D" title="kissing_cat" data-aliases="" data-emoji="kissing_cat" data-unicode-name="1F63D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61A" title="kissing_closed_eyes" data-aliases="" data-emoji="kissing_closed_eyes" data-unicode-name="1F61A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F618" title="kissing_heart" data-aliases="" data-emoji="kissing_heart" data-unicode-name="1F618"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F619" title="kissing_smiling_eyes" data-aliases="" data-emoji="kissing_smiling_eyes" data-unicode-name="1F619"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F606" title="laughing" data-aliases=":satisfied:" data-emoji="laughing" data-unicode-name="1F606"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F444" title="lips" data-aliases="" data-emoji="lips" data-unicode-name="1F444"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F484" title="lipstick" data-aliases="" data-emoji="lipstick" data-unicode-name="1F484"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48C" title="love_letter" data-aliases="" data-emoji="love_letter" data-unicode-name="1F48C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F468" title="man" data-aliases="" data-emoji="man" data-unicode-name="1F468"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F472" title="man_with_gua_pi_mao" data-aliases="" data-emoji="man_with_gua_pi_mao" data-unicode-name="1F472"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F473" title="man_with_turban" data-aliases="" data-emoji="man_with_turban" data-unicode-name="1F473"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45E" title="mans_shoe" data-aliases="" data-emoji="mans_shoe" data-unicode-name="1F45E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F637" title="mask" data-aliases="" data-emoji="mask" data-unicode-name="1F637"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F486" title="massage" data-aliases="" data-emoji="massage" data-unicode-name="1F486"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AA" title="muscle" data-aliases="" data-emoji="muscle" data-unicode-name="1F4AA"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F485" title="nail_care" data-aliases="" data-emoji="nail_care" data-unicode-name="1F485"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F454" title="necktie" data-aliases="" data-emoji="necktie" data-unicode-name="1F454"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F610" title="neutral_face" data-aliases="" data-emoji="neutral_face" data-unicode-name="1F610"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F645" title="no_good" data-aliases="" data-emoji="no_good" data-unicode-name="1F645"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F636" title="no_mouth" data-aliases="" data-emoji="no_mouth" data-unicode-name="1F636"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F443" title="nose" data-aliases="" data-emoji="nose" data-unicode-name="1F443"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44C" title="ok_hand" data-aliases="" data-emoji="ok_hand" data-unicode-name="1F44C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F646" title="ok_woman" data-aliases="" data-emoji="ok_woman" data-unicode-name="1F646"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F474" title="older_man" data-aliases="" data-emoji="older_man" data-unicode-name="1F474"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F475" title="older_woman" data-aliases=":grandma:" data-emoji="older_woman" data-unicode-name="1F475"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F450" title="open_hands" data-aliases="" data-emoji="open_hands" data-unicode-name="1F450"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62E" title="open_mouth" data-aliases="" data-emoji="open_mouth" data-unicode-name="1F62E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F614" title="pensive" data-aliases="" data-emoji="pensive" data-unicode-name="1F614"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F623" title="persevere" data-aliases="" data-emoji="persevere" data-unicode-name="1F623"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64D" title="person_frowning" data-aliases="" data-emoji="person_frowning" data-unicode-name="1F64D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F471" title="person_with_blond_hair" data-aliases="" data-emoji="person_with_blond_hair" data-unicode-name="1F471"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64E" title="person_with_pouting_face" data-aliases="" data-emoji="person_with_pouting_face" data-unicode-name="1F64E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F447" title="point_down" data-aliases="" data-emoji="point_down" data-unicode-name="1F447"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F448" title="point_left" data-aliases="" data-emoji="point_left" data-unicode-name="1F448"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F449" title="point_right" data-aliases="" data-emoji="point_right" data-unicode-name="1F449"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-261D" title="point_up" data-aliases="" data-emoji="point_up" data-unicode-name="261D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F446" title="point_up_2" data-aliases="" data-emoji="point_up_2" data-unicode-name="1F446"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A9" title="poop" data-aliases=":shit: :hankey: :poo:" data-emoji="poop" data-unicode-name="1F4A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45D" title="pouch" data-aliases="" data-emoji="pouch" data-unicode-name="1F45D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63E" title="pouting_cat" data-aliases="" data-emoji="pouting_cat" data-unicode-name="1F63E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64F" title="pray" data-aliases="" data-emoji="pray" data-unicode-name="1F64F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F478" title="princess" data-aliases="" data-emoji="princess" data-unicode-name="1F478"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44A" title="punch" data-aliases="" data-emoji="punch" data-unicode-name="1F44A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49C" title="purple_heart" data-aliases="" data-emoji="purple_heart" data-unicode-name="1F49C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45B" title="purse" data-aliases="" data-emoji="purse" data-unicode-name="1F45B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F621" title="rage" data-aliases="" data-emoji="rage" data-unicode-name="1F621"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270B" title="raised_hand" data-aliases="" data-emoji="raised_hand" data-unicode-name="270B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64C" title="raised_hands" data-aliases="" data-emoji="raised_hands" data-unicode-name="1F64C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64B" title="raising_hand" data-aliases="" data-emoji="raising_hand" data-unicode-name="1F64B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-263A" title="relaxed" data-aliases="" data-emoji="relaxed" data-unicode-name="263A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60C" title="relieved" data-aliases="" data-emoji="relieved" data-unicode-name="1F60C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49E" title="revolving_hearts" data-aliases="" data-emoji="revolving_hearts" data-unicode-name="1F49E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F380" title="ribbon" data-aliases="" data-emoji="ribbon" data-unicode-name="1F380"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F48D" title="ring" data-aliases="" data-emoji="ring" data-unicode-name="1F48D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3C3" title="runner" data-aliases="" data-emoji="runner" data-unicode-name="1F3C3"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3BD" title="running_shirt_with_sash" data-aliases="" data-emoji="running_shirt_with_sash" data-unicode-name="1F3BD"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F461" title="sandal" data-aliases="" data-emoji="sandal" data-unicode-name="1F461"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F631" title="scream" data-aliases="" data-emoji="scream" data-unicode-name="1F631"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F640" title="scream_cat" data-aliases="" data-emoji="scream_cat" data-unicode-name="1F640"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F648" title="see_no_evil" data-aliases="" data-emoji="see_no_evil" data-unicode-name="1F648"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F455" title="shirt" data-aliases="" data-emoji="shirt" data-unicode-name="1F455"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F480" title="skull" data-aliases=":skeleton:" data-emoji="skull" data-unicode-name="1F480"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F634" title="sleeping" data-aliases="" data-emoji="sleeping" data-unicode-name="1F634"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62A" title="sleepy" data-aliases="" data-emoji="sleepy" data-unicode-name="1F62A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F604" title="smile" data-aliases="" data-emoji="smile" data-unicode-name="1F604"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F638" title="smile_cat" data-aliases="" data-emoji="smile_cat" data-unicode-name="1F638"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F603" title="smiley" data-aliases="" data-emoji="smiley" data-unicode-name="1F603"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63A" title="smiley_cat" data-aliases="" data-emoji="smiley_cat" data-unicode-name="1F63A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F608" title="smiling_imp" data-aliases="" data-emoji="smiling_imp" data-unicode-name="1F608"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60F" title="smirk" data-aliases="" data-emoji="smirk" data-unicode-name="1F60F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F63C" title="smirk_cat" data-aliases="" data-emoji="smirk_cat" data-unicode-name="1F63C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62D" title="sob" data-aliases="" data-emoji="sob" data-unicode-name="1F62D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-2728" title="sparkles" data-aliases="" data-emoji="sparkles" data-unicode-name="2728"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F496" title="sparkling_heart" data-aliases="" data-emoji="sparkling_heart" data-unicode-name="1F496"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F64A" title="speak_no_evil" data-aliases="" data-emoji="speak_no_evil" data-unicode-name="1F64A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AC" title="speech_balloon" data-aliases="" data-emoji="speech_balloon" data-unicode-name="1F4AC"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F31F" title="star2" data-aliases="" data-emoji="star2" data-unicode-name="1F31F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61B" title="stuck_out_tongue" data-aliases="" data-emoji="stuck_out_tongue" data-unicode-name="1F61B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61D" title="stuck_out_tongue_closed_eyes" data-aliases="" data-emoji="stuck_out_tongue_closed_eyes" data-unicode-name="1F61D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61C" title="stuck_out_tongue_winking_eye" data-aliases="" data-emoji="stuck_out_tongue_winking_eye" data-unicode-name="1F61C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60E" title="sunglasses" data-aliases="" data-emoji="sunglasses" data-unicode-name="1F60E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F613" title="sweat" data-aliases="" data-emoji="sweat" data-unicode-name="1F613"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A6" title="sweat_drops" data-aliases="" data-emoji="sweat_drops" data-unicode-name="1F4A6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F605" title="sweat_smile" data-aliases="" data-emoji="sweat_smile" data-unicode-name="1F605"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4AD" title="thought_balloon" data-aliases="" data-emoji="thought_balloon" data-unicode-name="1F4AD"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44E" title="thumbsdown" data-aliases=":-1:" data-emoji="thumbsdown" data-unicode-name="1F44E"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44D" title="thumbsup" data-aliases=":+1:" data-emoji="thumbsup" data-unicode-name="1F44D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F62B" title="tired_face" data-aliases="" data-emoji="tired_face" data-unicode-name="1F62B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F445" title="tongue" data-aliases="" data-emoji="tongue" data-unicode-name="1F445"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F3A9" title="tophat" data-aliases="" data-emoji="tophat" data-unicode-name="1F3A9"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F624" title="triumph" data-aliases="" data-emoji="triumph" data-unicode-name="1F624"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F495" title="two_hearts" data-aliases="" data-emoji="two_hearts" data-unicode-name="1F495"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46C" title="two_men_holding_hands" data-aliases="" data-emoji="two_men_holding_hands" data-unicode-name="1F46C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F46D" title="two_women_holding_hands" data-aliases="" data-emoji="two_women_holding_hands" data-unicode-name="1F46D"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F612" title="unamused" data-aliases="" data-emoji="unamused" data-unicode-name="1F612"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-270C" title="v" data-aliases="" data-emoji="v" data-unicode-name="270C"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F6B6" title="walking" data-aliases="" data-emoji="walking" data-unicode-name="1F6B6"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F44B" title="wave" data-aliases="" data-emoji="wave" data-unicode-name="1F44B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F629" title="weary" data-aliases="" data-emoji="weary" data-unicode-name="1F629"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F609" title="wink" data-aliases="" data-emoji="wink" data-unicode-name="1F609"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F469" title="woman" data-aliases="" data-emoji="woman" data-unicode-name="1F469"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F45A" title="womans_clothes" data-aliases="" data-emoji="womans_clothes" data-unicode-name="1F45A"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F452" title="womans_hat" data-aliases="" data-emoji="womans_hat" data-unicode-name="1F452"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F61F" title="worried" data-aliases="" data-emoji="worried" data-unicode-name="1F61F"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F49B" title="yellow_heart" data-aliases="" data-emoji="yellow_heart" data-unicode-name="1F49B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F60B" title="yum" data-aliases="" data-emoji="yum" data-unicode-name="1F60B"></div> - </button> - </li> - <li class='pull-left text-center emoji-menu-list-item'> - <button class='emoji-menu-btn text-center js-emoji-btn' type='button'> - <div class="icon emoji-icon emoji-1F4A4" title="zzz" data-aliases="" data-emoji="zzz" data-unicode-name="1F4A4"></div> - </button> - </li> - </ul> - </div> - </div> -""" diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js new file mode 100644 index 00000000000..99e3f7247bd --- /dev/null +++ b/spec/javascripts/fixtures/emoji_menu.js @@ -0,0 +1,4 @@ +(function() { + window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>"; + +}).call(this); diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js new file mode 100644 index 00000000000..dc6231ebb38 --- /dev/null +++ b/spec/javascripts/issue_spec.js @@ -0,0 +1,121 @@ + +/*= require lib/utils/text_utility */ + + +/*= require issue */ + +(function() { + describe('Issue', function() { + return describe('task lists', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('/foo'); + return expect(req.data.issue.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + + describe('reopen/close issue', function() { + fixture.preload('issues_show.html'); + beforeEach(function() { + fixture.load('issues_show.html'); + return this.issue = new Issue(); + }); + it('closes an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/close'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeVisible(); + expect($btnClose).toBeHidden(); + expect($('div.status-box-closed')).toBeVisible(); + return expect($('div.status-box-open')).toBeHidden(); + }); + it('fails to close an issue with success:false', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.success({ + saved: false + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + it('fails to closes an issue with HTTP error', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://goesnowhere.nothing/whereami'); + return req.error(); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + $btnClose.attr('href', 'http://goesnowhere.nothing/whereami'); + expect($btnReopen).toBeHidden(); + expect($btnClose.text()).toBe('Close'); + expect(typeof $btnClose.prop('disabled')).toBe('undefined'); + $btnClose.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-closed')).toBeHidden(); + expect($('div.status-box-open')).toBeVisible(); + expect($('div.flash-alert')).toBeVisible(); + return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.'); + }); + return it('reopens an issue', function() { + var $btnClose, $btnReopen; + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PUT'); + expect(req.url).toBe('http://gitlab.com/issues/6/reopen'); + return req.success({ + id: 34 + }); + }); + $btnClose = $('a.btn-close'); + $btnReopen = $('a.btn-reopen'); + expect($btnReopen.text()).toBe('Reopen'); + $btnReopen.trigger('click'); + expect($btnReopen).toBeHidden(); + expect($btnClose).toBeVisible(); + expect($('div.status-box-open')).toBeVisible(); + return expect($('div.status-box-closed')).toBeHidden(); + }); + }); + +}).call(this); diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee deleted file mode 100644 index d84d80f266b..00000000000 --- a/spec/javascripts/issue_spec.js.coffee +++ /dev/null @@ -1,109 +0,0 @@ -#= require lib/utils/text_utility -#= require issue - -describe 'Issue', -> - describe 'task lists', -> - fixture.preload('issues_show.html') - - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.issue.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') -describe 'reopen/close issue', -> - fixture.preload('issues_show.html') - beforeEach -> - fixture.load('issues_show.html') - @issue = new Issue() - it 'closes an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/close') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeVisible() - expect($btnClose).toBeHidden() - expect($('div.status-box-closed')).toBeVisible() - expect($('div.status-box-open')).toBeHidden() - - it 'fails to close an issue with success:false', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.success saved: false - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'fails to closes an issue with HTTP error', -> - - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://goesnowhere.nothing/whereami') - req.error() - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - $btnClose.attr('href','http://goesnowhere.nothing/whereami') - expect($btnReopen).toBeHidden() - expect($btnClose.text()).toBe('Close') - expect(typeof $btnClose.prop('disabled')).toBe('undefined') - - $btnClose.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() - expect($('div.status-box-open')).toBeVisible() - expect($('div.flash-alert')).toBeVisible() - expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') - - it 'reopens an issue', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PUT') - expect(req.url).toBe('http://gitlab.com/issues/6/reopen') - req.success id: 34 - - $btnClose = $('a.btn-close') - $btnReopen = $('a.btn-reopen') - expect($btnReopen.text()).toBe('Reopen') - - $btnReopen.trigger('click') - - expect($btnReopen).toBeHidden() - expect($btnClose).toBeVisible() - expect($('div.status-box-open')).toBeVisible() - expect($('div.status-box-closed')).toBeHidden() diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js new file mode 100644 index 00000000000..e2789571607 --- /dev/null +++ b/spec/javascripts/line_highlighter_spec.js @@ -0,0 +1,229 @@ + +/*= require line_highlighter */ + +(function() { + describe('LineHighlighter', function() { + var clickLine; + fixture.preload('line_highlighter.html'); + clickLine = function(number, eventData) { + var e; + if (eventData == null) { + eventData = {}; + } + if ($.isEmptyObject(eventData)) { + return $("#L" + number).mousedown().click(); + } else { + e = $.Event('mousedown', eventData); + return $("#L" + number).trigger(e).click(); + } + }; + beforeEach(function() { + fixture.load('line_highlighter.html'); + this["class"] = new LineHighlighter(); + this.css = this["class"].highlightClass; + return this.spies = { + __setLocationHash__: spyOn(this["class"], '__setLocationHash__').and.callFake(function() {}) + }; + }); + describe('behavior', function() { + it('highlights one line given in the URL hash', function() { + new LineHighlighter('#L13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('highlights a range of lines given in the URL hash', function() { + var i, line, results; + new LineHighlighter('#L5-25'); + expect($("." + this.css).length).toBe(21); + results = []; + for (line = i = 5; i <= 25; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + it('scrolls to the first highlighted line on initial load', function() { + var spy; + spy = spyOn($, 'scrollTo'); + new LineHighlighter('#L5-25'); + return expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()); + }); + it('discards click events', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'click'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + return it('handles garbage input from the hash', function() { + var func; + func = function() { + return new LineHighlighter('#blob-content-holder'); + }; + return expect(func).not.toThrow(); + }); + }); + describe('#clickHandler', function() { + it('discards the mousedown event', function() { + var spy; + spy = spyOnEvent('a[data-line-number]', 'mousedown'); + clickLine(13); + return expect(spy).toHaveBeenPrevented(); + }); + it('handles clicking on a child icon element', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + $('#L13 i').mousedown().click(); + expect(spy).toHaveBeenCalledWith(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + describe('without shiftKey', function() { + it('highlights one line when clicked', function() { + clickLine(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + it('unhighlights previously highlighted lines', function() { + clickLine(13); + clickLine(20); + expect($('#LC13')).not.toHaveClass(this.css); + return expect($('#LC20')).toHaveClass(this.css); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + return describe('with shiftKey', function() { + it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash').and.callThrough(); + clickLine(13); + clickLine(20, { + shiftKey: true + }); + expect(spy).toHaveBeenCalledWith(13); + return expect(spy).toHaveBeenCalledWith(13, 20); + }); + describe('without existing highlight', function() { + it('highlights the clicked line', function() { + clickLine(13, { + shiftKey: true + }); + expect($('#LC13')).toHaveClass(this.css); + return expect($("." + this.css).length).toBe(1); + }); + return it('sets the hash', function() { + var spy; + spy = spyOn(this["class"], 'setHash'); + clickLine(13, { + shiftKey: true + }); + return expect(spy).toHaveBeenCalledWith(13); + }); + }); + describe('with existing single-line highlight', function() { + it('uses existing line as last line when target is lesser', function() { + var i, line, results; + clickLine(20); + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 15; i <= 20; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses existing line as first line when target is greater', function() { + var i, line, results; + clickLine(5); + clickLine(10, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + return describe('with existing multi-line highlight', function() { + beforeEach(function() { + clickLine(10, { + shiftKey: true + }); + return clickLine(13, { + shiftKey: true + }); + }); + it('uses target as first line when it is less than existing first line', function() { + var i, line, results; + clickLine(5, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 5; i <= 10; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + return it('uses target as last line when it is greater than existing first line', function() { + var i, line, results; + clickLine(15, { + shiftKey: true + }); + expect($("." + this.css).length).toBe(6); + results = []; + for (line = i = 10; i <= 15; line = ++i) { + results.push(expect($("#LC" + line)).toHaveClass(this.css)); + } + return results; + }); + }); + }); + }); + describe('#hashToRange', function() { + beforeEach(function() { + return this.subject = this["class"].hashToRange; + }); + it('extracts a single line number from the hash', function() { + return expect(this.subject('#L5')).toEqual([5, null]); + }); + it('extracts a range of line numbers from the hash', function() { + return expect(this.subject('#L5-15')).toEqual([5, 15]); + }); + return it('returns [null, null] when the hash is not a line number', function() { + return expect(this.subject('#foo')).toEqual([null, null]); + }); + }); + describe('#highlightLine', function() { + beforeEach(function() { + return this.subject = this["class"].highlightLine; + }); + it('highlights the specified line', function() { + this.subject(13); + return expect($('#LC13')).toHaveClass(this.css); + }); + return it('accepts a String-based number', function() { + this.subject('13'); + return expect($('#LC13')).toHaveClass(this.css); + }); + }); + return describe('#setHash', function() { + beforeEach(function() { + return this.subject = this["class"].setHash; + }); + it('sets the location hash for a single line', function() { + this.subject(5); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5'); + }); + return it('sets the location hash for a range', function() { + this.subject(5, 15); + return expect(this.spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/line_highlighter_spec.js.coffee b/spec/javascripts/line_highlighter_spec.js.coffee deleted file mode 100644 index a073f21e7bc..00000000000 --- a/spec/javascripts/line_highlighter_spec.js.coffee +++ /dev/null @@ -1,158 +0,0 @@ -#= require line_highlighter - -describe 'LineHighlighter', -> - fixture.preload('line_highlighter.html') - - clickLine = (number, eventData = {}) -> - if $.isEmptyObject(eventData) - $("#L#{number}").mousedown().click() - else - e = $.Event 'mousedown', eventData - $("#L#{number}").trigger(e).click() - - beforeEach -> - fixture.load('line_highlighter.html') - @class = new LineHighlighter() - @css = @class.highlightClass - @spies = { - __setLocationHash__: spyOn(@class, '__setLocationHash__').and.callFake -> - } - - describe 'behavior', -> - it 'highlights one line given in the URL hash', -> - new LineHighlighter('#L13') - expect($('#LC13')).toHaveClass(@css) - - it 'highlights a range of lines given in the URL hash', -> - new LineHighlighter('#L5-25') - expect($(".#{@css}").length).toBe(21) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..25] - - it 'scrolls to the first highlighted line on initial load', -> - spy = spyOn($, 'scrollTo') - new LineHighlighter('#L5-25') - expect(spy).toHaveBeenCalledWith('#L5', jasmine.anything()) - - it 'discards click events', -> - spy = spyOnEvent('a[data-line-number]', 'click') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles garbage input from the hash', -> - func = -> new LineHighlighter('#blob-content-holder') - expect(func).not.toThrow() - - describe '#clickHandler', -> - it 'discards the mousedown event', -> - spy = spyOnEvent('a[data-line-number]', 'mousedown') - clickLine(13) - expect(spy).toHaveBeenPrevented() - - it 'handles clicking on a child icon element', -> - spy = spyOn(@class, 'setHash').and.callThrough() - - $('#L13 i').mousedown().click() - - expect(spy).toHaveBeenCalledWith(13) - expect($('#LC13')).toHaveClass(@css) - - describe 'without shiftKey', -> - it 'highlights one line when clicked', -> - clickLine(13) - expect($('#LC13')).toHaveClass(@css) - - it 'unhighlights previously highlighted lines', -> - clickLine(13) - clickLine(20) - - expect($('#LC13')).not.toHaveClass(@css) - expect($('#LC20')).toHaveClass(@css) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with shiftKey', -> - it 'sets the hash', -> - spy = spyOn(@class, 'setHash').and.callThrough() - clickLine(13) - clickLine(20, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - expect(spy).toHaveBeenCalledWith(13, 20) - - describe 'without existing highlight', -> - it 'highlights the clicked line', -> - clickLine(13, shiftKey: true) - expect($('#LC13')).toHaveClass(@css) - expect($(".#{@css}").length).toBe(1) - - it 'sets the hash', -> - spy = spyOn(@class, 'setHash') - clickLine(13, shiftKey: true) - expect(spy).toHaveBeenCalledWith(13) - - describe 'with existing single-line highlight', -> - it 'uses existing line as last line when target is lesser', -> - clickLine(20) - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [15..20] - - it 'uses existing line as first line when target is greater', -> - clickLine(5) - clickLine(10, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - describe 'with existing multi-line highlight', -> - beforeEach -> - clickLine(10, shiftKey: true) - clickLine(13, shiftKey: true) - - it 'uses target as first line when it is less than existing first line', -> - clickLine(5, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [5..10] - - it 'uses target as last line when it is greater than existing first line', -> - clickLine(15, shiftKey: true) - expect($(".#{@css}").length).toBe(6) - expect($("#LC#{line}")).toHaveClass(@css) for line in [10..15] - - describe '#hashToRange', -> - beforeEach -> - @subject = @class.hashToRange - - it 'extracts a single line number from the hash', -> - expect(@subject('#L5')).toEqual([5, null]) - - it 'extracts a range of line numbers from the hash', -> - expect(@subject('#L5-15')).toEqual([5, 15]) - - it 'returns [null, null] when the hash is not a line number', -> - expect(@subject('#foo')).toEqual([null, null]) - - describe '#highlightLine', -> - beforeEach -> - @subject = @class.highlightLine - - it 'highlights the specified line', -> - @subject(13) - expect($('#LC13')).toHaveClass(@css) - - it 'accepts a String-based number', -> - @subject('13') - expect($('#LC13')).toHaveClass(@css) - - describe '#setHash', -> - beforeEach -> - @subject = @class.setHash - - it 'sets the location hash for a single line', -> - @subject(5) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5') - - it 'sets the location hash for a range', -> - @subject(5, 15) - expect(@spies.__setLocationHash__).toHaveBeenCalledWith('#L5-15') diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js new file mode 100644 index 00000000000..61830d267a9 --- /dev/null +++ b/spec/javascripts/merge_request_spec.js @@ -0,0 +1,28 @@ + +/*= require merge_request */ + +(function() { + describe('MergeRequest', function() { + return describe('task lists', function() { + fixture.preload('merge_requests_show.html'); + beforeEach(function() { + fixture.load('merge_requests_show.html'); + return this.merge = new MergeRequest(); + }); + it('modifies the Markdown field', function() { + spyOn(jQuery, 'ajax').and.stub(); + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits an ajax request on tasklist:changed', function() { + spyOn(jQuery, 'ajax').and.callFake(function(req) { + expect(req.type).toBe('PATCH'); + expect(req.url).toBe('/foo'); + return expect(req.data.merge_request.description).not.toBe(null); + }); + return $('.js-task-list-field').trigger('tasklist:changed'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_spec.js.coffee b/spec/javascripts/merge_request_spec.js.coffee deleted file mode 100644 index 3cb67d51c85..00000000000 --- a/spec/javascripts/merge_request_spec.js.coffee +++ /dev/null @@ -1,23 +0,0 @@ -#= require merge_request - -describe 'MergeRequest', -> - describe 'task lists', -> - fixture.preload('merge_requests_show.html') - - beforeEach -> - fixture.load('merge_requests_show.html') - @merge = new MergeRequest() - - it 'modifies the Markdown field', -> - spyOn(jQuery, 'ajax').and.stub() - - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits an ajax request on tasklist:changed', -> - spyOn(jQuery, 'ajax').and.callFake (req) -> - expect(req.type).toBe('PATCH') - expect(req.url).toBe('/foo') - expect(req.data.merge_request.description).not.toBe(null) - - $('.js-task-list-field').trigger('tasklist:changed') diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js new file mode 100644 index 00000000000..395032a7416 --- /dev/null +++ b/spec/javascripts/merge_request_tabs_spec.js @@ -0,0 +1,106 @@ + +/*= require merge_request_tabs */ + +(function() { + describe('MergeRequestTabs', function() { + var stubLocation; + stubLocation = function(stubs) { + var defaults; + defaults = { + pathname: '', + search: '', + hash: '' + }; + return $.extend(defaults, stubs); + }; + fixture.preload('merge_request_tabs.html'); + beforeEach(function() { + this["class"] = new MergeRequestTabs(); + return this.spies = { + ajax: spyOn($, 'ajax').and.callFake(function() {}), + history: spyOn(history, 'replaceState').and.callFake(function() {}) + }; + }); + describe('#activateTab', function() { + beforeEach(function() { + fixture.load('merge_request_tabs.html'); + return this.subject = this["class"].activateTab; + }); + it('shows the first tab when action is show', function() { + this.subject('show'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the notes tab when action is notes', function() { + this.subject('notes'); + return expect($('#notes')).toHaveClass('active'); + }); + it('shows the commits tab when action is commits', function() { + this.subject('commits'); + return expect($('#commits')).toHaveClass('active'); + }); + return it('shows the diffs tab when action is diffs', function() { + this.subject('diffs'); + return expect($('#diffs')).toHaveClass('active'); + }); + }); + return describe('#setCurrentAction', function() { + beforeEach(function() { + return this.subject = this["class"].setCurrentAction; + }); + it('changes from commits', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + }); + it('changes from diffs', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from diffs.html', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs.html' + }); + expect(this.subject('notes')).toBe('/foo/bar/merge_requests/1'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('changes from notes', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + expect(this.subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs'); + return expect(this.subject('commits')).toBe('/foo/bar/merge_requests/1/commits'); + }); + it('includes search parameters and hash string', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/diffs', + search: '?view=parallel', + hash: '#L15-35' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35'); + }); + it('replaces the current history state', function() { + var new_state; + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1' + }); + new_state = this.subject('commits'); + return expect(this.spies.history).toHaveBeenCalledWith({ + turbolinks: true, + url: new_state + }, document.title, new_state); + }); + return it('treats "show" like "notes"', function() { + this["class"]._location = stubLocation({ + pathname: '/foo/bar/merge_requests/1/commits' + }); + return expect(this.subject('show')).toBe('/foo/bar/merge_requests/1'); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_tabs_spec.js.coffee b/spec/javascripts/merge_request_tabs_spec.js.coffee deleted file mode 100644 index a0cfba455ea..00000000000 --- a/spec/javascripts/merge_request_tabs_spec.js.coffee +++ /dev/null @@ -1,88 +0,0 @@ -#= require merge_request_tabs - -describe 'MergeRequestTabs', -> - stubLocation = (stubs) -> - defaults = {pathname: '', search: '', hash: ''} - $.extend(defaults, stubs) - - fixture.preload('merge_request_tabs.html') - - beforeEach -> - @class = new MergeRequestTabs() - @spies = { - ajax: spyOn($, 'ajax').and.callFake -> - history: spyOn(history, 'replaceState').and.callFake -> - } - - describe '#activateTab', -> - beforeEach -> - fixture.load('merge_request_tabs.html') - @subject = @class.activateTab - - it 'shows the first tab when action is show', -> - @subject('show') - expect($('#notes')).toHaveClass('active') - - it 'shows the notes tab when action is notes', -> - @subject('notes') - expect($('#notes')).toHaveClass('active') - - it 'shows the commits tab when action is commits', -> - @subject('commits') - expect($('#commits')).toHaveClass('active') - - it 'shows the diffs tab when action is diffs', -> - @subject('diffs') - expect($('#diffs')).toHaveClass('active') - - describe '#setCurrentAction', -> - beforeEach -> - @subject = @class.setCurrentAction - - it 'changes from commits', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - - it 'changes from diffs', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from diffs.html', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/diffs.html') - - expect(@subject('notes')).toBe('/foo/bar/merge_requests/1') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'changes from notes', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - - expect(@subject('diffs')).toBe('/foo/bar/merge_requests/1/diffs') - expect(@subject('commits')).toBe('/foo/bar/merge_requests/1/commits') - - it 'includes search parameters and hash string', -> - @class._location = stubLocation({ - pathname: '/foo/bar/merge_requests/1/diffs' - search: '?view=parallel' - hash: '#L15-35' - }) - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1?view=parallel#L15-35') - - it 'replaces the current history state', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1') - new_state = @subject('commits') - - expect(@spies.history).toHaveBeenCalledWith( - {turbolinks: true, url: new_state}, - document.title, - new_state - ) - - it 'treats "show" like "notes"', -> - @class._location = stubLocation(pathname: '/foo/bar/merge_requests/1/commits') - - expect(@subject('show')).toBe('/foo/bar/merge_requests/1') diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js new file mode 100644 index 00000000000..17b32914ec3 --- /dev/null +++ b/spec/javascripts/merge_request_widget_spec.js @@ -0,0 +1,74 @@ + +/*= require merge_request_widget */ + +(function() { + describe('MergeRequestWidget', function() { + beforeEach(function() { + window.notifyPermissions = function() {}; + window.notify = function() {}; + this.opts = { + ci_status_url: "http://sampledomain.local/ci/getstatus", + ci_status: "", + ci_message: { + normal: "Build {{status}} for \"{{title}}\"", + preparing: "{{status}} build for \"{{title}}\"" + }, + ci_title: { + preparing: "{{status}} build", + normal: "Build {{status}}" + }, + gitlab_icon: "gitlab_logo.png", + builds_path: "http://sampledomain.local/sampleBuildsPath" + }; + this["class"] = new MergeRequestWidget(this.opts); + return this.ciStatusData = { + "title": "Sample MR title", + "sha": "12a34bc5", + "status": "success", + "coverage": 98 + }; + }); + return describe('getCIStatus', function() { + beforeEach(function() { + return spyOn(jQuery, 'getJSON').and.callFake((function(_this) { + return function(req, cb) { + return cb(_this.ciStatusData); + }; + })(this)); + }); + it('should call showCIStatus even if a notification should not be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCIStatus when a notification should be displayed', function() { + var spy; + spy = spyOn(this["class"], 'showCIStatus').and.stub(); + this["class"].getCIStatus(true); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.status); + }); + it('should call showCICoverage when the coverage rate is set', function() { + var spy; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).toHaveBeenCalledWith(this.ciStatusData.coverage); + }); + it('should not call showCICoverage when the coverage rate is not set', function() { + var spy; + this.ciStatusData.coverage = null; + spy = spyOn(this["class"], 'showCICoverage').and.stub(); + this["class"].getCIStatus(false); + return expect(spy).not.toHaveBeenCalled(); + }); + return it('should not display a notification on the first check after the widget has been created', function() { + var spy; + spy = spyOn(window, 'notify'); + this["class"] = new MergeRequestWidget(this.opts); + this["class"].getCIStatus(true); + return expect(spy).not.toHaveBeenCalled(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/merge_request_widget_spec.js.coffee b/spec/javascripts/merge_request_widget_spec.js.coffee deleted file mode 100644 index 92b7eeb1116..00000000000 --- a/spec/javascripts/merge_request_widget_spec.js.coffee +++ /dev/null @@ -1,55 +0,0 @@ -#= require merge_request_widget - -describe 'MergeRequestWidget', -> - - beforeEach -> - window.notifyPermissions = () -> - window.notify = () -> - @opts = { - ci_status_url:"http://sampledomain.local/ci/getstatus", - ci_status:"", - ci_message: { - normal: "Build {{status}} for \"{{title}}\"", - preparing: "{{status}} build for \"{{title}}\"" - }, - ci_title: { - preparing: "{{status}} build", - normal: "Build {{status}}" - }, - gitlab_icon:"gitlab_logo.png", - builds_path:"http://sampledomain.local/sampleBuildsPath" - } - @class = new MergeRequestWidget(@opts) - @ciStatusData = {"title":"Sample MR title","sha":"12a34bc5","status":"success","coverage":98} - - describe 'getCIStatus', -> - beforeEach -> - spyOn(jQuery, 'getJSON').and.callFake (req, cb) => - cb(@ciStatusData) - - it 'should call showCIStatus even if a notification should not be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCIStatus when a notification should be displayed', -> - spy = spyOn(@class, 'showCIStatus').and.stub() - @class.getCIStatus(true) - expect(spy).toHaveBeenCalledWith(@ciStatusData.status) - - it 'should call showCICoverage when the coverage rate is set', -> - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).toHaveBeenCalledWith(@ciStatusData.coverage) - - it 'should not call showCICoverage when the coverage rate is not set', -> - @ciStatusData.coverage = null - spy = spyOn(@class, 'showCICoverage').and.stub() - @class.getCIStatus(false) - expect(spy).not.toHaveBeenCalled() - - it 'should not display a notification on the first check after the widget has been created', -> - spy = spyOn(window, 'notify') - @class = new MergeRequestWidget(@opts) - @class.getCIStatus(true) - expect(spy).not.toHaveBeenCalled() diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js new file mode 100644 index 00000000000..25d3f5b6c04 --- /dev/null +++ b/spec/javascripts/new_branch_spec.js @@ -0,0 +1,170 @@ + +/*= require jquery-ui/autocomplete */ + + +/*= require new_branch_form */ + +(function() { + describe('Branch', function() { + return describe('create a new branch', function() { + var expectToHaveError, fillNameWith; + fixture.preload('new_branch.html'); + fillNameWith = function(value) { + return $('.js-branch-name').val(value).trigger('blur'); + }; + expectToHaveError = function(error) { + return expect($('.js-branch-name-error span').text()).toEqual(error); + }; + beforeEach(function() { + fixture.load('new_branch.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.form = new NewBranchForm($('.js-create-branch-form'), []); + }); + it("can't start with a dot", function() { + fillNameWith('.foo'); + return expectToHaveError("can't start with '.'"); + }); + it("can't start with a slash", function() { + fillNameWith('/foo'); + return expectToHaveError("can't start with '/'"); + }); + it("can't have two consecutive dots", function() { + fillNameWith('foo..bar'); + return expectToHaveError("can't contain '..'"); + }); + it("can't have spaces anywhere", function() { + fillNameWith(' foo'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo bar'); + expectToHaveError("can't contain spaces"); + fillNameWith('foo '); + return expectToHaveError("can't contain spaces"); + }); + it("can't have ~ anywhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have tilde anwhere", function() { + fillNameWith('~foo'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~bar'); + expectToHaveError("can't contain '~'"); + fillNameWith('foo~'); + return expectToHaveError("can't contain '~'"); + }); + it("can't have caret anywhere", function() { + fillNameWith('^foo'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^bar'); + expectToHaveError("can't contain '^'"); + fillNameWith('foo^'); + return expectToHaveError("can't contain '^'"); + }); + it("can't have : anywhere", function() { + fillNameWith(':foo'); + expectToHaveError("can't contain ':'"); + fillNameWith('foo:bar'); + expectToHaveError("can't contain ':'"); + fillNameWith(':foo'); + return expectToHaveError("can't contain ':'"); + }); + it("can't have question mark anywhere", function() { + fillNameWith('?foo'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?bar'); + expectToHaveError("can't contain '?'"); + fillNameWith('foo?'); + return expectToHaveError("can't contain '?'"); + }); + it("can't have asterisk anywhere", function() { + fillNameWith('*foo'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*bar'); + expectToHaveError("can't contain '*'"); + fillNameWith('foo*'); + return expectToHaveError("can't contain '*'"); + }); + it("can't have open bracket anywhere", function() { + fillNameWith('[foo'); + expectToHaveError("can't contain '['"); + fillNameWith('foo[bar'); + expectToHaveError("can't contain '['"); + fillNameWith('foo['); + return expectToHaveError("can't contain '['"); + }); + it("can't have a backslash anywhere", function() { + fillNameWith('\\foo'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\bar'); + expectToHaveError("can't contain '\\'"); + fillNameWith('foo\\'); + return expectToHaveError("can't contain '\\'"); + }); + it("can't contain a sequence @{ anywhere", function() { + fillNameWith('@{foo'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{bar'); + expectToHaveError("can't contain '@{'"); + fillNameWith('foo@{'); + return expectToHaveError("can't contain '@{'"); + }); + it("can't have consecutive slashes", function() { + fillNameWith('foo//bar'); + return expectToHaveError("can't contain consecutive slashes"); + }); + it("can't end with a slash", function() { + fillNameWith('foo/'); + return expectToHaveError("can't end in '/'"); + }); + it("can't end with a dot", function() { + fillNameWith('foo.'); + return expectToHaveError("can't end in '.'"); + }); + it("can't end with .lock", function() { + fillNameWith('foo.lock'); + return expectToHaveError("can't end in '.lock'"); + }); + it("can't be the single character @", function() { + fillNameWith('@'); + return expectToHaveError("can't be '@'"); + }); + it("concatenates all error messages", function() { + fillNameWith('/foo bar?~.'); + return expectToHaveError("can't start with '/', can't contain spaces, '?', '~', can't end in '.'"); + }); + it("doesn't duplicate error messages", function() { + fillNameWith('?foo?bar?zoo?'); + return expectToHaveError("can't contain '?'"); + }); + it("removes the error message when is a valid name", function() { + fillNameWith('foo?bar'); + expect($('.js-branch-name-error span').length).toEqual(1); + fillNameWith('foobar'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have dashes anywhere", function() { + fillNameWith('-foo-bar-zoo-'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have underscores anywhere", function() { + fillNameWith('_foo_bar_zoo_'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + it("can have numbers anywhere", function() { + fillNameWith('1foo2bar3zoo4'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + return it("can be only letters", function() { + fillNameWith('foo'); + return expect($('.js-branch-name-error span').length).toEqual(0); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/new_branch_spec.js.coffee b/spec/javascripts/new_branch_spec.js.coffee deleted file mode 100644 index ce773793817..00000000000 --- a/spec/javascripts/new_branch_spec.js.coffee +++ /dev/null @@ -1,160 +0,0 @@ -#= require jquery-ui/autocomplete -#= require new_branch_form - -describe 'Branch', -> - describe 'create a new branch', -> - fixture.preload('new_branch.html') - - fillNameWith = (value) -> - $('.js-branch-name').val(value).trigger('blur') - - expectToHaveError = (error) -> - expect($('.js-branch-name-error span').text()).toEqual(error) - - beforeEach -> - fixture.load('new_branch.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @form = new NewBranchForm($('.js-create-branch-form'), []) - - it "can't start with a dot", -> - fillNameWith '.foo' - expectToHaveError "can't start with '.'" - - it "can't start with a slash", -> - fillNameWith '/foo' - expectToHaveError "can't start with '/'" - - it "can't have two consecutive dots", -> - fillNameWith 'foo..bar' - expectToHaveError "can't contain '..'" - - it "can't have spaces anywhere", -> - fillNameWith ' foo' - expectToHaveError "can't contain spaces" - fillNameWith 'foo bar' - expectToHaveError "can't contain spaces" - fillNameWith 'foo ' - expectToHaveError "can't contain spaces" - - it "can't have ~ anywhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have tilde anwhere", -> - fillNameWith '~foo' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~bar' - expectToHaveError "can't contain '~'" - fillNameWith 'foo~' - expectToHaveError "can't contain '~'" - - it "can't have caret anywhere", -> - fillNameWith '^foo' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^bar' - expectToHaveError "can't contain '^'" - fillNameWith 'foo^' - expectToHaveError "can't contain '^'" - - it "can't have : anywhere", -> - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - fillNameWith 'foo:bar' - expectToHaveError "can't contain ':'" - fillNameWith ':foo' - expectToHaveError "can't contain ':'" - - it "can't have question mark anywhere", -> - fillNameWith '?foo' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?bar' - expectToHaveError "can't contain '?'" - fillNameWith 'foo?' - expectToHaveError "can't contain '?'" - - it "can't have asterisk anywhere", -> - fillNameWith '*foo' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*bar' - expectToHaveError "can't contain '*'" - fillNameWith 'foo*' - expectToHaveError "can't contain '*'" - - it "can't have open bracket anywhere", -> - fillNameWith '[foo' - expectToHaveError "can't contain '['" - fillNameWith 'foo[bar' - expectToHaveError "can't contain '['" - fillNameWith 'foo[' - expectToHaveError "can't contain '['" - - it "can't have a backslash anywhere", -> - fillNameWith '\\foo' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\bar' - expectToHaveError "can't contain '\\'" - fillNameWith 'foo\\' - expectToHaveError "can't contain '\\'" - - it "can't contain a sequence @{ anywhere", -> - fillNameWith '@{foo' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{bar' - expectToHaveError "can't contain '@{'" - fillNameWith 'foo@{' - expectToHaveError "can't contain '@{'" - - it "can't have consecutive slashes", -> - fillNameWith 'foo//bar' - expectToHaveError "can't contain consecutive slashes" - - it "can't end with a slash", -> - fillNameWith 'foo/' - expectToHaveError "can't end in '/'" - - it "can't end with a dot", -> - fillNameWith 'foo.' - expectToHaveError "can't end in '.'" - - it "can't end with .lock", -> - fillNameWith 'foo.lock' - expectToHaveError "can't end in '.lock'" - - it "can't be the single character @", -> - fillNameWith '@' - expectToHaveError "can't be '@'" - - it "concatenates all error messages", -> - fillNameWith '/foo bar?~.' - expectToHaveError "can't start with '/', can't contain spaces, '?', '~', can't end in '.'" - - it "doesn't duplicate error messages", -> - fillNameWith '?foo?bar?zoo?' - expectToHaveError "can't contain '?'" - - it "removes the error message when is a valid name", -> - fillNameWith 'foo?bar' - expect($('.js-branch-name-error span').length).toEqual(1) - fillNameWith 'foobar' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have dashes anywhere", -> - fillNameWith '-foo-bar-zoo-' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have underscores anywhere", -> - fillNameWith '_foo_bar_zoo_' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can have numbers anywhere", -> - fillNameWith '1foo2bar3zoo4' - expect($('.js-branch-name-error span').length).toEqual(0) - - it "can be only letters", -> - fillNameWith 'foo' - expect($('.js-branch-name-error span').length).toEqual(0) diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js new file mode 100644 index 00000000000..14dc6bfdfde --- /dev/null +++ b/spec/javascripts/notes_spec.js @@ -0,0 +1,41 @@ + +/*= require notes */ + + +/*= require gl_form */ + +(function() { + window.gon || (window.gon = {}); + + window.disableButtonIfEmptyField = function() { + return null; + }; + + describe('Notes', function() { + return describe('task lists', function() { + fixture.preload('issue_note.html'); + beforeEach(function() { + fixture.load('issue_note.html'); + $('form').on('submit', function(e) { + return e.preventDefault(); + }); + return this.notes = new Notes(); + }); + it('modifies the Markdown field', function() { + $('input[type=checkbox]').attr('checked', true).trigger('change'); + return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item'); + }); + return it('submits the form on tasklist:changed', function() { + var submitted; + submitted = false; + $('form').on('submit', function(e) { + submitted = true; + return e.preventDefault(); + }); + $('.js-task-list-field').trigger('tasklist:changed'); + return expect(submitted).toBe(true); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/notes_spec.js.coffee b/spec/javascripts/notes_spec.js.coffee deleted file mode 100644 index 3a3c8d63e82..00000000000 --- a/spec/javascripts/notes_spec.js.coffee +++ /dev/null @@ -1,26 +0,0 @@ -#= require notes -#= require gl_form - -window.gon or= {} -window.disableButtonIfEmptyField = -> null - -describe 'Notes', -> - describe 'task lists', -> - fixture.preload('issue_note.html') - - beforeEach -> - fixture.load('issue_note.html') - $('form').on 'submit', (e) -> e.preventDefault() - - @notes = new Notes() - - it 'modifies the Markdown field', -> - $('input[type=checkbox]').attr('checked', true).trigger('change') - expect($('.js-task-list-field').val()).toBe('- [x] Task List Item') - - it 'submits the form on tasklist:changed', -> - submitted = false - $('form').on 'submit', (e) -> submitted = true; e.preventDefault() - - $('.js-task-list-field').trigger('tasklist:changed') - expect(submitted).toBe(true) diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js new file mode 100644 index 00000000000..ffe49828492 --- /dev/null +++ b/spec/javascripts/project_title_spec.js @@ -0,0 +1,60 @@ + +/*= require bootstrap */ + + +/*= require select2 */ + + +/*= require lib/utils/type_utility */ + + +/*= require gl_dropdown */ + + +/*= require api */ + + +/*= require project_select */ + + +/*= require project */ + +(function() { + window.gon || (window.gon = {}); + + window.gon.api_version = 'v3'; + + describe('Project Title', function() { + fixture.preload('project_title.html'); + fixture.preload('projects.json'); + beforeEach(function() { + fixture.load('project_title.html'); + return this.project = new Project(); + }); + return describe('project list', function() { + beforeEach((function(_this) { + return function() { + _this.projects_data = fixture.load('projects.json')[0]; + return spyOn(jQuery, 'ajax').and.callFake(function(req) { + var d; + expect(req.url).toBe('/api/v3/projects.json?simple=true'); + d = $.Deferred(); + d.resolve(_this.projects_data); + return d.promise(); + }); + }; + })(this)); + it('to show on toggle click', (function(_this) { + return function() { + $('.js-projects-dropdown-toggle').click(); + return expect($('.header-content').hasClass('open')).toBe(true); + }; + })(this)); + return it('hide dropdown', function() { + $(".dropdown-menu-close-icon").click(); + return expect($('.header-content').hasClass('open')).toBe(false); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee deleted file mode 100644 index 0244119fa0e..00000000000 --- a/spec/javascripts/project_title_spec.js.coffee +++ /dev/null @@ -1,37 +0,0 @@ -#= require bootstrap -#= require select2 -#= require lib/utils/type_utility -#= require gl_dropdown -#= require api -#= require project_select -#= require project - -window.gon or= {} -window.gon.api_version = 'v3' - -describe 'Project Title', -> - fixture.preload('project_title.html') - fixture.preload('projects.json') - - beforeEach -> - fixture.load('project_title.html') - @project = new Project() - - describe 'project list', -> - beforeEach => - @projects_data = fixture.load('projects.json')[0] - - spyOn(jQuery, 'ajax').and.callFake (req) => - expect(req.url).toBe('/api/v3/projects.json?simple=true') - d = $.Deferred() - d.resolve @projects_data - d.promise() - - it 'to show on toggle click', => - $('.js-projects-dropdown-toggle').click() - expect($('.header-content').hasClass('open')).toBe(true) - - it 'hide dropdown', -> - $(".dropdown-menu-close-icon").click() - - expect($('.header-content').hasClass('open')).toBe(false) diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js new file mode 100644 index 00000000000..38b3b2653ec --- /dev/null +++ b/spec/javascripts/right_sidebar_spec.js @@ -0,0 +1,70 @@ + +/*= require right_sidebar */ + + +/*= require jquery */ + + +/*= require jquery.cookie */ + +(function() { + var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState; + + this.sidebar = null; + + $aside = null; + + $toggle = null; + + $icon = null; + + $page = null; + + $labelsIcon = null; + + assertSidebarState = function(state) { + var shouldBeCollapsed, shouldBeExpanded; + shouldBeExpanded = state === 'expanded'; + shouldBeCollapsed = state === 'collapsed'; + expect($aside.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($page.hasClass('right-sidebar-expanded')).toBe(shouldBeExpanded); + expect($icon.hasClass('fa-angle-double-right')).toBe(shouldBeExpanded); + expect($aside.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + expect($page.hasClass('right-sidebar-collapsed')).toBe(shouldBeCollapsed); + return expect($icon.hasClass('fa-angle-double-left')).toBe(shouldBeCollapsed); + }; + + describe('RightSidebar', function() { + fixture.preload('right_sidebar.html'); + beforeEach(function() { + fixture.load('right_sidebar.html'); + this.sidebar = new Sidebar; + $aside = $('.right-sidebar'); + $page = $('.page-with-sidebar'); + $icon = $aside.find('i'); + $toggle = $aside.find('.js-sidebar-toggle'); + return $labelsIcon = $aside.find('.sidebar-collapsed-icon'); + }); + it('should expand the sidebar when arrow is clicked', function() { + $toggle.click(); + return assertSidebarState('expanded'); + }); + it('should collapse the sidebar when arrow is clicked', function() { + $toggle.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + it('should float over the page and when sidebar icons clicked', function() { + $labelsIcon.click(); + return assertSidebarState('expanded'); + }); + return it('should collapse when the icon arrow clicked while it is floating on page', function() { + $labelsIcon.click(); + assertSidebarState('expanded'); + $toggle.click(); + return assertSidebarState('collapsed'); + }); + }); + +}).call(this); diff --git a/spec/javascripts/right_sidebar_spec.js.coffee b/spec/javascripts/right_sidebar_spec.js.coffee deleted file mode 100644 index 2075cacdb67..00000000000 --- a/spec/javascripts/right_sidebar_spec.js.coffee +++ /dev/null @@ -1,69 +0,0 @@ -#= require right_sidebar -#= require jquery -#= require jquery.cookie - -@sidebar = null -$aside = null -$toggle = null -$icon = null -$page = null -$labelsIcon = null - - -assertSidebarState = (state) -> - - shouldBeExpanded = state is 'expanded' - shouldBeCollapsed = state is 'collapsed' - - expect($aside.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($page.hasClass('right-sidebar-expanded')).toBe shouldBeExpanded - expect($icon.hasClass('fa-angle-double-right')).toBe shouldBeExpanded - - expect($aside.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($page.hasClass('right-sidebar-collapsed')).toBe shouldBeCollapsed - expect($icon.hasClass('fa-angle-double-left')).toBe shouldBeCollapsed - - -describe 'RightSidebar', -> - - fixture.preload 'right_sidebar.html' - - beforeEach -> - fixture.load 'right_sidebar.html' - - @sidebar = new Sidebar - $aside = $ '.right-sidebar' - $page = $ '.page-with-sidebar' - $icon = $aside.find 'i' - $toggle = $aside.find '.js-sidebar-toggle' - $labelsIcon = $aside.find '.sidebar-collapsed-icon' - - - it 'should expand the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - - it 'should collapse the sidebar when arrow is clicked', -> - - $toggle.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' - - - it 'should float over the page and when sidebar icons clicked', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - - it 'should collapse when the icon arrow clicked while it is floating on page', -> - - $labelsIcon.click() - assertSidebarState 'expanded' - - $toggle.click() - assertSidebarState 'collapsed' diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js new file mode 100644 index 00000000000..68d64483d67 --- /dev/null +++ b/spec/javascripts/search_autocomplete_spec.js @@ -0,0 +1,159 @@ + +/*= require gl_dropdown */ + + +/*= require search_autocomplete */ + + +/*= require jquery */ + + +/*= require lib/utils/common_utils */ + + +/*= require lib/utils/type_utility */ + + +/*= require fuzzaldrin-plus */ + +(function() { + var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget; + + widget = null; + + userId = 1; + + window.gon || (window.gon = {}); + + window.gon.current_user_id = userId; + + dashboardIssuesPath = '/dashboard/issues'; + + dashboardMRsPath = '/dashboard/merge_requests'; + + projectIssuesPath = '/gitlab-org/gitlab-ce/issues'; + + projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests'; + + groupIssuesPath = '/groups/gitlab-org/issues'; + + groupMRsPath = '/groups/gitlab-org/merge_requests'; + + projectName = 'GitLab Community Edition'; + + groupName = 'Gitlab Org'; + + addBodyAttributes = function(section) { + var $body; + if (section == null) { + section = 'dashboard'; + } + $body = $('body'); + $body.removeAttr('data-page'); + $body.removeAttr('data-project'); + $body.removeAttr('data-group'); + switch (section) { + case 'dashboard': + return $body.data('page', 'root:index'); + case 'group': + $body.data('page', 'groups:show'); + return $body.data('group', 'gitlab-org'); + case 'project': + $body.data('page', 'projects:show'); + return $body.data('project', 'gitlab-ce'); + } + }; + + mockDashboardOptions = function() { + window.gl || (window.gl = {}); + return window.gl.dashboardOptions = { + issuesPath: dashboardIssuesPath, + mrPath: dashboardMRsPath + }; + }; + + mockProjectOptions = function() { + window.gl || (window.gl = {}); + return window.gl.projectOptions = { + 'gitlab-ce': { + issuesPath: projectIssuesPath, + mrPath: projectMRsPath, + projectName: projectName + } + }; + }; + + mockGroupOptions = function() { + window.gl || (window.gl = {}); + return window.gl.groupOptions = { + 'gitlab-org': { + issuesPath: groupIssuesPath, + mrPath: groupMRsPath, + projectName: groupName + } + }; + }; + + assertLinks = function(list, issuesPath, mrsPath) { + var a1, a2, a3, a4, issuesAssignedToMeLink, issuesIHaveCreatedLink, mrsAssignedToMeLink, mrsIHaveCreatedLink; + issuesAssignedToMeLink = issuesPath + "/?assignee_id=" + userId; + issuesIHaveCreatedLink = issuesPath + "/?author_id=" + userId; + mrsAssignedToMeLink = mrsPath + "/?assignee_id=" + userId; + mrsIHaveCreatedLink = mrsPath + "/?author_id=" + userId; + a1 = "a[href='" + issuesAssignedToMeLink + "']"; + a2 = "a[href='" + issuesIHaveCreatedLink + "']"; + a3 = "a[href='" + mrsAssignedToMeLink + "']"; + a4 = "a[href='" + mrsIHaveCreatedLink + "']"; + expect(list.find(a1).length).toBe(1); + expect(list.find(a1).text()).toBe(' Issues assigned to me '); + expect(list.find(a2).length).toBe(1); + expect(list.find(a2).text()).toBe(" Issues I've created "); + expect(list.find(a3).length).toBe(1); + expect(list.find(a3).text()).toBe(' Merge requests assigned to me '); + expect(list.find(a4).length).toBe(1); + return expect(list.find(a4).text()).toBe(" Merge requests I've created "); + }; + + describe('Search autocomplete dropdown', function() { + fixture.preload('search_autocomplete.html'); + beforeEach(function() { + fixture.load('search_autocomplete.html'); + return widget = new SearchAutocomplete; + }); + it('should show Dashboard specific dropdown menu', function() { + var list; + addBodyAttributes(); + mockDashboardOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, dashboardIssuesPath, dashboardMRsPath); + }); + it('should show Group specific dropdown menu', function() { + var list; + addBodyAttributes('group'); + mockGroupOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, groupIssuesPath, groupMRsPath); + }); + it('should show Project specific dropdown menu', function() { + var list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + return assertLinks(list, projectIssuesPath, projectMRsPath); + }); + return it('should not show category related menu if there is text in the input', function() { + var link, list; + addBodyAttributes('project'); + mockProjectOptions(); + widget.searchInput.val('help'); + widget.searchInput.focus(); + list = widget.wrap.find('.dropdown-menu').find('ul'); + link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']"; + return expect(list.find(link).length).toBe(0); + }); + }); + +}).call(this); diff --git a/spec/javascripts/search_autocomplete_spec.js.coffee b/spec/javascripts/search_autocomplete_spec.js.coffee deleted file mode 100644 index 1c1faca3333..00000000000 --- a/spec/javascripts/search_autocomplete_spec.js.coffee +++ /dev/null @@ -1,149 +0,0 @@ -#= require gl_dropdown -#= require search_autocomplete -#= require jquery -#= require lib/utils/common_utils -#= require lib/utils/type_utility -#= require fuzzaldrin-plus - - -widget = null -userId = 1 -window.gon or= {} -window.gon.current_user_id = userId - -dashboardIssuesPath = '/dashboard/issues' -dashboardMRsPath = '/dashboard/merge_requests' -projectIssuesPath = '/gitlab-org/gitlab-ce/issues' -projectMRsPath = '/gitlab-org/gitlab-ce/merge_requests' -groupIssuesPath = '/groups/gitlab-org/issues' -groupMRsPath = '/groups/gitlab-org/merge_requests' -projectName = 'GitLab Community Edition' -groupName = 'Gitlab Org' - - -# Add required attributes to body before starting the test. -# section would be dashboard|group|project -addBodyAttributes = (section = 'dashboard') -> - - $body = $ 'body' - - $body.removeAttr 'data-page' - $body.removeAttr 'data-project' - $body.removeAttr 'data-group' - - switch section - when 'dashboard' - $body.data 'page', 'root:index' - when 'group' - $body.data 'page', 'groups:show' - $body.data 'group', 'gitlab-org' - when 'project' - $body.data 'page', 'projects:show' - $body.data 'project', 'gitlab-ce' - - -# Mock `gl` object in window for dashboard specific page. App code will need it. -mockDashboardOptions = -> - - window.gl or= {} - window.gl.dashboardOptions = - issuesPath: dashboardIssuesPath - mrPath : dashboardMRsPath - - -# Mock `gl` object in window for project specific page. App code will need it. -mockProjectOptions = -> - - window.gl or= {} - window.gl.projectOptions = - 'gitlab-ce' : - issuesPath : projectIssuesPath - mrPath : projectMRsPath - projectName : projectName - - -mockGroupOptions = -> - - window.gl or= {} - window.gl.groupOptions = - 'gitlab-org' : - issuesPath : groupIssuesPath - mrPath : groupMRsPath - projectName : groupName - - -assertLinks = (list, issuesPath, mrsPath) -> - - issuesAssignedToMeLink = "#{issuesPath}/?assignee_id=#{userId}" - issuesIHaveCreatedLink = "#{issuesPath}/?author_id=#{userId}" - mrsAssignedToMeLink = "#{mrsPath}/?assignee_id=#{userId}" - mrsIHaveCreatedLink = "#{mrsPath}/?author_id=#{userId}" - - a1 = "a[href='#{issuesAssignedToMeLink}']" - a2 = "a[href='#{issuesIHaveCreatedLink}']" - a3 = "a[href='#{mrsAssignedToMeLink}']" - a4 = "a[href='#{mrsIHaveCreatedLink}']" - - expect(list.find(a1).length).toBe 1 - expect(list.find(a1).text()).toBe ' Issues assigned to me ' - - expect(list.find(a2).length).toBe 1 - expect(list.find(a2).text()).toBe " Issues I've created " - - expect(list.find(a3).length).toBe 1 - expect(list.find(a3).text()).toBe ' Merge requests assigned to me ' - - expect(list.find(a4).length).toBe 1 - expect(list.find(a4).text()).toBe " Merge requests I've created " - - -describe 'Search autocomplete dropdown', -> - - fixture.preload 'search_autocomplete.html' - - beforeEach -> - - fixture.load 'search_autocomplete.html' - widget = new SearchAutocomplete - - - it 'should show Dashboard specific dropdown menu', -> - - addBodyAttributes() - mockDashboardOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, dashboardIssuesPath, dashboardMRsPath - - - it 'should show Group specific dropdown menu', -> - - addBodyAttributes 'group' - mockGroupOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, groupIssuesPath, groupMRsPath - - - it 'should show Project specific dropdown menu', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - assertLinks list, projectIssuesPath, projectMRsPath - - - it 'should not show category related menu if there is text in the input', -> - - addBodyAttributes 'project' - mockProjectOptions() - widget.searchInput.val 'help' - widget.searchInput.focus() - - list = widget.wrap.find('.dropdown-menu').find 'ul' - link = "a[href='#{projectIssuesPath}/?assignee_id=#{userId}']" - expect(list.find(link).length).toBe 0 diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js new file mode 100644 index 00000000000..7b6b55fe545 --- /dev/null +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -0,0 +1,74 @@ + +/*= require shortcuts_issuable */ + +(function() { + describe('ShortcutsIssuable', function() { + fixture.preload('issuable.html'); + beforeEach(function() { + fixture.load('issuable.html'); + return this.shortcut = new ShortcutsIssuable(); + }); + return describe('#replyWithSelectedText', function() { + var stubSelection; + stubSelection = function(text) { + return window.getSelection = function() { + return text; + }; + }; + beforeEach(function() { + return this.selector = 'form.js-main-target-form textarea#note_note'; + }); + describe('with empty selection', function() { + return it('does nothing', function() { + stubSelection(''); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe(''); + }); + }); + describe('with any selection', function() { + beforeEach(function() { + return stubSelection('Selected text.'); + }); + it('leaves existing input intact', function() { + $(this.selector).val('This text was already here.'); + expect($(this.selector).val()).toBe('This text was already here.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("This text was already here.\n> Selected text.\n\n"); + }); + it('triggers `input`', function() { + var triggered; + triggered = false; + $(this.selector).on('input', function() { + return triggered = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(triggered).toBe(true); + }); + return it('triggers `focus`', function() { + var focused; + focused = false; + $(this.selector).on('focus', function() { + return focused = true; + }); + this.shortcut.replyWithSelectedText(); + return expect(focused).toBe(true); + }); + }); + describe('with a one-line selection', function() { + return it('quotes the selection', function() { + stubSelection('This text has been selected.'); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> This text has been selected.\n\n"); + }); + }); + return describe('with a multi-line selection', function() { + return it('quotes the selected lines as a group', function() { + stubSelection("Selected line one.\n\nSelected line two.\nSelected line three.\n"); + this.shortcut.replyWithSelectedText(); + return expect($(this.selector).val()).toBe("> Selected line one.\n> Selected line two.\n> Selected line three.\n\n"); + }); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/shortcuts_issuable_spec.js.coffee b/spec/javascripts/shortcuts_issuable_spec.js.coffee deleted file mode 100644 index a01ad7140dd..00000000000 --- a/spec/javascripts/shortcuts_issuable_spec.js.coffee +++ /dev/null @@ -1,82 +0,0 @@ -#= require shortcuts_issuable - -describe 'ShortcutsIssuable', -> - fixture.preload('issuable.html') - - beforeEach -> - fixture.load('issuable.html') - @shortcut = new ShortcutsIssuable() - - describe '#replyWithSelectedText', -> - # Stub window.getSelection to return the provided String. - stubSelection = (text) -> - window.getSelection = -> text - - beforeEach -> - @selector = 'form.js-main-target-form textarea#note_note' - - describe 'with empty selection', -> - it 'does nothing', -> - stubSelection('') - @shortcut.replyWithSelectedText() - expect($(@selector).val()).toBe('') - - describe 'with any selection', -> - beforeEach -> - stubSelection('Selected text.') - - it 'leaves existing input intact', -> - $(@selector).val('This text was already here.') - expect($(@selector).val()).toBe('This text was already here.') - - @shortcut.replyWithSelectedText() - expect($(@selector).val()). - toBe("This text was already here.\n> Selected text.\n\n") - - it 'triggers `input`', -> - triggered = false - $(@selector).on 'input', -> triggered = true - @shortcut.replyWithSelectedText() - - expect(triggered).toBe(true) - - it 'triggers `focus`', -> - focused = false - $(@selector).on 'focus', -> focused = true - @shortcut.replyWithSelectedText() - - expect(focused).toBe(true) - - describe 'with a one-line selection', -> - it 'quotes the selection', -> - stubSelection('This text has been selected.') - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe("> This text has been selected.\n\n") - - describe 'with a multi-line selection', -> - it 'quotes the selected lines as a group', -> - stubSelection( - """ - Selected line one. - - Selected line two. - Selected line three. - - """ - ) - - @shortcut.replyWithSelectedText() - - expect($(@selector).val()). - toBe( - """ - > Selected line one. - > Selected line two. - > Selected line three. - - - """ - ) diff --git a/spec/javascripts/spec_helper.coffee b/spec/javascripts/spec_helper.coffee deleted file mode 100644 index 90b02a6aec5..00000000000 --- a/spec/javascripts/spec_helper.coffee +++ /dev/null @@ -1,47 +0,0 @@ -# PhantomJS (Teaspoons default driver) doesn't have support for -# Function.prototype.bind, which has caused confusion. Use this polyfill to -# avoid the confusion. - -#= require support/bind-poly - -# You can require your own javascript files here. By default this will include -# everything in application, however you may get better load performance if you -# require the specific files that are being used in the spec that tests them. - -#= require jquery -#= require jquery.turbolinks -#= require bootstrap -#= require underscore - -# Teaspoon includes some support files, but you can use anything from your own -# support path too. - -# require support/jasmine-jquery-1.7.0 -# require support/jasmine-jquery-2.0.0 -#= require support/jasmine-jquery-2.1.0 -# require support/sinon -# require support/your-support-file - -# Deferring execution - -# If you're using CommonJS, RequireJS or some other asynchronous library you can -# defer execution. Call Teaspoon.execute() after everything has been loaded. -# Simple example of a timeout: - -# Teaspoon.defer = true -# setTimeout(Teaspoon.execute, 1000) - -# Matching files - -# By default Teaspoon will look for files that match -# _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path -# and it'll be included in the default suite automatically. If you want to -# customize suites, check out the configuration in teaspoon_env.rb - -# Manifest - -# If you'd rather require your spec files manually (to control order for -# instance) you can disable the suite matcher in the configuration and use this -# file as a manifest. - -# For more information: http://github.com/modeset/teaspoon diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js new file mode 100644 index 00000000000..7d91ed0f855 --- /dev/null +++ b/spec/javascripts/spec_helper.js @@ -0,0 +1,22 @@ + +/*= require support/bind-poly */ + + +/*= require jquery */ + + +/*= require jquery.turbolinks */ + + +/*= require bootstrap */ + + +/*= require underscore */ + + +/*= require support/jasmine-jquery-2.1.0 */ + +(function() { + + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js new file mode 100644 index 00000000000..4e5dd1e59bf --- /dev/null +++ b/spec/javascripts/syntax_highlight_spec.js @@ -0,0 +1,44 @@ + +/*= require syntax_highlight */ + +(function() { + describe('Syntax Highlighter', function() { + var stubUserColorScheme; + stubUserColorScheme = function(value) { + if (window.gon == null) { + window.gon = {}; + } + return window.gon.user_color_scheme = value; + }; + describe('on a js-syntax-highlight element', function() { + beforeEach(function() { + return fixture.set('<div class="js-syntax-highlight"></div>'); + }); + return it('applies syntax highlighting', function() { + stubUserColorScheme('monokai'); + $('.js-syntax-highlight').syntaxHighlight(); + return expect($('.js-syntax-highlight')).toHaveClass('monokai'); + }); + }); + return describe('on a parent element', function() { + beforeEach(function() { + return fixture.set("<div class=\"parent\">\n <div class=\"js-syntax-highlight\"></div>\n <div class=\"foo\"></div>\n <div class=\"js-syntax-highlight\"></div>\n</div>"); + }); + it('applies highlighting to all applicable children', function() { + stubUserColorScheme('monokai'); + $('.parent').syntaxHighlight(); + expect($('.parent, .foo')).not.toHaveClass('monokai'); + return expect($('.monokai').length).toBe(2); + }); + return it('prevents an infinite loop when no matches exist', function() { + var highlight; + fixture.set('<div></div>'); + highlight = function() { + return $('div').syntaxHighlight(); + }; + return expect(highlight).not.toThrow(); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee deleted file mode 100644 index 6a73b6bf32c..00000000000 --- a/spec/javascripts/syntax_highlight_spec.js.coffee +++ /dev/null @@ -1,42 +0,0 @@ -#= require syntax_highlight - -describe 'Syntax Highlighter', -> - stubUserColorScheme = (value) -> - window.gon ?= {} - window.gon.user_color_scheme = value - - describe 'on a js-syntax-highlight element', -> - beforeEach -> - fixture.set('<div class="js-syntax-highlight"></div>') - - it 'applies syntax highlighting', -> - stubUserColorScheme('monokai') - - $('.js-syntax-highlight').syntaxHighlight() - - expect($('.js-syntax-highlight')).toHaveClass('monokai') - - describe 'on a parent element', -> - beforeEach -> - fixture.set """ - <div class="parent"> - <div class="js-syntax-highlight"></div> - <div class="foo"></div> - <div class="js-syntax-highlight"></div> - </div> - """ - - it 'applies highlighting to all applicable children', -> - stubUserColorScheme('monokai') - - $('.parent').syntaxHighlight() - - expect($('.parent, .foo')).not.toHaveClass('monokai') - expect($('.monokai').length).toBe(2) - - it 'prevents an infinite loop when no matches exist', -> - fixture.set('<div></div>') - - highlight = -> $('div').syntaxHighlight() - - expect(highlight).not.toThrow() diff --git a/spec/javascripts/u2f/authenticate_spec.coffee b/spec/javascripts/u2f/authenticate_spec.coffee deleted file mode 100644 index 8ffeda11704..00000000000 --- a/spec/javascripts/u2f/authenticate_spec.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require u2f/authenticate -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FAuthenticate', -> - fixture.load('u2f/authenticate') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-authenticate-u2f") - @component = new U2FAuthenticate(@container, {sign_requests: []}, "token") - @component.start() - - it 'allows authenticating via a U2F device', -> - setupButton = @container.find("#js-login-u2f-device") - setupMessage = @container.find("p") - expect(setupMessage.text()).toContain('Insert your security key') - expect(setupButton.text()).toBe('Login Via U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.find("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - deviceResponse = @container.find('#js-device-response') - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "displays an error message", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying authentication after an error", -> - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({errorCode: "error!"}) - retryButton = @container.find("#js-u2f-try-again") - retryButton.trigger('click') - - setupButton = @container.find("#js-login-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToAuthenticateRequest({deviceData: "this is data from the device"}) - authenticatedMessage = @container.find("p") - expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server") diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js new file mode 100644 index 00000000000..e008ce956ad --- /dev/null +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -0,0 +1,75 @@ + +/*= require u2f/authenticate */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FAuthenticate', function() { + fixture.load('u2f/authenticate'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-authenticate-u2f"); + this.component = new U2FAuthenticate(this.container, { + sign_requests: [] + }, "token"); + return this.component.start(); + }); + it('allows authenticating via a U2F device', function() { + var authenticatedMessage, deviceResponse, inProgressMessage, setupButton, setupMessage; + setupButton = this.container.find("#js-login-u2f-device"); + setupMessage = this.container.find("p"); + expect(setupMessage.text()).toContain('Insert your security key'); + expect(setupButton.text()).toBe('Login Via U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.find("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + deviceResponse = this.container.find('#js-device-response'); + expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("displays an error message", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying authentication after an error", function() { + var authenticatedMessage, retryButton, setupButton; + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#js-u2f-try-again"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-login-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToAuthenticateRequest({ + deviceData: "this is data from the device" + }); + authenticatedMessage = this.container.find("p"); + return expect(authenticatedMessage.text()).toContain("Click this button to authenticate with the GitLab server"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js new file mode 100644 index 00000000000..ca91a716ba3 --- /dev/null +++ b/spec/javascripts/u2f/mock_u2f_device.js @@ -0,0 +1,33 @@ +(function() { + var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; }; + + this.MockU2FDevice = (function() { + function MockU2FDevice() { + this.respondToAuthenticateRequest = bind(this.respondToAuthenticateRequest, this); + this.respondToRegisterRequest = bind(this.respondToRegisterRequest, this); + window.u2f || (window.u2f = {}); + window.u2f.register = (function(_this) { + return function(appId, registerRequests, signRequests, callback) { + return _this.registerCallback = callback; + }; + })(this); + window.u2f.sign = (function(_this) { + return function(appId, challenges, signRequests, callback) { + return _this.authenticateCallback = callback; + }; + })(this); + } + + MockU2FDevice.prototype.respondToRegisterRequest = function(params) { + return this.registerCallback(params); + }; + + MockU2FDevice.prototype.respondToAuthenticateRequest = function(params) { + return this.authenticateCallback(params); + }; + + return MockU2FDevice; + + })(); + +}).call(this); diff --git a/spec/javascripts/u2f/mock_u2f_device.js.coffee b/spec/javascripts/u2f/mock_u2f_device.js.coffee deleted file mode 100644 index 97ed0e83a0e..00000000000 --- a/spec/javascripts/u2f/mock_u2f_device.js.coffee +++ /dev/null @@ -1,15 +0,0 @@ -class @MockU2FDevice - constructor: () -> - window.u2f ||= {} - - window.u2f.register = (appId, registerRequests, signRequests, callback) => - @registerCallback = callback - - window.u2f.sign = (appId, challenges, signRequests, callback) => - @authenticateCallback = callback - - respondToRegisterRequest: (params) => - @registerCallback(params) - - respondToAuthenticateRequest: (params) => - @authenticateCallback(params) diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js new file mode 100644 index 00000000000..21c5266c60e --- /dev/null +++ b/spec/javascripts/u2f/register_spec.js @@ -0,0 +1,81 @@ + +/*= require u2f/register */ + + +/*= require u2f/util */ + + +/*= require u2f/error */ + + +/*= require u2f */ + + +/*= require ./mock_u2f_device */ + +(function() { + describe('U2FRegister', function() { + fixture.load('u2f/register'); + beforeEach(function() { + this.u2fDevice = new MockU2FDevice; + this.container = $("#js-register-u2f"); + this.component = new U2FRegister(this.container, $("#js-register-u2f-templates"), {}, "token"); + return this.component.start(); + }); + it('allows registering a U2F device', function() { + var deviceResponse, inProgressMessage, registeredMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + expect(setupButton.text()).toBe('Setup New U2F Device'); + setupButton.trigger('click'); + inProgressMessage = this.container.children("p"); + expect(inProgressMessage.text()).toContain("Trying to communicate with your device"); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find('p'); + deviceResponse = this.container.find('#js-device-response'); + expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + return expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}'); + }); + return describe("errors", function() { + it("doesn't allow the same device to be registered twice (for the same user", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: 4 + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("already been registered with us"); + }); + it("displays an error message for other errors", function() { + var errorMessage, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + errorMessage = this.container.find("p"); + return expect(errorMessage.text()).toContain("There was a problem communicating with your device"); + }); + return it("allows retrying registration after an error", function() { + var registeredMessage, retryButton, setupButton; + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + errorCode: "error!" + }); + retryButton = this.container.find("#U2FTryAgain"); + retryButton.trigger('click'); + setupButton = this.container.find("#js-setup-u2f-device"); + setupButton.trigger('click'); + this.u2fDevice.respondToRegisterRequest({ + deviceData: "this is data from the device" + }); + registeredMessage = this.container.find("p"); + return expect(registeredMessage.text()).toContain("Your device was successfully set up!"); + }); + }); + }); + +}).call(this); diff --git a/spec/javascripts/u2f/register_spec.js.coffee b/spec/javascripts/u2f/register_spec.js.coffee deleted file mode 100644 index 87dc769792b..00000000000 --- a/spec/javascripts/u2f/register_spec.js.coffee +++ /dev/null @@ -1,56 +0,0 @@ -#= require u2f/register -#= require u2f/util -#= require u2f/error -#= require u2f -#= require ./mock_u2f_device - -describe 'U2FRegister', -> - fixture.load('u2f/register') - - beforeEach -> - @u2fDevice = new MockU2FDevice - @container = $("#js-register-u2f") - @component = new U2FRegister(@container, $("#js-register-u2f-templates"), {}, "token") - @component.start() - - it 'allows registering a U2F device', -> - setupButton = @container.find("#js-setup-u2f-device") - expect(setupButton.text()).toBe('Setup New U2F Device') - setupButton.trigger('click') - - inProgressMessage = @container.children("p") - expect(inProgressMessage.text()).toContain("Trying to communicate with your device") - - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find('p') - deviceResponse = @container.find('#js-device-response') - expect(registeredMessage.text()).toContain("Your device was successfully set up!") - expect(deviceResponse.val()).toBe('{"deviceData":"this is data from the device"}') - - describe "errors", -> - it "doesn't allow the same device to be registered twice (for the same user", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: 4}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("already been registered with us") - - it "displays an error message for other errors", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - errorMessage = @container.find("p") - expect(errorMessage.text()).toContain("There was a problem communicating with your device") - - it "allows retrying registration after an error", -> - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({errorCode: "error!"}) - retryButton = @container.find("#U2FTryAgain") - retryButton.trigger('click') - - setupButton = @container.find("#js-setup-u2f-device") - setupButton.trigger('click') - @u2fDevice.respondToRegisterRequest({deviceData: "this is data from the device"}) - registeredMessage = @container.find("p") - expect(registeredMessage.text()).toContain("Your device was successfully set up!") diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js new file mode 100644 index 00000000000..3d680ec8ea3 --- /dev/null +++ b/spec/javascripts/zen_mode_spec.js @@ -0,0 +1,73 @@ + +/*= require zen_mode */ + +(function() { + var enterZen, escapeKeydown, exitZen; + + describe('ZenMode', function() { + fixture.preload('zen_mode.html'); + beforeEach(function() { + fixture.load('zen_mode.html'); + spyOn(Dropzone, 'forElement').and.callFake(function() { + return { + enable: function() { + return true; + } + }; + }); + this.zen = new ZenMode(); + return this.zen.scroll_position = 456; + }); + describe('on enter', function() { + it('pauses Mousetrap', function() { + spyOn(Mousetrap, 'pause'); + enterZen(); + return expect(Mousetrap.pause).toHaveBeenCalled(); + }); + return it('removes textarea styling', function() { + $('textarea').attr('style', 'height: 400px'); + enterZen(); + return expect('textarea').not.toHaveAttr('style'); + }); + }); + describe('in use', function() { + beforeEach(function() { + return enterZen(); + }); + return it('exits on Escape', function() { + escapeKeydown(); + return expect($('.zen-backdrop')).not.toHaveClass('fullscreen'); + }); + }); + return describe('on exit', function() { + beforeEach(function() { + return enterZen(); + }); + it('unpauses Mousetrap', function() { + spyOn(Mousetrap, 'unpause'); + exitZen(); + return expect(Mousetrap.unpause).toHaveBeenCalled(); + }); + return it('restores the scroll position', function() { + spyOn(this.zen, 'scrollTo'); + exitZen(); + return expect(this.zen.scrollTo).toHaveBeenCalled(); + }); + }); + }); + + enterZen = function() { + return $('a.js-zen-enter').click(); + }; + + exitZen = function() { + return $('a.js-zen-leave').click(); + }; + + escapeKeydown = function() { + return $('textarea').trigger($.Event('keydown', { + keyCode: 27 + })); + }; + +}).call(this); diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee deleted file mode 100644 index b790fce01ed..00000000000 --- a/spec/javascripts/zen_mode_spec.js.coffee +++ /dev/null @@ -1,51 +0,0 @@ -#= require zen_mode - -describe 'ZenMode', -> - fixture.preload('zen_mode.html') - - beforeEach -> - fixture.load('zen_mode.html') - - # Stub Dropzone.forElement(...).enable() - spyOn(Dropzone, 'forElement').and.callFake -> - enable: -> true - - @zen = new ZenMode() - - # Set this manually because we can't actually scroll the window - @zen.scroll_position = 456 - - describe 'on enter', -> - it 'pauses Mousetrap', -> - spyOn(Mousetrap, 'pause') - enterZen() - expect(Mousetrap.pause).toHaveBeenCalled() - - it 'removes textarea styling', -> - $('textarea').attr('style', 'height: 400px') - enterZen() - expect('textarea').not.toHaveAttr('style') - - describe 'in use', -> - beforeEach -> enterZen() - - it 'exits on Escape', -> - escapeKeydown() - expect($('.zen-backdrop')).not.toHaveClass('fullscreen') - - describe 'on exit', -> - beforeEach -> enterZen() - - it 'unpauses Mousetrap', -> - spyOn(Mousetrap, 'unpause') - exitZen() - expect(Mousetrap.unpause).toHaveBeenCalled() - - it 'restores the scroll position', -> - spyOn(@zen, 'scrollTo') - exitZen() - expect(@zen.scrollTo).toHaveBeenCalled() - -enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm -exitZen = -> $('a.js-zen-leave').click() -escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27})) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index b9e4a4eaf0e..224baca8030 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::RelativeLinkFilter, lib: true do @@ -19,6 +17,10 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do %(<img src="#{path}" />) end + def video(path) + %(<video src="#{path}"></video>) + end + def link(path) %(<a href="#{path}">#{path}</a>) end @@ -39,6 +41,12 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do doc = filter(image('files/images/logo-black.png')) expect(doc.at_css('img')['src']).to eq 'files/images/logo-black.png' end + + it 'does not modify any relative URL in video' do + doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video') + + expect(doc.at_css('video')['src']).to eq 'files/videos/intro.mp4' + end end shared_examples :relative_to_requested do @@ -70,12 +78,24 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end context 'with a valid repository' do + it 'rebuilds absolute URL for a file in the repo' do + doc = filter(link('/doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo' do doc = filter(link('doc/api/README.md')) expect(doc.at_css('a')['href']). to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repo with leading ./' do + doc = filter(link('./doc/api/README.md')) + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" + end + it 'rebuilds relative URL for a file in the repo up one directory' do relative_link = link('../api/README.md') doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md') @@ -113,11 +133,26 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do end it 'rebuilds relative URL for an image in the repo' do + doc = filter(image('files/images/logo-black.png')) + + expect(doc.at_css('img')['src']). + to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" + end + + it 'rebuilds relative URL for link to an image in the repo' do doc = filter(link('files/images/logo-black.png')) + expect(doc.at_css('a')['href']). to eq "/#{project_path}/raw/#{ref}/files/images/logo-black.png" end + it 'rebuilds relative URL for a video in the repo' do + doc = filter(video('files/videos/intro.mp4'), commit: project.commit('video'), ref: 'video') + + expect(doc.at_css('video')['src']). + to eq "/#{project_path}/raw/video/files/videos/intro.mp4" + end + it 'does not modify relative URL with an anchor only' do doc = filter(link('#section-1')) expect(doc.at_css('a')['href']).to eq '#section-1' diff --git a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb index 6a5d003e87f..356dd01a03a 100644 --- a/spec/lib/banzai/filter/table_of_contents_filter_spec.rb +++ b/spec/lib/banzai/filter/table_of_contents_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::TableOfContentsFilter, lib: true do diff --git a/spec/lib/banzai/filter/upload_link_filter_spec.rb b/spec/lib/banzai/filter/upload_link_filter_spec.rb index 273d2ed709a..8b76c1d73c9 100644 --- a/spec/lib/banzai/filter/upload_link_filter_spec.rb +++ b/spec/lib/banzai/filter/upload_link_filter_spec.rb @@ -1,5 +1,3 @@ -# encoding: UTF-8 - require 'spec_helper' describe Banzai::Filter::UploadLinkFilter, lib: true do diff --git a/spec/lib/banzai/filter/video_link_filter_spec.rb b/spec/lib/banzai/filter/video_link_filter_spec.rb new file mode 100644 index 00000000000..cc4349f80ba --- /dev/null +++ b/spec/lib/banzai/filter/video_link_filter_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Banzai::Filter::VideoLinkFilter, lib: true do + def filter(doc, contexts = {}) + contexts.reverse_merge!({ + project: project + }) + + described_class.call(doc, contexts) + end + + def link_to_image(path) + %(<img src="#{path}" />) + end + + let(:project) { create(:project) } + + context 'when the element src has a video extension' do + UploaderHelper::VIDEO_EXT.each do |ext| + it "replaces the image tag 'path/video.#{ext}' with a video tag" do + container = filter(link_to_image("/path/video.#{ext}")).children.first + + expect(container.name).to eq 'div' + expect(container['class']).to eq 'video-container' + + video, paragraph = container.children + + expect(video.name).to eq 'video' + expect(video['src']).to eq "/path/video.#{ext}" + + expect(paragraph.name).to eq 'p' + + link = paragraph.children.first + + expect(link.name).to eq 'a' + expect(link['href']).to eq "/path/video.#{ext}" + expect(link['target']).to eq '_blank' + end + end + end + + context 'when the element src is an image' do + it 'leaves the document unchanged' do + element = filter(link_to_image('/path/my_image.jpg')).children.first + + expect(element.name).to eq 'img' + expect(element['src']).to eq '/path/my_image.jpg' + end + end + +end diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 514c752546d..85cfe728b6a 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -16,17 +16,17 @@ describe Banzai::ReferenceParser::IssueParser, lib: true do end it 'returns the nodes when the user can read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(true) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([issue]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array when the user can not read the issue' do - expect(Ability.abilities).to receive(:allowed?). - with(user, :read_issue, issue). - and_return(false) + expect(Ability).to receive(:issues_readable_by_user). + with([issue], user). + and_return([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index ad6587b4c25..61490555ff5 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -162,7 +162,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: only parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:only config should be an array of strings or regexps') end end @@ -318,7 +318,7 @@ module Ci shared_examples 'raises an error' do it do - expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'rspec job: except parameter should be an array of strings or regexps') + expect { processor }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'jobs:rspec:except config should be an array of strings or regexps') end end @@ -559,7 +559,7 @@ module Ci it 'raises error' do expect { subject } .to raise_error(GitlabCiYamlProcessor::ValidationError, - /job: variables should be a map/) + /jobs:rspec:variables config should be a hash of key value pairs/) end end @@ -774,7 +774,7 @@ module Ci let(:environment) { 1 } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end @@ -782,7 +782,7 @@ module Ci let(:environment) { 'production staging' } it 'raises error' do - expect { builds }.to raise_error("deploy_to_production job: environment parameter #{Gitlab::Regex.environment_name_regex_message}") + expect { builds }.to raise_error("jobs:deploy_to_production environment #{Gitlab::Regex.environment_name_regex_message}") end end end @@ -973,7 +973,7 @@ EOT config = YAML.dump({ rspec: { script: "test", tags: "mysql" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings") end it "returns errors if before_script parameter is invalid" do @@ -987,7 +987,7 @@ EOT config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings") end it "returns errors if after_script parameter is invalid" do @@ -1001,7 +1001,7 @@ EOT config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings") end it "returns errors if image parameter is invalid" do @@ -1015,21 +1015,21 @@ EOT config = YAML.dump({ '' => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank") end it "returns errors if job name is non-string" do config = YAML.dump({ 10 => { script: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "job name should be non-empty string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol") end it "returns errors if job image parameter is invalid" do config = YAML.dump({ rspec: { script: "test", image: ["test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a string") end it "returns errors if services parameter is not an array" do @@ -1050,49 +1050,56 @@ EOT config = YAML.dump({ rspec: { script: "test", services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end it "returns errors if job services parameter is not an array of strings" do config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be an array of strings") end - it "returns errors if there are unknown parameters" do + it "returns error if job configuration is invalid" do config = YAML.dump({ extra: "bundle update" }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash") end it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do config = YAML.dump({ extra: { services: "test" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be an array of strings") end it "returns errors if there are no jobs defined" do config = YAML.dump({ before_script: ["bundle update"] }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") + end + + it "returns errors if there are no visible jobs defined" do + config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } }) + expect do + GitlabCiYamlProcessor.new(config, path) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job") end it "returns errors if job allow_failure parameter is not an boolean" do config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value") end it "returns errors if job stage is not a string" do config = YAML.dump({ rspec: { script: "test", type: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string") end it "returns errors if job stage is not a pre-defined stage" do @@ -1141,49 +1148,49 @@ EOT config = YAML.dump({ rspec: { script: "test", when: 1 } }) expect do GitlabCiYamlProcessor.new(config, path) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual") end it "returns errors if job artifacts:name is not an a string" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string") end it "returns errors if job artifacts:when is not an a predefined value" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { when: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:when parameter should be on_success, on_failure or always") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always") end it "returns errors if job artifacts:expire_in is not an a string" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:expire_in is not an a valid duration" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:expire_in parameter should be a duration") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration") end it "returns errors if job artifacts:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value") end it "returns errors if job artifacts:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings") end it "returns errors if cache:untracked is not an array of strings" do @@ -1211,28 +1218,28 @@ EOT config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol") end it "returns errors if job cache:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:untracked parameter should be an boolean") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value") end it "returns errors if job cache:paths is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { paths: "string" } } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings") end it "returns errors if job dependencies is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } }) expect do GitlabCiYamlProcessor.new(config) - end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings") + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings") end end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb index 88a71528867..b08396da4d2 100644 --- a/spec/lib/gitlab/akismet_helper_spec.rb +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Gitlab::AkismetHelper, type: :helper do - let(:project) { create(:project) } + let(:project) { create(:project, :public) } let(:user) { create(:user) } before do @@ -11,13 +11,13 @@ describe Gitlab::AkismetHelper, type: :helper do end describe '#check_for_spam?' do - it 'returns true for non-member' do - expect(helper.check_for_spam?(project, user)).to eq(true) + it 'returns true for public project' do + expect(helper.check_for_spam?(project)).to eq(true) end - it 'returns false for member' do - project.team << [user, :guest] - expect(helper.check_for_spam?(project, user)).to eq(false) + it 'returns false for private project' do + project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PRIVATE) + expect(helper.check_for_spam?(project)).to eq(false) end end diff --git a/spec/lib/gitlab/badge/build_spec.rb b/spec/lib/gitlab/badge/build_spec.rb index 2034445a197..f3b522a02f5 100644 --- a/spec/lib/gitlab/badge/build_spec.rb +++ b/spec/lib/gitlab/badge/build_spec.rb @@ -113,7 +113,7 @@ describe Gitlab::Badge::Build do sha: sha, ref: branch) - create(:ci_build, pipeline: pipeline) + create(:ci_build, pipeline: pipeline, stage: 'notify') end def status_node(data, status) diff --git a/spec/lib/gitlab/ci/config/node/artifacts_spec.rb b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb new file mode 100644 index 00000000000..c09a0a9c793 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/artifacts_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Artifacts do + let(:entry) { described_class.new(config) } + + describe 'validation' do + context 'when entry config value is correct' do + let(:config) { { paths: %w[public/] } } + + describe '#value' do + it 'returns artifacs configuration' do + expect(entry.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'when value of attribute is invalid' do + let(:config) { { name: 10 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts name should be a string' + end + end + + context 'when there is an unknown key present' do + let(:config) { { test: 100 } } + + it 'reports error' do + expect(entry.errors) + .to include 'artifacts config contains unknown keys: test' + end + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/attributable_spec.rb b/spec/lib/gitlab/ci/config/node/attributable_spec.rb new file mode 100644 index 00000000000..24d9daafd88 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/attributable_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Attributable do + let(:node) { Class.new } + let(:instance) { node.new } + + before do + node.include(described_class) + + node.class_eval do + attributes :name, :test + end + end + + context 'config is a hash' do + before do + allow(instance) + .to receive(:config) + .and_return({ name: 'some name', test: 'some test' }) + end + + it 'returns the value of config' do + expect(instance.name).to eq 'some name' + expect(instance.test).to eq 'some test' + end + + it 'returns no method error for unknown attributes' do + expect { instance.unknown }.to raise_error(NoMethodError) + end + end + + context 'config is not a hash' do + before do + allow(instance) + .to receive(:config) + .and_return('some test') + end + + it 'returns nil' do + expect(instance.test).to be_nil + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/commands_spec.rb b/spec/lib/gitlab/ci/config/node/commands_spec.rb new file mode 100644 index 00000000000..e373c40706f --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/commands_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Commands do + let(:entry) { described_class.new(config) } + + context 'when entry config value is an array' do + let(:config) { ['ls', 'pwd'] } + + describe '#value' do + it 'returns array of strings' do + expect(entry.value).to eq config + end + end + + describe '#errors' do + it 'does not append errors' do + expect(entry.errors).to be_empty + end + end + end + + context 'when entry config value is a string' do + let(:config) { 'ls' } + + describe '#value' do + it 'returns array with single element' do + expect(entry.value).to eq ['ls'] + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not valid' do + let(:config) { 1 } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'commands config should be a ' \ + 'string or an array of strings' + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb index 91ddef7bfbf..d26185ba585 100644 --- a/spec/lib/gitlab/ci/config/node/factory_spec.rb +++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Factory do describe '#create!' do - let(:factory) { described_class.new(entry_class) } - let(:entry_class) { Gitlab::Ci::Config::Node::Script } + let(:factory) { described_class.new(node) } + let(:node) { Gitlab::Ci::Config::Node::Script } - context 'when setting up a value' do + context 'when setting a concrete value' do it 'creates entry with valid value' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .create! expect(entry.value).to eq ['ls', 'pwd'] @@ -17,7 +17,7 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting description' do it 'creates entry with description' do entry = factory - .with(value: ['ls', 'pwd']) + .value(['ls', 'pwd']) .with(description: 'test description') .create! @@ -29,7 +29,8 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when setting key' do it 'creates entry with custom key' do entry = factory - .with(value: ['ls', 'pwd'], key: 'test key') + .value(['ls', 'pwd']) + .with(key: 'test key') .create! expect(entry.key).to eq 'test key' @@ -37,19 +38,20 @@ describe Gitlab::Ci::Config::Node::Factory do end context 'when setting a parent' do - let(:parent) { Object.new } + let(:object) { Object.new } it 'creates entry with valid parent' do entry = factory - .with(value: 'ls', parent: parent) + .value('ls') + .with(parent: object) .create! - expect(entry.parent).to eq parent + expect(entry.parent).to eq object end end end - context 'when not setting up a value' do + context 'when not setting a value' do it 'raises error' do expect { factory.create! }.to raise_error( Gitlab::Ci::Config::Node::Factory::InvalidFactory @@ -60,11 +62,25 @@ describe Gitlab::Ci::Config::Node::Factory do context 'when creating entry with nil value' do it 'creates an undefined entry' do entry = factory - .with(value: nil) + .value(nil) .create! expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end + + context 'when passing metadata' do + let(:node) { spy('node') } + + it 'passes metadata as a parameter' do + factory + .value('some value') + .metadata(some: 'hash') + .create! + + expect(node).to have_received(:new) + .with('some value', { some: 'hash' }) + end + end end end diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb index c87c9e97bc8..2f87d270b36 100644 --- a/spec/lib/gitlab/ci/config/node/global_spec.rb +++ b/spec/lib/gitlab/ci/config/node/global_spec.rb @@ -22,38 +22,40 @@ describe Gitlab::Ci::Config::Node::Global do variables: { VAR: 'value' }, after_script: ['make clean'], stages: ['build', 'pages'], - cache: { key: 'k', untracked: true, paths: ['public/'] } } + cache: { key: 'k', untracked: true, paths: ['public/'] }, + rspec: { script: %w[rspec ls] }, + spinach: { script: 'spinach' } } end describe '#process!' do before { global.process! } it 'creates nodes hash' do - expect(global.nodes).to be_an Array + expect(global.descendants).to be_an Array end it 'creates node object for each entry' do - expect(global.nodes.count).to eq 8 + expect(global.descendants.count).to eq 8 end it 'creates node object using valid class' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Script - expect(global.nodes.second) + expect(global.descendants.second) .to be_an_instance_of Gitlab::Ci::Config::Node::Image end it 'sets correct description for nodes' do - expect(global.nodes.first.description) + expect(global.descendants.first.description) .to eq 'Script that will be executed before each job.' - expect(global.nodes.second.description) + expect(global.descendants.second.description) .to eq 'Docker image that will be used to execute jobs.' end - end - describe '#leaf?' do - it 'is not leaf' do - expect(global).not_to be_leaf + describe '#leaf?' do + it 'is not leaf' do + expect(global).not_to be_leaf + end end end @@ -63,6 +65,12 @@ describe Gitlab::Ci::Config::Node::Global do expect(global.before_script).to be nil end end + + describe '#leaf?' do + it 'is leaf' do + expect(global).to be_leaf + end + end end context 'when processed' do @@ -106,7 +114,10 @@ describe Gitlab::Ci::Config::Node::Global do end context 'when deprecated types key defined' do - let(:hash) { { types: ['test', 'deploy'] } } + let(:hash) do + { types: ['test', 'deploy'], + rspec: { script: 'rspec' } } + end it 'returns array of types as stages' do expect(global.stages).to eq %w[test deploy] @@ -120,20 +131,33 @@ describe Gitlab::Ci::Config::Node::Global do .to eq(key: 'k', untracked: true, paths: ['public/']) end end + + describe '#jobs' do + it 'returns jobs configuration' do + expect(global.jobs).to eq( + rspec: { name: :rspec, + script: %w[rspec ls], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' } + ) + end + end end end context 'when most of entires not defined' do - let(:hash) { { cache: { key: 'a' }, rspec: {} } } + let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } } before { global.process! } describe '#nodes' do it 'instantizes all nodes' do - expect(global.nodes.count).to eq 8 + expect(global.descendants.count).to eq 8 end it 'contains undefined nodes' do - expect(global.nodes.first) + expect(global.descendants.first) .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined end end @@ -164,7 +188,7 @@ describe Gitlab::Ci::Config::Node::Global do # details. # context 'when entires specified but not defined' do - let(:hash) { { variables: nil } } + let(:hash) { { variables: nil, rspec: { script: 'rspec' } } } before { global.process! } describe '#variables' do @@ -196,10 +220,8 @@ describe Gitlab::Ci::Config::Node::Global do end describe '#before_script' do - it 'raises error' do - expect { global.before_script }.to raise_error( - Gitlab::Ci::Config::Node::Entry::InvalidError - ) + it 'returns nil' do + expect(global.before_script).to be_nil end end end @@ -220,9 +242,9 @@ describe Gitlab::Ci::Config::Node::Global do end end - describe '#defined?' do + describe '#specified?' do it 'is concrete entry that is defined' do - expect(global.defined?).to be true + expect(global.specified?).to be true end end end diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb new file mode 100644 index 00000000000..cc44e2cc054 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::HiddenJob do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { image: 'ruby:2.2' } } + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq(image: 'ruby:2.2') + end + end + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'hidden job config should be a hash' + end + end + end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end + end + end + + describe '#leaf?' do + it 'is a leaf' do + expect(entry).to be_leaf + end + end + + describe '#relevant?' do + it 'is not a relevant entry' do + expect(entry).not_to be_relevant + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb new file mode 100644 index 00000000000..1484fb60dd8 --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/job_spec.rb @@ -0,0 +1,86 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Job do + let(:entry) { described_class.new(config, name: :rspec) } + + before { entry.process! } + + describe 'validations' do + context 'when entry config value is correct' do + let(:config) { { script: 'rspec' } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + context 'when job name is empty' do + let(:entry) { described_class.new(config, name: ''.to_sym) } + + it 'reports error' do + expect(entry.errors) + .to include "job name can't be blank" + end + end + end + + context 'when entry value is not correct' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + describe '#errors' do + it 'reports error about a config type' do + expect(entry.errors) + .to include 'job config should be a hash' + end + end + end + + context 'when config is empty' do + let(:config) { {} } + + describe '#valid' do + it 'is invalid' do + expect(entry).not_to be_valid + end + end + end + + context 'when unknown keys detected' do + let(:config) { { unknown: true } } + + describe '#valid' do + it 'is not valid' do + expect(entry).not_to be_valid + end + end + end + end + end + + describe '#value' do + context 'when entry is correct' do + let(:config) do + { before_script: %w[ls pwd], + script: 'rspec', + after_script: %w[cleanup] } + end + + it 'returns correct value' do + expect(entry.value) + .to eq(name: :rspec, + before_script: %w[ls pwd], + script: %w[rspec], + stage: 'test', + after_script: %w[cleanup]) + end + end + end + + describe '#relevant?' do + it 'is a relevant entry' do + expect(entry).to be_relevant + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb new file mode 100644 index 00000000000..b8d9c70479c --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Jobs do + let(:entry) { described_class.new(config) } + + describe 'validations' do + before { entry.process! } + + context 'when entry config value is correct' do + let(:config) { { rspec: { script: 'rspec' } } } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when entry value is not correct' do + describe '#errors' do + context 'incorrect config value type' do + let(:config) { ['incorrect'] } + + it 'returns error about incorrect type' do + expect(entry.errors) + .to include 'jobs config should be a hash' + end + end + + context 'when job is unspecified' do + let(:config) { { rspec: nil } } + + it 'reports error' do + expect(entry.errors).to include "rspec config can't be blank" + end + end + + context 'when no visible jobs present' do + let(:config) { { '.hidden'.to_sym => { script: [] } } } + + it 'returns error about no visible jobs defined' do + expect(entry.errors) + .to include 'jobs config should contain at least one visible job' + end + end + end + end + end + + context 'when valid job entries processed' do + before { entry.process! } + + let(:config) do + { rspec: { script: 'rspec' }, + spinach: { script: 'spinach' }, + '.hidden'.to_sym => {} } + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq( + rspec: { name: :rspec, + script: %w[rspec], + stage: 'test' }, + spinach: { name: :spinach, + script: %w[spinach], + stage: 'test' }) + end + end + + describe '#descendants' do + it 'creates valid descendant nodes' do + expect(entry.descendants.count).to eq 3 + expect(entry.descendants.first(2)) + .to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job)) + expect(entry.descendants.last) + .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob) + end + end + + describe '#value' do + it 'returns value of visible jobs only' do + expect(entry.value.keys).to eq [:rspec, :spinach] + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb new file mode 100644 index 00000000000..1ab5478dcfa --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/null_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Null do + let(:null) { described_class.new(nil) } + + describe '#leaf?' do + it 'is leaf node' do + expect(null).to be_leaf + end + end + + describe '#valid?' do + it 'is always valid' do + expect(null).to be_valid + end + end + + describe '#errors' do + it 'is does not contain errors' do + expect(null.errors).to be_empty + end + end + + describe '#value' do + it 'returns nil' do + expect(null.value).to eq nil + end + end + + describe '#relevant?' do + it 'is not relevant' do + expect(null.relevant?).to eq false + end + end + + describe '#specified?' do + it 'is not defined' do + expect(null.specified?).to eq false + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/stage_spec.rb b/spec/lib/gitlab/ci/config/node/stage_spec.rb new file mode 100644 index 00000000000..fb9ec70762a --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/stage_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Stage do + let(:stage) { described_class.new(config) } + + describe 'validations' do + context 'when stage config value is correct' do + let(:config) { 'build' } + + describe '#value' do + it 'returns a stage key' do + expect(stage.value).to eq config + end + end + + describe '#valid?' do + it 'is valid' do + expect(stage).to be_valid + end + end + end + + context 'when value has a wrong type' do + let(:config) { { test: true } } + + it 'reports errors about wrong type' do + expect(stage.errors) + .to include 'stage config should be a string' + end + end + end + + describe '.default' do + it 'returns default stage' do + expect(described_class.default).to eq 'test' + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/trigger_spec.rb b/spec/lib/gitlab/ci/config/node/trigger_spec.rb new file mode 100644 index 00000000000..a4a3e36754e --- /dev/null +++ b/spec/lib/gitlab/ci/config/node/trigger_spec.rb @@ -0,0 +1,56 @@ +require 'spec_helper' + +describe Gitlab::Ci::Config::Node::Trigger do + let(:entry) { described_class.new(config) } + + describe 'validations' do + context 'when entry config value is valid' do + context 'when config is a branch or tag name' do + let(:config) { %w[master feature/branch] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + + describe '#value' do + it 'returns key value' do + expect(entry.value).to eq config + end + end + end + + context 'when config is a regexp' do + let(:config) { ['/^issue-.*$/'] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + + context 'when config is a special keyword' do + let(:config) { %w[tags triggers branches] } + + describe '#valid?' do + it 'is valid' do + expect(entry).to be_valid + end + end + end + end + + context 'when entry value is not valid' do + let(:config) { [1] } + + describe '#errors' do + it 'saves errors' do + expect(entry.errors) + .to include 'trigger config should be an array of strings or regexps' + end + end + end + end +end diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb index 0c6608d906d..2d43e1c1a9d 100644 --- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb +++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb @@ -2,39 +2,31 @@ require 'spec_helper' describe Gitlab::Ci::Config::Node::Undefined do let(:undefined) { described_class.new(entry) } - let(:entry) { Class.new } - - describe '#leaf?' do - it 'is leaf node' do - expect(undefined).to be_leaf - end - end + let(:entry) { spy('Entry') } describe '#valid?' do - it 'is always valid' do - expect(undefined).to be_valid + it 'delegates method to entry' do + expect(undefined.valid).to eq entry end end describe '#errors' do - it 'is does not contain errors' do - expect(undefined.errors).to be_empty + it 'delegates method to entry' do + expect(undefined.errors).to eq entry end end describe '#value' do - before do - allow(entry).to receive(:default).and_return('some value') - end - - it 'returns default value for entry' do - expect(undefined.value).to eq 'some value' + it 'delegates method to entry' do + expect(undefined.value).to eq entry end end - describe '#undefined?' do - it 'is not a defined entry' do - expect(undefined.defined?).to be false + describe '#specified?' do + it 'is always false' do + allow(entry).to receive(:specified?).and_return(true) + + expect(undefined.specified?).to be false end end end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb index 5f76b70c6f5..2aa5ae44f54 100644 --- a/spec/lib/gitlab/diff/parallel_diff_spec.rb +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -11,11 +11,51 @@ describe Gitlab::Diff::ParallelDiff, lib: true do let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: commit.diff_refs, repository: repository) } subject { described_class.new(diff_file) } - let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } - describe '#parallelize' do it 'should return an array of arrays containing the parsed diff' do - expect(subject.parallelize).to match_array(parallel_diff_result_array) + diff_lines = diff_file.highlighted_diff_lines + expected = [ + # Unchanged lines + { left: diff_lines[0], right: diff_lines[0] }, + { left: diff_lines[1], right: diff_lines[1] }, + { left: diff_lines[2], right: diff_lines[2] }, + { left: diff_lines[3], right: diff_lines[3] }, + { left: diff_lines[4], right: diff_lines[5] }, + { left: diff_lines[6], right: diff_lines[6] }, + { left: diff_lines[7], right: diff_lines[7] }, + { left: diff_lines[8], right: diff_lines[8] }, + + # Changed lines + { left: diff_lines[9], right: diff_lines[11] }, + { left: diff_lines[10], right: diff_lines[12] }, + + # Added lines + { left: nil, right: diff_lines[13] }, + { left: nil, right: diff_lines[14] }, + { left: nil, right: diff_lines[15] }, + { left: nil, right: diff_lines[16] }, + { left: nil, right: diff_lines[17] }, + { left: nil, right: diff_lines[18] }, + + # Unchanged lines + { left: diff_lines[19], right: diff_lines[19] }, + { left: diff_lines[20], right: diff_lines[20] }, + { left: diff_lines[21], right: diff_lines[21] }, + { left: diff_lines[22], right: diff_lines[22] }, + { left: diff_lines[23], right: diff_lines[23] }, + { left: diff_lines[24], right: diff_lines[24] }, + { left: diff_lines[25], right: diff_lines[25] }, + + # Added line + { left: nil, right: diff_lines[26] }, + + # Unchanged lines + { left: diff_lines[27], right: diff_lines[27] }, + { left: diff_lines[28], right: diff_lines[28] }, + { left: diff_lines[29], right: diff_lines[29] } + ] + + expect(subject.parallelize).to eq(expected) end end end diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb index cf28628cb96..10537bea008 100644 --- a/spec/lib/gitlab/diff/position_spec.rb +++ b/spec/lib/gitlab/diff/position_spec.rb @@ -338,4 +338,28 @@ describe Gitlab::Diff::Position, lib: true do end end 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 + } + 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) + 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 end diff --git a/spec/lib/gitlab/downtime_check/message_spec.rb b/spec/lib/gitlab/downtime_check/message_spec.rb new file mode 100644 index 00000000000..93094cda776 --- /dev/null +++ b/spec/lib/gitlab/downtime_check/message_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe Gitlab::DowntimeCheck::Message do + describe '#to_s' do + it 'returns an ANSI formatted String for an offline migration' do + message = described_class.new('foo.rb', true, 'hello') + + expect(message.to_s).to eq("[\e[32moffline\e[0m]: foo.rb: hello") + end + + it 'returns an ANSI formatted String for an online migration' do + message = described_class.new('foo.rb') + + expect(message.to_s).to eq("[\e[31monline\e[0m]: foo.rb") + end + end +end diff --git a/spec/lib/gitlab/downtime_check_spec.rb b/spec/lib/gitlab/downtime_check_spec.rb new file mode 100644 index 00000000000..42d895e548e --- /dev/null +++ b/spec/lib/gitlab/downtime_check_spec.rb @@ -0,0 +1,113 @@ +require 'spec_helper' + +describe Gitlab::DowntimeCheck do + subject { described_class.new } + let(:path) { 'foo.rb' } + + describe '#check' do + before do + expect(subject).to receive(:require).with(path) + end + + context 'when a migration does not specify if downtime is required' do + it 'raises RuntimeError' do + expect(subject).to receive(:class_for_migration_file). + with(path). + and_return(Class.new) + + expect { subject.check([path]) }. + to raise_error(RuntimeError, /it requires downtime/) + end + end + + context 'when a migration requires downtime' do + context 'when no reason is specified' do + it 'raises RuntimeError' do + stub_const('TestMigration::DOWNTIME', true) + + expect(subject).to receive(:class_for_migration_file). + with(path). + and_return(TestMigration) + + expect { subject.check([path]) }. + to raise_error(RuntimeError, /no reason was given/) + end + end + + context 'when a reason is specified' do + it 'returns an Array of messages' do + stub_const('TestMigration::DOWNTIME', true) + stub_const('TestMigration::DOWNTIME_REASON', 'foo') + + expect(subject).to receive(:class_for_migration_file). + with(path). + and_return(TestMigration) + + messages = subject.check([path]) + + expect(messages).to be_an_instance_of(Array) + expect(messages[0]).to be_an_instance_of(Gitlab::DowntimeCheck::Message) + + message = messages[0] + + expect(message.path).to eq(path) + expect(message.offline).to eq(true) + expect(message.reason).to eq('foo') + end + end + end + end + + describe '#check_and_print' do + it 'checks the migrations and prints the results to STDOUT' do + stub_const('TestMigration::DOWNTIME', true) + stub_const('TestMigration::DOWNTIME_REASON', 'foo') + + expect(subject).to receive(:require).with(path) + + expect(subject).to receive(:class_for_migration_file). + with(path). + and_return(TestMigration) + + expect(subject).to receive(:puts).with(an_instance_of(String)) + + subject.check_and_print([path]) + end + end + + describe '#class_for_migration_file' do + it 'returns the class for a migration file path' do + expect(subject.class_for_migration_file('123_string.rb')).to eq(String) + end + end + + describe '#online?' do + it 'returns true when a migration can be performed online' do + stub_const('TestMigration::DOWNTIME', false) + + expect(subject.online?(TestMigration)).to eq(true) + end + + it 'returns false when a migration can not be performed online' do + stub_const('TestMigration::DOWNTIME', true) + + expect(subject.online?(TestMigration)).to eq(false) + end + end + + describe '#downtime_reason' do + context 'when a reason is defined' do + it 'returns the downtime reason' do + stub_const('TestMigration::DOWNTIME_REASON', 'hello') + + expect(subject.downtime_reason(TestMigration)).to eq('hello') + end + end + + context 'when a reason is not defined' do + it 'returns nil' do + expect(subject.downtime_reason(Class.new)).to be_nil + end + end + end +end diff --git a/spec/lib/gitlab/email/attachment_uploader_spec.rb b/spec/lib/gitlab/email/attachment_uploader_spec.rb index 476a21bf996..08b2577ecc4 100644 --- a/spec/lib/gitlab/email/attachment_uploader_spec.rb +++ b/spec/lib/gitlab/email/attachment_uploader_spec.rb @@ -11,7 +11,6 @@ describe Gitlab::Email::AttachmentUploader, lib: true do link = links.first expect(link).not_to be_nil - expect(link[:is_image]).to be_truthy expect(link[:alt]).to eq("bricks") expect(link[:url]).to include("bricks.png") end diff --git a/spec/lib/gitlab/email/email_shared_blocks.rb b/spec/lib/gitlab/email/email_shared_blocks.rb new file mode 100644 index 00000000000..19298e261e3 --- /dev/null +++ b/spec/lib/gitlab/email/email_shared_blocks.rb @@ -0,0 +1,41 @@ +require 'gitlab/email/receiver' + +shared_context :email_shared_context do + let(:mail_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } + let(:receiver) { Gitlab::Email::Receiver.new(email_raw) } + let(:markdown) { "![image](uploads/image.png)" } + + def setup_attachment + allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( + [ + { + url: "uploads/image.png", + alt: "image", + markdown: markdown + } + ] + ) + end +end + +shared_examples :email_shared_examples do + context "when the user could not be found" do + before do + user.destroy + end + + it "raises a UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when the user is not authorized to the project" do + before do + project.update_attribute(:visibility_level, Project::PRIVATE) + end + + it "raises a ProjectNotFound" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb new file mode 100644 index 00000000000..e1153154778 --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_new_issue.eml') } + let(:namespace) { create(:namespace, path: 'gitlabhq') } + + let!(:project) { create(:project, :public, namespace: namespace) } + let!(:user) do + create( + :user, + email: 'jake@adventuretime.ooo', + authentication_token: 'auth_token' + ) + end + + context "when everything is fine" do + it "creates a new issue" do + setup_attachment + + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to include('reply by email') + expect(issue.description).to include(markdown) + end + + context "when the reply is blank" do + let(:email_raw) { fixture_file("emails/valid_new_issue_empty.eml") } + + it "creates a new issue" do + expect { receiver.execute }.to change { project.issues.count }.by(1) + issue = project.issues.last + + expect(issue.author).to eq(user) + expect(issue.title).to eq('New Issue by email') + expect(issue.description).to eq('') + end + end + end + + context "something is wrong" do + context "when the issue could not be saved" do + before do + allow_any_instance_of(Issue).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidIssueError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidIssueError) + end + end + + context "when we can't find the authentication_token" do + let(:email_raw) { fixture_file("emails/wrong_authentication_token.eml") } + + it "raises an UserNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UserNotFoundError) + end + end + + context "when project is private" do + let(:project) { create(:project, :private, namespace: namespace) } + + it "raises a ProjectNotFound if the user is not a member" do + expect { receiver.execute }.to raise_error(Gitlab::Email::ProjectNotFound) + end + end + end +end diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb new file mode 100644 index 00000000000..a2119b0dadf --- /dev/null +++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' +require_relative '../email_shared_blocks' + +describe Gitlab::Email::Handler::CreateNoteHandler, lib: true do + include_context :email_shared_context + it_behaves_like :email_shared_examples + + before do + stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") + stub_config_setting(host: 'localhost') + end + + let(:email_raw) { fixture_file('emails/valid_reply.eml') } + let(:project) { create(:project, :public) } + let(:noteable) { create(:issue, project: project) } + let(:user) { create(:user) } + + let!(:sent_notification) { SentNotification.record(noteable, user.id, mail_key) } + + context "when the recipient address doesn't include a mail key" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "") } + + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) + end + end + + context "when no sent notification for the mail key could be found" do + let(:email_raw) { fixture_file('emails/wrong_mail_key.eml') } + + it "raises a SentNotificationNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::SentNotificationNotFoundError) + end + end + + context "when the email was auto generated" do + let!(:mail_key) { '636ca428858779856c226bb145ef4fad' } + let!(:email_raw) { fixture_file("emails/auto_reply.eml") } + + it "raises an AutoGeneratedEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::AutoGeneratedEmailError) + end + end + + context "when the noteable could not be found" do + before do + noteable.destroy + end + + it "raises a NoteableNotFoundError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::NoteableNotFoundError) + end + end + + context "when the note could not be saved" do + before do + allow_any_instance_of(Note).to receive(:persisted?).and_return(false) + end + + it "raises an InvalidNoteError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError) + end + end + + context "when the reply is blank" do + let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } + + it "raises an EmptyEmailError" do + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) + end + end + + context "when everything is fine" do + before do + setup_attachment + end + + it "creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include("I could not disagree more.") + end + + it "adds all attachments" do + receiver.execute + + note = noteable.notes.last + + expect(note.note).to include(markdown) + end + + context 'when sub-addressing is not supported' do + before do + stub_incoming_email_setting(enabled: true, address: nil) + end + + shared_examples 'an email that contains a mail key' do |header| + it "fetches the mail key from the #{header} header and creates a comment" do + expect { receiver.execute }.to change { noteable.notes.count }.by(1) + note = noteable.notes.last + + expect(note.author).to eq(sent_notification.recipient) + expect(note.note).to include('I could not disagree more.') + end + end + + context 'mail key is in the References header' do + let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } + + it_behaves_like 'an email that contains a mail key', 'References' + end + end + end +end diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index 36267faeb93..2a86b427806 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -1,34 +1,14 @@ -require "spec_helper" +require 'spec_helper' +require_relative 'email_shared_blocks' describe Gitlab::Email::Receiver, lib: true do - before do - stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo") - stub_config_setting(host: 'localhost') - end - - let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" } - let(:email_raw) { fixture_file('emails/valid_reply.eml') } - - let(:project) { create(:project, :public) } - let(:noteable) { create(:issue, project: project) } - let(:user) { create(:user) } - let!(:sent_notification) { SentNotification.record(noteable, user.id, reply_key) } - - let(:receiver) { described_class.new(email_raw) } - - context "when the recipient address doesn't include a reply key" do - let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(reply_key, "") } - - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) - end - end + include_context :email_shared_context - context "when no sent notificiation for the reply key could be found" do - let(:email_raw) { fixture_file('emails/wrong_reply_key.eml') } + context "when we cannot find a capable handler" do + let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") } - it "raises a SentNotificationNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::SentNotificationNotFoundError) + it "raises a UnknownIncomingEmail" do + expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail) end end @@ -36,129 +16,7 @@ describe Gitlab::Email::Receiver, lib: true do let(:email_raw) { "" } it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the email was auto generated" do - let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } - let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - - it "raises an AutoGeneratedEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) - end - end - - context "when the user could not be found" do - before do - user.destroy - end - - it "raises a UserNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotFoundError) - end - end - - context "when the user has been blocked" do - before do - user.block - end - - it "raises a UserBlockedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserBlockedError) - end - end - - context "when the user is not authorized to create a note" do - before do - project.update_attribute(:visibility_level, Project::PRIVATE) - end - - it "raises a UserNotAuthorizedError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::UserNotAuthorizedError) - end - end - - context "when the noteable could not be found" do - before do - noteable.destroy - end - - it "raises a NoteableNotFoundError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::NoteableNotFoundError) - end - end - - context "when the reply is blank" do - let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - - it "raises an EmptyEmailError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) - end - end - - context "when the note could not be saved" do - before do - allow_any_instance_of(Note).to receive(:persisted?).and_return(false) - end - - it "raises an InvalidNoteError" do - expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::InvalidNoteError) - end - end - - context "when everything is fine" do - let(:markdown) { "![image](uploads/image.png)" } - - before do - allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( - [ - { - url: "uploads/image.png", - is_image: true, - alt: "image", - markdown: markdown - } - ] - ) - end - - it "creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include("I could not disagree more.") - end - - it "adds all attachments" do - receiver.execute - - note = noteable.notes.last - - expect(note.note).to include(markdown) - end - - context 'when sub-addressing is not supported' do - before do - stub_incoming_email_setting(enabled: true, address: nil) - end - - shared_examples 'an email that contains a reply key' do |header| - it "fetches the reply key from the #{header} header and creates a comment" do - expect { receiver.execute }.to change { noteable.notes.count }.by(1) - note = noteable.notes.last - - expect(note.author).to eq(sent_notification.recipient) - expect(note.note).to include('I could not disagree more.') - end - end - - context 'reply key is in the References header' do - let(:email_raw) { fixture_file('emails/reply_without_subaddressing_and_key_inside_references.eml') } - - it_behaves_like 'an email that contains a reply key', 'References' - end + expect { receiver.execute }.to raise_error(Gitlab::Email::EmptyEmailError) end end end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index db33c7a22bb..8447305a316 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -44,12 +44,12 @@ describe Gitlab::GitAccess, lib: true do end describe 'download_access_check' do + subject { access.check('git-upload-pack') } + describe 'master permissions' do before { project.team << [user, :master] } context 'pull code' do - subject { access.download_access_check } - it { expect(subject.allowed?).to be_truthy } end end @@ -58,8 +58,6 @@ describe Gitlab::GitAccess, lib: true do before { project.team << [user, :guest] } context 'pull code' do - subject { access.download_access_check } - it { expect(subject.allowed?).to be_falsey } end end @@ -71,16 +69,12 @@ describe Gitlab::GitAccess, lib: true do end context 'pull code' do - subject { access.download_access_check } - it { expect(subject.allowed?).to be_falsey } end end describe 'without acccess to project' do context 'pull code' do - subject { access.download_access_check } - it { expect(subject.allowed?).to be_falsey } end end @@ -90,10 +84,31 @@ describe Gitlab::GitAccess, lib: true do let(:actor) { key } context 'pull code' do - before { key.projects << project } - subject { access.download_access_check } + context 'when project is authorized' do + before { key.projects << project } - it { expect(subject.allowed?).to be_truthy } + it { expect(subject).to be_allowed } + end + + context 'when unauthorized' do + context 'from public project' do + let(:project) { create(:project, :public) } + + it { expect(subject).to be_allowed } + end + + context 'from internal project' do + let(:project) { create(:project, :internal) } + + it { expect(subject).not_to be_allowed } + end + + context 'from private project' do + let(:project) { create(:project, :internal) } + + it { expect(subject).not_to be_allowed } + end + end end end end @@ -136,7 +151,13 @@ describe Gitlab::GitAccess, lib: true do def self.run_permission_checks(permissions_matrix) permissions_matrix.keys.each do |role| describe "#{role} access" do - before { project.team << [user, role] } + before do + if role == :admin + user.update_attribute(:admin, true) + else + project.team << [user, role] + end + end permissions_matrix[role].each do |action, allowed| context action do @@ -150,6 +171,17 @@ describe Gitlab::GitAccess, lib: true do end permissions_matrix = { + admin: { + push_new_branch: true, + push_master: true, + push_protected_branch: true, + push_remove_protected_branch: false, + push_tag: true, + push_new_tag: true, + push_all: true, + merge_into_protected_branch: true + }, + master: { push_new_branch: true, push_master: true, @@ -202,19 +234,20 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix) end - context "when 'developers can push' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_push: true, project: project) } + context "when developers are allowed to push into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end - context "when 'developers can merge' is turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, project: project) } + context "developers are allowed to merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, name: protected_branch_name, project: project) } context "when a merge request exists for the given source/target branch" do context "when the merge request is in progress" do before do - create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) + create(:merge_request, source_project: project, source_branch: unprotected_branch, target_branch: 'feature', + state: 'locked', in_progress_merge_commit_sha: merge_into_protected_branch) end run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: true })) @@ -227,18 +260,61 @@ describe Gitlab::GitAccess, lib: true do run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) end - end - context "when a merge request does not exist for the given source/target branch" do - run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + context "when a merge request does not exist for the given source/target branch" do + run_permission_checks(permissions_matrix.deep_merge(developer: { merge_into_protected_branch: false })) + end end end - context "when 'developers can merge' and 'developers can push' are turned on for the #{protected_branch_type} protected branch" do - before { create(:protected_branch, name: protected_branch_name, developers_can_merge: true, developers_can_push: true, project: project) } + context "when developers are allowed to push and merge into the #{protected_branch_type} protected branch" do + before { create(:protected_branch, :developers_can_merge, :developers_can_push, name: protected_branch_name, project: project) } run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: true, push_all: true, merge_into_protected_branch: true })) end + + context "when no one is allowed to push to the #{protected_branch_name} protected branch" do + before { create(:protected_branch, :no_one_can_push, name: protected_branch_name, project: project) } + + run_permission_checks(permissions_matrix.deep_merge(developer: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + master: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }, + admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) + end + end + end + + describe 'deploy key permissions' do + let(:key) { create(:deploy_key) } + let(:actor) { key } + + context 'push code' do + subject { access.check('git-receive-pack') } + + context 'when project is authorized' do + before { key.projects << project } + + it { expect(subject).not_to be_allowed } + end + + context 'when unauthorized' do + context 'to public project' do + let(:project) { create(:project, :public) } + + it { expect(subject).not_to be_allowed } + end + + context 'to internal project' do + let(:project) { create(:project, :internal) } + + it { expect(subject).not_to be_allowed } + end + + context 'to private project' do + let(:project) { create(:project, :internal) } + + it { expect(subject).not_to be_allowed } + end + end end end end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb index 364532e94e3..fc021416d92 100644 --- a/spec/lib/gitlab/highlight_spec.rb +++ b/spec/lib/gitlab/highlight_spec.rb @@ -17,6 +17,18 @@ describe Gitlab::Highlight, lib: true do expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) end + + describe 'with CRLF' do + let(:branch) { 'crlf-diff' } + let(:blob) { repository.blob_at_branch(branch, path) } + let(:lines) do + Gitlab::Highlight.highlight_lines(project.repository, 'crlf-diff', 'files/whitespace') + end + + it 'strips extra LFs' do + expect(lines[0]).to eq("<span id=\"LC1\" class=\"line\">test </span>") + end + end end describe 'custom highlighting from .gitattributes' do diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb new file mode 100644 index 00000000000..5ae178414cc --- /dev/null +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AvatarRestorer, lib: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:project) { create(:empty_project) } + + before do + allow_any_instance_of(described_class).to receive(:avatar_export_file) + .and_return(Rails.root + "spec/fixtures/dk.png") + end + + after do + project.remove_avatar! + end + + it 'restores a project avatar' do + expect(described_class.new(project: project, shared: shared).restore).to be true + end + + it 'saves the avatar into the project' do + described_class.new(project: project, shared: shared).restore + + expect(project.reload.avatar.file.exists?).to be true + end +end diff --git a/spec/lib/gitlab/import_export/avatar_saver_spec.rb b/spec/lib/gitlab/import_export/avatar_saver_spec.rb new file mode 100644 index 00000000000..d6ee94442cb --- /dev/null +++ b/spec/lib/gitlab/import_export/avatar_saver_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::AvatarSaver, lib: true do + let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } + let(:export_path) { "#{Dir::tmpdir}/project_tree_saver_spec" } + let(:project_with_avatar) { create(:empty_project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } + let(:project) { create(:empty_project) } + + before do + FileUtils.mkdir_p("#{shared.export_path}/avatar/") + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + end + + after do + FileUtils.rm_rf("#{shared.export_path}/avatar") + end + + it 'saves a project avatar' do + described_class.new(project: project_with_avatar, shared: shared).save + + expect(File).to exist("#{shared.export_path}/avatar/dk.png") + end + + it 'is fine not to have an avatar' do + expect(described_class.new(project: project, shared: shared).save).to be true + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4113d829c3c..b5550ca1963 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -18,7 +18,6 @@ "position": 0, "branch_name": null, "description": "Aliquam enim illo et possimus.", - "milestone_id": 18, "state": "opened", "iid": 10, "updated_by_id": null, @@ -27,6 +26,52 @@ "due_date": null, "moved_to_id": null, "test_ee_field": "test", + "milestone": { + "id": 1, + "title": "v0.0", + "project_id": 8, + "description": "test milestone", + "due_date": null, + "created_at": "2016-06-14T15:02:04.415Z", + "updated_at": "2016-06-14T15:02:04.415Z", + "state": "active", + "iid": 1, + "events": [ + { + "id": 487, + "target_type": "Milestone", + "target_id": 1, + "title": null, + "data": null, + "project_id": 46, + "created_at": "2016-06-14T15:02:04.418Z", + "updated_at": "2016-06-14T15:02:04.418Z", + "action": 1, + "author_id": 18 + } + ] + }, + "label_links": [ + { + "id": 2, + "label_id": 2, + "target_id": 3, + "target_type": "Issue", + "created_at": "2016-07-22T08:57:02.840Z", + "updated_at": "2016-07-22T08:57:02.840Z", + "label": { + "id": 2, + "title": "test2", + "color": "#428bca", + "project_id": 8, + "created_at": "2016-07-22T08:55:44.161Z", + "updated_at": "2016-07-22T08:55:44.161Z", + "template": false, + "description": "", + "priority": null + } + } + ], "notes": [ { "id": 351, @@ -233,7 +278,6 @@ "position": 0, "branch_name": null, "description": "Voluptate vel reprehenderit facilis omnis voluptas magnam tenetur.", - "milestone_id": 16, "state": "opened", "iid": 9, "updated_by_id": null, @@ -447,7 +491,6 @@ "position": 0, "branch_name": null, "description": "Ea recusandae neque autem tempora.", - "milestone_id": 16, "state": "closed", "iid": 8, "updated_by_id": null, @@ -661,7 +704,6 @@ "position": 0, "branch_name": null, "description": "Maiores architecto quos in dolorem.", - "milestone_id": 17, "state": "opened", "iid": 7, "updated_by_id": null, @@ -875,7 +917,6 @@ "position": 0, "branch_name": null, "description": "Ut aut ut et tenetur velit aut id modi.", - "milestone_id": 16, "state": "opened", "iid": 6, "updated_by_id": null, @@ -1089,7 +1130,6 @@ "position": 0, "branch_name": null, "description": "Dicta nisi nihil non ipsa velit.", - "milestone_id": 20, "state": "closed", "iid": 5, "updated_by_id": null, @@ -1303,7 +1343,6 @@ "position": 0, "branch_name": null, "description": "Ut et explicabo vel voluptatem consequuntur ut sed.", - "milestone_id": 19, "state": "closed", "iid": 4, "updated_by_id": null, @@ -1517,7 +1556,6 @@ "position": 0, "branch_name": null, "description": "Non asperiores velit accusantium voluptate.", - "milestone_id": 18, "state": "closed", "iid": 3, "updated_by_id": null, @@ -1731,7 +1769,6 @@ "position": 0, "branch_name": null, "description": "Molestiae corporis magnam et fugit aliquid nulla quia.", - "milestone_id": 17, "state": "closed", "iid": 2, "updated_by_id": null, @@ -1945,7 +1982,6 @@ "position": 0, "branch_name": null, "description": "Quod ad architecto qui est sed quia.", - "milestone_id": 20, "state": "closed", "iid": 1, "updated_by_id": null, @@ -2259,117 +2295,6 @@ "author_id": 25 } ] - }, - { - "id": 18, - "title": "v2.0", - "project_id": 5, - "description": "Error dolorem rerum aut nulla.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.576Z", - "updated_at": "2016-06-14T15:02:04.576Z", - "state": "active", - "iid": 3, - "events": [ - { - "id": 242, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 1 - }, - { - "id": 58, - "target_type": "Milestone", - "target_id": 18, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.579Z", - "updated_at": "2016-06-14T15:02:04.579Z", - "action": 1, - "author_id": 22 - } - ] - }, - { - "id": 17, - "title": "v1.0", - "project_id": 5, - "description": "Molestiae perspiciatis voluptates doloremque commodi veniam consequatur.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.569Z", - "updated_at": "2016-06-14T15:02:04.569Z", - "state": "active", - "iid": 2, - "events": [ - { - "id": 243, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 1 - }, - { - "id": 57, - "target_type": "Milestone", - "target_id": 17, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.570Z", - "updated_at": "2016-06-14T15:02:04.570Z", - "action": 1, - "author_id": 20 - } - ] - }, - { - "id": 16, - "title": "v0.0", - "project_id": 5, - "description": "Velit numquam et sed sit.", - "due_date": null, - "created_at": "2016-06-14T15:02:04.561Z", - "updated_at": "2016-06-14T15:02:04.561Z", - "state": "closed", - "iid": 1, - "events": [ - { - "id": 244, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 36, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - }, - { - "id": 56, - "target_type": "Milestone", - "target_id": 16, - "title": null, - "data": null, - "project_id": 5, - "created_at": "2016-06-14T15:02:04.563Z", - "updated_at": "2016-06-14T15:02:04.563Z", - "action": 1, - "author_id": 26 - } - ] } ], "snippets": [ @@ -2471,7 +2396,6 @@ "title": "Cannot be automatically merged", "created_at": "2016-06-14T15:02:36.568Z", "updated_at": "2016-06-14T15:02:56.815Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -2765,7 +2689,7 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "Binary files a/.DS_Store and /dev/null differ\n", "new_path": ".DS_Store", @@ -2909,7 +2833,6 @@ "title": "Can be automatically merged", "created_at": "2016-06-14T15:02:36.418Z", "updated_at": "2016-06-14T15:02:57.013Z", - "milestone_id": null, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3138,7 +3061,7 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "--- /dev/null\n+++ b/files/ruby/feature.rb\n@@ -0,0 +1,5 @@\n+class Feature\n+ def foo\n+ puts 'bar'\n+ end\n+end\n", "new_path": "files/ruby/feature.rb", @@ -3194,7 +3117,6 @@ "title": "Qui accusantium et inventore facilis doloribus occaecati officiis.", "created_at": "2016-06-14T15:02:25.168Z", "updated_at": "2016-06-14T15:02:59.521Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3423,7 +3345,7 @@ "committer_email": "james@jameslopez.es" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "--- /dev/null\n+++ b/test\n", "new_path": "test", @@ -3479,7 +3401,6 @@ "title": "In voluptas aut sequi voluptatem ullam vel corporis illum consequatur.", "created_at": "2016-06-14T15:02:24.760Z", "updated_at": "2016-06-14T15:02:59.749Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -3960,7 +3881,7 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "Binary files a/.DS_Store and /dev/null differ\n", "new_path": ".DS_Store", @@ -4170,7 +4091,6 @@ "title": "Voluptates consequatur eius nemo amet libero animi illum delectus tempore.", "created_at": "2016-06-14T15:02:24.415Z", "updated_at": "2016-06-14T15:02:59.958Z", - "milestone_id": 17, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -4597,7 +4517,7 @@ "committer_email": "marmis85@gmail.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n", "new_path": "CHANGELOG", @@ -4719,7 +4639,6 @@ "title": "In a rerum harum nihil accusamus aut quia nobis non.", "created_at": "2016-06-14T15:02:24.000Z", "updated_at": "2016-06-14T15:03:00.225Z", - "milestone_id": 19, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5108,7 +5027,7 @@ "committer_email": "stanhu@packetzoom.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "--- a/CHANGELOG\n+++ b/CHANGELOG\n@@ -1,4 +1,6 @@\n-v 6.7.0\n+v6.8.0\n+\n+v6.7.0\n - Add support for Gemnasium as a Project Service (Olivier Gonzalez)\n - Add edit file button to MergeRequest diff\n - Public groups (Jason Hollingsworth)\n", "new_path": "CHANGELOG", @@ -5219,7 +5138,6 @@ "title": "Corporis provident similique perspiciatis dolores eos animi.", "created_at": "2016-06-14T15:02:23.767Z", "updated_at": "2016-06-14T15:03:00.475Z", - "milestone_id": 18, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5434,7 +5352,7 @@ "id": 11, "state": "empty", "st_commits": null, - "st_diffs": [ + "utf8_st_diffs": [ ], "merge_request_id": 11, @@ -5480,7 +5398,6 @@ "title": "Eligendi reprehenderit doloribus quia et sit id.", "created_at": "2016-06-14T15:02:23.014Z", "updated_at": "2016-06-14T15:03:00.685Z", - "milestone_id": 20, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -5961,7 +5878,7 @@ "committer_email": "dmitriy.zaporozhets@gmail.com" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "Binary files a/.DS_Store and /dev/null differ\n", "new_path": ".DS_Store", @@ -6171,7 +6088,6 @@ "title": "Et ipsam voluptas velit sequi illum ut.", "created_at": "2016-06-14T15:02:22.825Z", "updated_at": "2016-06-14T15:03:00.904Z", - "milestone_id": 16, "state": "opened", "merge_status": "unchecked", "target_project_id": 5, @@ -6400,7 +6316,7 @@ "committer_email": "james@jameslopez.es" } ], - "st_diffs": [ + "utf8_st_diffs": [ { "diff": "--- /dev/null\n+++ b/test\n", "new_path": "test", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 877be300262..32c0d6462f1 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe 'restore project tree' do + let(:user) { create(:user) } let(:namespace) { create(:namespace, owner: user) } let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } @@ -53,6 +54,24 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do expect(event.note.noteable.project).not_to be_nil end end + + it 'has the correct data for merge request st_diffs' do + # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ + + expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) + end + + it 'has labels associated to label links, associated to issues' do + restored_project_json + + expect(Label.first.label_links.first.target).not_to be_nil + end + + it 'has milestones associated to issues' do + restored_project_json + + expect(Milestone.find_by_description('test milestone').issues).not_to be_empty + end end end end diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 1424de9e60b..3a86a4ce07c 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -31,10 +31,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json).to include({ "visibility_level" => 20 }) end - it 'has events' do - expect(saved_project_json['milestones'].first['events']).not_to be_empty - end - it 'has milestones' do expect(saved_project_json['milestones']).not_to be_empty end @@ -43,8 +39,12 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do expect(saved_project_json['merge_requests']).not_to be_empty end - it 'has labels' do - expect(saved_project_json['labels']).not_to be_empty + it 'has merge request\'s milestones' do + expect(saved_project_json['merge_requests'].first['milestone']).not_to be_empty + end + + it 'has events' do + expect(saved_project_json['merge_requests'].first['milestone']['events']).not_to be_empty end it 'has snippets' do @@ -102,25 +102,38 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do it 'has ci pipeline notes' do expect(saved_project_json['pipelines'].first['notes']).not_to be_empty end + + it 'has labels with no associations' do + expect(saved_project_json['labels']).not_to be_empty + end + + it 'has labels associated to records' do + expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty + end + + it 'does not complain about non UTF-8 characters in MR diffs' do + ActiveRecord::Base.connection.execute("UPDATE merge_request_diffs SET st_diffs = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'") + + expect(project_tree_saver.save).to be true + end end end def setup_project issue = create(:issue, assignee: user) - merge_request = create(:merge_request) - label = create(:label) snippet = create(:project_snippet) release = create(:release) project = create(:project, :public, issues: [issue], - merge_requests: [merge_request], - labels: [label], snippets: [snippet], releases: [release] ) - + label = create(:label, project: project) + create(:label_link, label: label, target: issue) + milestone = create(:milestone, project: project) + merge_request = create(:merge_request, source_project: project, milestone: milestone) commit_status = create(:commit_status, project: project) ci_pipeline = create(:ci_pipeline, @@ -130,7 +143,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do statuses: [commit_status]) create(:ci_build, pipeline: ci_pipeline, project: project) - milestone = create(:milestone, project: project) + create(:milestone, project: project) create(:note, noteable: issue, project: project) create(:note, noteable: merge_request, project: project) create(:note, noteable: snippet, project: project) diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb index afb3e26f8fb..1dcf2c0668b 100644 --- a/spec/lib/gitlab/incoming_email_spec.rb +++ b/spec/lib/gitlab/incoming_email_spec.rb @@ -43,9 +43,9 @@ describe Gitlab::IncomingEmail, lib: true do end end - context 'self.key_from_fallback_reply_message_id' do + context 'self.key_from_fallback_message_id' do it 'returns reply key' do - expect(described_class.key_from_fallback_reply_message_id('reply-key@localhost')).to eq('key') + expect(described_class.key_from_fallback_message_id('reply-key@localhost')).to eq('key') end end end diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index 8809b7e3f12..d88bcae41fb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -39,6 +39,12 @@ describe Gitlab::Metrics::Instrumentation do allow(@dummy).to receive(:name).and_return('Dummy') end + describe '.series' do + it 'returns a String' do + expect(described_class.series).to be_an_instance_of(String) + end + end + describe '.configure' do it 'yields self' do described_class.configure do |c| @@ -78,8 +84,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy.foo') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.foo end @@ -157,8 +162,7 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) - expect(transaction).to receive(:measure_method). - with('Dummy#bar') + expect_any_instance_of(Gitlab::Metrics::MethodCall).to receive(:measure) @dummy.new.bar end diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb index cf0e282c2fb..9e2ea89a712 100644 --- a/spec/lib/gitlab/metrics/system_spec.rb +++ b/spec/lib/gitlab/metrics/system_spec.rb @@ -28,20 +28,20 @@ describe Gitlab::Metrics::System do end describe '.cpu_time' do - it 'returns a Float' do - expect(described_class.cpu_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.cpu_time).to be_an_instance_of(Fixnum) end end describe '.real_time' do - it 'returns a Float' do - expect(described_class.real_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.real_time).to be_an_instance_of(Fixnum) end end describe '.monotonic_time' do - it 'returns a Float' do - expect(described_class.monotonic_time).to be_an_instance_of(Float) + it 'returns a Fixnum' do + expect(described_class.monotonic_time).to be_an_instance_of(Fixnum) end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 3b1c67a2147..f1a191d9410 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -46,19 +46,11 @@ describe Gitlab::Metrics::Transaction do end end - describe '#measure_method' do - it 'adds a new method if it does not exist already' do - transaction.measure_method('Foo#bar') { 'foo' } + describe '#method_call_for' do + it 'returns a MethodCall' do + method = transaction.method_call_for('Foo#bar') - expect(transaction.methods['Foo#bar']). - to be_an_instance_of(Gitlab::Metrics::MethodCall) - end - - it 'adds timings to an existing method call' do - transaction.measure_method('Foo#bar') { 'foo' } - transaction.measure_method('Foo#bar') { 'foo' } - - expect(transaction.methods['Foo#bar'].call_count).to eq(2) + expect(method).to be_an_instance_of(Gitlab::Metrics::MethodCall) end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 96f7eabbca6..84f9475a0f8 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -147,4 +147,10 @@ describe Gitlab::Metrics do end end end + + describe '#series_prefix' do + it 'returns a String' do + expect(described_class.series_prefix).to be_an_instance_of(String) + end + end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index aa9ec243498..5bb095366fa 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -44,7 +44,7 @@ describe Gitlab::UserAccess, lib: true do describe 'push to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_push: true + @branch = create :protected_branch, :developers_can_push, project: project end it 'returns true if user is a master' do @@ -65,7 +65,7 @@ describe Gitlab::UserAccess, lib: true do describe 'merge to protected branch if allowed for developers' do before do - @branch = create :protected_branch, project: project, developers_can_merge: true + @branch = create :protected_branch, :developers_can_merge, project: project end it 'returns true if user is a master' do diff --git a/spec/lib/repository_cache_spec.rb b/spec/lib/repository_cache_spec.rb index 63b5292b098..f227926f39c 100644 --- a/spec/lib/repository_cache_spec.rb +++ b/spec/lib/repository_cache_spec.rb @@ -1,33 +1,34 @@ -require_relative '../../lib/repository_cache' +require 'spec_helper' describe RepositoryCache, lib: true do + let(:project) { create(:project) } let(:backend) { double('backend').as_null_object } - let(:cache) { RepositoryCache.new('example', backend) } + let(:cache) { RepositoryCache.new('example', project.id, backend) } describe '#cache_key' do it 'includes the namespace' do - expect(cache.cache_key(:foo)).to eq 'foo:example' + expect(cache.cache_key(:foo)).to eq "foo:example:#{project.id}" end end describe '#expire' do it 'expires the given key from the cache' do cache.expire(:foo) - expect(backend).to have_received(:delete).with('foo:example') + expect(backend).to have_received(:delete).with("foo:example:#{project.id}") end end describe '#fetch' do it 'fetches the given key from the cache' do cache.fetch(:bar) - expect(backend).to have_received(:fetch).with('bar:example') + expect(backend).to have_received(:fetch).with("bar:example:#{project.id}") end it 'accepts a block' do p = -> {} cache.fetch(:baz, &p) - expect(backend).to have_received(:fetch).with('baz:example', &p) + expect(backend).to have_received(:fetch).with("baz:example:#{project.id}", &p) end end end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 0a9b10bebea..3685b2b17b5 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -12,7 +12,7 @@ describe Notify do context 'for a project' do describe 'items that are assignable, the email' do let(:current_user) { create(:user, email: "current@email.com") } - let(:assignee) { create(:user, email: 'assignee@example.com') } + let(:assignee) { create(:user, email: 'assignee@example.com', name: 'John Doe') } let(:previous_assignee) { create(:user, name: 'Previous Assignee') } shared_examples 'an assignee email' do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 1acb5846fcf..853f6943cef 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -1,6 +1,62 @@ require 'spec_helper' describe Ability, lib: true do + describe '.can_edit_note?' do + let(:project) { create(:empty_project) } + let!(:note) { create(:note_on_issue, project: project) } + + context 'using an anonymous user' do + it 'returns false' do + expect(described_class.can_edit_note?(nil, note)).to be_falsy + end + end + + context 'using a system note' do + it 'returns false' do + system_note = create(:note, system: true) + user = create(:user) + + expect(described_class.can_edit_note?(user, system_note)).to be_falsy + end + end + + context 'using users with different access levels' do + let(:user) { create(:user) } + + it 'returns true for the author' do + expect(described_class.can_edit_note?(note.author, note)).to be_truthy + end + + it 'returns false for a guest user' do + project.team << [user, :guest] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns false for a developer' do + project.team << [user, :developer] + + expect(described_class.can_edit_note?(user, note)).to be_falsy + end + + it 'returns true for a master' do + project.team << [user, :master] + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + + it 'returns true for a group owner' do + group = create(:group) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::MASTER) + group.add_owner(user) + + expect(described_class.can_edit_note?(user, note)).to be_truthy + end + end + end + describe '.users_that_can_read_project' do context 'using a public project' do it 'returns all the users' do @@ -114,4 +170,52 @@ describe Ability, lib: true do end end end + + describe '.issues_readable_by_user' do + context 'with an admin user' do + it 'returns all given issues' do + user = build(:user, admin: true) + issue = build(:issue) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + end + + context 'with a regular user' do + it 'returns the issues readable by the user' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(described_class.issues_readable_by_user([issue], user)). + to eq([issue]) + end + + it 'returns an empty Array when no issues are readable' do + user = build(:user) + issue = build(:issue) + + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(described_class.issues_readable_by_user([issue], user)).to eq([]) + end + end + + context 'without a regular user' do + it 'returns issues that are publicly visible' do + hidden_issue = build(:issue) + visible_issue = build(:issue) + + expect(hidden_issue).to receive(:publicly_visible?).and_return(false) + expect(visible_issue).to receive(:publicly_visible?).and_return(true) + + issues = described_class. + issues_readable_by_user([hidden_issue, visible_issue]) + + expect(issues).to eq([visible_issue]) + end + end + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 2ea1320267c..fb040ba82bc 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -54,23 +54,60 @@ describe ApplicationSetting, models: true do context 'restricted signup domains' do it 'set single domain' do - setting.restricted_signup_domains_raw = 'example.com' - expect(setting.restricted_signup_domains).to eq(['example.com']) + setting.domain_whitelist_raw = 'example.com' + expect(setting.domain_whitelist).to eq(['example.com']) end it 'set multiple domains with spaces' do - setting.restricted_signup_domains_raw = 'example.com *.example.com' - expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + setting.domain_whitelist_raw = 'example.com *.example.com' + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) end it 'set multiple domains with newlines and a space' do - setting.restricted_signup_domains_raw = "example.com\n *.example.com" - expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + setting.domain_whitelist_raw = "example.com\n *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) end it 'set multiple domains with commas' do - setting.restricted_signup_domains_raw = "example.com, *.example.com" - expect(setting.restricted_signup_domains).to eq(['example.com', '*.example.com']) + setting.domain_whitelist_raw = "example.com, *.example.com" + expect(setting.domain_whitelist).to eq(['example.com', '*.example.com']) + end + end + + context 'blacklisted signup domains' do + it 'set single domain' do + setting.domain_blacklist_raw = 'example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com') + end + + it 'set multiple domains with spaces' do + setting.domain_blacklist_raw = 'example.com *.example.com' + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'set multiple domains with newlines and a space' do + setting.domain_blacklist_raw = "example.com\n *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'set multiple domains with commas' do + setting.domain_blacklist_raw = "example.com, *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'set multiple domains with semicolon' do + setting.domain_blacklist_raw = "example.com; *.example.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com') + end + + it 'set multiple domains with mixture of everything' do + setting.domain_blacklist_raw = "example.com; *.example.com\n test.com\sblock.com yes.com" + expect(setting.domain_blacklist).to contain_exactly('example.com', '*.example.com', 'test.com', 'block.com', 'yes.com') + end + + it 'set multiple domain with file' do + setting.domain_blacklist_file = File.open(Rails.root.join('spec/fixtures/', 'domain_blacklist.txt')) + expect(setting.domain_blacklist).to contain_exactly('example.com', 'test.com', 'foo.bar') end end end diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb index 78e95c8fac5..1e5d6a34f83 100644 --- a/spec/models/blob_spec.rb +++ b/spec/models/blob_spec.rb @@ -33,6 +33,22 @@ describe Blob do end end + describe '#video?' do + it 'is falsey with image extension' do + git_blob = Gitlab::Git::Blob.new(name: 'image.png') + + expect(described_class.decorate(git_blob)).not_to be_video + end + + UploaderHelper::VIDEO_EXT.each do |ext| + it "is truthy when extension is .#{ext}" do + git_blob = Gitlab::Git::Blob.new(name: "video.#{ext}") + + expect(described_class.decorate(git_blob)).to be_video + end + end + end + describe '#to_partial_path' do def stubbed_blob(overrides = {}) overrides.reverse_merge!( diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 481416319dd..dc88697199b 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -5,7 +5,9 @@ describe Ci::Build, models: true do let(:pipeline) do create(:ci_pipeline, project: project, - sha: project.commit.id) + sha: project.commit.id, + ref: project.default_branch, + status: 'success') end let(:build) { create(:ci_build, pipeline: pipeline) } @@ -191,77 +193,185 @@ describe Ci::Build, models: true do end describe '#variables' do + let(:container_registry_enabled) { false } + let(:predefined_variables) do + [ + { key: 'CI', value: 'true', public: true }, + { key: 'GITLAB_CI', value: 'true', public: true }, + { key: 'CI_BUILD_ID', value: build.id.to_s, public: true }, + { key: 'CI_BUILD_TOKEN', value: build.token, public: false }, + { key: 'CI_BUILD_REF', value: build.sha, public: true }, + { key: 'CI_BUILD_BEFORE_SHA', value: build.before_sha, public: true }, + { key: 'CI_BUILD_REF_NAME', value: 'master', public: true }, + { key: 'CI_BUILD_NAME', value: 'test', public: true }, + { key: 'CI_BUILD_STAGE', value: 'test', public: true }, + { key: 'CI_SERVER_NAME', value: 'GitLab', public: true }, + { key: 'CI_SERVER_VERSION', value: Gitlab::VERSION, public: true }, + { key: 'CI_SERVER_REVISION', value: Gitlab::REVISION, public: true }, + { key: 'CI_PROJECT_ID', value: project.id.to_s, public: true }, + { key: 'CI_PROJECT_NAME', value: project.path, public: true }, + { key: 'CI_PROJECT_PATH', value: project.path_with_namespace, public: true }, + { key: 'CI_PROJECT_NAMESPACE', value: project.namespace.path, public: true }, + { key: 'CI_PROJECT_URL', value: project.web_url, public: true }, + { key: 'CI_PIPELINE_ID', value: pipeline.id.to_s, public: true } + ] + end + + before do + stub_container_registry_config(enabled: container_registry_enabled, host_port: 'registry.example.com') + end + + subject { build.variables } + context 'returns variables' do - subject { build.variables } + before do + build.yaml_variables = [] + end - let(:predefined_variables) do - [ - { key: :CI_BUILD_NAME, value: 'test', public: true }, - { key: :CI_BUILD_STAGE, value: 'stage', public: true }, - ] + it { is_expected.to eq(predefined_variables) } + end + + context 'when build is for tag' do + let(:tag_variable) do + { key: 'CI_BUILD_TAG', value: 'master', public: true } end - let(:yaml_variables) do - [ - { key: :DB_NAME, value: 'postgres', public: true } - ] + before do + build.update_attributes(tag: true) + end + + it { is_expected.to include(tag_variable) } + end + + context 'when secure variable is defined' do + let(:secure_variable) do + { key: 'SECRET_KEY', value: 'secret_value', public: false } end before do - build.update_attributes(stage: 'stage', yaml_variables: yaml_variables) + build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') end - it { is_expected.to eq(predefined_variables + yaml_variables) } + it { is_expected.to include(secure_variable) } + end - context 'for tag' do - let(:tag_variable) do - [ - { key: :CI_BUILD_TAG, value: 'master', public: true } - ] - end + context 'when build is for triggers' do + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } + let(:user_trigger_variable) do + { key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1', public: false } + end + let(:predefined_trigger_variable) do + { key: 'CI_BUILD_TRIGGERED', value: 'true', public: true } + end - before do - build.update_attributes(tag: true) - end + before do + build.trigger_request = trigger_request + end - it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) } + it { is_expected.to include(user_trigger_variable) } + it { is_expected.to include(predefined_trigger_variable) } + end + + context 'when yaml_variables are undefined' do + before do + build.yaml_variables = nil end - context 'and secure variables' do - let(:secure_variables) do - [ - { key: 'SECRET_KEY', value: 'secret_value', public: false } - ] + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) end - before do - build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value') + context 'if config is not found' do + let(:config) { nil } + + it { is_expected.to eq(predefined_variables) } end - it { is_expected.to eq(predefined_variables + yaml_variables + secure_variables) } + context 'if config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq(predefined_variables) } + end - context 'and trigger variables' do - let(:trigger) { create(:ci_trigger, project: project) } - let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline, trigger: trigger) } - let(:trigger_variables) do - [ - { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false } - ] + context 'if config has variables' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + variables: { + KEY: 'value' + } + } + }) end - let(:predefined_trigger_variable) do - [ - { key: :CI_BUILD_TRIGGERED, value: 'true', public: true } - ] + let(:variables) do + [{ key: :KEY, value: 'value', public: true }] end - before do - build.trigger_request = trigger_request - end + it { is_expected.to eq(predefined_variables + variables) } + end + end + end - it { is_expected.to eq(predefined_variables + predefined_trigger_variable + yaml_variables + secure_variables + trigger_variables) } + context 'when container registry is enabled' do + let(:container_registry_enabled) { true } + let(:ci_registry) do + { key: 'CI_REGISTRY', value: 'registry.example.com', public: true } + end + let(:ci_registry_image) do + { key: 'CI_REGISTRY_IMAGE', value: project.container_registry_repository_url, public: true } + end + + context 'and is disabled for project' do + before do + project.update(container_registry_enabled: false) end + + it { is_expected.to include(ci_registry) } + it { is_expected.not_to include(ci_registry_image) } + end + + context 'and is enabled for project' do + before do + project.update(container_registry_enabled: true) + end + + it { is_expected.to include(ci_registry) } + it { is_expected.to include(ci_registry_image) } end end + + context 'when runner is assigned to build' do + let(:runner) { create(:ci_runner, description: 'description', tag_list: ['docker', 'linux']) } + + before do + build.update(runner: runner) + end + + it { is_expected.to include({ key: 'CI_RUNNER_ID', value: runner.id.to_s, public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_DESCRIPTION', value: 'description', public: true }) } + it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } + end + + context 'returns variables in valid order' do + before do + allow(build).to receive(:predefined_variables) { ['predefined'] } + allow(project).to receive(:predefined_variables) { ['project'] } + allow(pipeline).to receive(:predefined_variables) { ['pipeline'] } + allow(build).to receive(:yaml_variables) { ['yaml'] } + allow(project).to receive(:secret_variables) { ['secret'] } + end + + it { is_expected.to eq(%w[predefined project pipeline yaml secret]) } + end end describe '#has_tags?' do @@ -612,7 +722,7 @@ describe Ci::Build, models: true do describe '#erasable?' do subject { build.erasable? } - it { is_expected.to eq true } + it { is_expected.to be_truthy } end describe '#erased?' do @@ -620,7 +730,7 @@ describe Ci::Build, models: true do subject { build.erased? } context 'build has not been erased' do - it { is_expected.to be false } + it { is_expected.to be_falsey } end context 'build has been erased' do @@ -628,12 +738,13 @@ describe Ci::Build, models: true do build.erase end - it { is_expected.to be true } + it { is_expected.to be_truthy } end end context 'metadata and build trace are not available' do let!(:build) { create(:ci_build, :success, :artifacts) } + before do build.remove_artifacts_metadata! end @@ -655,6 +766,138 @@ describe Ci::Build, models: true do describe '#retryable?' do context 'when build is running' do + before do + build.run! + end + + it { expect(build).not_to be_retryable } + end + + context 'when build is finished' do + before do + build.success! + end + + it { expect(build).to be_retryable } + end + end + + describe '#manual?' do + before do + build.update(when: value) + end + + subject { build.manual? } + + context 'when is set to manual' do + let(:value) { 'manual' } + + it { is_expected.to be_truthy } + end + + context 'when set to something else' do + let(:value) { 'something else' } + + it { is_expected.to be_falsey } + end + end + + describe '#other_actions' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + let!(:other_build) { create(:ci_build, :manual, pipeline: pipeline, name: 'other action') } + + subject { build.other_actions } + + it 'returns other actions' do + is_expected.to contain_exactly(other_build) + end + + context 'when build is retried' do + let!(:new_build) { Ci::Build.retry(build) } + + it 'does not return any of them' do + is_expected.not_to include(build, new_build) + end + end + + context 'when other build is retried' do + let!(:retried_build) { Ci::Build.retry(other_build) } + + it 'returns a retried build' do + is_expected.to contain_exactly(retried_build) + end + end + end + + describe '#play' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + subject { build.play } + + it 'enques a build' do + is_expected.to be_pending + is_expected.to eq(build) + end + + context 'for success build' do + before { build.queue } + + it 'creates a new build' do + is_expected.to be_pending + is_expected.not_to eq(build) + end + end + end + + describe '#when' do + subject { build.when } + + context 'if is undefined' do + before do + build.when = nil + end + + context 'use from gitlab-ci.yml' do + before do + stub_ci_pipeline_yaml_file(config) + end + + context 'if config is not found' do + let(:config) { nil } + + it { is_expected.to eq('on_success') } + end + + context 'if config does not have a questioned job' do + let(:config) do + YAML.dump({ + test_other: { + script: 'Hello World' + } + }) + end + + it { is_expected.to eq('on_success') } + end + + context 'if config has when' do + let(:config) do + YAML.dump({ + test: { + script: 'Hello World', + when: 'always' + } + }) + end + + it { is_expected.to eq('always') } + end + end + end + end + + describe '#retryable?' do + context 'when build is running' do before { build.run! } it 'should return false' do diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 10db79bd15f..0d4c86955ce 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -260,6 +260,70 @@ describe Ci::Pipeline, models: true do expect(pipeline.reload.status).to eq('canceled') end end + + context 'when listing manual actions' do + let(:yaml) do + { + stages: ["build", "test", "staging", "production", "cleanup"], + build: { + stage: "build", + script: "BUILD", + }, + test: { + stage: "test", + script: "TEST", + }, + staging: { + stage: "staging", + script: "PUBLISH", + }, + production: { + stage: "production", + script: "PUBLISH", + when: "manual", + }, + cleanup: { + stage: "cleanup", + script: "TIDY UP", + when: "always", + }, + clear_cache: { + stage: "cleanup", + script: "CLEAR CACHE", + when: "manual", + } + } + end + + it 'returns only for skipped builds' do + # currently all builds are created + expect(create_builds).to be_truthy + expect(manual_actions).to be_empty + + # succeed stage build + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_empty + + # succeed stage test + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_empty + + # succeed stage staging and skip stage production + pipeline.builds.running_or_pending.each(&:success) + expect(manual_actions).to be_many # production and clear cache + + # succeed stage cleanup + pipeline.builds.running_or_pending.each(&:success) + + # after processing a pipeline we should have 6 builds, 5 succeeded + expect(pipeline.builds.count).to eq(6) + expect(pipeline.builds.success.count).to eq(4) + end + + def manual_actions + pipeline.manual_actions + end + end end context 'when no builds created' do @@ -416,4 +480,66 @@ describe Ci::Pipeline, models: true do end end end + + describe '#manual_actions' do + subject { pipeline.manual_actions } + + it 'when none defined' do + is_expected.to be_empty + end + + context 'when action defined' do + let!(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } + + it 'returns one action' do + is_expected.to contain_exactly(manual) + end + + context 'there are multiple of the same name' do + let!(:manual2) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy') } + + it 'returns latest one' do + is_expected.to contain_exactly(manual2) + end + end + end + end + + describe '#has_warnings?' do + subject { pipeline.has_warnings? } + + context 'build which is allowed to fail fails' do + before do + create :ci_build, :success, pipeline: pipeline, name: 'rspec' + create :ci_build, :allowed_to_fail, :failed, pipeline: pipeline, name: 'rubocop' + end + + it 'returns true' do + is_expected.to be_truthy + end + end + + context 'build which is allowed to fail succeeds' do + before do + create :ci_build, :success, pipeline: pipeline, name: 'rspec' + create :ci_build, :allowed_to_fail, :success, pipeline: pipeline, name: 'rubocop' + end + + it 'returns false' do + is_expected.to be_falsey + end + end + + context 'build is retried and succeeds' do + before do + create :ci_build, :success, pipeline: pipeline, name: 'rubocop' + create :ci_build, :failed, pipeline: pipeline, name: 'rspec' + create :ci_build, :success, pipeline: pipeline, name: 'rspec' + end + + it 'returns false' do + is_expected.to be_falsey + end + end + end end diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ba02d5fe977..d3e6a6648cc 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -13,6 +13,26 @@ describe Commit, models: true do it { is_expected.to include_module(StaticModel) } end + describe '#author' do + it 'looks up the author in a case-insensitive way' do + user = create(:user, email: commit.author_email.upcase) + expect(commit.author).to eq(user) + end + + it 'caches the author' do + user = create(:user, email: commit.author_email) + expect(RequestStore).to receive(:active?).twice.and_return(true) + expect_any_instance_of(Commit).to receive(:find_author_by_any_email).and_call_original + + expect(commit.author).to eq(user) + key = "commit_author:#{commit.author_email}" + expect(RequestStore.store[key]).to eq(user) + + expect(commit.author).to eq(user) + RequestStore.store.clear + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(commit.to_reference).to eq commit.id @@ -66,6 +86,27 @@ eos end end + describe '#full_title' do + it "returns no_commit_message when safe_message is blank" do + allow(commit).to receive(:safe_message).and_return('') + expect(commit.full_title).to eq("--no commit message") + end + + it "returns entire message if there is no newline" do + message = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec sodales id felis id blandit. Vivamus egestas lacinia lacus, sed rutrum mauris.' + + allow(commit).to receive(:safe_message).and_return(message) + expect(commit.full_title).to eq(message) + end + + it "returns first line of message if there is a newLine" do + message = commit.safe_message.split(" ").first + + allow(commit).to receive(:safe_message).and_return(message + "\n" + message) + expect(commit.full_title).to eq(message) + end + end + describe "delegation" do subject { commit } @@ -212,6 +253,7 @@ eos it 'returns the URI type at the given path' do expect(commit.uri_type('files/html')).to be(:tree) expect(commit.uri_type('files/images/logo-black.png')).to be(:raw) + expect(project.commit('video').uri_type('files/videos/intro.mp4')).to be(:raw) expect(commit.uri_type('files/js/application.js')).to be(:blob) end diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 5e652660e2c..549b0042038 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -68,7 +68,7 @@ describe Issue, "Mentionable" do describe '#create_cross_references!' do let(:project) { create(:project) } - let(:author) { double('author') } + let(:author) { build(:user) } let(:commit) { project.commit } let(:commit2) { project.commit } @@ -88,6 +88,10 @@ describe Issue, "Mentionable" do let(:author) { create(:author) } let(:issues) { create_list(:issue, 2, project: project, author: author) } + before do + project.team << [author, Gitlab::Access::DEVELOPER] + end + context 'before changes are persisted' do it 'ignores pre-existing references' do issue = create_issue(description: issues[0].to_reference) diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb index b273018707f..7df3df4bb9e 100644 --- a/spec/models/deployment_spec.rb +++ b/spec/models/deployment_spec.rb @@ -11,6 +11,7 @@ describe Deployment, models: true do it { is_expected.to delegate_method(:name).to(:environment).with_prefix } it { is_expected.to delegate_method(:commit).to(:project) } it { is_expected.to delegate_method(:commit_title).to(:commit).as(:try) } + it { is_expected.to delegate_method(:manual_actions).to(:deployable).as(:try) } it { is_expected.to validate_presence_of(:ref) } it { is_expected.to validate_presence_of(:sha) } diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb index af8e890ca95..1fa96eb1f15 100644 --- a/spec/models/diff_note_spec.rb +++ b/spec/models/diff_note_spec.rb @@ -119,7 +119,7 @@ describe DiffNote, models: true do context "when the merge request's diff refs don't match that of the diff note" do before do - allow(subject.noteable).to receive(:diff_refs).and_return(commit.diff_refs) + allow(subject.noteable).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "returns false" do @@ -168,7 +168,7 @@ describe DiffNote, models: true do context "when the note is outdated" do before do - allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs) + allow(merge_request).to receive(:diff_sha_refs).and_return(commit.diff_refs) end it "uses the DiffPositionUpdateService" do diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 7629af6a570..8a84ac0a7c7 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -11,4 +11,23 @@ describe Environment, models: true do it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_within(0..255) } + + it { is_expected.to validate_length_of(:external_url).is_within(0..255) } + + # To circumvent a not null violation of the name column: + # https://github.com/thoughtbot/shoulda-matchers/issues/336 + it 'validates uniqueness of :external_url' do + create(:environment) + + is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) + end + + describe '#nullify_external_url' do + it 'replaces a blank url with nil' do + env = build(:environment, external_url: "") + + expect(env.save).to be true + expect(env.external_url).to be_nil + end + end end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index b87d68283e6..3259f795296 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,6 +22,26 @@ describe Issue, models: true do it { is_expected.to have_db_index(:deleted_at) } end + describe 'visible_to_user' do + let(:user) { create(:user) } + let(:authorized_user) { create(:user) } + let(:project) { create(:project, namespace: authorized_user.namespace) } + let!(:public_issue) { create(:issue, project: project) } + let!(:confidential_issue) { create(:issue, project: project, confidential: true) } + + it 'returns non confidential issues for nil user' do + expect(Issue.visible_to_user(nil).count).to be(1) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(Issue.visible_to_user(user).count).to be(1) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(Issue.visible_to_user(authorized_user).count).to be(2) + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(subject.to_reference).to eq "##{subject.iid}" @@ -286,4 +306,257 @@ describe Issue, models: true do expect(user2.assigned_open_issues_count).to eq(1) end end + + describe '#visible_to_user?' do + context 'with a user' do + let(:user) { build(:user) } + let(:issue) { build(:issue) } + + it 'returns true when the issue is readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(true) + + expect(issue.visible_to_user?(user)).to eq(true) + end + + it 'returns false when the issue is not readable' do + expect(issue).to receive(:readable_by?).with(user).and_return(false) + + expect(issue.visible_to_user?(user)).to eq(false) + end + end + + context 'without a user' do + let(:issue) { build(:issue) } + + it 'returns true when the issue is publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(true) + + expect(issue.visible_to_user?).to eq(true) + end + + it 'returns false when the issue is not publicly visible' do + expect(issue).to receive(:publicly_visible?).and_return(false) + + expect(issue.visible_to_user?).to eq(false) + end + end + end + + describe '#readable_by?' do + describe 'with a regular user that is not a team member' do + let(:user) { create(:user) } + + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, project: project, confidential: true) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + context 'using an internal user' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + + context 'using an external user' do + before do + allow(user).to receive(:external?).and_return(true) + end + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + + context 'when the user is the project owner' do + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_readable_by(user) + end + end + end + end + + context 'with a regular user that is a team member' do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + context 'using a public project' do + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + before do + project.team << [user, Gitlab::Access::DEVELOPER] + end + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + context 'with an admin user' do + let(:project) { create(:empty_project) } + let(:user) { create(:user, admin: true) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_readable_by(user) + end + + it 'returns true for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).to be_readable_by(user) + end + end + end + + describe '#publicly_visible?' do + context 'using a public project' do + let(:project) { create(:empty_project, :public) } + + it 'returns true for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using an internal project' do + let(:project) { create(:empty_project, :internal) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + + context 'using a private project' do + let(:project) { create(:empty_project, :private) } + + it 'returns false for a regular issue' do + issue = build(:issue, project: project) + + expect(issue).not_to be_publicly_visible + end + + it 'returns false for a confidential issue' do + issue = build(:issue, :confidential, project: project) + + expect(issue).not_to be_publicly_visible + end + end + end end diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 49cf3d8633a..6d68e52a822 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -16,12 +16,13 @@ describe Key, models: true do end describe "Methods" do + let(:user) { create(:user) } it { is_expected.to respond_to :projects } it { is_expected.to respond_to :publishable_key } describe "#publishable_keys" do - it 'strips all personal information' do - expect(build(:key).publishable_key).not_to match(/dummy@gitlab/) + it 'replaces SSH key comment with simple identifier of username + hostname' do + expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)") end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 40181a8b906..44cd3c08718 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -79,6 +79,18 @@ describe Member, models: true do @accepted_request_member = project.requesters.find_by(user_id: accepted_request_user.id).tap { |m| m.accept_request } end + describe '.access_for_user_ids' do + it 'returns the right access levels' do + users = [@owner_user.id, @master_user.id] + expected = { + @owner_user.id => Gitlab::Access::OWNER, + @master_user.id => Gitlab::Access::MASTER + } + + expect(described_class.access_for_user_ids(users)).to eq(expected) + end + end + describe '.invite' do it { expect(described_class.invite).not_to include @master } it { expect(described_class.invite).to include @invited_member } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c8ad7ab3e7f..21d22c776e9 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -65,11 +65,11 @@ describe MergeRequest, models: true do end describe '#target_branch_sha' do - context 'when the target branch does not exist anymore' do - let(:project) { create(:project) } + let(:project) { create(:project) } - subject { create(:merge_request, source_project: project, target_project: project) } + subject { create(:merge_request, source_project: project, target_project: project) } + context 'when the target branch does not exist' do before do project.repository.raw_repository.delete_branch(subject.target_branch) end @@ -78,6 +78,12 @@ describe MergeRequest, models: true do expect(subject.target_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.target_branch_sha = '8ffb3c15a5475e59ae909384297fede4badcb4c7' + + expect(subject.target_branch_sha).to eq '8ffb3c15a5475e59ae909384297fede4badcb4c7' + end end describe '#source_branch_sha' do @@ -103,6 +109,12 @@ describe MergeRequest, models: true do expect(subject.source_branch_sha).to be_nil end end + + it 'returns memoized value' do + subject.source_branch_sha = '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + + expect(subject.source_branch_sha).to eq '2e5d3239642f9161dcbbc4b70a211a68e5e45e2b' + end end describe '#to_reference' do @@ -674,4 +686,28 @@ describe MergeRequest, models: true do subject.reload_diff end end + + describe "#diff_sha_refs" do + context "with diffs" do + subject { create(:merge_request, :with_diffs) } + + it "does not touch the repository" do + subject # Instantiate the object + + expect_any_instance_of(Repository).not_to receive(:commit) + + subject.diff_sha_refs + end + + it "returns expected diff_refs" do + expected_diff_refs = Gitlab::Diff::DiffRefs.new( + base_sha: subject.merge_request_diff.base_commit_sha, + start_sha: subject.merge_request_diff.start_commit_sha, + head_sha: subject.merge_request_diff.head_commit_sha + ) + + expect(subject.diff_sha_refs).to eq(expected_diff_refs) + end + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7d0697dab42..1243f5420a7 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -135,22 +135,30 @@ describe Note, models: true do let!(:note2) { create(:note_on_issue) } it "reads the rendered note body from the cache" do - expect(Banzai::Renderer).to receive(:render). - with(note1.note, - pipeline: :note, - cache_key: [note1, "note"], - project: note1.project, - author: note1.author) - - expect(Banzai::Renderer).to receive(:render). - with(note2.note, - pipeline: :note, - cache_key: [note2, "note"], - project: note2.project, - author: note2.author) - - note1.all_references - note2.all_references + expect(Banzai::Renderer).to receive(:cache_collection_render). + with([{ + text: note1.note, + context: { + pipeline: :note, + cache_key: [note1, "note"], + project: note1.project, + author: note1.author + } + }]).and_call_original + + expect(Banzai::Renderer).to receive(:cache_collection_render). + with([{ + text: note2.note, + context: { + pipeline: :note, + cache_key: [note2, "note"], + project: note2.project, + author: note2.author + } + }]).and_call_original + + note1.all_references.users + note2.all_references.users end end diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 5f618322aab..62ae5f6cf74 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -340,18 +340,36 @@ describe HipchatService, models: true do end context "#message_options" do - it "should be set to the defaults" do - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'yellow' }) + it "is set to the defaults" do + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'yellow' }) end - it "should set notfiy to true" do + it "sets notify to true" do allow(hipchat).to receive(:notify).and_return('1') - expect(hipchat.send(:message_options)).to eq({ notify: true, color: 'yellow' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: true, color: 'yellow' }) end - it "should set the color" do + it "sets the color" do allow(hipchat).to receive(:color).and_return('red') - expect(hipchat.send(:message_options)).to eq({ notify: false, color: 'red' }) + + expect(hipchat.__send__(:message_options)).to eq({ notify: false, color: 'red' }) + end + + context 'with a successful build' do + it 'uses the green color' do + build_data = { object_kind: 'build', commit: { status: 'success' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'green' }) + end + end + + context 'with a failed build' do + it 'uses the red color' do + build_data = { object_kind: 'build', commit: { status: 'failed' } } + + expect(hipchat.__send__(:message_options, build_data)).to eq({ notify: false, color: 'red' }) + end end end end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb index 155f3e74e0d..df511b1bc4c 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/models/project_services/slack_service_spec.rb @@ -124,6 +124,7 @@ describe SlackService, models: true do and_return( double(:slack_service).as_null_object ) + slack.execute(push_sample_data) end @@ -136,6 +137,76 @@ describe SlackService, models: true do ) slack.execute(push_sample_data) end + + context "event channels" do + it "uses the right channel for push event" do + slack.update_attributes(push_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + slack.execute(push_sample_data) + end + + it "uses the right channel for merge request event" do + slack.update_attributes(merge_request_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + slack.execute(@merge_sample_data) + end + + it "uses the right channel for issue event" do + slack.update_attributes(issue_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + slack.execute(@issues_sample_data) + end + + it "uses the right channel for wiki event" do + slack.update_attributes(wiki_page_channel: "random") + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + slack.execute(@wiki_page_sample_data) + end + + context "note event" do + let(:issue_note) do + create(:note_on_issue, project: project, note: "issue note") + end + + it "uses the right channel" do + slack.update_attributes(note_channel: "random") + + note_data = Gitlab::NoteDataBuilder.build(issue_note, user) + + expect(Slack::Notifier).to receive(:new). + with(webhook_url, channel: "random"). + and_return( + double(:slack_service).as_null_object + ) + + slack.execute(note_data) + end + end + end end describe "Note events" do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 9dc34276f18..e365e4e98b2 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -245,6 +245,34 @@ describe Project, models: true do end end + describe "#new_issue_address" do + let(:project) { create(:empty_project, path: "somewhere") } + let(:user) { create(:user) } + + context 'incoming email enabled' do + before do + stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab") + end + + it 'returns the address to create a new issue' do + token = user.authentication_token + address = "p+#{project.namespace.path}/#{project.path}+#{token}@gl.ab" + + expect(project.new_issue_address(user)).to eq(address) + end + end + + context 'incoming email disabled' do + before do + stub_incoming_email_setting(enabled: false) + end + + it 'returns nil' do + expect(project.new_issue_address(user)).to be_nil + end + end + end + describe 'last_activity methods' do let(:project) { create(:project) } let(:last_event) { double(created_at: Time.now) } @@ -372,12 +400,30 @@ describe Project, models: true do it { expect(@project.to_param).to eq('gitlabhq') } end + + context 'with invalid path' do + it 'returns previous path to keep project suitable for use in URLs when persisted' do + project = create(:empty_project, path: 'gitlab') + project.path = 'foo&bar' + + expect(project).not_to be_valid + expect(project.to_param).to eq 'gitlab' + end + + it 'returns current path when new record' do + project = build(:empty_project, path: 'gitlab') + project.path = 'foo&bar' + + expect(project).not_to be_valid + expect(project.to_param).to eq 'foo&bar' + end + end end describe '#repository' do let(:project) { create(:project) } - it 'should return valid repo' do + it 'returns valid repo' do expect(project.repository).to be_kind_of(Repository) end end @@ -458,6 +504,57 @@ describe Project, models: true do end end + describe '#external_wiki' do + let(:project) { create(:project) } + + context 'with an active external wiki' do + before do + create(:service, project: project, type: 'ExternalWikiService', active: true) + project.external_wiki + end + + it 'sets :has_external_wiki as true' do + expect(project.has_external_wiki).to be(true) + end + + it 'sets :has_external_wiki as false if an external wiki service is destroyed later' do + expect(project.has_external_wiki).to be(true) + + project.services.external_wikis.first.destroy + + expect(project.has_external_wiki).to be(false) + end + end + + context 'with an inactive external wiki' do + before do + create(:service, project: project, type: 'ExternalWikiService', active: false) + end + + it 'sets :has_external_wiki as false' do + expect(project.has_external_wiki).to be(false) + end + end + + context 'with no external wiki' do + before do + project.external_wiki + end + + it 'sets :has_external_wiki as false' do + expect(project.has_external_wiki).to be(false) + end + + it 'sets :has_external_wiki as true if an external wiki service is created later' do + expect(project.has_external_wiki).to be(false) + + create(:service, project: project, type: 'ExternalWikiService', active: true) + + expect(project.has_external_wiki).to be(true) + end + end + end + describe '#open_branches' do let(:project) { create(:project) } @@ -998,46 +1095,6 @@ describe Project, models: true do end end - describe "#developers_can_push_to_protected_branch?" do - let(:project) { create(:empty_project) } - - context "when the branch matches a protected branch via direct match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production')).to be false - end - end - - context "when the branch matches a protected branch via wilcard match" do - it "returns true if 'Developers can Push' is turned on" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be true - end - - it "returns false if 'Developers can Push' is turned off" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: false) - - expect(project.developers_can_push_to_protected_branch?('production/some-branch')).to be false - end - end - - context "when the branch does not match a protected branch" do - it "returns false" do - create(:protected_branch, name: "production/*", project: project, developers_can_push: true) - - expect(project.developers_can_push_to_protected_branch?('staging/some-branch')).to be false - end - end - end - describe '#container_registry_path_with_namespace' do let(:project) { create(:empty_project, path: 'PROJECT') } @@ -1114,6 +1171,111 @@ describe Project, models: true do end end + describe '#latest_successful_builds_for' do + def create_pipeline(status = 'success') + create(:ci_pipeline, project: project, + sha: project.commit.sha, + ref: project.default_branch, + status: status) + end + + def create_build(new_pipeline = pipeline, name = 'test') + create(:ci_build, :success, :artifacts, + pipeline: new_pipeline, + status: new_pipeline.status, + name: name) + end + + let(:project) { create(:project) } + let(:pipeline) { create_pipeline } + + context 'with many builds' do + it 'gives the latest builds from latest pipeline' do + pipeline1 = create_pipeline + pipeline2 = create_pipeline + build1_p2 = create_build(pipeline2, 'test') + create_build(pipeline1, 'test') + create_build(pipeline1, 'test2') + build2_p2 = create_build(pipeline2, 'test2') + + latest_builds = project.latest_successful_builds_for + + expect(latest_builds).to contain_exactly(build2_p2, build1_p2) + end + end + + context 'with succeeded pipeline' do + let!(:build) { create_build } + + context 'standalone pipeline' do + it 'returns builds for ref for default_branch' do + builds = project.latest_successful_builds_for + + expect(builds).to contain_exactly(build) + end + + it 'returns empty relation if the build cannot be found' do + builds = project.latest_successful_builds_for('TAIL') + + expect(builds).to be_kind_of(ActiveRecord::Relation) + expect(builds).to be_empty + end + end + + context 'with some pending pipeline' do + before do + create_build(create_pipeline('pending')) + end + + it 'gives the latest build from latest pipeline' do + latest_build = project.latest_successful_builds_for + + expect(latest_build).to contain_exactly(build) + end + end + end + + context 'with pending pipeline' do + before do + pipeline.update(status: 'pending') + create_build(pipeline) + end + + it 'returns empty relation' do + builds = project.latest_successful_builds_for + + expect(builds).to be_kind_of(ActiveRecord::Relation) + expect(builds).to be_empty + end + end + end + + describe '#add_import_job' do + context 'forked' do + let(:forked_project_link) { create(:forked_project_link) } + let(:forked_from_project) { forked_project_link.forked_from_project } + let(:project) { forked_project_link.forked_to_project } + + it 'schedules a RepositoryForkWorker job' do + expect(RepositoryForkWorker).to receive(:perform_async). + with(project.id, forked_from_project.repository_storage_path, + forked_from_project.path_with_namespace, project.namespace.path) + + project.add_import_job + end + end + + context 'not forked' do + let(:project) { create(:project) } + + it 'schedules a RepositoryImportWorker job' do + expect(RepositoryImportWorker).to receive(:perform_async).with(project.id) + + project.add_import_job + end + end + end + describe '.where_paths_in' do context 'without any paths' do it 'returns an empty relation' do @@ -1146,4 +1308,53 @@ describe Project, models: true do end end end + + describe 'authorized_for_user' do + let(:group) { create(:group) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:personal_project) { create(:project, namespace: developer.namespace) } + let(:group_project) { create(:project, namespace: group) } + let(:members_project) { create(:project) } + let(:shared_project) { create(:project) } + + before do + group.add_master(master) + group.add_developer(developer) + + members_project.team << [developer, :developer] + members_project.team << [master, :master] + + create(:project_group_link, project: shared_project, group: group) + end + + it 'returns false for no user' do + expect(personal_project.authorized_for_user?(nil)).to be(false) + end + + it 'returns true for personal projects of the user' do + expect(personal_project.authorized_for_user?(developer)).to be(true) + end + + it 'returns true for projects of groups the user is a member of' do + expect(group_project.authorized_for_user?(developer)).to be(true) + end + + it 'returns true for projects for which the user is a member of' do + expect(members_project.authorized_for_user?(developer)).to be(true) + end + + it 'returns true for projects shared on a group the user is a member of' do + expect(shared_project.authorized_for_user?(developer)).to be(true) + end + + it 'checks for the correct minimum level access' do + expect(group_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) + expect(group_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) + expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) + expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) + expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) + expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) + end + end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 9262aeb6ed8..5eaf0d3b7a6 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -151,8 +151,8 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } end context 'when project is shared with group' do @@ -168,14 +168,14 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } context 'but share_with_group_lock is true' do before { project.namespace.update(share_with_group_lock: true) } - it { expect(project.team.max_member_access(master.id)).to be_nil } - it { expect(project.team.max_member_access(reporter.id)).to be_nil } + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::NO_ACCESS) } end end end @@ -194,8 +194,74 @@ describe ProjectTeam, models: true do it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::MASTER) } it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } it { expect(project.team.max_member_access(guest.id)).to eq(Gitlab::Access::GUEST) } - it { expect(project.team.max_member_access(nonmember.id)).to be_nil } - it { expect(project.team.max_member_access(requester.id)).to be_nil } + it { expect(project.team.max_member_access(nonmember.id)).to eq(Gitlab::Access::NO_ACCESS) } + it { expect(project.team.max_member_access(requester.id)).to eq(Gitlab::Access::NO_ACCESS) } end end + + shared_examples_for "#max_member_access_for_users" do |enable_request_store| + describe "#max_member_access_for_users" do + before do + RequestStore.begin! if enable_request_store + end + + after do + if enable_request_store + RequestStore.end! + RequestStore.clear! + end + end + + it 'returns correct roles for different users' do + master = create(:user) + reporter = create(:user) + promoted_guest = create(:user) + guest = create(:user) + project = create(:project) + + project.team << [master, :master] + project.team << [reporter, :reporter] + project.team << [promoted_guest, :guest] + project.team << [guest, :guest] + + group = create(:group) + group_developer = create(:user) + second_developer = create(:user) + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER) + + group.add_master(promoted_guest) + group.add_developer(group_developer) + group.add_developer(second_developer) + + second_group = create(:group) + project.project_group_links.create( + group: second_group, + group_access: Gitlab::Access::MASTER) + second_group.add_master(second_developer) + + users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id) + + expected = { + master.id => Gitlab::Access::MASTER, + reporter.id => Gitlab::Access::REPORTER, + promoted_guest.id => Gitlab::Access::DEVELOPER, + guest.id => Gitlab::Access::GUEST, + group_developer.id => Gitlab::Access::DEVELOPER, + second_developer.id => Gitlab::Access::MASTER + } + + expect(project.team.max_member_access_for_user_ids(users)).to eq(expected) + end + end + end + + describe '#max_member_access_for_users with RequestStore' do + it_behaves_like "#max_member_access_for_users", true + end + + describe '#max_member_access_for_users without RequestStore' do + it_behaves_like "#max_member_access_for_users", false + end end diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index e14cec589fe..cce15538b93 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -50,8 +50,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now) double_last = double(committed_date: Time.now - 1.second) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_first) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_last) + allow(tag_a).to receive(:target).and_return(double_first) + allow(tag_b).to receive(:target).and_return(double_last) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.0.0', 'v1.1.0']) } @@ -64,8 +65,9 @@ describe Repository, models: true do double_first = double(committed_date: Time.now - 1.second) double_last = double(committed_date: Time.now) - allow(repository).to receive(:commit).with(tag_a.target).and_return(double_last) - allow(repository).to receive(:commit).with(tag_b.target).and_return(double_first) + allow(tag_a).to receive(:target).and_return(double_last) + allow(tag_b).to receive(:target).and_return(double_first) + allow(repository).to receive(:tags).and_return([tag_a, tag_b]) end it { is_expected.to eq(['v1.1.0', 'v1.0.0']) } @@ -130,6 +132,36 @@ describe Repository, models: true do end end + describe :commit_file do + it 'commits change to a file successfully' do + expect do + repository.commit_file(user, 'CHANGELOG', 'Changelog!', + 'Updates file content', + 'master', true) + end.to change { repository.commits('master').count }.by(1) + + blob = repository.blob_at('master', 'CHANGELOG') + + expect(blob.data).to eq('Changelog!') + end + end + + describe :update_file do + it 'updates filename successfully' do + expect do + repository.update_file(user, 'NEWLICENSE', 'Copyright!', + branch: 'master', + previous_path: 'LICENSE', + message: 'Changes filename') + end.to change { repository.commits('master').count }.by(1) + + files = repository.ls_files('master') + + expect(files).not_to include('LICENSE') + expect(files).to include('NEWLICENSE') + end + end + describe "search_files" do let(:results) { repository.search_files('feature', 'master') } subject { results } @@ -351,9 +383,13 @@ describe Repository, models: true do end describe '#rm_branch' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + let(:blank_sha) { '0000000000000000000000000000000000000000' } + context 'when pre hooks were successful' do it 'should run without errors' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) + expect_any_instance_of(GitHooksService).to receive(:execute). + with(user, project.repository.path_to_repo, old_rev, blank_sha, 'refs/heads/feature') expect { repository.rm_branch(user, 'feature') }.not_to raise_error end @@ -388,10 +424,13 @@ describe Repository, models: true do end describe '#commit_with_hooks' do + let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature + context 'when pre hooks were successful' do before do expect_any_instance_of(GitHooksService).to receive(:execute). - and_return(true) + with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature'). + and_yield.and_return(true) end it 'should run without errors' do @@ -405,6 +444,14 @@ describe Repository, models: true do repository.commit_with_hooks(user, 'feature') { sample_commit.id } end + + context "when the branch wasn't empty" do + it 'updates the head' do + expect(repository.find_branch('feature').target.id).to eq(old_rev) + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + expect(repository.find_branch('feature').target.id).to eq(sample_commit.id) + end + end end context 'when pre hooks failed' do @@ -416,6 +463,43 @@ describe Repository, models: true do end.to raise_error(GitHooksService::PreReceiveError) end end + + context 'when target branch is different from source branch' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires branch cache' do + expect(repository).not_to receive(:expire_exists_cache) + expect(repository).not_to receive(:expire_root_ref_cache) + expect(repository).not_to receive(:expire_emptiness_caches) + expect(repository).to receive(:expire_branches_cache) + expect(repository).to receive(:expire_has_visible_content_cache) + expect(repository).to receive(:expire_branch_count_cache) + + repository.commit_with_hooks(user, 'new-feature') { sample_commit.id } + end + end + + context 'when repository is empty' do + before do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) + end + + it 'expires creation and branch cache' do + empty_repository = create(:empty_project, :empty_repo).repository + + expect(empty_repository).to receive(:expire_exists_cache) + expect(empty_repository).to receive(:expire_root_ref_cache) + expect(empty_repository).to receive(:expire_emptiness_caches) + expect(empty_repository).to receive(:expire_branches_cache) + expect(empty_repository).to receive(:expire_has_visible_content_cache) + expect(empty_repository).to receive(:expire_branch_count_cache) + + empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', + 'Updates file content', 'master', false) + end + end end describe '#exists?' do @@ -719,6 +803,30 @@ describe Repository, models: true do repository.before_delete end + it 'flushes the tags cache' do + expect(repository).to receive(:expire_tags_cache) + + repository.before_delete + end + + it 'flushes the tag count cache' do + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_delete + end + + it 'flushes the branches cache' do + expect(repository).to receive(:expire_branches_cache) + + repository.before_delete + end + + it 'flushes the branch count cache' do + expect(repository).to receive(:expire_branch_count_cache) + + repository.before_delete + end + it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) @@ -749,6 +857,30 @@ describe Repository, models: true do repository.before_delete end + it 'flushes the tags cache' do + expect(repository).to receive(:expire_tags_cache) + + repository.before_delete + end + + it 'flushes the tag count cache' do + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_delete + end + + it 'flushes the branches cache' do + expect(repository).to receive(:expire_branches_cache) + + repository.before_delete + end + + it 'flushes the branch count cache' do + expect(repository).to receive(:expire_branch_count_cache) + + repository.before_delete + end + it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) @@ -1083,51 +1215,31 @@ describe Repository, models: true do end end - describe '#local_branches' do - it 'returns the local branches' do - masterrev = repository.find_branch('master').target - create_remote_branch('joe', 'remote_branch', masterrev) - repository.add_branch(user, 'local_branch', masterrev) - - expect(repository.local_branches.any? { |branch| branch.name == 'remote_branch' }).to eq(false) - expect(repository.local_branches.any? { |branch| branch.name == 'local_branch' }).to eq(true) + describe "#keep_around" do + it "does not fail if we attempt to reference bad commit" do + expect(repository.kept_around?('abc1234')).to be_falsey end - end - - describe '.clean_old_archives' do - let(:path) { Gitlab.config.gitlab.repository_downloads_path } - - context 'when the downloads directory does not exist' do - it 'does not remove any archives' do - expect(File).to receive(:directory?).with(path).and_return(false) - expect(Gitlab::Popen).not_to receive(:popen) + it "stores a reference to the specified commit sha so it isn't garbage collected" do + repository.keep_around(sample_commit.id) - described_class.clean_old_archives - end + expect(repository.kept_around?(sample_commit.id)).to be_truthy end - context 'when the downloads directory exists' do - it 'removes old archives' do - expect(File).to receive(:directory?).with(path).and_return(true) - - expect(Gitlab::Popen).to receive(:popen) + it "attempting to call keep_around on truncated ref does not fail" do + repository.keep_around(sample_commit.id) + ref = repository.send(:keep_around_ref_name, sample_commit.id) + path = File.join(repository.path, ref) + # Corrupt the reference + File.truncate(path, 0) - described_class.clean_old_archives - end - end - end + expect(repository.kept_around?(sample_commit.id)).to be_falsey - describe "#keep_around" do - it "stores a reference to the specified commit sha so it isn't garbage collected" do repository.keep_around(sample_commit.id) - expect(repository.kept_around?(sample_commit.id)).to be_truthy - end - end + expect(repository.kept_around?(sample_commit.id)).to be_falsey - def create_remote_branch(remote_name, branch_name, target) - rugged = repository.rugged - rugged.references.create("refs/remotes/#{remote_name}/#{branch_name}", target) + File.delete(path) + end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index fc74488ac0e..9f432501c59 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -89,9 +89,9 @@ describe User, models: true do end describe 'email' do - context 'when no signup domains listed' do + context 'when no signup domains whitelisted' do before do - allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return([]) + allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return([]) end it 'accepts any email' do @@ -100,9 +100,9 @@ describe User, models: true do end end - context 'when a signup domain is listed and subdomains are allowed' do + context 'when a signup domain is whitelisted and subdomains are allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com', '*.example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com', '*.example.com']) end it 'accepts info@example.com' do @@ -121,9 +121,9 @@ describe User, models: true do end end - context 'when a signup domain is listed and subdomains are not allowed' do + context 'when a signup domain is whitelisted and subdomains are not allowed' do before do - allow_any_instance_of(ApplicationSetting).to receive(:restricted_signup_domains).and_return(['example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['example.com']) end it 'accepts info@example.com' do @@ -142,6 +142,53 @@ describe User, models: true do end end + context 'domain blacklist' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist_enabled?).and_return(true) + allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['example.com']) + end + + context 'when a signup domain is blacklisted' do + it 'accepts info@test.com' do + user = build(:user, email: 'info@test.com') + expect(user).to be_valid + end + + it 'rejects info@example.com' do + user = build(:user, email: 'info@example.com') + expect(user).not_to be_valid + end + end + + context 'when a signup domain is blacklisted but a wildcard subdomain is allowed' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:domain_blacklist).and_return(['test.example.com']) + allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['*.example.com']) + end + + it 'should give priority to whitelist and allow info@test.example.com' do + user = build(:user, email: 'info@test.example.com') + expect(user).to be_valid + end + end + + context 'with both lists containing a domain' do + before do + allow_any_instance_of(ApplicationSetting).to receive(:domain_whitelist).and_return(['test.com']) + end + + it 'accepts info@test.com' do + user = build(:user, email: 'info@test.com') + expect(user).to be_valid + end + + it 'rejects info@example.com' do + user = build(:user, email: 'info@example.com') + expect(user).not_to be_valid + end + end + end + context 'owns_notification_email' do it 'accepts temp_oauth_email emails' do user = build(:user, email: "temp-email-for-oauth@example.com") @@ -596,7 +643,7 @@ describe User, models: true do user = create :user key = create :key, key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQD33bWLBxu48Sev9Fert1yzEO4WGcWglWF7K/AwblIUFselOt/QdOL9DSjpQGxLagO1s9wl53STIO8qGS4Ms0EJZyIXOEFMjFJ5xmjSy+S37By4sG7SsltQEHMxtbtFOaW5LV2wCrX+rUsRNqLMamZjgjcPO0/EgGCXIGMAYW4O7cwGZdXWYIhQ1Vwy+CsVMDdPkPgBXqK7nR/ey8KMs8ho5fMNgB5hBw/AL9fNGhRw3QTD6Q12Nkhl4VZES2EsZqlpNnJttnPdp847DUsT6yuLRlfiQfz5Cn9ysHFdXObMN5VYIiPFwHeYCZp1X2S4fDZooRE8uOLTfxWHPXwrhqSH", user_id: user.id - expect(user.all_ssh_keys).to include(key.key) + expect(user.all_ssh_keys).to include(a_string_starting_with(key.key)) end end @@ -887,16 +934,25 @@ describe User, models: true do end describe '#authorized_projects' do - let!(:user) { create(:user) } - let!(:private_project) { create(:project, :private) } + context 'with a minimum access level' do + it 'includes projects for which the user is an owner' do + user = create(:user) + project = create(:empty_project, :private, namespace: user.namespace) - before do - private_project.team << [user, Gitlab::Access::MASTER] - end + expect(user.authorized_projects(Gitlab::Access::REPORTER)) + .to contain_exactly(project) + end + + it 'includes projects for which the user is a master' do + user = create(:user) + project = create(:empty_project, :private) - subject { user.authorized_projects } + project.team << [user, Gitlab::Access::MASTER] - it { is_expected.to eq([private_project]) } + expect(user.authorized_projects(Gitlab::Access::REPORTER)) + .to contain_exactly(project) + end + end end describe '#ci_authorized_runners' do diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb index 3d5c19aeff3..831889afb6c 100644 --- a/spec/requests/api/api_helpers_spec.rb +++ b/spec/requests/api/api_helpers_spec.rb @@ -211,4 +211,27 @@ describe API::Helpers, api: true do expect(sudo_identifier).to eq(' 123') end end + + describe '.to_boolean' do + it 'converts a valid string to a boolean' do + expect(to_boolean('true')).to be_truthy + expect(to_boolean('YeS')).to be_truthy + expect(to_boolean('t')).to be_truthy + expect(to_boolean('1')).to be_truthy + expect(to_boolean('ON')).to be_truthy + expect(to_boolean('FaLse')).to be_falsy + expect(to_boolean('F')).to be_falsy + expect(to_boolean('NO')).to be_falsy + expect(to_boolean('n')).to be_falsy + expect(to_boolean('0')).to be_falsy + expect(to_boolean('oFF')).to be_falsy + end + + it 'converts an invalid string to nil' do + expect(to_boolean('fals')).to be_nil + expect(to_boolean('yeah')).to be_nil + expect(to_boolean('')).to be_nil + expect(to_boolean(nil)).to be_nil + end + end end diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index b11ca26ee68..e8fd697965f 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -32,6 +32,8 @@ describe API::API, api: true do expect(json_response['name']).to eq(branch_name) expect(json_response['commit']['id']).to eq(branch_sha) expect(json_response['protected']).to eq(false) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) end it "should return a 403 error if guest" do @@ -45,14 +47,95 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/repository/branches/:branch/protect" do - it "should protect a single branch" do + describe 'PUT /projects/:id/repository/branches/:branch/protect' do + it 'protects a single branch' do put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user) + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it 'protects a single branch and developers can push' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(false) + end + it 'protects a single branch and developers can merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_merge: true + + expect(response).to have_http_status(200) expect(json_response['name']).to eq(branch_name) expect(json_response['commit']['id']).to eq(branch_sha) expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(true) + end + + it 'protects a single branch and developers can push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: true, developers_can_merge: true + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end + + it 'protects a single branch and developers cannot push and merge' do + put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user), + developers_can_push: 'tru', developers_can_merge: 'tr' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(branch_name) + expect(json_response['commit']['id']).to eq(branch_sha) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + context 'on a protected branch' do + let(:protected_branch) { 'foo' } + + before do + project.repository.add_branch(user, protected_branch, 'master') + create(:protected_branch, :developers_can_push, :developers_can_merge, project: project, name: protected_branch) + end + + it 'updates that a developer can push' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), + developers_can_push: false, developers_can_merge: false + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(false) + expect(json_response['developers_can_merge']).to eq(false) + end + + it 'does not update that a developer can push' do + put api("/projects/#{project.id}/repository/branches/#{protected_branch}/protect", user), + developers_can_push: 'foobar', developers_can_merge: 'foo' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq(protected_branch) + expect(json_response['protected']).to eq(true) + expect(json_response['developers_can_push']).to eq(true) + expect(json_response['developers_can_merge']).to eq(true) + end end it "should return a 404 error if branch not found" do diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb index f5b39c3d698..86a7b242fbe 100644 --- a/spec/requests/api/builds_spec.rb +++ b/spec/requests/api/builds_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe API::API, api: true do +describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } let(:api_user) { user } - let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } let!(:developer) { create(:project_member, :developer, user: user, project: project) } - let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } - let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) } + let(:reporter) { create(:project_member, :reporter, project: project) } + let(:guest) { create(:project_member, :guest, project: project) } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id, ref: project.default_branch) } let!(:build) { create(:ci_build, pipeline: pipeline) } describe 'GET /projects/:id/builds ' do @@ -172,10 +172,104 @@ describe API::API, api: true do end end + describe 'GET /projects/:id/artifacts/:ref_name/download?job=name' do + let(:api_user) { reporter.user } + let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + def path_for_ref(ref = pipeline.ref, job = build.name) + api("/projects/#{project.id}/builds/artifacts/#{ref}/download?job=#{job}", api_user) + end + + context 'when not logged in' do + let(:api_user) { nil } + + before do + get path_for_ref + end + + it 'gives 401' do + expect(response).to have_http_status(401) + end + end + + context 'when logging as guest' do + let(:api_user) { guest.user } + + before do + get path_for_ref + end + + it 'gives 403' do + expect(response).to have_http_status(403) + end + end + + context 'non-existing build' do + shared_examples 'not found' do + it { expect(response).to have_http_status(:not_found) } + end + + context 'has no such ref' do + before do + get path_for_ref('TAIL', build.name) + end + + it_behaves_like 'not found' + end + + context 'has no such build' do + before do + get path_for_ref(pipeline.ref, 'NOBUILD') + end + + it_behaves_like 'not found' + end + end + + context 'find proper build' do + shared_examples 'a valid file' do + let(:download_headers) do + { 'Content-Transfer-Encoding' => 'binary', + 'Content-Disposition' => + "attachment; filename=#{build.artifacts_file.filename}" } + end + + it { expect(response).to have_http_status(200) } + it { expect(response.headers).to include(download_headers) } + end + + context 'with regular branch' do + before do + pipeline.update(ref: 'master', + sha: project.commit('master').sha) + + get path_for_ref('master') + end + + it_behaves_like 'a valid file' + end + + context 'with branch name containing slash' do + before do + pipeline.update(ref: 'improve/awesome', + sha: project.commit('improve/awesome').sha) + end + + before do + get path_for_ref('improve/awesome') + end + + it_behaves_like 'a valid file' + end + end + end + describe 'GET /projects/:id/builds/:build_id/trace' do let(:build) { create(:ci_build, :trace, pipeline: pipeline) } - before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } + before do + get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) + end context 'authorized user' do it 'should return specific build trace' do @@ -205,7 +299,7 @@ describe API::API, api: true do end context 'user without :update_build permission' do - let(:api_user) { user2 } + let(:api_user) { reporter.user } it 'should not cancel build' do expect(response).to have_http_status(403) @@ -237,7 +331,7 @@ describe API::API, api: true do end context 'user without :update_build permission' do - let(:api_user) { user2 } + let(:api_user) { reporter.user } it 'should not retry build' do expect(response).to have_http_status(403) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 5219c808791..e4ea8506598 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -73,9 +73,13 @@ describe API::API, api: true do context "authorized user" do it "should return a commit by sha" do get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user) + expect(response).to have_http_status(200) expect(json_response['id']).to eq(project.repository.commit.id) expect(json_response['title']).to eq(project.repository.commit.title) + expect(json_response['stats']['additions']).to eq(project.repository.commit.stats.additions) + expect(json_response['stats']['deletions']).to eq(project.repository.commit.stats.deletions) + expect(json_response['stats']['total']).to eq(project.repository.commit.stats.total) end it "should return a 404 error if not found" do diff --git a/spec/requests/api/deploy_keys.rb b/spec/requests/api/deploy_keys.rb new file mode 100644 index 00000000000..ac42288bc34 --- /dev/null +++ b/spec/requests/api/deploy_keys.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:project) { create(:project, creator_id: user.id) } + let!(:deploy_keys_project) { create(:deploy_keys_project, project: project) } + let(:admin) { create(:admin) } + + describe 'GET /deploy_keys' do + before { admin } + + context 'when unauthenticated' do + it 'should return authentication error' do + get api('/deploy_keys') + expect(response.status).to eq(401) + end + end + + context 'when authenticated as non-admin user' do + it 'should return a 403 error' do + get api('/deploy_keys', user) + expect(response.status).to eq(403) + end + end + + context 'when authenticated as admin' do + it 'should return all deploy keys' do + get api('/deploy_keys', admin) + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) + end + end + end +end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb new file mode 100644 index 00000000000..05e57905343 --- /dev/null +++ b/spec/requests/api/environments_spec.rb @@ -0,0 +1,129 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:non_member) { create(:user) } + let(:project) { create(:project, :private, namespace: user.namespace) } + let!(:environment) { create(:environment, project: project) } + + before do + project.team << [user, :master] + end + + describe 'GET /projects/:id/environments' do + context 'as member of the project' do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/environments", user) } + end + + it 'returns project environments' do + get api("/projects/#{project.id}/environments", user) + + expect(response).to have_http_status(200) + expect(json_response).to be_an Array + expect(json_response.size).to eq(1) + expect(json_response.first['name']).to eq(environment.name) + expect(json_response.first['external_url']).to eq(environment.external_url) + end + end + + context 'as non member' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments", non_member) + + expect(response).to have_http_status(404) + end + end + end + + describe 'POST /projects/:id/environments' do + context 'as a member' do + it 'creates a environment with valid params' do + post api("/projects/#{project.id}/environments", user), name: "mepmep" + + expect(response).to have_http_status(201) + expect(json_response['name']).to eq('mepmep') + expect(json_response['external']).to be nil + end + + it 'requires name to be passed' do + post api("/projects/#{project.id}/environments", user), external_url: 'test.gitlab.com' + + expect(response).to have_http_status(400) + end + + it 'returns a 400 if environment already exists' do + post api("/projects/#{project.id}/environments", user), name: environment.name + + expect(response).to have_http_status(400) + end + end + + context 'a non member' do + it 'rejects the request' do + post api("/projects/#{project.id}/environments", non_member), name: 'gitlab.com' + + expect(response).to have_http_status(404) + end + + it 'returns a 400 when the required params are missing' do + post api("/projects/12345/environments", non_member), external_url: 'http://env.git.com' + end + end + end + + describe 'PUT /projects/:id/environments/:environment_id' do + it 'returns a 200 if name and external_url are changed' do + url = 'https://mepmep.whatever.ninja' + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep', external_url: url + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it "won't update the external_url if only the name is passed" do + url = environment.external_url + put api("/projects/#{project.id}/environments/#{environment.id}", user), + name: 'Mepmep' + + expect(response).to have_http_status(200) + expect(json_response['name']).to eq('Mepmep') + expect(json_response['external_url']).to eq(url) + end + + it 'returns a 404 if the environment does not exist' do + put api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + end + end + + describe 'DELETE /projects/:id/environments/:environment_id' do + context 'as a master' do + it 'returns a 200 for an existing environment' do + delete api("/projects/#{project.id}/environments/#{environment.id}", user) + + expect(response).to have_http_status(200) + end + + it 'returns a 404 for non existing id' do + delete api("/projects/#{project.id}/environments/12345", user) + + expect(response).to have_http_status(404) + expect(json_response['message']).to eq('404 Not found') + end + end + + context 'a non member' do + it 'rejects the request' do + delete api("/projects/#{project.id}/environments/#{environment.id}", non_member) + + expect(response).to have_http_status(404) + end + end + end +end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 12f2cfa6942..9d3d28e0b91 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -531,10 +531,8 @@ describe API::API, api: true do describe 'POST /projects/:id/issues with spam filtering' do before do - Grape::Endpoint.before_each do |endpoint| - allow(endpoint).to receive(:check_for_spam?).and_return(true) - allow(endpoint).to receive(:is_spam?).and_return(true) - end + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:check_for_spam?).and_return(true) + allow_any_instance_of(Gitlab::AkismetHelper).to receive(:is_spam?).and_return(true) end let(:params) do diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 152cd802839..8c6a7e6529d 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -396,7 +396,6 @@ describe API::API, api: true do expect(json_response['alt']).to eq("dk") expect(json_response['url']).to start_with("/uploads/") expect(json_response['url']).to end_with("/dk.png") - expect(json_response['is_image']).to eq(true) end end @@ -647,33 +646,33 @@ describe API::API, api: true do let(:deploy_keys_project) { create(:deploy_keys_project, project: project) } let(:deploy_key) { deploy_keys_project.deploy_key } - describe 'GET /projects/:id/keys' do + describe 'GET /projects/:id/deploy_keys' do before { deploy_key } it 'should return array of ssh keys' do - get api("/projects/#{project.id}/keys", user) + get api("/projects/#{project.id}/deploy_keys", user) expect(response).to have_http_status(200) expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end end - describe 'GET /projects/:id/keys/:key_id' do + describe 'GET /projects/:id/deploy_keys/:key_id' do it 'should return a single key' do - get api("/projects/#{project.id}/keys/#{deploy_key.id}", user) + get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user) expect(response).to have_http_status(200) expect(json_response['title']).to eq(deploy_key.title) end it 'should return 404 Not Found with invalid ID' do - get api("/projects/#{project.id}/keys/404", user) + get api("/projects/#{project.id}/deploy_keys/404", user) expect(response).to have_http_status(404) end end - describe 'POST /projects/:id/keys' do + describe 'POST /projects/:id/deploy_keys' do it 'should not create an invalid ssh key' do - post api("/projects/#{project.id}/keys", user), { title: 'invalid key' } + post api("/projects/#{project.id}/deploy_keys", user), { title: 'invalid key' } expect(response).to have_http_status(400) expect(json_response['message']['key']).to eq([ 'can\'t be blank', @@ -683,7 +682,7 @@ describe API::API, api: true do end it 'should not create a key without title' do - post api("/projects/#{project.id}/keys", user), key: 'some key' + post api("/projects/#{project.id}/deploy_keys", user), key: 'some key' expect(response).to have_http_status(400) expect(json_response['message']['title']).to eq([ 'can\'t be blank', @@ -694,22 +693,22 @@ describe API::API, api: true do it 'should create new ssh key' do key_attrs = attributes_for :key expect do - post api("/projects/#{project.id}/keys", user), key_attrs + post api("/projects/#{project.id}/deploy_keys", user), key_attrs end.to change{ project.deploy_keys.count }.by(1) end end - describe 'DELETE /projects/:id/keys/:key_id' do + describe 'DELETE /projects/:id/deploy_keys/:key_id' do before { deploy_key } it 'should delete existing key' do expect do - delete api("/projects/#{project.id}/keys/#{deploy_key.id}", user) + delete api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", user) end.to change{ project.deploy_keys.count }.by(-1) end it 'should return 404 Not Found with invalid ID' do - delete api("/projects/#{project.id}/keys/404", user) + delete api("/projects/#{project.id}/deploy_keys/404", user) expect(response).to have_http_status(404) end end diff --git a/spec/requests/api/todos_spec.rb b/spec/requests/api/todos_spec.rb index 92a4fa216cd..3ccd0af652f 100644 --- a/spec/requests/api/todos_spec.rb +++ b/spec/requests/api/todos_spec.rb @@ -134,8 +134,7 @@ describe API::Todos, api: true do delete api('/todos', john_doe) expect(response.status).to eq(200) - expect(json_response).to be_an Array - expect(json_response.length).to eq(3) + expect(response.body).to eq('3') expect(pending_1.reload).to be_done expect(pending_2.reload).to be_done expect(pending_3.reload).to be_done diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index e7cbc3dd3a7..cf1e8d9b514 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -73,12 +73,12 @@ describe Ci::API::API do post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } expect(response).to have_http_status(201) - expect(json_response["variables"]).to eq([ + expect(json_response["variables"]).to include( { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false } - ]) + ) end it "returns variables for triggers" do @@ -92,14 +92,14 @@ describe Ci::API::API do post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } expect(response).to have_http_status(201) - expect(json_response["variables"]).to eq([ + expect(json_response["variables"]).to include( { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true }, { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true }, { "key" => "CI_BUILD_TRIGGERED", "value" => "true", "public" => true }, { "key" => "DB_NAME", "value" => "postgres", "public" => true }, { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false }, - { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, - ]) + { "key" => "TRIGGER_KEY_1", "value" => "TRIGGER_VALUE_1", "public" => false } + ) end it "returns dependent builds" do diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 8b19936ae6d..69eeb45ed71 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -1,6 +1,5 @@ require 'spec_helper' -# team_update_admin_user PUT /admin/users/:id/team_update(.:format) admin/users#team_update # block_admin_user PUT /admin/users/:id/block(.:format) admin/users#block # unblock_admin_user PUT /admin/users/:id/unblock(.:format) admin/users#unblock # admin_users GET /admin/users(.:format) admin/users#index @@ -11,10 +10,6 @@ require 'spec_helper' # PUT /admin/users/:id(.:format) admin/users#update # DELETE /admin/users/:id(.:format) admin/users#destroy describe Admin::UsersController, "routing" do - it "to #team_update" do - expect(put("/admin/users/1/team_update")).to route_to('admin/users#team_update', id: '1') - end - it "to #block" do expect(put("/admin/users/1/block")).to route_to('admin/users#block', id: '1') end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 620f328a114..b941e78f983 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -135,10 +135,6 @@ describe Projects::RepositoriesController, 'routing' do it 'to #archive format:tar.bz2' do expect(get('/gitlab/gitlabhq/repository/archive.tar.bz2')).to route_to('projects/repositories#archive', namespace_id: 'gitlab', project_id: 'gitlabhq', format: 'tar.bz2') end - - it 'to #show' do - expect(get('/gitlab/gitlabhq/repository')).to route_to('projects/repositories#show', namespace_id: 'gitlab', project_id: 'gitlabhq') - end end describe Projects::BranchesController, 'routing' do @@ -483,13 +479,16 @@ end describe Projects::NetworkController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/network/master')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') - expect(get('/gitlab/gitlabhq/network/master.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + expect(get('/gitlab/gitlabhq/network/ends-with.json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/network/master?format=json')).to route_to('projects/network#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end describe Projects::GraphsController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq/graphs/master')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + expect(get('/gitlab/gitlabhq/graphs/ends-with.json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'ends-with.json') + expect(get('/gitlab/gitlabhq/graphs/master?format=json')).to route_to('projects/graphs#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 2c755919456..1d4df9197f6 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -116,12 +116,9 @@ describe HelpController, "routing" do expect(get(path)).to route_to('help#show', path: 'workflow/protected_branches/protected_branches1', format: 'png') - path = '/help/shortcuts' - expect(get(path)).to route_to('help#show', - path: 'shortcuts') + path = '/help/ui' - expect(get(path)).to route_to('help#show', - path: 'ui') + expect(get(path)).to route_to('help#ui') end end @@ -179,18 +176,10 @@ describe Profiles::KeysController, "routing" do expect(post("/profile/keys")).to route_to('profiles/keys#create') end - it "to #edit" do - expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') - end - it "to #show" do expect(get("/profile/keys/1")).to route_to('profiles/keys#show', id: '1') end - it "to #update" do - expect(put("/profile/keys/1")).to route_to('profiles/keys#update', id: '1') - end - it "to #destroy" do expect(delete("/profile/keys/1")).to route_to('profiles/keys#destroy', id: '1') end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 47c0580e0f0..ffa998dffc3 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -7,6 +7,7 @@ describe GitPushService, services: true do let(:project) { create :project } before do + project.team << [user, :master] @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @@ -172,7 +173,7 @@ describe GitPushService, services: true do describe "Push Event" do before do service = execute_service(project, user, @oldrev, @newrev, @ref ) - @event = Event.last + @event = Event.find_by_action(Event::PUSHED) @push_data = service.push_data end @@ -224,8 +225,10 @@ describe GitPushService, services: true do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: false }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -233,8 +236,8 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).not_to receive(:create) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).to be_empty end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -242,9 +245,12 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true, developers_can_merge: false }) - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.last.push_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(project.protected_branches.last.merge_access_level.access_level).to eq(Gitlab::Access::MASTER) end it "when pushing a branch for the first time with default branch protection set to 'developers can merge'" do @@ -252,8 +258,10 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false, developers_can_merge: true }) execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(project.protected_branches).not_to be_empty + expect(project.protected_branches.first.push_access_level.access_level).to eq(Gitlab::Access::MASTER) + expect(project.protected_branches.first.merge_access_level.access_level).to eq(Gitlab::Access::DEVELOPER) end it "when pushing new commits to existing branch" do diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issues/bulk_update_service_spec.rb index ba3a4dfc048..321b54ac39d 100644 --- a/spec/services/issues/bulk_update_service_spec.rb +++ b/spec/services/issues/bulk_update_service_spec.rb @@ -1,118 +1,106 @@ require 'spec_helper' describe Issues::BulkUpdateService, services: true do - let(:user) { create(:user) } - let(:project) { Projects::CreateService.new(user, namespace: user.namespace, name: 'test').execute } + let(:user) { create(:user) } + let(:project) { create(:empty_project, namespace: user.namespace) } - let!(:result) { Issues::BulkUpdateService.new(project, user, params).execute } + def bulk_update(issues, extra_params = {}) + bulk_update_params = extra_params + .reverse_merge(issues_ids: Array(issues).map(&:id).join(',')) - describe :close_issue do - let(:issues) { create_list(:issue, 5, project: project) } - let(:params) do - { - state_event: 'close', - issues_ids: issues.map(&:id).join(',') - } - end + Issues::BulkUpdateService.new(project, user, bulk_update_params).execute + end + + describe 'close issues' do + let(:issues) { create_list(:issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'close') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'closes all the issues passed' do + bulk_update(issues, state_event: 'close') + expect(project.issues.opened).to be_empty expect(project.issues.closed).not_to be_empty end end - describe :reopen_issues do - let(:issues) { create_list(:closed_issue, 5, project: project) } - let(:params) do - { - state_event: 'reopen', - issues_ids: issues.map(&:id).join(',') - } - end + describe 'reopen issues' do + let(:issues) { create_list(:closed_issue, 2, project: project) } it 'succeeds and returns the correct number of issues updated' do + result = bulk_update(issues, state_event: 'reopen') + expect(result[:success]).to be_truthy expect(result[:count]).to eq(issues.count) end it 'reopens all the issues passed' do + bulk_update(issues, state_event: 'reopen') + expect(project.issues.closed).to be_empty expect(project.issues.opened).not_to be_empty end end describe 'updating assignee' do - let(:issue) do - create(:issue, project: project) { |issue| issue.update_attributes(assignee: user) } - end - - let(:params) do - { - assignee_id: assignee_id, - issues_ids: issue.id.to_s - } - end + let(:issue) { create(:issue, project: project, assignee: user) } context 'when the new assignee ID is a valid user' do - let(:new_assignee) { create(:user) } - let(:assignee_id) { new_assignee.id } - it 'succeeds' do + result = bulk_update(issue, assignee_id: create(:user).id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the assignee to the use ID passed' do - expect(issue.reload.assignee).to eq(new_assignee) + assignee = create(:user) + + expect { bulk_update(issue, assignee_id: assignee.id) } + .to change { issue.reload.assignee }.from(user).to(assignee) end end context 'when the new assignee ID is -1' do - let(:assignee_id) { -1 } - it 'unassigns the issues' do - expect(issue.reload.assignee).to be_nil + expect { bulk_update(issue, assignee_id: -1) } + .to change { issue.reload.assignee }.to(nil) end end context 'when the new assignee ID is not present' do - let(:assignee_id) { nil } - it 'does not unassign' do - expect(issue.reload.assignee).to eq(user) + expect { bulk_update(issue, assignee_id: nil) } + .not_to change { issue.reload.assignee } end end end describe 'updating milestones' do - let(:issue) { create(:issue, project: project) } + let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project) } - let(:params) do - { - issues_ids: issue.id.to_s, - milestone_id: milestone.id - } - end - it 'succeeds' do + result = bulk_update(issue, milestone_id: milestone.id) + expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end it 'updates the issue milestone' do - expect(project.issues.first.milestone).to eq(milestone) + expect { bulk_update(issue, milestone_id: milestone.id) } + .to change { issue.reload.milestone }.from(nil).to(milestone) end end describe 'updating labels' do def create_issue_with_labels(labels) - create(:issue, project: project) { |issue| issue.update_attributes(labels: labels) } + create(:labeled_issue, project: project, labels: labels) end let(:bug) { create(:label, project: project) } @@ -129,15 +117,18 @@ describe Issues::BulkUpdateService, services: true do let(:add_labels) { [] } let(:remove_labels) { [] } - let(:params) do + let(:bulk_update_params) do { - label_ids: labels.map(&:id), - add_label_ids: add_labels.map(&:id), + label_ids: labels.map(&:id), + add_label_ids: add_labels.map(&:id), remove_label_ids: remove_labels.map(&:id), - issues_ids: issues.map(&:id).join(',') } end + before do + bulk_update(issues, bulk_update_params) + end + context 'when label_ids are passed' do let(:issues) { [issue_all_labels, issue_no_labels] } let(:labels) { [bug, regression] } @@ -263,40 +254,28 @@ describe Issues::BulkUpdateService, services: true do end end - describe :subscribe_issues do - let(:issues) { create_list(:issue, 5, project: project) } - let(:params) do - { - subscription_event: 'subscribe', - issues_ids: issues.map(&:id).join(',') - } - end + describe 'subscribe to issues' do + let(:issues) { create_list(:issue, 2, project: project) } it 'subscribes the given user' do - issues.each do |issue| - expect(issue.subscribed?(user)).to be_truthy - end - end - end + bulk_update(issues, subscription_event: 'subscribe') - describe :unsubscribe_issues do - let(:issues) { create_list(:closed_issue, 5, project: project) } - let(:params) do - { - subscription_event: 'unsubscribe', - issues_ids: issues.map(&:id).join(',') - } + expect(issues).to all(be_subscribed(user)) end + end - before do - issues.each do |issue| + describe 'unsubscribe from issues' do + let(:issues) do + create_list(:closed_issue, 2, project: project) do |issue| issue.subscriptions.create(user: user, subscribed: true) end end it 'unsubscribes the given user' do + bulk_update(issues, subscription_event: 'unsubscribe') + issues.each do |issue| - expect(issue.subscribed?(user)).to be_falsey + expect(issue).not_to be_subscribed(user) end end end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index f5bf3c1e367..8ffebcac698 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -75,6 +75,17 @@ describe MergeRequests::MergeService, services: true do expect(merge_request.merge_error).to eq("error") end + + it 'aborts if there is a merge conflict' do + allow_any_instance_of(Repository).to receive(:merge).and_return(false) + allow(service).to receive(:execute_hooks) + + service.execute(merge_request) + + expect(merge_request.open?).to be_truthy + expect(merge_request.merge_commit_sha).to be_nil + expect(merge_request.merge_error).to eq("Conflicts detected during merge") + end end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index ce643b3f860..781ee7ffed3 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -57,7 +57,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update') + with(@merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).not_to be_empty } @@ -113,7 +113,7 @@ describe MergeRequests::RefreshService, services: true do it 'should execute hooks with update action' do expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', @oldrev) end it { expect(@merge_request.notes).to be_empty } @@ -158,7 +158,7 @@ describe MergeRequests::RefreshService, services: true do it 'refreshes the merge request' do expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update') + with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index f252e2c5902..122a7cea2a1 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -35,8 +35,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be true } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } it { expect(@link_to_file[:alt]).to eq('rails_sample') } end @@ -49,8 +47,6 @@ describe Projects::DownloadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to be false } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') } end diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/projects/upload_service_spec.rb index 9268a9fb1a2..c42eeba4b9c 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/projects/upload_service_spec.rb @@ -15,9 +15,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('banana_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('banana_sample.gif') } end @@ -31,8 +29,6 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } it { expect(@link_to_file).to have_value('dk') } - it { expect(@link_to_file).to have_key(:is_image) } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('dk.png') } end @@ -44,9 +40,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('rails_sample') } - it { expect(@link_to_file[:is_image]).to equal(true) } it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } end @@ -58,9 +52,7 @@ describe Projects::UploadService, services: true do it { expect(@link_to_file).to have_key(:alt) } it { expect(@link_to_file).to have_key(:url) } - it { expect(@link_to_file).to have_key(:is_image) } it { expect(@link_to_file).to have_value('doc_sample.txt') } - it { expect(@link_to_file[:is_image]).to equal(false) } it { expect(@link_to_file[:url]).to match('doc_sample.txt') } end diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb new file mode 100644 index 00000000000..842585f9e54 --- /dev/null +++ b/spec/services/repository_archive_clean_up_service_spec.rb @@ -0,0 +1,81 @@ +require 'spec_helper' + +describe RepositoryArchiveCleanUpService, services: true do + describe '#execute' do + subject(:service) { described_class.new } + + context 'when the downloads directory does not exist' do + it 'does not remove any archives' do + path = '/invalid/path/' + stub_repository_downloads_path(path) + + expect(File).to receive(:directory?).with(path).and_return(false) + expect(service).not_to receive(:clean_up_old_archives) + expect(service).not_to receive(:clean_up_empty_directories) + + service.execute + end + end + + context 'when the downloads directory exists' do + shared_examples 'invalid archive files' do |dirname, extensions, mtime| + it 'does not remove files and directoy' do + in_directory_with_files(dirname, extensions, mtime) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq true } + expect(File.directory?(dir)).to eq true + end + end + end + + it 'removes files older than 2 hours that matches valid archive extensions' do + in_directory_with_files('sample.git', %w[tar tar.bz2 tar.gz zip], 2.hours) do |dir, files| + service.execute + + files.each { |file| expect(File.exist?(file)).to eq false } + expect(File.directory?(dir)).to eq false + end + end + + context 'with files older than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 2.hours + end + + context 'with files older than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'john_doe/sample.git', %w[conf rb tar tar.gz], 2.hours + end + + context 'with files newer than 2 hours that matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[tar tar.bz2 tar.gz zip], 1.hour + end + + context 'with files newer than 2 hours that does not matches valid archive extensions' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb], 1.hour + end + + context 'with files newer than 2 hours inside invalid directories' do + it_behaves_like 'invalid archive files', 'sample.git', %w[conf rb tar tar.gz], 1.hour + end + end + + def in_directory_with_files(dirname, extensions, mtime) + Dir.mktmpdir do |tmpdir| + stub_repository_downloads_path(tmpdir) + dir = File.join(tmpdir, dirname) + files = create_temporary_files(dir, extensions, mtime) + + yield(dir, files) + end + end + + def stub_repository_downloads_path(path) + allow(Gitlab.config.gitlab).to receive(:repository_downloads_path).and_return(path) + end + + def create_temporary_files(dir, extensions, mtime) + FileUtils.mkdir_p(dir) + FileUtils.touch(extensions.map { |ext| File.join(dir, "sample.#{ext}") }, mtime: Time.now - mtime) + end + end +end diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb new file mode 100644 index 00000000000..6f8f7109e14 --- /dev/null +++ b/spec/simplecov_env.rb @@ -0,0 +1,54 @@ +require 'simplecov' + +module SimpleCovEnv + extend self + + def start! + return unless ENV['SIMPLECOV'] + + configure_profile + configure_job + + SimpleCov.start + end + + def configure_job + SimpleCov.configure do + if ENV['CI_BUILD_NAME'] + coverage_dir "coverage/#{ENV['CI_BUILD_NAME']}" + command_name ENV['CI_BUILD_NAME'] + end + + if ENV['CI'] + SimpleCov.at_exit do + # In CI environment don't generate formatted reports + # Only generate .resultset.json + SimpleCov.result + end + end + end + end + + def configure_profile + SimpleCov.configure do + load_profile 'test_frameworks' + track_files '{app,lib}/**/*.rb' + + add_filter '/vendor/ruby/' + add_filter 'config/initializers/' + + add_group 'Controllers', 'app/controllers' + add_group 'Models', 'app/models' + add_group 'Mailers', 'app/mailers' + add_group 'Helpers', 'app/helpers' + add_group 'Workers', %w(app/jobs app/workers) + add_group 'Libraries', 'lib' + add_group 'Services', 'app/services' + add_group 'Finders', 'app/finders' + add_group 'Uploaders', 'app/uploaders' + add_group 'Validators', 'app/validators' + + merge_timeout 7200 + end + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3638dcbb2d3..4f3aacf55be 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,7 +1,5 @@ -if ENV['SIMPLECOV'] - require 'simplecov' - SimpleCov.start :rails -end +require './spec/simplecov_env' +SimpleCovEnv.start! ENV["RAILS_ENV"] ||= 'test' diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb index 1b3cafb497c..68b196d9033 100644 --- a/spec/support/api_helpers.rb +++ b/spec/support/api_helpers.rb @@ -24,8 +24,11 @@ module ApiHelpers (path.index('?') ? '' : '?') + # Append private_token if given a User object - (user.respond_to?(:private_token) ? - "&private_token=#{user.private_token}" : "") + if user.respond_to?(:private_token) + "&private_token=#{user.private_token}" + else + '' + end end def ci_api(path, user = nil) @@ -35,8 +38,11 @@ module ApiHelpers (path.index('?') ? '' : '?') + # Append private_token if given a User object - (user.respond_to?(:private_token) ? - "&private_token=#{user.private_token}" : "") + if user.respond_to?(:private_token) + "&private_token=#{user.private_token}" + else + '' + end end def json_response diff --git a/spec/support/issue_helpers.rb b/spec/support/issue_helpers.rb new file mode 100644 index 00000000000..85241793743 --- /dev/null +++ b/spec/support/issue_helpers.rb @@ -0,0 +1,13 @@ +module IssueHelpers + def visit_issues(project, opts = {}) + visit namespace_project_issues_path project.namespace, project, opts + end + + def first_issue + page.all('ul.issues-list > li').first.text + end + + def last_issue + page.all('ul.issues-list > li').last.text + end +end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index e005058ba5b..8c98b1f988c 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -178,6 +178,17 @@ module MarkdownMatchers expect(actual).to have_selector('span.idiff.deletion', count: 2) end end + + # VideoLinkFilter + matcher :parse_video_links do + set_default_markdown_messages + + match do |actual| + video = actual.at_css('video') + + expect(video['src']).to end_with('/assets/videos/gitlab-demo.mp4') + end + end end # Monkeypatch the matcher DSL so that we can reduce some noisy duplication for diff --git a/spec/support/merge_request_helpers.rb b/spec/support/merge_request_helpers.rb new file mode 100644 index 00000000000..d5801c8272f --- /dev/null +++ b/spec/support/merge_request_helpers.rb @@ -0,0 +1,13 @@ +module MergeRequestHelpers + def visit_merge_requests(project, opts = {}) + visit namespace_project_merge_requests_path project.namespace, project, opts + end + + def first_merge_request + page.all('ul.mr-list > li').first.text + end + + def last_merge_request + page.all('ul.mr-list > li').last.text + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 83f2ad96fd8..1c0c66969e3 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -6,6 +6,7 @@ module TestEnv # When developing the seed repository, comment out the branch you will modify. BRANCH_SHA = { 'empty-branch' => '7efb185', + 'ends-with.json' => '98b0d8b3', 'flatten-dir' => 'e56497b', 'feature' => '0b4bc9a', 'feature_conflict' => 'bb5206f', @@ -20,7 +21,9 @@ module TestEnv 'gitattributes' => '5a62481', 'expand-collapse-diffs' => '4842455', 'expand-collapse-files' => '025db92', - 'expand-collapse-lines' => '238e82d' + 'expand-collapse-lines' => '238e82d', + 'video' => '8879059', + 'crlf-diff' => '5938907' } # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/teaspoon_env.rb b/spec/teaspoon_env.rb index 69b2b9b6d5b..1a3bbb9c8cc 100644 --- a/spec/teaspoon_env.rb +++ b/spec/teaspoon_env.rb @@ -38,7 +38,7 @@ Teaspoon.configure do |config| # Specify a file matcher as a regular expression and all matching files will be loaded when the suite is run. These # files need to be within an asset path. You can add asset paths using the `config.asset_paths`. - suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.coffee,coffee}" + suite.matcher = "{spec/javascripts,app/assets}/**/*_spec.{js,js.es6,es6}" # Load additional JS files, but requiring them in your spec helper is the preferred way to do this. # suite.javascripts = [] diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb new file mode 100644 index 00000000000..e8300abed5d --- /dev/null +++ b/spec/uploaders/file_uploader_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe FileUploader do + let(:project) { create(:project) } + + before do + @previous_enable_processing = FileUploader.enable_processing + FileUploader.enable_processing = false + @uploader = FileUploader.new(project) + end + + after do + FileUploader.enable_processing = @previous_enable_processing + @uploader.remove! + end + + describe '#image_or_video?' do + context 'given an image file' do + before do + @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'rails_sample.jpg'))) + end + + it 'detects an image based on file extension' do + expect(@uploader.image_or_video?).to be true + end + end + + context 'given an video file' do + before do + video_file = File.new(Rails.root.join('spec', 'fixtures', 'video_sample.mp4')) + @uploader.store!(video_file) + end + + it 'detects a video based on file extension' do + expect(@uploader.image_or_video?).to be true + end + end + + it 'does not return image_or_video? for other types' do + @uploader.store!(File.new(Rails.root.join('spec', 'fixtures', 'doc_sample.txt'))) + + expect(@uploader.image_or_video?).to be false + end + end +end diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb new file mode 100644 index 00000000000..dae858a52f6 --- /dev/null +++ b/spec/views/admin/dashboard/index.html.haml_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe 'admin/dashboard/index.html.haml' do + include Devise::TestHelpers + + before do + assign(:projects, create_list(:empty_project, 1)) + assign(:users, create_list(:user, 1)) + assign(:groups, create_list(:group, 1)) + + allow(view).to receive(:admin?).and_return(true) + end + + it "shows version of GitLab Workhorse" do + render + + expect(rendered).to have_content 'GitLab Workhorse' + expect(rendered).to have_content Gitlab::Workhorse.version + end +end diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb index 42220a20c75..464051063d8 100644 --- a/spec/views/projects/builds/show.html.haml_spec.rb +++ b/spec/views/projects/builds/show.html.haml_spec.rb @@ -44,9 +44,29 @@ describe 'projects/builds/show' do it 'shows commit title and not show commit message' do render - + expect(rendered).to have_css('p.build-light-text.append-bottom-0', text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) end end + + describe 'shows trigger variables in sidebar' do + let(:trigger_request) { create(:ci_trigger_request_with_variables, pipeline: pipeline) } + + before do + build.trigger_request = trigger_request + render + end + + it 'shows trigger variables in separate lines' do + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1')) + expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2')) + end + end + + private + + def variable_regexp(key, value) + /\A#{Regexp.escape("#{key}=#{value}")}\Z/ + end end diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb new file mode 100644 index 00000000000..78af61f15a7 --- /dev/null +++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe 'projects/issues/_related_branches' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:branch) { project.repository.find_branch('feature') } + let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') } + + before do + assign(:project, project) + assign(:related_branches, ['feature']) + + render + end + + it 'shows the related branches with their build status' do + expect(rendered).to match('feature') + expect(rendered).to have_css('.related-branch-ci-status') + end +end diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb new file mode 100644 index 00000000000..0f3fc1ee1ac --- /dev/null +++ b/spec/views/projects/tree/show.html.haml_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe 'projects/tree/show' do + include Devise::TestHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + + before do + assign(:project, project) + assign(:repository, repository) + + allow(view).to receive(:can?).and_return(true) + allow(view).to receive(:can_collaborate_with_project?).and_return(true) + end + + context 'for branch names ending on .json' do + let(:ref) { 'ends-with.json' } + let(:commit) { repository.commit(ref) } + let(:path) { '' } + let(:tree) { repository.tree(commit.id, path) } + + before do + assign(:ref, ref) + assign(:commit, commit) + assign(:id, commit.id) + assign(:tree, tree) + assign(:path, path) + end + + it 'displays correctly' do + render + expect(rendered).to have_css('.js-project-refs-dropdown .dropdown-toggle-text', text: ref) + expect(rendered).to have_css('.readme-holder .file-content', text: ref) + end + end +end diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb index de40a6f78af..fe70501eeac 100644 --- a/spec/workers/email_receiver_worker_spec.rb +++ b/spec/workers/email_receiver_worker_spec.rb @@ -17,7 +17,7 @@ describe EmailReceiverWorker do context "when an error occurs" do before do - allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::Receiver::EmptyEmailError) + allow_any_instance_of(Gitlab::Email::Receiver).to receive(:execute).and_raise(Gitlab::Email::EmptyEmailError) end it "sends out a rejection email" do diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb index 439da765c2c..796751efe8d 100644 --- a/spec/workers/emails_on_push_worker_spec.rb +++ b/spec/workers/emails_on_push_worker_spec.rb @@ -12,6 +12,42 @@ describe EmailsOnPushWorker do subject { EmailsOnPushWorker.new } describe "#perform" do + context "when push is a new branch" do + let(:email) { ActionMailer::Base.deliveries.last } + + before do + data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA) + + subject.perform(project.id, recipients, data_new_branch) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include("Pushed new branch") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + + context "when push is a deleted branch" do + let(:email) { ActionMailer::Base.deliveries.last } + + before do + data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA) + + subject.perform(project.id, recipients, data_deleted_branch) + end + + it "sends a mail with the correct subject" do + expect(email.subject).to include("Deleted branch") + end + + it "sends the mail to the correct recipient" do + expect(email.to).to eq([user.email]) + end + end + context "when there are no errors in sending" do let(:email) { ActionMailer::Base.deliveries.last } diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index 5f762282b5e..60605460adb 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -14,21 +14,24 @@ describe RepositoryForkWorker do describe "#perform" do it "creates a new repository from a fork" do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end it 'flushes various caches' do expect(shell).to receive(:fork_repository).with( - project.repository_storage_path, + '/test/path', project.path_with_namespace, + project.repository_storage_path, fork_project.namespace.path ).and_return(true) @@ -38,7 +41,7 @@ describe RepositoryForkWorker do expect_any_instance_of(Repository).to receive(:expire_exists_cache). and_call_original - subject.perform(project.id, project.path_with_namespace, + subject.perform(project.id, '/test/path', project.path_with_namespace, fork_project.namespace.path) end @@ -49,6 +52,7 @@ describe RepositoryForkWorker do subject.perform( project.id, + '/test/path', project.path_with_namespace, fork_project.namespace.path) end |