diff options
author | Lin Jen-Shin <godfat@godfat.org> | 2016-12-19 18:26:15 +0800 |
---|---|---|
committer | Lin Jen-Shin <godfat@godfat.org> | 2016-12-19 18:26:15 +0800 |
commit | 4bf59f87b321d59de665a0100fed9114f4b747ea (patch) | |
tree | 548628a91be85631ac3b3bd8ec85c484ee70d707 /spec | |
parent | e485b3f6ad3c220655e4aa909d93bca7a4ae6afc (diff) | |
parent | a50cd9eb4b4392004e47e57b2fa37c12def5827f (diff) | |
download | gitlab-ce-4bf59f87b321d59de665a0100fed9114f4b747ea.tar.gz |
Merge remote-tracking branch 'upstream/master' into fix-forbidden-for-build-api-for-deleted-project
* upstream/master: (333 commits)
Fix typo
Always use `fixture_file_upload` helper to upload files in tests.
Add CHANGELOG
Move admin application spinach test to rspec
Move admin deploy keys spinach test to rspec
Fix rubocop failures
Store mattermost_url in settings
Improve Mattermost Session specs
Ensure the session is destroyed
Improve session tests
Setup mattermost session
Fix query in Projects::ProjectMembersController to fetch members
Improve test for sort dropdown on members page
Fix sort dropdown alignment
Undo changes on members search button stylesheet
Use factories to create project/group membership on specs
Remove unused id from shared members sort dropdown
Fix sort functionality on project/group members to return invited users
Refactor MembersHelper#filter_group_project_member_path
Remove unnecessary curly braces from sort dropdown partial
...
Diffstat (limited to 'spec')
133 files changed, 2989 insertions, 491 deletions
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb index 1d3c9fbbe2f..ce7c0b334ee 100644 --- a/spec/controllers/import/bitbucket_controller_spec.rb +++ b/spec/controllers/import/bitbucket_controller_spec.rb @@ -6,11 +6,11 @@ describe Import::BitbucketController do let(:user) { create(:user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } - let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } + let(:refresh_token) { SecureRandom.hex(15) } + let(:access_params) { { token: token, expires_at: nil, expires_in: nil, refresh_token: nil } } def assign_session_tokens - session[:bitbucket_access_token] = token - session[:bitbucket_access_token_secret] = secret + session[:bitbucket_token] = token end before do @@ -24,29 +24,36 @@ describe Import::BitbucketController do end it "updates access token" do - access_token = double(token: token, secret: secret) - allow_any_instance_of(Gitlab::BitbucketImport::Client). + expires_at = Time.now + 1.day + expires_in = 1.day + access_token = double(token: token, + secret: secret, + expires_at: expires_at, + expires_in: expires_in, + refresh_token: refresh_token) + allow_any_instance_of(OAuth2::Client). to receive(:get_token).and_return(access_token) stub_omniauth_provider('bitbucket') get :callback - expect(session[:bitbucket_access_token]).to eq(token) - expect(session[:bitbucket_access_token_secret]).to eq(secret) + expect(session[:bitbucket_token]).to eq(token) + expect(session[:bitbucket_refresh_token]).to eq(refresh_token) + expect(session[:bitbucket_expires_at]).to eq(expires_at) + expect(session[:bitbucket_expires_in]).to eq(expires_in) expect(controller).to redirect_to(status_import_bitbucket_url) end end describe "GET status" do before do - @repo = OpenStruct.new(slug: 'vim', owner: 'asd') + @repo = double(slug: 'vim', owner: 'asd', full_name: 'asd/vim', "valid?" => true) assign_session_tokens end it "assigns variables" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id) - client = stub_client(projects: [@repo]) - allow(client).to receive(:incompatible_projects).and_return([]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status @@ -57,7 +64,7 @@ describe Import::BitbucketController do it "does not show already added project" do @project = create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim') - stub_client(projects: [@repo]) + allow_any_instance_of(Bitbucket::Client).to receive(:repos).and_return([@repo]) get :status @@ -70,19 +77,16 @@ describe Import::BitbucketController do let(:bitbucket_username) { user.username } let(:bitbucket_user) do - { user: { username: bitbucket_username } }.with_indifferent_access + double(username: bitbucket_username) end let(:bitbucket_repo) do - { slug: "vim", owner: bitbucket_username }.with_indifferent_access + double(slug: "vim", owner: bitbucket_username, name: 'vim') end before do - allow(Gitlab::BitbucketImport::KeyAdder). - to receive(:new).with(bitbucket_repo, user, access_params). - and_return(double(execute: true)) - - stub_client(user: bitbucket_user, project: bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:repo).and_return(bitbucket_repo) + allow_any_instance_of(Bitbucket::Client).to receive(:user).and_return(bitbucket_user) assign_session_tokens end @@ -90,7 +94,7 @@ describe Import::BitbucketController do context "when the Bitbucket user and GitLab user's usernames match" do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -102,7 +106,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -114,7 +118,7 @@ describe Import::BitbucketController do let(:other_username) { "someone_else" } before do - bitbucket_repo["owner"] = other_username + allow(bitbucket_repo).to receive(:owner).and_return(other_username) end context "when a namespace with the Bitbucket user's username already exists" do @@ -123,7 +127,7 @@ describe Import::BitbucketController do context "when the namespace is owned by the GitLab user" do it "takes the existing namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, existing_namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, existing_namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -156,7 +160,7 @@ describe Import::BitbucketController do it "takes the new namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, an_instance_of(Group), user, access_params). and_return(double(execute: true)) post :create, format: :js @@ -177,7 +181,7 @@ describe Import::BitbucketController do it "takes the current user's namespace" do expect(Gitlab::BitbucketImport::ProjectCreator). - to receive(:new).with(bitbucket_repo, user.namespace, user, access_params). + to receive(:new).with(bitbucket_repo, bitbucket_repo.name, user.namespace, user, access_params). and_return(double(execute: true)) post :create, format: :js diff --git a/spec/controllers/profiles/personal_access_tokens_spec.rb b/spec/controllers/profiles/personal_access_tokens_spec.rb new file mode 100644 index 00000000000..45534a3a587 --- /dev/null +++ b/spec/controllers/profiles/personal_access_tokens_spec.rb @@ -0,0 +1,49 @@ +require 'spec_helper' + +describe Profiles::PersonalAccessTokensController do + let(:user) { create(:user) } + + describe '#create' do + def created_token + PersonalAccessToken.order(:created_at).last + end + + before { sign_in(user) } + + it "allows creation of a token" do + name = FFaker::Product.brand + + post :create, personal_access_token: { name: name } + + expect(created_token).not_to be_nil + expect(created_token.name).to eq(name) + expect(created_token.expires_at).to be_nil + expect(PersonalAccessToken.active).to include(created_token) + end + + it "allows creation of a token with an expiry date" do + expires_at = 5.days.from_now + + post :create, personal_access_token: { name: FFaker::Product.brand, expires_at: expires_at } + + expect(created_token).not_to be_nil + expect(created_token.expires_at.to_i).to eq(expires_at.to_i) + end + + context "scopes" do + it "allows creation of a token with scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: ['api', 'read_user'] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq(['api', 'read_user']) + end + + it "allows creation of a token with no scopes" do + post :create, personal_access_token: { name: FFaker::Product.brand, scopes: [] } + + expect(created_token).not_to be_nil + expect(created_token.scopes).to eq([]) + end + end + end +end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb new file mode 100644 index 00000000000..b7bb9290712 --- /dev/null +++ b/spec/controllers/search_controller_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe SearchController do + let(:user) { create(:user) } + let(:project) { create(:empty_project, :public) } + + before do + sign_in(user) + end + + it 'finds issue comments' do + project = create(:empty_project, :public) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].first).to eq note + end + + context 'on restricted projects' do + context 'when signed out' do + before { sign_out(user) } + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end + + it "doesn't expose comments on issues" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on merge_requests" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + + it "doesn't expose comments on snippets" do + project = create(:empty_project, :public, snippets_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_project_snippet, project: project) + + get :show, project_id: project.id, scope: 'notes', search: note.note + + expect(assigns[:search_objects].count).to eq(0) + end + end +end diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb new file mode 100644 index 00000000000..a7c5283df94 --- /dev/null +++ b/spec/db/production/settings.rb @@ -0,0 +1,16 @@ +require 'spec_helper' +require 'rainbow/ext/string' + +describe 'seed production settings', lib: true do + context 'GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN is set in the environment' do + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('GITLAB_SHARED_RUNNERS_REGISTRATION_TOKEN').and_return('013456789') + end + + it 'writes the token to the database' do + load(File.join(__dir__, '../../../db/fixtures/production/010_settings.rb')) + expect(ApplicationSetting.current.runners_registration_token).to eq('013456789') + end + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 6919002dedc..a10ba629760 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -67,7 +67,7 @@ FactoryGirl.define do end trait :on_project_snippet do - noteable { create(:snippet, project: project) } + noteable { create(:project_snippet, project: project) } end trait :system do diff --git a/spec/factories/personal_access_tokens.rb b/spec/factories/personal_access_tokens.rb index da4c72bcb5b..811eab7e15b 100644 --- a/spec/factories/personal_access_tokens.rb +++ b/spec/factories/personal_access_tokens.rb @@ -5,5 +5,6 @@ FactoryGirl.define do name { FFaker::Product.brand } revoked false expires_at { 5.days.from_now } + scopes ['api'] end end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1166498ddff..0d072d6a690 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -133,4 +133,17 @@ FactoryGirl.define do ) end end + + factory :kubernetes_project, parent: :empty_project do + after :create do |project| + project.create_kubernetes_service( + active: true, + properties: { + namespace: project.path, + api_url: 'https://kubernetes.example.com/api', + token: 'a' * 40, + } + ) + end + end end diff --git a/spec/features/admin/admin_active_tab_spec.rb b/spec/features/admin/admin_active_tab_spec.rb new file mode 100644 index 00000000000..16064d60ce2 --- /dev/null +++ b/spec/features/admin/admin_active_tab_spec.rb @@ -0,0 +1,90 @@ +require 'spec_helper' + +RSpec.describe 'admin active tab' do + before do + login_as :admin + end + + shared_examples 'page has active tab' do |title| + it "activates #{title} tab" do + expect(page).to have_selector('.layout-nav .nav-links > li.active', count: 1) + expect(page.find('.layout-nav li.active')).to have_content(title) + end + end + + shared_examples 'page has active sub tab' do |title| + it "activates #{title} sub tab" do + expect(page).to have_selector('.sub-nav li.active', count: 1) + expect(page.find('.sub-nav li.active')).to have_content(title) + end + end + + context 'on home page' do + before do + visit admin_root_path + end + + it_behaves_like 'page has active tab', 'Overview' + end + + context 'on projects' do + before do + visit admin_projects_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Projects' + end + + context 'on groups' do + before do + visit admin_groups_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Groups' + end + + context 'on users' do + before do + visit admin_users_path + end + + it_behaves_like 'page has active tab', 'Overview' + it_behaves_like 'page has active sub tab', 'Users' + end + + context 'on logs' do + before do + visit admin_logs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Logs' + end + + context 'on messages' do + before do + visit admin_broadcast_messages_path + end + + it_behaves_like 'page has active tab', 'Messages' + end + + context 'on hooks' do + before do + visit admin_hooks_path + end + + it_behaves_like 'page has active tab', 'Hooks' + end + + context 'on background jobs' do + before do + visit admin_background_jobs_path + end + + it_behaves_like 'page has active tab', 'Monitoring' + it_behaves_like 'page has active sub tab', 'Background Jobs' + end +end diff --git a/spec/features/admin/admin_deploy_keys_spec.rb b/spec/features/admin/admin_deploy_keys_spec.rb new file mode 100644 index 00000000000..8bf68480785 --- /dev/null +++ b/spec/features/admin/admin_deploy_keys_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +RSpec.describe 'admin deploy keys', type: :feature do + let!(:deploy_key) { create(:deploy_key, public: true) } + let!(:another_deploy_key) { create(:another_deploy_key, public: true) } + + before do + login_as(:admin) + end + + it 'show all public deploy keys' do + visit admin_deploy_keys_path + + expect(page).to have_content(deploy_key.title) + expect(page).to have_content(another_deploy_key.title) + end + + it 'creates new deploy key' do + visit admin_deploy_keys_path + + click_link 'New Deploy Key' + fill_in 'deploy_key_title', with: 'laptop' + fill_in 'deploy_key_key', with: 'ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop' + click_button 'Create' + + expect(current_path).to eq admin_deploy_keys_path + expect(page).to have_content('laptop') + end +end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index f6d625fa7f6..0aa01fc499a 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -21,7 +21,7 @@ feature 'Admin Groups', feature: true do scenario 'shows the visibility level radio populated with the group visibility_level value' do group = create(:group, :private) - visit edit_admin_group_path(group) + visit admin_group_edit_path(group) expect_selected_visibility(group.visibility_level) end diff --git a/spec/features/admin/admin_manage_applications_spec.rb b/spec/features/admin/admin_manage_applications_spec.rb new file mode 100644 index 00000000000..c2c618b5659 --- /dev/null +++ b/spec/features/admin/admin_manage_applications_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +RSpec.describe 'admin manage applications', feature: true do + before do + login_as :admin + end + + it do + visit admin_applications_path + + click_on 'New Application' + expect(page).to have_content('New application') + + fill_in :doorkeeper_application_name, with: 'test' + fill_in :doorkeeper_application_redirect_uri, with: 'https://test.com' + click_on 'Submit' + expect(page).to have_content('Application: test') + expect(page).to have_content('Application Id') + expect(page).to have_content('Secret') + + click_on 'Edit' + expect(page).to have_content('Edit application') + + fill_in :doorkeeper_application_name, with: 'test_changed' + click_on 'Submit' + expect(page).to have_content('test_changed') + expect(page).to have_content('Application Id') + expect(page).to have_content('Secret') + + visit admin_applications_path + page.within '.oauth-applications' do + click_on 'Destroy' + end + expect(page.find('.oauth-applications')).not_to have_content('test_changed') + end +end diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb index 30ded9202a4..a36bfd574cb 100644 --- a/spec/features/admin/admin_projects_spec.rb +++ b/spec/features/admin/admin_projects_spec.rb @@ -8,11 +8,11 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects" do before do - visit admin_namespaces_projects_path + visit admin_projects_path end it "is ok" do - expect(current_path).to eq(admin_namespaces_projects_path) + expect(current_path).to eq(admin_projects_path) end it "has projects list" do @@ -22,7 +22,7 @@ describe "Admin::Projects", feature: true do describe "GET /admin/projects/:id" do before do - visit admin_namespaces_projects_path + visit admin_projects_path click_link "#{@project.name}" end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index 23a504ff965..8f561c8f90b 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -107,7 +107,7 @@ describe 'Commits' do describe 'Cancel build' do it 'cancels build' do visit ci_status_path(pipeline) - click_on 'Cancel' + find('a.btn[title="Cancel"]').click expect(page).to have_content 'canceled' end end diff --git a/spec/features/groups/members/sorting_spec.rb b/spec/features/groups/members/sorting_spec.rb new file mode 100644 index 00000000000..608aedd3471 --- /dev/null +++ b/spec/features/groups/members/sorting_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +feature 'Groups > Members > Sorting', feature: true do + let(:owner) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:group) { create(:group) } + + background do + create(:group_member, :owner, user: owner, group: group, created_at: 5.days.ago) + create(:group_member, :developer, user: developer, group: group, created_at: 3.days.ago) + + login_as(owner) + end + + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(owner.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(owner.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') + end + + def visit_members_list(sort:) + visit group_group_members_path(group.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end diff --git a/spec/features/issues/bulk_assignment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb index bc2c087c9b9..832757b24d4 100644 --- a/spec/features/issues/bulk_assignment_labels_spec.rb +++ b/spec/features/issues/bulk_assignment_labels_spec.rb @@ -9,6 +9,7 @@ feature 'Issues > Labels bulk assignment', feature: true do let!(:issue2) { create(:issue, project: project, title: "Issue 2") } let!(:bug) { create(:label, project: project, title: 'bug') } let!(:feature) { create(:label, project: project, title: 'feature') } + let!(:wontfix) { create(:label, project: project, title: 'wontfix') } context 'as an allowed user', js: true do before do @@ -291,6 +292,45 @@ feature 'Issues > Labels bulk assignment', feature: true do expect(find("#issue_#{issue1.id}")).not_to have_content 'feature' end end + + # Special case https://gitlab.com/gitlab-org/gitlab-ce/issues/24877 + context 'unmarking common label' do + before do + issue1.labels << bug + issue1.labels << feature + issue2.labels << bug + + visit namespace_project_issues_path(project.namespace, project) + end + + it 'applies label from filtered results' do + check 'check_all_issues' + + page.within('.issues_bulk_update') do + click_button 'Labels' + wait_for_ajax + + expect(find('.dropdown-menu-labels li', text: 'bug')).to have_css('.is-active') + expect(find('.dropdown-menu-labels li', text: 'feature')).to have_css('.is-indeterminate') + + click_link 'bug' + find('.dropdown-input-field', visible: true).set('wontfix') + click_link 'wontfix' + end + + update_issues + + page.within '.issues-holder' do + expect(find("#issue_#{issue1.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue1.id}")).to have_content 'feature' + expect(find("#issue_#{issue1.id}")).to have_content 'wontfix' + + expect(find("#issue_#{issue2.id}")).not_to have_content 'bug' + expect(find("#issue_#{issue2.id}")).not_to have_content 'feature' + expect(find("#issue_#{issue2.id}")).to have_content 'wontfix' + end + end + end end context 'as a guest' do @@ -320,7 +360,7 @@ feature 'Issues > Labels bulk assignment', feature: true do def open_labels_dropdown(items = [], unmark = false) page.within('.issues_bulk_update') do - click_button 'Label' + click_button 'Labels' wait_for_ajax items.map do |item| click_link item diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index cd0512a37e6..da64827b377 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -89,4 +89,12 @@ feature 'GFM autocomplete', feature: true, js: true do end end end + + it 'doesnt open autocomplete after non-word character' do + page.within '.timeline-content-form' do + find('#note_note').native.send_keys("@#{user.username[0..2]}!") + end + + expect(page).not_to have_selector('.atwho-view') + end end diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb new file mode 100644 index 00000000000..dc32c8f7373 --- /dev/null +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +feature 'Merge Request closing issues message', feature: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: merge_request_description + ) + end + let(:merge_request_description) { 'Merge Request Description' } + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + end + + context 'not closing or mentioning any issue' do + it 'does not display closing issue message' do + expect(page).not_to have_css('.mr-widget-footer') + end + end + + context 'closing issues but not mentioning any other issue' do + let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issues #{issue_1.to_reference} and #{issue_2.to_reference}") + end + end + + context 'mentioning issues but not closing them' do + let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not closed.") + end + end + + context 'closing some issues and mentioning, but not closing, others' do + let(:merge_request_description) { "Description\n\ncloses #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } + + it 'does not display closing issue message' do + expect(page).to have_content("Accepting this merge request will close issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not closed.") + end + end +end diff --git a/spec/features/merge_requests/merge_commit_message_toggle_spec.rb b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb new file mode 100644 index 00000000000..3dbe26cddb0 --- /dev/null +++ b/spec/features/merge_requests/merge_commit_message_toggle_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +feature 'Clicking toggle commit message link', feature: true, js: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue_1) { create(:issue, project: project)} + let(:issue_2) { create(:issue, project: project)} + let(:merge_request) do + create( + :merge_request, + :simple, + source_project: project, + description: "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" + ) + end + let(:textbox) { page.find(:css, '.js-commit-message', visible: false) } + let(:include_link) { page.find(:css, '.js-with-description-link', visible: false) } + let(:do_not_include_link) { page.find(:css, '.js-without-description-link', visible: false) } + let(:default_message) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + "Closes #{issue_1.to_reference} and #{issue_2.to_reference}", + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + let(:message_with_description) do + [ + "Merge branch 'feature' into 'master'", + merge_request.title, + merge_request.description, + "See merge request #{merge_request.to_reference}" + ].join("\n\n") + end + + before do + project.team << [user, :master] + + login_as user + + visit namespace_project_merge_request_path(project.namespace, project, merge_request) + + expect(textbox).not_to be_visible + click_link "Modify commit message" + expect(textbox).to be_visible + end + + it "toggles commit message between message with description and without description " do + expect(textbox.value).to eq(default_message) + + click_link "Include description in commit message" + + expect(textbox.value).to eq(message_with_description) + + click_link "Don't include description in commit message" + + expect(textbox.value).to eq(default_message) + end + + it "toggles link between 'Include description' and 'Don't include description'" do + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + + click_link "Include description in commit message" + + expect(include_link).not_to be_visible + expect(do_not_include_link).to be_visible + + click_link "Don't include description in commit message" + + expect(include_link).to be_visible + expect(do_not_include_link).not_to be_visible + end +end diff --git a/spec/features/participants_autocomplete_spec.rb b/spec/features/participants_autocomplete_spec.rb index a78a1c9c890..c2545b0c259 100644 --- a/spec/features/participants_autocomplete_spec.rb +++ b/spec/features/participants_autocomplete_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' feature 'Member autocomplete', feature: true do + include WaitForAjax + let(:project) { create(:project, :public) } let(:user) { create(:user) } let(:participant) { create(:user) } @@ -79,11 +81,10 @@ feature 'Member autocomplete', feature: true do end def open_member_suggestions - sleep 1 page.within('.new-note') do - sleep 1 - find('#note_note').native.send_keys('@') + find('#note_note').send_keys('@') end + wait_for_ajax end def visit_issue(project, issue) diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index a85930c7543..55a01057c83 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -27,28 +27,25 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do describe "token creation" do it "allows creation of a token" do - visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand - - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) - expect(active_personal_access_tokens).to have_text("Never") - end + name = FFaker::Product.brand - it "allows creation of a token with an expiry date" do visit profile_personal_access_tokens_path - fill_in "Name", with: FFaker::Product.brand + fill_in "Name", with: name # Set date to 1st of next month find_field("Expires at").trigger('focus') find("a[title='Next']").click click_on "1" - expect {click_on "Create Personal Access Token"}.to change { PersonalAccessToken.count }.by(1) - expect(created_personal_access_token).to eq(PersonalAccessToken.last.token) - expect(active_personal_access_tokens).to have_text(PersonalAccessToken.last.name) + # Scopes + check "api" + check "read_user" + + click_on "Create Personal Access Token" + expect(active_personal_access_tokens).to have_text(name) expect(active_personal_access_tokens).to have_text(Date.today.next_month.at_beginning_of_month.to_s(:medium)) + expect(active_personal_access_tokens).to have_text('api') + expect(active_personal_access_tokens).to have_text('read_user') end context "when creation fails" do @@ -85,7 +82,7 @@ describe 'Profile > Personal Access Tokens', feature: true, js: true do disallow_personal_access_token_saves! visit profile_personal_access_tokens_path - expect { click_on "Revoke" }.not_to change { PersonalAccessToken.inactive.count } + click_on "Revoke" expect(active_personal_access_tokens).to have_text(personal_access_token.name) expect(page).to have_content("Could not revoke") end diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb new file mode 100644 index 00000000000..32f33a3ca97 --- /dev/null +++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +feature 'User wants to add a Dockerfile file', feature: true do + include WaitForAjax + + before do + user = create(:user) + project = create(:project) + project.team << [user, :master] + login_as user + visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'Dockerfile') + end + + scenario 'user can see Dockerfile dropdown' do + expect(page).to have_css('.dockerfile-selector') + end + + scenario 'user can pick a Dockerfile file from the dropdown', js: true do + find('.js-dockerfile-selector').click + wait_for_ajax + within '.dockerfile-selector' do + find('.dropdown-input-field').set('HTTPd') + find('.dropdown-content li', text: 'HTTPd').click + end + wait_for_ajax + + expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'HTTPd') + expect(page).to have_content('COPY ./ /usr/local/apache2/htdocs/') + end +end diff --git a/spec/features/projects/gfm_autocomplete_load_spec.rb b/spec/features/projects/gfm_autocomplete_load_spec.rb index 1921ea6d8ae..dd9622f16a0 100644 --- a/spec/features/projects/gfm_autocomplete_load_spec.rb +++ b/spec/features/projects/gfm_autocomplete_load_spec.rb @@ -10,12 +10,12 @@ describe 'GFM autocomplete loading', feature: true, js: true do end it 'does not load on project#show' do - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).to eq({}) end it 'loads on new issue page' do visit new_namespace_project_issue_path(project.namespace, project) - expect(evaluate_script('GitLab.GfmAutoComplete.dataSource')).not_to eq('') + expect(evaluate_script('gl.GfmAutoComplete.dataSources')).not_to eq({}) end end diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz Binary files differindex bfe59bdb90e..d3165d07d7b 100644 --- a/spec/features/projects/import_export/test_project_export.tar.gz +++ b/spec/features/projects/import_export/test_project_export.tar.gz diff --git a/spec/features/projects/members/sorting_spec.rb b/spec/features/projects/members/sorting_spec.rb new file mode 100644 index 00000000000..d6ebb523f95 --- /dev/null +++ b/spec/features/projects/members/sorting_spec.rb @@ -0,0 +1,98 @@ +require 'spec_helper' + +feature 'Projects > Members > Sorting', feature: true do + let(:master) { create(:user, name: 'John Doe') } + let(:developer) { create(:user, name: 'Mary Jane', last_sign_in_at: 5.days.ago) } + let(:project) { create(:empty_project) } + + background do + create(:project_member, :master, user: master, project: project, created_at: 5.days.ago) + create(:project_member, :developer, user: developer, project: project, created_at: 3.days.ago) + + login_as(master) + end + + scenario 'sorts alphabetically by default' do + visit_members_list(sort: nil) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by access level ascending' do + visit_members_list(sort: :access_level_asc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, ascending') + end + + scenario 'sorts by access level descending' do + visit_members_list(sort: :access_level_desc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Access level, descending') + end + + scenario 'sorts by last joined' do + visit_members_list(sort: :last_joined) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Last joined') + end + + scenario 'sorts by oldest joined' do + visit_members_list(sort: :oldest_joined) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest joined') + end + + scenario 'sorts by name ascending' do + visit_members_list(sort: :name_asc) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, ascending') + end + + scenario 'sorts by name descending' do + visit_members_list(sort: :name_desc) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Name, descending') + end + + scenario 'sorts by recent sign in' do + visit_members_list(sort: :recent_sign_in) + + expect(first_member).to include(master.name) + expect(second_member).to include(developer.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Recent sign in') + end + + scenario 'sorts by oldest sign in' do + visit_members_list(sort: :oldest_sign_in) + + expect(first_member).to include(developer.name) + expect(second_member).to include(master.name) + expect(page).to have_css('.member-sort-dropdown .dropdown-toggle-text', text: 'Oldest sign in') + end + + def visit_members_list(sort:) + visit namespace_project_project_members_path(project.namespace.to_param, project.to_param, sort: sort) + end + + def first_member + page.all('ul.content-list > li').first.text + end + + def second_member + page.all('ul.content-list > li').last.text + end +end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 3350a3aeefc..0a77eaa123c 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -38,6 +38,91 @@ describe "Pipelines", feature: true, js: true do expect(page).to have_css('#js-tab-pipeline.active') end + describe 'pipeline graph' do + context 'when pipeline has running builds' do + it 'shows a running icon and a cancel action for the running build' do + page.within('a[data-title="deploy - running"]') do + expect(page).to have_selector('.ci-status-icon-running') + expect(page).to have_content('deploy') + end + + page.within('a[data-title="deploy - running"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-ban') + end + end + + it 'should be possible to cancel the running build' do + find('a[data-title="deploy - running"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Cancel running') + end + end + + context 'when pipeline has successful builds' do + it 'shows the success icon and a retry action for the successfull build' do + page.within('a[data-title="build - passed"]') do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('build') + end + + page.within('a[data-title="build - passed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the success build' do + find('a[data-title="build - passed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has failed builds' do + it 'shows the failed icon and a retry action for the failed build' do + page.within('a[data-title="test - failed"]') do + expect(page).to have_selector('.ci-status-icon-failed') + expect(page).to have_content('test') + end + + page.within('a[data-title="test - failed"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-refresh') + end + end + + it 'should be possible to retry the failed build' do + find('a[data-title="test - failed"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Retry build') + end + end + + context 'when pipeline has manual builds' do + it 'shows the skipped icon and a play action for the manual build' do + page.within('a[data-title="manual build - manual play action"]') do + expect(page).to have_selector('.ci-status-icon-skipped') + expect(page).to have_content('manual') + end + + page.within('a[data-title="manual build - manual play action"] + .ci-action-icon-container') do + expect(page).to have_selector('.ci-action-icon-container .fa-play') + end + end + + it 'should be possible to play the manual build' do + find('a[data-title="manual build - manual play action"] + .ci-action-icon-container').trigger('click') + + expect(page).not_to have_content('Play build') + end + end + + context 'when pipeline has external build' do + it 'shows the success icon and the generic comit status build' do + expect(page).to have_selector('.ci-status-icon-success') + expect(page).to have_content('jenkins') + end + end + end + context 'page tabs' do it 'shows Pipeline and Builds tabs with link' do expect(page).to have_link('Pipeline') diff --git a/spec/features/projects/services/slack_service_spec.rb b/spec/features/projects/services/slack_service_spec.rb index 16541f51d98..320ed13a01d 100644 --- a/spec/features/projects/services/slack_service_spec.rb +++ b/spec/features/projects/services/slack_service_spec.rb @@ -2,8 +2,8 @@ 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) } + let(:service) { SlackNotificationService.new } + let(:project) { create(:project, slack_notification_service: service) } background do service.fields diff --git a/spec/features/security/admin_access_spec.rb b/spec/features/security/admin_access_spec.rb index fe8cd7b7602..e180ca53eb5 100644 --- a/spec/features/security/admin_access_spec.rb +++ b/spec/features/security/admin_access_spec.rb @@ -4,7 +4,7 @@ describe "Admin::Projects", feature: true do include AccessMatchers describe "GET /admin/projects" do - subject { admin_namespaces_projects_path } + subject { admin_projects_path } it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for :user } diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb index 7f69e888f32..97737d7ddc7 100644 --- a/spec/finders/issues_finder_spec.rb +++ b/spec/finders/issues_finder_spec.rb @@ -10,24 +10,24 @@ describe IssuesFinder do let(:issue1) { create(:issue, author: user, assignee: user, project: project1, milestone: milestone, title: 'gitlab') } let(:issue2) { create(:issue, author: user, assignee: user, project: project2, description: 'gitlab') } let(:issue3) { create(:issue, author: user2, assignee: user2, project: project2) } - let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } - let!(:label_link) { create(:label_link, label: label, target: issue2) } - - before do - project1.team << [user, :master] - project2.team << [user, :developer] - project2.team << [user2, :developer] - - issue1 - issue2 - issue3 - end describe '#execute' do + let(:closed_issue) { create(:issue, author: user2, assignee: user2, project: project2, state: 'closed') } + let!(:label_link) { create(:label_link, label: label, target: issue2) } let(:search_user) { user } let(:params) { {} } let(:issues) { IssuesFinder.new(search_user, params.reverse_merge(scope: scope, state: 'opened')).execute } + before do + project1.team << [user, :master] + project2.team << [user, :developer] + project2.team << [user2, :developer] + + issue1 + issue2 + issue3 + end + context 'scope: all' do let(:scope) { 'all' } @@ -193,6 +193,15 @@ describe IssuesFinder do expect(issues).to contain_exactly(issue2, issue3) end end + + it 'finds issues user can access due to group' do + group = create(:group) + project = create(:empty_project, group: group) + issue = create(:issue, project: project) + group.add_user(user, :owner) + + expect(issues).to include(issue) + end end context 'personal scope' do @@ -210,5 +219,43 @@ describe IssuesFinder do end end end + + context 'when project restricts issues' do + let(:scope) { nil } + + it "doesn't return team-only issues to non team members" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + issue = create(:issue, project: project) + + expect(issues).not_to include(issue) + end + + it "doesn't return issues if feature disabled" do + [project1, project2].each do |project| + project.project_feature.update!(issues_access_level: ProjectFeature::DISABLED) + end + + expect(issues.count).to eq 0 + end + end + end + + describe '.not_restricted_by_confidentiality' do + let(:authorized_user) { create(:user) } + let(:project) { create(:empty_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(IssuesFinder.send(:not_restricted_by_confidentiality, nil)).to include(public_issue) + end + + it 'returns non confidential issues for user not authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, user)).to include(public_issue) + end + + it 'returns all issues for user authorized for the issues projects' do + expect(IssuesFinder.send(:not_restricted_by_confidentiality, authorized_user)).to include(public_issue, confidential_issue) + end end end diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb index 7c6860372cc..4d21254323c 100644 --- a/spec/finders/notes_finder_spec.rb +++ b/spec/finders/notes_finder_spec.rb @@ -2,59 +2,203 @@ require 'spec_helper' describe NotesFinder do let(:user) { create :user } - let(:project) { create :project } - let(:note1) { create :note_on_commit, project: project } - let(:note2) { create :note_on_commit, project: project } - let(:commit) { note1.noteable } + let(:project) { create(:empty_project) } before do project.team << [user, :master] end describe '#execute' do - let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + it 'finds notes on snippets when project is public and user isnt a member' - before do - note1 - note2 + it 'finds notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'finds all notes' do - notes = NotesFinder.new.execute(project, user, params) - expect(notes.size).to eq(2) + it 'finds notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, user).execute + + expect(notes.count).to eq(1) end - it 'raises an exception for an invalid target_type' do - params.merge!(target_type: 'invalid') - expect { NotesFinder.new.execute(project, user, params) }.to raise_error('invalid target_type') + it "excludes notes on commits the author can't download" do + project = create(:project, :private) + note = create(:note_on_commit, project: project) + params = { target_type: 'commit', target_id: note.noteable.id } + + notes = described_class.new(project, create(:user), params).execute + + expect(notes.count).to eq(0) end - it 'filters out old notes' do - note2.update_attribute(:updated_at, 2.hours.ago) - notes = NotesFinder.new.execute(project, user, params) - expect(notes).to eq([note1]) + it 'succeeds when no notes found' do + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) end - context 'confidential issue notes' do - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + context 'on restricted projects' do + let(:project) do + create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE, + snippets_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + it 'publicly excludes notes on merge requests' do + create(:note_on_merge_request, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on issues' do + create(:note_on_issue, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + + it 'publicly excludes notes on snippets' do + create(:note_on_project_snippet, project: project) + + notes = described_class.new(project, create(:user)).execute + + expect(notes.count).to eq(0) + end + end + + context 'for target' do + let(:project) { create(:project) } + let(:note1) { create :note_on_commit, project: project } + let(:note2) { create :note_on_commit, project: project } + let(:commit) { note1.noteable } + let(:params) { { target_id: commit.id, target_type: 'commit', last_fetched_at: 1.hour.ago.to_i } } + + before do + note1 + note2 + end + + it 'finds all notes' do + notes = described_class.new(project, user, params).execute + expect(notes.size).to eq(2) + end + + it 'finds notes on merge requests' do + note = create(:note_on_merge_request, project: project) + params = { target_type: 'merge_request', target_id: note.noteable.id } + + notes = described_class.new(project, user, params).execute + + expect(notes).to include(note) + end + + it 'finds notes on snippets' do + note = create(:note_on_project_snippet, project: project) + params = { target_type: 'snippet', target_id: note.noteable.id } - let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + notes = described_class.new(project, user, params).execute - it 'returns notes if user can see the issue' do - expect(NotesFinder.new.execute(project, user, params)).to eq([confidential_note]) + expect(notes.count).to eq(1) end - it 'raises an error if user can not see the issue' do + it 'raises an exception for an invalid target_type' do + params.merge!(target_type: 'invalid') + expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type') + end + + it 'filters out old notes' do + note2.update_attribute(:updated_at, 2.hours.ago) + notes = described_class.new(project, user, params).execute + expect(notes).to eq([note1]) + end + + context 'confidential issue notes' do + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let!(:confidential_note) { create(:note, noteable: confidential_issue, project: confidential_issue.project) } + + let(:params) { { target_id: confidential_issue.id, target_type: 'issue', last_fetched_at: 1.hour.ago.to_i } } + + it 'returns notes if user can see the issue' do + expect(described_class.new(project, user, params).execute).to eq([confidential_note]) + end + + it 'raises an error if user can not see the issue' do + user = create(:user) + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises an error for project members with guest role' do + user = create(:user) + project.team << [user, :guest] + + expect { described_class.new(project, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + end + + describe '.search' do + let(:project) { create(:empty_project, :public) } + let(:note) { create(:note_on_issue, note: 'WoW', project: project) } + + it 'returns notes with matching content' do + expect(described_class.new(note.project, nil, search: note.note).execute).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.new(note.project, nil, search: 'WOW').execute).to eq([note]) + end + + it 'returns commit notes user can access' do + note = create(:note_on_commit, project: project) + + expect(described_class.new(note.project, create(:user), search: note.note).execute).to eq([note]) + end + + context "confidential issues" do + let(:user) { create(:user) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } + let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } + + it "returns notes with matching content if user can see the issue" do + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to eq([confidential_note]) + end + + it "does not return notes with matching content if user can not see the issue" do user = create(:user) - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty end - it 'raises an error for project members with guest role' do + it "does not return notes with matching content for project members with guest role" do user = create(:user) project.team << [user, :guest] + expect(described_class.new(confidential_note.project, user, search: confidential_note.note).execute).to be_empty + end + + it "does not return notes with matching content for unauthenticated users" do + expect(described_class.new(confidential_note.project, nil, search: confidential_note.note).execute).to be_empty + end + end + + context 'inlines SQL filters on subqueries for performance' do + let(:sql) { described_class.new(note.project, nil, search: note.note).execute.to_sql } + let(:number_of_noteable_types) { 4 } + + specify 'project_id check' do + expect(sql.scan(/project_id/).count).to be >= (number_of_noteable_types + 2) + end - expect { NotesFinder.new.execute(project, user, params) }.to raise_error(ActiveRecord::RecordNotFound) + specify 'search filter' do + expect(sql.scan(/LIKE/).count).to be >= number_of_noteable_types end end end diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 15863d444f8..92053e5a7c6 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe ApplicationHelper do + include UploadHelpers + describe 'current_controller?' do it 'returns true when controller matches argument' do stub_controller_name('foo') @@ -52,10 +54,8 @@ describe ApplicationHelper do end describe 'project_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - project = create(:project, avatar: File.open(avatar_file_path)) + project = create(:project, avatar: File.open(uploaded_image_temp_path)) avatar_url = "http://#{Gitlab.config.gitlab.host}/uploads/project/avatar/#{project.id}/banana_sample.gif" expect(helper.project_icon("#{project.namespace.to_param}/#{project.to_param}").to_s). @@ -74,10 +74,8 @@ describe ApplicationHelper do end describe 'avatar_icon' do - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - it 'returns an url for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -88,7 +86,7 @@ describe ApplicationHelper do # Must be stubbed after the stub above, and separately stub_config_setting(url: Settings.send(:build_gitlab_url)) - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user.email).to_s). to match("/gitlab/uploads/user/avatar/#{user.id}/banana_sample.gif") @@ -102,7 +100,7 @@ describe ApplicationHelper do describe 'using a User' do it 'returns an URL for the avatar' do - user = create(:user, avatar: File.open(avatar_file_path)) + user = create(:user, avatar: File.open(uploaded_image_temp_path)) expect(helper.avatar_icon(user).to_s). to match("/uploads/user/avatar/#{user.id}/banana_sample.gif") diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 233d00534e5..c8b0d86425f 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -6,7 +6,7 @@ describe GroupsHelper do it 'returns an url for the avatar' do group = create(:group) - group.avatar = File.open(avatar_file_path) + group.avatar = fixture_file_upload(avatar_file_path) group.save! expect(group_icon(group.path).to_s). to match("/uploads/group/avatar/#{group.id}/banana_sample.gif") diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6 index a3171353bfb..49e56249565 100644 --- a/spec/javascripts/abuse_reports_spec.js.es6 +++ b/spec/javascripts/abuse_reports_spec.js.es6 @@ -1,42 +1,44 @@ -/* eslint-disable */ +/*= require lib/utils/text_utility */ /*= require abuse_reports */ -/*= require jquery */ - ((global) => { - const FIXTURE = 'abuse_reports.html'; - const MAX_MESSAGE_LENGTH = 500; + describe('Abuse Reports', () => { + const FIXTURE = 'abuse_reports/abuse_reports_list.html.raw'; + const MAX_MESSAGE_LENGTH = 500; + + let messages; - function assertMaxLength($message) { - expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); - } + const assertMaxLength = $message => expect($message.text().length).toEqual(MAX_MESSAGE_LENGTH); + const findMessage = searchText => messages.filter( + (index, element) => element.innerText.indexOf(searchText) > -1, + ).first(); - describe('Abuse Reports', function() { fixture.preload(FIXTURE); - beforeEach(function() { + beforeEach(function () { fixture.load(FIXTURE); - new global.AbuseReports(); + this.abuseReports = new global.AbuseReports(); + messages = $('.abuse-reports .message'); }); - it('should truncate long messages', function() { - const $longMessage = $('#long'); + + it('should truncate long messages', () => { + const $longMessage = findMessage('LONG MESSAGE'); expect($longMessage.data('original-message')).toEqual(jasmine.anything()); assertMaxLength($longMessage); }); - it('should not truncate short messages', function() { - const $shortMessage = $('#short'); + it('should not truncate short messages', () => { + const $shortMessage = findMessage('SHORT MESSAGE'); expect($shortMessage.data('original-message')).not.toEqual(jasmine.anything()); }); - it('should allow clicking a truncated message to expand and collapse the full message', function() { - const $longMessage = $('#long'); + it('should allow clicking a truncated message to expand and collapse the full message', () => { + const $longMessage = findMessage('LONG MESSAGE'); $longMessage.click(); expect($longMessage.data('original-message').length).toEqual($longMessage.text().length); $longMessage.click(); assertMaxLength($longMessage); }); }); - })(window.gl); diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6 index 8640cd44085..192da4ee8d9 100644 --- a/spec/javascripts/activities_spec.js.es6 +++ b/spec/javascripts/activities_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-unused-expressions, comma-spacing, prefer-const, no-prototype-builtins, semi, no-new, keyword-spacing, no-plusplus, no-shadow, max-len */ + /*= require js.cookie.js */ /*= require jquery.endless-scroll.js */ /*= require pager */ diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js index 7b2e3db6218..89201c8cb8b 100644 --- a/spec/javascripts/awards_handler_spec.js +++ b/spec/javascripts/awards_handler_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, no-undef, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-unused-expressions, comma-dangle, new-parens, no-unused-vars, quotes, jasmine/no-spec-dupes, prefer-template, padded-blocks, max-len */ +/* global AwardsHandler */ /*= require awards_handler */ /*= require jquery */ diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js index efb1203eb2f..0f61000bc37 100644 --- a/spec/javascripts/behaviors/quick_submit_spec.js +++ b/spec/javascripts/behaviors/quick_submit_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, no-undef, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-return-assign, comma-dangle, jasmine/no-spec-dupes, new-cap, padded-blocks, max-len */ /*= require behaviors/quick_submit */ diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6 index b84dfc8197b..b3a1afa28a5 100644 --- a/spec/javascripts/boards/boards_store_spec.js.es6 +++ b/spec/javascripts/boards/boards_store_spec.js.es6 @@ -1,4 +1,11 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, one-var, no-unused-vars, indent */ +/* global Vue */ +/* global BoardService */ +/* global boardsMockInterceptor */ +/* global Cookies */ +/* global listObj */ +/* global listObjDuplicate */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6 index 90cb8926545..c8a61a0a9b5 100644 --- a/spec/javascripts/boards/issue_spec.js.es6 +++ b/spec/javascripts/boards/issue_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global BoardService */ +/* global ListIssue */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6 index dfbcbe3a7c1..7d942ec3d65 100644 --- a/spec/javascripts/boards/list_spec.js.es6 +++ b/spec/javascripts/boards/list_spec.js.es6 @@ -1,4 +1,10 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle */ +/* global Vue */ +/* global boardsMockInterceptor */ +/* global BoardService */ +/* global List */ +/* global listObj */ + //= require jquery //= require jquery_ujs //= require js.cookie diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6 index fcb3d8f17d8..8d3e2237fda 100644 --- a/spec/javascripts/boards/mock_data.js.es6 +++ b/spec/javascripts/boards/mock_data.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, no-unused-vars, quote-props */ + const listObj = { id: 1, position: 0, diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6 index 93f73fa0e9a..aadf6f518a8 100644 --- a/spec/javascripts/dashboard_spec.js.es6 +++ b/spec/javascripts/dashboard_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable no-new, padded-blocks */ + /*= require sidebar */ /*= require jquery */ /*= require js.cookie */ diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6 index 9fdbab3a9e9..8ece24555c5 100644 --- a/spec/javascripts/datetime_utility_spec.js.es6 +++ b/spec/javascripts/datetime_utility_spec.js.es6 @@ -1,5 +1,5 @@ -/* eslint-disable */ //= require lib/utils/datetime_utility + (() => { describe('Date time utils', () => { describe('get day name', () => { diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6 index 9b2845af608..18805d26ac0 100644 --- a/spec/javascripts/diff_comments_store_spec.js.es6 +++ b/spec/javascripts/diff_comments_store_spec.js.es6 @@ -1,8 +1,11 @@ -/* eslint-disable */ +/* eslint-disable no-extra-semi, jasmine/no-global-setup, dot-notation, jasmine/no-expect-in-setup-teardown, max-len */ +/* global CommentsStore */ + //= require vue //= require diff_notes/models/discussion //= require diff_notes/models/note //= require diff_notes/stores/comments + (() => { function createDiscussion(noteId = 1, resolved = true) { CommentsStore.create('a', noteId, true, resolved, 'test'); diff --git a/spec/javascripts/extensions/object_spec.js.es6 b/spec/javascripts/extensions/object_spec.js.es6 new file mode 100644 index 00000000000..3b71c255b30 --- /dev/null +++ b/spec/javascripts/extensions/object_spec.js.es6 @@ -0,0 +1,25 @@ +/*= require extensions/object */ + +describe('Object extensions', () => { + describe('assign', () => { + it('merges source object into target object', () => { + const targetObj = {}; + const sourceObj = { + foo: 'bar', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('bar'); + }); + + it('merges object with the same properties', () => { + const targetObj = { + foo: 'bar', + }; + const sourceObj = { + foo: 'baz', + }; + Object.assign(targetObj, sourceObj); + expect(targetObj.foo).toBe('baz'); + }); + }); +}); diff --git a/spec/javascripts/fixtures/abuse_reports.html.haml b/spec/javascripts/fixtures/abuse_reports.html.haml deleted file mode 100644 index 2ec302abcb7..00000000000 --- a/spec/javascripts/fixtures/abuse_reports.html.haml +++ /dev/null @@ -1,16 +0,0 @@ -.abuse-reports - .message#long - Cat ipsum dolor sit amet, hide head under blanket so no one can see. - Gate keepers of hell eat and than sleep on your face but hunt by meowing - loudly at 5am next to human slave food dispenser cats go for world - domination or chase laser, yet poop on grasses chirp at birds. Cat is love, - cat is life chase after silly colored fish toys around the house climb a - tree, wait for a fireman jump to fireman then scratch his face fall asleep - on the washing machine lies down always hungry so caticus cuteicus. Sit on - human. Spot something, big eyes, big eyes, crouch, shake butt, prepare to - pounce sleep in the bathroom sink hiss at vacuum cleaner hide head under - blanket so no one can see throwup on your pillow. - .message#short - Cat ipsum dolor sit amet, groom yourself 4 hours - checked, have your - beauty sleep 18 hours - checked, be fabulous for the rest of the day - - checked! for shake treat bag. diff --git a/spec/javascripts/fixtures/abuse_reports.rb b/spec/javascripts/fixtures/abuse_reports.rb new file mode 100644 index 00000000000..de673f94d72 --- /dev/null +++ b/spec/javascripts/fixtures/abuse_reports.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Admin::AbuseReportsController, '(JavaScript fixtures)', type: :controller do + include JavaScriptFixturesHelpers + + let(:admin) { create(:admin) } + let!(:abuse_report) { create(:abuse_report) } + let!(:abuse_report_with_short_message) { create(:abuse_report, message: 'SHORT MESSAGE') } + let!(:abuse_report_with_long_message) { create(:abuse_report, message: "LONG MESSAGE\n" * 50) } + + render_views + + before(:all) do + clean_frontend_fixtures('abuse_reports/') + end + + before(:each) do + sign_in(admin) + end + + it 'abuse_reports/abuse_reports_list.html.raw' do |example| + get :index + + expect(response).to be_success + store_frontend_fixture(response, example.description) + end +end diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6 index 8ba238018cd..bfaf90e2aee 100644 --- a/spec/javascripts/gl_dropdown_spec.js.es6 +++ b/spec/javascripts/gl_dropdown_spec.js.es6 @@ -1,4 +1,6 @@ -/* eslint-disable */ +/* eslint-disable comma-dangle, prefer-const, no-param-reassign, no-plusplus, semi, no-unused-expressions, arrow-spacing, max-len */ +/* global Turbolinks */ + /*= require jquery */ /*= require gl_dropdown */ /*= require turbolinks */ diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6 index 0713e30e485..5018e87ad6c 100644 --- a/spec/javascripts/gl_field_errors_spec.js.es6 +++ b/spec/javascripts/gl_field_errors_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable space-before-function-paren, arrow-body-style, indent, padded-blocks */ + //= require jquery //= require gl_field_errors diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js index a406e6cc36a..bc5cbeb6a40 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js @@ -1,4 +1,8 @@ -/* eslint-disable quotes, no-undef, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* eslint-disable quotes, indent, semi, object-curly-spacing, jasmine/no-suite-dupes, vars-on-top, no-var, padded-blocks, spaced-comment, max-len */ +/* global d3 */ +/* global ContributorsGraph */ +/* global ContributorsMasterGraph */ + //= require graphs/stat_graph_contributors_graph describe("ContributorsGraph", function () { diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js index 96f39abe13e..751f3d175e2 100644 --- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js +++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, no-undef, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces, max-len */ +/* eslint-disable quotes, padded-blocks, no-var, camelcase, object-curly-spacing, semi, indent, object-property-newline, comma-dangle, comma-spacing, spaced-comment, max-len, key-spacing, vars-on-top, quote-props, no-multi-spaces */ +/* global ContributorsStatGraphUtil */ + //= require graphs/stat_graph_contributors_util describe("ContributorsStatGraphUtil", function () { diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js index f78573b992b..0da124632ae 100644 --- a/spec/javascripts/graphs/stat_graph_spec.js +++ b/spec/javascripts/graphs/stat_graph_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable quotes, padded-blocks, no-undef, semi */ +/* eslint-disable quotes, padded-blocks, semi */ +/* global StatGraph */ + //= require graphs/stat_graph describe("StatGraph", function () { diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js index 14af6644de1..faab5ae00c2 100644 --- a/spec/javascripts/issue_spec.js +++ b/spec/javascripts/issue_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-undef, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-use-before-define, indent, no-trailing-spaces, comma-dangle, padded-blocks, max-len */ +/* global Issue */ /*= require lib/utils/text_utility */ /*= require issue */ @@ -70,7 +71,7 @@ $('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', function() { spyOn(jQuery, 'ajax').and.callFake(function(req) { expect(req.type).toBe('PATCH'); diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6 index 49687048eb5..0c48d04776f 100644 --- a/spec/javascripts/labels_issue_sidebar_spec.js.es6 +++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6 @@ -1,4 +1,7 @@ -/* eslint-disable */ +/* eslint-disable no-new, no-plusplus, object-curly-spacing, prefer-const, semi */ +/* global IssuableContext */ +/* global LabelsSelect */ + //= require lib/utils/type_utility //= require jquery //= require bootstrap diff --git a/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 new file mode 100644 index 00000000000..3645dd70c55 --- /dev/null +++ b/spec/javascripts/lib/utils/custom_event_polyfill_spec.js.es6 @@ -0,0 +1,43 @@ +//= require lib/utils/custom_event_polyfill + +describe('Custom Event Polyfill', () => { + it('should be defined', () => { + expect(window.CustomEvent).toBeDefined(); + }); + + it('should create a `CustomEvent` instance', () => { + const e = new window.CustomEvent('foo'); + + expect(e.type).toEqual('foo'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail).toBeFalsy(); + }); + + it('should create a `CustomEvent` instance with a `details` object', () => { + const e = new window.CustomEvent('bar', { detail: { foo: 'bar' } }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(false); + expect(e.detail.foo).toEqual('bar'); + }); + + it('should create a `CustomEvent` instance with a `bubbles` boolean', () => { + const e = new window.CustomEvent('bar', { bubbles: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(true); + expect(e.cancelable).toBe(false); + expect(e.detail).toBeFalsy(); + }); + + it('should create a `CustomEvent` instance with a `cancelable` boolean', () => { + const e = new window.CustomEvent('bar', { cancelable: true }); + + expect(e.type).toEqual('bar'); + expect(e.bubbles).toBe(false); + expect(e.cancelable).toBe(true); + expect(e.detail).toBeFalsy(); + }); +}); diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js index b8b174a2e53..decdf583410 100644 --- a/spec/javascripts/line_highlighter_spec.js +++ b/spec/javascripts/line_highlighter_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-undef, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, no-param-reassign, quotes, prefer-template, no-else-return, new-cap, dot-notation, no-return-assign, comma-dangle, no-new, one-var, one-var-declaration-per-line, no-plusplus, jasmine/no-spec-dupes, no-underscore-dangle, padded-blocks, max-len */ +/* global LineHighlighter */ /*= require line_highlighter */ diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js index cbe2634d3a4..4cf1693af1b 100644 --- a/spec/javascripts/merge_request_spec.js +++ b/spec/javascripts/merge_request_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, padded-blocks */ +/* eslint-disable space-before-function-paren, no-return-assign, padded-blocks */ +/* global MergeRequest */ /*= require merge_request */ diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js index 8828970d984..a6cb9e47744 100644 --- a/spec/javascripts/new_branch_spec.js +++ b/spec/javascripts/new_branch_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, no-undef, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, one-var, no-var, one-var-declaration-per-line, no-return-assign, quotes, padded-blocks, max-len */ +/* global NewBranchForm */ /*= require jquery-ui/autocomplete */ /*= require new_branch_form */ diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js index 2db182d702b..d3bfa7730fa 100644 --- a/spec/javascripts/notes_spec.js +++ b/spec/javascripts/notes_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-undef, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-var, object-shorthand, comma-dangle, semi, padded-blocks, max-len */ +/* global Notes */ + /*= require notes */ /*= require autosize */ /*= require gl_form */ diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js index 65de1756201..bb802a4b5e3 100644 --- a/spec/javascripts/project_title_spec.js +++ b/spec/javascripts/project_title_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-undef, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-unused-expressions, no-return-assign, no-param-reassign, no-var, new-cap, wrap-iife, no-unused-vars, quotes, jasmine/no-expect-in-setup-teardown, padded-blocks, max-len */ + +/* global Project */ /*= require bootstrap */ /*= require select2 */ diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 0a9bc546144..a083dbf033a 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-undef, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, new-parens, no-return-assign, new-cap, vars-on-top, semi, padded-blocks, max-len */ +/* global Sidebar */ /*= require right_sidebar */ /*= require jquery */ diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js index e37816b0a8c..7bc898aed5d 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/shortcuts_issuable_spec.js @@ -1,4 +1,5 @@ -/* eslint-disable space-before-function-paren, no-return-assign, no-undef, no-var, quotes, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-return-assign, no-var, quotes, padded-blocks */ +/* global ShortcutsIssuable */ /*= require shortcuts_issuable */ diff --git a/spec/javascripts/subbable_resource_spec.js.es6 b/spec/javascripts/subbable_resource_spec.js.es6 index df395296791..6a70dd856a7 100644 --- a/spec/javascripts/subbable_resource_spec.js.es6 +++ b/spec/javascripts/subbable_resource_spec.js.es6 @@ -1,4 +1,5 @@ -/* eslint-disable */ +/* eslint-disable max-len, arrow-parens, comma-dangle, no-plusplus */ + //= vue //= vue-resource //= require jquery diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js index 944df6d23f7..a8874ab12d3 100644 --- a/spec/javascripts/u2f/authenticate_spec.js +++ b/spec/javascripts/u2f/authenticate_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, comma-dangle, no-var, one-var, one-var-declaration-per-line, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FAuthenticate */ /*= require u2f/authenticate */ /*= require u2f/util */ diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index 0c73c5772bd..189592ea87a 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -1,4 +1,6 @@ -/* eslint-disable space-before-function-paren, new-parens, no-undef, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, new-parens, quotes, no-var, one-var, one-var-declaration-per-line, comma-dangle, padded-blocks, max-len */ +/* global MockU2FDevice */ +/* global U2FRegister */ /*= require u2f/register */ /*= require u2f/util */ diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index b9acaaa5a0d..d80ce5a7f7e 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,4 +1,7 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, no-undef, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, padded-blocks, max-len */ +/* global Dropzone */ +/* global Mousetrap */ +/* global ZenMode */ /*= require zen_mode */ diff --git a/spec/lib/banzai/filter/math_filter_spec.rb b/spec/lib/banzai/filter/math_filter_spec.rb new file mode 100644 index 00000000000..3fe2c7f5d5d --- /dev/null +++ b/spec/lib/banzai/filter/math_filter_spec.rb @@ -0,0 +1,120 @@ +require 'spec_helper' + +describe Banzai::Filter::MathFilter, lib: true do + include FilterSpecHelper + + it 'leaves regular inline code unchanged' do + input = "<code>2+2</code>" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'removes surrounding dollar signs and adds class code, math and js-render-math' do + doc = filter("$<code>2+2</code>$") + + expect(doc.to_s).to eq '<code class="code math js-render-math" data-math-style="inline">2+2</code>' + end + + it 'only removes surrounding dollar signs' do + doc = filter("test $<code>2+2</code>$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test ' + expect(after.to_s).to eq ' test' + end + + it 'only removes surrounding single dollar sign' do + doc = filter("test $$<code>2+2</code>$$ test") + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq 'test $' + expect(after.to_s).to eq '$ test' + end + + it 'adds data-math-style inline attribute to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code['data-math-style']).to eq 'inline' + end + + it 'adds class code and math to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("code") + expect(code[:class]).to include("math") + end + + it 'adds js-render-math class to inline math' do + doc = filter('$<code>2+2</code>$') + code = doc.xpath('descendant-or-self::code').first + + expect(code[:class]).to include("js-render-math") + end + + # Cases with faulty syntax. Should be a no-op + + it 'ignores cases with missing dolar sign at the end' do + input = "test $<code>2+2</code> test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores cases with missing dolar sign at the beginning' do + input = "test <code>2+2</code>$ test" + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'ignores dollar signs if it is not adjacent' do + input = '<p>We check strictly $<code>2+2</code> and <code>2+2</code>$ </p>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + # Display math + + it 'adds data-math-style display attribute to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre['data-math-style']).to eq 'display' + end + + it 'adds js-render-math class to display math' do + doc = filter('<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>') + pre = doc.xpath('descendant-or-self::pre').first + + expect(pre[:class]).to include("js-render-math") + end + + it 'ignores code blocks that are not math' do + input = '<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'requires the pre to contain both code and math' do + input = '<pre class="highlight js-syntax-highlight plaintext math" v-pre="true"><code>2+2</code></pre>' + doc = filter(input) + + expect(doc.to_s).to eq input + end + + it 'dollar signs around to display math' do + doc = filter('$<pre class="code highlight js-syntax-highlight math" v-pre="true"><code>2+2</code></pre>$') + before = doc.xpath('descendant-or-self::text()[1]').first + after = doc.xpath('descendant-or-self::text()[3]').first + + expect(before.to_s).to eq '$' + expect(after.to_s).to eq '$' + end +end diff --git a/spec/lib/bitbucket/collection_spec.rb b/spec/lib/bitbucket/collection_spec.rb new file mode 100644 index 00000000000..015a7f80e03 --- /dev/null +++ b/spec/lib/bitbucket/collection_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +# Emulates paginator. It returns 2 pages with results +class TestPaginator + def initialize + @current_page = 0 + end + + def items + @current_page += 1 + + raise StopIteration if @current_page > 2 + + ["result_1_page_#{@current_page}", "result_2_page_#{@current_page}"] + end +end + +describe Bitbucket::Collection do + it "iterates paginator" do + collection = described_class.new(TestPaginator.new) + + expect(collection.to_a).to match(["result_1_page_1", "result_2_page_1", "result_1_page_2", "result_2_page_2"]) + end +end diff --git a/spec/lib/bitbucket/connection_spec.rb b/spec/lib/bitbucket/connection_spec.rb new file mode 100644 index 00000000000..14faeb231a9 --- /dev/null +++ b/spec/lib/bitbucket/connection_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Bitbucket::Connection do + before do + allow_any_instance_of(described_class).to receive(:provider).and_return(double(app_id: '', app_secret: '')) + end + + describe '#get' do + it 'calls OAuth2::AccessToken::get' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:get).and_return(double(parsed: true)) + + connection = described_class.new({}) + + connection.get('/users') + end + end + + describe '#expired?' do + it 'calls connection.expired?' do + expect_any_instance_of(OAuth2::AccessToken).to receive(:expired?).and_return(true) + + expect(described_class.new({}).expired?).to be_truthy + end + end + + describe '#refresh!' do + it 'calls connection.refresh!' do + response = double(token: nil, expires_at: nil, expires_in: nil, refresh_token: nil) + + expect_any_instance_of(OAuth2::AccessToken).to receive(:refresh!).and_return(response) + + described_class.new({}).refresh! + end + end +end diff --git a/spec/lib/bitbucket/page_spec.rb b/spec/lib/bitbucket/page_spec.rb new file mode 100644 index 00000000000..04d5a0470b1 --- /dev/null +++ b/spec/lib/bitbucket/page_spec.rb @@ -0,0 +1,50 @@ +require 'spec_helper' + +describe Bitbucket::Page do + let(:response) { { 'values' => [{ 'username' => 'Ben' }], 'pagelen' => 2, 'next' => '' } } + + before do + # Autoloading hack + Bitbucket::Representation::User.new({}) + end + + describe '#items' do + it 'returns collection of needed objects' do + page = described_class.new(response, :user) + + expect(page.items.first).to be_a(Bitbucket::Representation::User) + expect(page.items.count).to eq(1) + end + end + + describe '#attrs' do + it 'returns attributes' do + page = described_class.new(response, :user) + + expect(page.attrs.keys).to include(:pagelen, :next) + end + end + + describe '#next?' do + it 'returns true' do + page = described_class.new(response, :user) + + expect(page.next?).to be_truthy + end + + it 'returns false' do + response['next'] = nil + page = described_class.new(response, :user) + + expect(page.next?).to be_falsey + end + end + + describe '#next' do + it 'returns next attribute' do + page = described_class.new(response, :user) + + expect(page.next).to eq('') + end + end +end diff --git a/spec/lib/bitbucket/paginator_spec.rb b/spec/lib/bitbucket/paginator_spec.rb new file mode 100644 index 00000000000..2c972da682e --- /dev/null +++ b/spec/lib/bitbucket/paginator_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Bitbucket::Paginator do + let(:last_page) { double(:page, next?: false, items: ['item_2']) } + let(:first_page) { double(:page, next?: true, next: last_page, items: ['item_1']) } + + describe 'items' do + it 'return items and raises StopIteration in the end' do + paginator = described_class.new(nil, nil, nil) + + allow(paginator).to receive(:fetch_next_page).and_return(first_page) + expect(paginator.items).to match(['item_1']) + + allow(paginator).to receive(:fetch_next_page).and_return(last_page) + expect(paginator.items).to match(['item_2']) + + allow(paginator).to receive(:fetch_next_page).and_return(nil) + expect{ paginator.items }.to raise_error(StopIteration) + end + end +end diff --git a/spec/lib/bitbucket/representation/comment_spec.rb b/spec/lib/bitbucket/representation/comment_spec.rb new file mode 100644 index 00000000000..fec243a9f96 --- /dev/null +++ b/spec/lib/bitbucket/representation/comment_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Comment do + describe '#author' do + it { expect(described_class.new('user' => { 'username' => 'Ben' }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#note' do + it { expect(described_class.new('content' => { 'raw' => 'Text' }).note).to eq('Text') } + it { expect(described_class.new({}).note).to be_nil } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('updated_on' => Date.today).updated_at).to eq(Date.today) } + it { expect(described_class.new('created_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/issue_spec.rb b/spec/lib/bitbucket/representation/issue_spec.rb new file mode 100644 index 00000000000..20f47224aa8 --- /dev/null +++ b/spec/lib/bitbucket/representation/issue_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::Issue do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#kind' do + it { expect(described_class.new('kind' => 'bug').kind).to eq('bug') } + end + + describe '#milestone' do + it { expect(described_class.new({ 'milestone' => { 'name' => '1.0' } }).milestone).to eq('1.0') } + it { expect(described_class.new({}).milestone).to be_nil } + end + + describe '#author' do + it { expect(described_class.new({ 'reporter' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'content' => { 'raw' => 'Text' } }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'invalid' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'wontfix' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'resolved' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'duplicate' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'closed' }).state).to eq('closed') } + it { expect(described_class.new({ 'state' => 'opened' }).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#created_at' do + it { expect(described_class.new('created_on' => Date.today).created_at).to eq(Date.today) } + end + + describe '#updated_at' do + it { expect(described_class.new('edited_on' => Date.today).updated_at).to eq(Date.today) } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_comment_spec.rb b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb new file mode 100644 index 00000000000..673dcf22ce8 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_comment_spec.rb @@ -0,0 +1,34 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequestComment do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#file_path' do + it { expect(described_class.new('inline' => { 'path' => '/path' }).file_path).to eq('/path') } + end + + describe '#old_pos' do + it { expect(described_class.new('inline' => { 'from' => 3 }).old_pos).to eq(3) } + end + + describe '#new_pos' do + it { expect(described_class.new('inline' => { 'to' => 3 }).new_pos).to eq(3) } + end + + describe '#parent_id' do + it { expect(described_class.new({ 'parent' => { 'id' => 2 } }).parent_id).to eq(2) } + it { expect(described_class.new({}).parent_id).to be_nil } + end + + describe '#inline?' do + it { expect(described_class.new('inline' => {}).inline?).to be_truthy } + it { expect(described_class.new({}).inline?).to be_falsey } + end + + describe '#has_parent?' do + it { expect(described_class.new('parent' => {}).has_parent?).to be_truthy } + it { expect(described_class.new({}).has_parent?).to be_falsey } + end +end diff --git a/spec/lib/bitbucket/representation/pull_request_spec.rb b/spec/lib/bitbucket/representation/pull_request_spec.rb new file mode 100644 index 00000000000..30453528be4 --- /dev/null +++ b/spec/lib/bitbucket/representation/pull_request_spec.rb @@ -0,0 +1,47 @@ +require 'spec_helper' + +describe Bitbucket::Representation::PullRequest do + describe '#iid' do + it { expect(described_class.new('id' => 1).iid).to eq(1) } + end + + describe '#author' do + it { expect(described_class.new({ 'author' => { 'username' => 'Ben' } }).author).to eq('Ben') } + it { expect(described_class.new({}).author).to be_nil } + end + + describe '#description' do + it { expect(described_class.new({ 'description' => 'Text' }).description).to eq('Text') } + it { expect(described_class.new({}).description).to be_nil } + end + + describe '#state' do + it { expect(described_class.new({ 'state' => 'MERGED' }).state).to eq('merged') } + it { expect(described_class.new({ 'state' => 'DECLINED' }).state).to eq('closed') } + it { expect(described_class.new({}).state).to eq('opened') } + end + + describe '#title' do + it { expect(described_class.new('title' => 'Issue').title).to eq('Issue') } + end + + describe '#source_branch_name' do + it { expect(described_class.new({ source: { branch: { name: 'feature' } } }.with_indifferent_access).source_branch_name).to eq('feature') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_name).to be_nil } + end + + describe '#source_branch_sha' do + it { expect(described_class.new({ source: { commit: { hash: 'abcd123' } } }.with_indifferent_access).source_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ source: {} }.with_indifferent_access).source_branch_sha).to be_nil } + end + + describe '#target_branch_name' do + it { expect(described_class.new({ destination: { branch: { name: 'master' } } }.with_indifferent_access).target_branch_name).to eq('master') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_name).to be_nil } + end + + describe '#target_branch_sha' do + it { expect(described_class.new({ destination: { commit: { hash: 'abcd123' } } }.with_indifferent_access).target_branch_sha).to eq('abcd123') } + it { expect(described_class.new({ destination: {} }.with_indifferent_access).target_branch_sha).to be_nil } + end +end diff --git a/spec/lib/bitbucket/representation/user_spec.rb b/spec/lib/bitbucket/representation/user_spec.rb new file mode 100644 index 00000000000..f79ff4edb7b --- /dev/null +++ b/spec/lib/bitbucket/representation/user_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe Bitbucket::Representation::User do + describe '#username' do + it 'returns correct value' do + user = described_class.new('username' => 'Ben') + + expect(user.username).to eq('Ben') + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 4aba783dc33..f3843ca64ff 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -11,7 +11,7 @@ module Gitlab it "converts the input using Asciidoctor and default options" do expected_asciidoc_opts = { safe: :secure, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS } @@ -27,7 +27,7 @@ module Gitlab it "merges the options with default ones" do expected_asciidoc_opts = { safe: :safe, - backend: :html5, + backend: :gitlab_html5, attributes: described_class::DEFAULT_ADOC_ATTRS + ['foo'] } diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb index c9d64e99f88..f251c0dd25a 100644 --- a/spec/lib/gitlab/auth_spec.rb +++ b/spec/lib/gitlab/auth_spec.rb @@ -47,54 +47,76 @@ describe Gitlab::Auth, lib: true do project.create_drone_ci_service(active: true) project.drone_ci_service.update(token: 'token') - ip = 'ip' - - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'drone-ci-token') - expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: ip)).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'drone-ci-token') + expect(gl_auth.find_for_git_client('drone-ci-token', 'token', project: project, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, project, :ci, build_authentication_abilities)) end it 'recognizes master passwords' do user = create(:user, password: 'password') - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, 'password', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :gitlab_or_ldap, full_authentication_abilities)) end it 'recognizes user lfs tokens' do user = create(:user) - ip = 'ip' token = Gitlab::LfsToken.new(user).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username) - expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.username) + expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities)) end it 'recognizes deploy key lfs tokens' do key = create(:deploy_key) - ip = 'ip' token = Gitlab::LfsToken.new(key).token - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}") - expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: "lfs+deploy-key-#{key.id}") + expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities)) end - it 'recognizes OAuth tokens' do - user = create(:user) - application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) - ip = 'ip' + context "while using OAuth tokens as passwords" do + it 'succeeds for OAuth tokens with the `api` scope' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + end - expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: 'oauth2') - expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :oauth, read_authentication_abilities)) + it 'fails for OAuth tokens with other scopes' do + user = create(:user) + application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) + token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "read_user") + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: 'oauth2') + expect(gl_auth.find_for_git_client("oauth2", token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end + end + + context "while using personal access tokens as passwords" do + it 'succeeds for personal access tokens with the `api` scope' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['api']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: true, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(user, nil, :personal_token, full_authentication_abilities)) + end + + it 'fails for personal access tokens with other scopes' do + user = create(:user) + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: user.email) + expect(gl_auth.find_for_git_client(user.email, personal_access_token.token, project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new(nil, nil)) + end end it 'returns double nil for invalid credentials' do login = 'foo' - ip = 'ip' - expect(gl_auth).to receive(:rate_limit!).with(ip, success: false, login: login) - expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new) + expect(gl_auth).to receive(:rate_limit!).with('ip', success: false, login: login) + expect(gl_auth.find_for_git_client(login, 'bar', project: nil, ip: 'ip')).to eq(Gitlab::Auth::Result.new) end end diff --git a/spec/lib/gitlab/badge/build/status_spec.rb b/spec/lib/gitlab/badge/build/status_spec.rb index 38eebb2a176..70f03021d36 100644 --- a/spec/lib/gitlab/badge/build/status_spec.rb +++ b/spec/lib/gitlab/badge/build/status_spec.rb @@ -69,8 +69,8 @@ describe Gitlab::Badge::Build::Status do new_build.success! end - it 'reports the compound status' do - expect(badge.status).to eq 'failed' + it 'does not take outdated pipeline into account' do + expect(badge.status).to eq 'success' end end end diff --git a/spec/lib/gitlab/bitbucket_import/client_spec.rb b/spec/lib/gitlab/bitbucket_import/client_spec.rb deleted file mode 100644 index 7543c29bcc4..00000000000 --- a/spec/lib/gitlab/bitbucket_import/client_spec.rb +++ /dev/null @@ -1,67 +0,0 @@ -require 'spec_helper' - -describe Gitlab::BitbucketImport::Client, lib: true do - include ImportSpecHelper - - let(:token) { '123456' } - let(:secret) { 'secret' } - let(:client) { Gitlab::BitbucketImport::Client.new(token, secret) } - - before do - stub_omniauth_provider('bitbucket') - end - - it 'all OAuth client options are symbols' do - client.consumer.options.keys.each do |key| - expect(key).to be_kind_of(Symbol) - end - end - - context 'issues' do - let(:per_page) { 50 } - let(:count) { 95 } - let(:sample_issues) do - issues = [] - - count.times do |i| - issues << { local_id: i } - end - - issues - end - let(:first_sample_data) { { count: count, issues: sample_issues[0..per_page - 1] } } - let(:second_sample_data) { { count: count, issues: sample_issues[per_page..count] } } - let(:project_id) { 'namespace/repo' } - - it 'retrieves issues over a number of pages' do - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=0"). - to_return(status: 200, - body: first_sample_data.to_json, - headers: {}) - - stub_request(:get, - "https://bitbucket.org/api/1.0/repositories/#{project_id}/issues?limit=50&sort=utc_created_on&start=50"). - to_return(status: 200, - body: second_sample_data.to_json, - headers: {}) - - issues = client.issues(project_id) - expect(issues.count).to eq(95) - end - end - - context 'project import' do - it 'calls .from_project with no errors' do - project = create(:empty_project) - project.import_url = "ssh://git@bitbucket.org/test/test.git" - project.create_or_update_import_data(credentials: - { user: "git", - password: nil, - bb_session: { bitbucket_access_token: "test", - bitbucket_access_token_secret: "test" } }) - - expect { described_class.from_project(project) }.not_to raise_error - end - end -end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb index aa00f32becb..53f3c73ade4 100644 --- a/spec/lib/gitlab/bitbucket_import/importer_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -18,15 +18,21 @@ describe Gitlab::BitbucketImport::Importer, lib: true do "closed" # undocumented status ] end + let(:sample_issues_statuses) do issues = [] statuses.map.with_index do |status, index| issues << { - local_id: index, - status: status, + id: index, + state: status, title: "Issue #{index}", - content: "Some content to issue #{index}" + kind: 'bug', + content: { + raw: "Some content to issue #{index}", + markup: "markdown", + html: "Some content to issue #{index}" + } } end @@ -34,14 +40,16 @@ describe Gitlab::BitbucketImport::Importer, lib: true do end let(:project_identifier) { 'namespace/repo' } + let(:data) do { 'bb_session' => { - 'bitbucket_access_token' => "123456", - 'bitbucket_access_token_secret' => "secret" + 'bitbucket_token' => "123456", + 'bitbucket_refresh_token' => "secret" } } end + let(:project) do create( :project, @@ -49,11 +57,13 @@ describe Gitlab::BitbucketImport::Importer, lib: true do import_data: ProjectImportData.new(credentials: data) ) end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do { count: sample_issues_statuses.count, - issues: sample_issues_statuses + values: sample_issues_statuses } end @@ -61,26 +71,46 @@ describe Gitlab::BitbucketImport::Importer, lib: true do before do stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}" - ).to_return(status: 200, body: { has_issues: true }.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: { has_issues: true, full_name: project_identifier }.to_json) stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues?limit=50&sort=utc_created_on&start=0" - ).to_return(status: 200, body: issues_statuses_sample_data.to_json) + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues?pagelen=50&sort=created_on" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: issues_statuses_sample_data.to_json) + + stub_request(:get, "https://api.bitbucket.org/2.0/repositories/namespace/repo?pagelen=50&sort=created_on"). + with(headers: { 'Accept' => '*/*', 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', 'Authorization' => 'Bearer', 'User-Agent' => 'Faraday v0.9.2' }). + to_return(status: 200, + body: "", + headers: {}) sample_issues_statuses.each_with_index do |issue, index| stub_request( :get, - "https://bitbucket.org/api/1.0/repositories/#{project_identifier}/issues/#{issue[:local_id]}/comments" + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/issues/#{issue[:id]}/comments?pagelen=50&sort=created_on" ).to_return( status: 200, - body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + headers: { "Content-Type" => "application/json" }, + body: { author_info: { username: "username" }, utc_created_on: index }.to_json ) end + + stub_request( + :get, + "https://api.bitbucket.org/2.0/repositories/#{project_identifier}/pullrequests?pagelen=50&sort=created_on&state=ALL" + ).to_return(status: 200, + headers: { "Content-Type" => "application/json" }, + body: {}.to_json) end it 'map statuses to open or closed' do + # HACK: Bitbucket::Representation.const_get('Issue') seems to return ::Issue without this + Bitbucket::Representation::Issue.new({}) importer.execute expect(project.issues.where(state: "closed").size).to eq(5) diff --git a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb index e1c60e07b4d..b6d052a4612 100644 --- a/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb +++ b/spec/lib/gitlab/bitbucket_import/project_creator_spec.rb @@ -2,14 +2,18 @@ require 'spec_helper' describe Gitlab::BitbucketImport::ProjectCreator, lib: true do let(:user) { create(:user) } + let(:repo) do - { - name: 'Vim', - slug: 'vim', - is_private: true, - owner: "asd" - }.with_indifferent_access + double(name: 'Vim', + slug: 'vim', + description: 'Test repo', + is_private: true, + owner: "asd", + full_name: 'Vim repo', + visibility_level: Gitlab::VisibilityLevel::PRIVATE, + clone_url: 'ssh://git@bitbucket.org/asd/vim.git') end + let(:namespace){ create(:group, owner: user) } let(:token) { "asdasd12345" } let(:secret) { "sekrettt" } @@ -22,7 +26,7 @@ describe Gitlab::BitbucketImport::ProjectCreator, lib: true do it 'creates project' do allow_any_instance_of(Project).to receive(:add_import_job) - project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, user, access_params) + project_creator = Gitlab::BitbucketImport::ProjectCreator.new(repo, 'vim', namespace, user, access_params) project = project_creator.execute expect(project.import_url).to eq("ssh://git@bitbucket.org/asd/vim.git") diff --git a/spec/lib/gitlab/checks/force_push_spec.rb b/spec/lib/gitlab/checks/force_push_spec.rb new file mode 100644 index 00000000000..f6288011494 --- /dev/null +++ b/spec/lib/gitlab/checks/force_push_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe Gitlab::Checks::ChangeAccess, lib: true do + let(:project) { create(:project) } + + context "exit code checking" do + it "does not raise a runtime error if the `popen` call to git returns a zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['normal output', 0]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.not_to raise_error + end + + it "raises a runtime error if the `popen` call to git returns a non-zero exit code" do + allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1]) + + expect { Gitlab::Checks::ForcePush.force_push?(project, 'oldrev', 'newrev') }.to raise_error(RuntimeError) + end + end +end diff --git a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb index d619e401897..f4703dc704f 100644 --- a/spec/lib/gitlab/gfm/reference_rewriter_spec.rb +++ b/spec/lib/gitlab/gfm/reference_rewriter_spec.rb @@ -29,7 +29,7 @@ describe Gitlab::Gfm::ReferenceRewriter do context 'description with ignored elements' do let(:text) do "Hi. This references #1, but not `#2`\n" + - '<pre>and not !1</pre>' + '<pre>and not !1</pre>' end it { is_expected.to include issue_first.to_reference(new_project) } diff --git a/spec/lib/gitlab/git/rev_list_spec.rb b/spec/lib/gitlab/git/rev_list_spec.rb new file mode 100644 index 00000000000..444639acbaa --- /dev/null +++ b/spec/lib/gitlab/git/rev_list_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Gitlab::Git::RevList, lib: true do + let(:project) { create(:project) } + + context "validations" do + described_class::ALLOWED_VARIABLES.each do |var| + context var do + it "accepts values starting with the project repo path" do + env = { var => "#{project.repository.path_to_repo}/objects" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).to be_valid + end + + it "rejects values starting not with the project repo path" do + env = { var => "/some/other/path" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + + it "rejects values containing the project repo path but not starting with it" do + env = { var => "/some/other/path/#{project.repository.path_to_repo}" } + rev_list = described_class.new('oldrev', 'newrev', project: project, env: env) + + expect(rev_list).not_to be_valid + end + end + end + end + + context "#execute" do + let(:env) { { "GIT_OBJECT_DIRECTORY" => project.repository.path_to_repo } } + let(:rev_list) { Gitlab::Git::RevList.new('oldrev', 'newrev', project: project, env: env) } + + it "calls out to `popen` without environment variables if the record is invalid" do + allow(rev_list).to receive(:valid?).and_return(false) + + expect(Open3).to receive(:popen3).with(hash_excluding(env), any_args) + + rev_list.execute + end + + it "calls out to `popen` with environment variables if the record is valid" do + allow(rev_list).to receive(:valid?).and_return(true) + + expect(Open3).to receive(:popen3).with(hash_including(env), any_args) + + rev_list.execute + end + end +end diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 8e1a28f2723..9b49d6837c3 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -136,7 +136,8 @@ project: - assembla_service - asana_service - gemnasium_service -- slack_service +- slack_notification_service +- mattermost_notification_service - buildkite_service - bamboo_service - teamcity_service @@ -147,6 +148,7 @@ project: - bugzilla_service - gitlab_issue_tracker_service - external_wiki_service +- kubernetes_service - forked_project_link - forked_from_project - forked_project_links diff --git a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb index 5ae178414cc..08a42fd27a2 100644 --- a/spec/lib/gitlab/import_export/avatar_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/avatar_restorer_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe Gitlab::ImportExport::AvatarRestorer, lib: true do + include UploadHelpers + 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") + .and_return(uploaded_image_temp_path) end after do diff --git a/spec/lib/gitlab/middleware/multipart_spec.rb b/spec/lib/gitlab/middleware/multipart_spec.rb new file mode 100644 index 00000000000..ab1ab22795c --- /dev/null +++ b/spec/lib/gitlab/middleware/multipart_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +require 'tempfile' + +describe Gitlab::Middleware::Multipart do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + it 'opens top-level files' do + Tempfile.open('top-level') do |tempfile| + env = post_env({ 'file' => tempfile.path }, { 'file.name' => 'filename' }, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['file'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'rejects headers signed with the wrong secret' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, 'x' * 32, 'gitlab-workhorse') + + expect { middleware.call(env) }.to raise_error(JWT::VerificationError) + end + + it 'rejects headers signed with the wrong issuer' do + env = post_env({ 'file' => '/var/empty/nonesuch' }, {}, Gitlab::Workhorse.secret, 'acme-inc') + + expect { middleware.call(env) }.to raise_error(JWT::InvalidIssuerError) + end + + it 'opens files one level deep' do + Tempfile.open('one-level') do |tempfile| + in_params = { 'user' => { 'avatar' => { '.name' => 'filename' } } } + env = post_env({ 'user[avatar]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['user']['avatar'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + it 'opens files two levels deep' do + Tempfile.open('two-levels') do |tempfile| + in_params = { 'project' => { 'milestone' => { 'themesong' => { '.name' => 'filename' } } } } + env = post_env({ 'project[milestone][themesong]' => tempfile.path }, in_params, Gitlab::Workhorse.secret, 'gitlab-workhorse') + + expect(app).to receive(:call) do |env| + file = Rack::Request.new(env).params['project']['milestone']['themesong'] + expect(file).to be_a(File) + expect(file.path).to eq(tempfile.path) + end + + middleware.call(env) + end + end + + def post_env(rewritten_fields, params, secret, issuer) + token = JWT.encode({ 'iss' => issuer, 'rewritten_fields' => rewritten_fields }, secret, 'HS256') + Rack::MockRequest.env_for( + '/', + method: 'post', + params: params, + described_class::RACK_ENV_KEY => token + ) + end +end diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index 3cd9863ec6a..14ee386dba6 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -149,4 +149,33 @@ describe Gitlab::ProjectSearchResults, lib: true do expect(results.issues_count).to eq 3 end end + + describe 'notes search' do + it 'lists notes' do + project = create(:empty_project, :public) + note = create(:note, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).to include note + end + + it "doesn't list issue notes when access is restricted" do + project = create(:empty_project, :public, issues_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_issue, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + + it "doesn't list merge_request notes when access is restricted" do + project = create(:empty_project, :public, merge_requests_access_level: ProjectFeature::PRIVATE) + note = create(:note_on_merge_request, project: project) + + results = described_class.new(user, project, note.note) + + expect(results.objects('notes')).not_to include note + end + end end diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index c51b10bdc69..c78cd30157e 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -29,4 +29,20 @@ describe Gitlab::Regex, lib: true do describe 'file path regex' do it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } end + + describe 'environment slug regex' do + def be_matched + match(Gitlab::Regex.environment_slug_regex) + end + + it { expect('foo').to be_matched } + it { expect('foo-1').to be_matched } + + it { expect('FOO').not_to be_matched } + it { expect('foo/1').not_to be_matched } + it { expect('foo.1').not_to be_matched } + it { expect('foo*1').not_to be_matched } + it { expect('9foo').not_to be_matched } + it { expect('foo-').not_to be_matched } + end end diff --git a/spec/lib/gitlab/sql/union_spec.rb b/spec/lib/gitlab/sql/union_spec.rb index 0cdbab87544..849edb09476 100644 --- a/spec/lib/gitlab/sql/union_spec.rb +++ b/spec/lib/gitlab/sql/union_spec.rb @@ -1,16 +1,26 @@ require 'spec_helper' describe Gitlab::SQL::Union, lib: true do + let(:relation_1) { User.where(email: 'alice@example.com').select(:id) } + let(:relation_2) { User.where(email: 'bob@example.com').select(:id) } + + def to_sql(relation) + relation.reorder(nil).to_sql + end + describe '#to_sql' do it 'returns a String joining relations together using a UNION' do - rel1 = User.where(email: 'alice@example.com') - rel2 = User.where(email: 'bob@example.com') - union = described_class.new([rel1, rel2]) + union = described_class.new([relation_1, relation_2]) + + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") + end - sql1 = rel1.reorder(nil).to_sql - sql2 = rel2.reorder(nil).to_sql + it 'skips Model.none segements' do + empty_relation = User.none + union = described_class.new([empty_relation, relation_1, relation_2]) - expect(union.to_sql).to eq("#{sql1}\nUNION\n#{sql2}") + expect{User.where("users.id IN (#{union.to_sql})").to_a}.not_to raise_error + expect(union.to_sql).to eq("#{to_sql(relation_1)}\nUNION\n#{to_sql(relation_2)}") end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb new file mode 100644 index 00000000000..3c2eddbd221 --- /dev/null +++ b/spec/lib/mattermost/session_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe Mattermost::Session, type: :request do + let(:user) { create(:user) } + + let(:gitlab_url) { "http://gitlab.com" } + let(:mattermost_url) { "http://mattermost.com" } + + subject { described_class.new(user) } + + # Needed for doorkeeper to function + it { is_expected.to respond_to(:current_resource_owner) } + it { is_expected.to respond_to(:request) } + it { is_expected.to respond_to(:authorization) } + it { is_expected.to respond_to(:strategy) } + + before do + described_class.base_uri(mattermost_url) + end + + describe '#with session' do + let(:location) { 'http://location.tld' } + let!(:stub) do + WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login"). + to_return(headers: { 'location' => location }, status: 307) + end + + context 'without oauth uri' do + it 'makes a request to the oauth uri' do + expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with oauth_uri' do + let!(:doorkeeper) do + Doorkeeper::Application.create( + name: "GitLab Mattermost", + redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", + scopes: "") + end + + context 'without token_uri' do + it 'can not create a session' do + expect do + subject.with_session + end.to raise_error(Mattermost::NoSessionError) + end + end + + context 'with token_uri' do + let(:state) { "state" } + let(:params) do + { response_type: "code", + client_id: doorkeeper.uid, + redirect_uri: "#{mattermost_url}/signup/gitlab/complete", + state: state } + end + let(:location) do + "#{gitlab_url}/oauth/authorize?#{URI.encode_www_form(params)}" + end + + before do + WebMock.stub_request(:get, "#{mattermost_url}/signup/gitlab/complete"). + with(query: hash_including({ 'state' => state })). + to_return do |request| + post "/oauth/token", + client_id: doorkeeper.uid, + client_secret: doorkeeper.secret, + redirect_uri: params[:redirect_uri], + grant_type: 'authorization_code', + code: request.uri.query_values['code'] + + if response.status == 200 + { headers: { 'token' => 'thisworksnow' }, status: 202 } + end + end + + WebMock.stub_request(:post, "#{mattermost_url}/api/v3/users/logout"). + to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) + end + + it 'can setup a session' do + subject.with_session do |session| + end + + expect(subject.token).not_to be_nil + end + + it 'returns the value of the block' do + result = subject.with_session do |session| + "value" + end + + expect(result).to eq("value") + end + end + end + end +end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 7f39aff7639..6f1c2ae0fd8 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -87,6 +87,26 @@ describe Ci::Build, models: true do end end + describe '#persisted_environment' do + before do + @environment = create(:environment, project: project, name: "foo-#{project.default_branch}") + end + + subject { build.persisted_environment } + + context 'referenced literally' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-#{project.default_branch}") } + + it { is_expected.to eq(@environment) } + end + + context 'referenced with a variable' do + let(:build) { create(:ci_build, pipeline: pipeline, environment: "foo-$CI_BUILD_REF_NAME") } + + it { is_expected.to eq(@environment) } + end + end + describe '#trace' do it { expect(build.trace).to be_nil } @@ -254,6 +274,24 @@ describe Ci::Build, models: true do end end + describe '#ref_slug' do + { + 'master' => 'master', + '1-foo' => '1-foo', + 'fix/1-foo' => 'fix-1-foo', + 'fix-1-foo' => 'fix-1-foo', + 'a' * 63 => 'a' * 63, + 'a' * 64 => 'a' * 63, + 'FOO' => 'foo', + }.each do |ref, slug| + it "transforms #{ref} to #{slug}" do + build.ref = ref + + expect(build.ref_slug).to eq(slug) + end + end + end + describe '#variables' do let(:container_registry_enabled) { false } let(:predefined_variables) do @@ -265,6 +303,7 @@ describe Ci::Build, models: true do { 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_REF_SLUG', 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 }, @@ -309,6 +348,22 @@ describe Ci::Build, models: true do it { user_variables.each { |v| is_expected.to include(v) } } end + context 'when build has an environment' do + before do + build.update(environment: 'production') + create(:environment, project: build.project, name: 'production', slug: 'prod-slug') + end + + let(:environment_variables) do + [ + { key: 'CI_ENVIRONMENT_NAME', value: 'production', public: true }, + { key: 'CI_ENVIRONMENT_SLUG', value: 'prod-slug', public: true } + ] + end + + it { environment_variables.each { |v| is_expected.to include(v) } } + end + context 'when build started manually' do before do build.update_attributes(when: :manual) @@ -451,6 +506,17 @@ describe Ci::Build, models: true do it { is_expected.to include({ key: 'CI_RUNNER_TAGS', value: 'docker, linux', public: true }) } end + context 'when build is for a deployment' do + let(:deployment_variable) { { key: 'KUBERNETES_TOKEN', value: 'TOKEN', public: false } } + + before do + build.environment = 'production' + allow(project).to receive(:deployment_variables).and_return([deployment_variable]) + end + + it { is_expected.to include(deployment_variable) } + end + context 'returns variables in valid order' do before do allow(build).to receive(:predefined_variables) { ['predefined'] } diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index e78ae14b737..52dd41065e9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -381,6 +381,65 @@ describe Ci::Pipeline, models: true do end end + shared_context 'with some outdated pipelines' do + before do + create_pipeline(:canceled, 'ref', 'A') + create_pipeline(:success, 'ref', 'A') + create_pipeline(:failed, 'ref', 'B') + create_pipeline(:skipped, 'feature', 'C') + end + + def create_pipeline(status, ref, sha) + create(:ci_empty_pipeline, status: status, ref: ref, sha: sha) + end + end + + describe '.latest' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:pipelines) { described_class.latest.all } + + it 'returns the latest pipeline for the same ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B', 'C') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed', 'skipped') + end + end + + context 'when ref is specified' do + let(:pipelines) { described_class.latest('ref').all } + + it 'returns the latest pipeline for ref and different sha' do + expect(pipelines.map(&:sha)).to contain_exactly('A', 'B') + expect(pipelines.map(&:status)). + to contain_exactly('success', 'failed') + end + end + end + + describe '.latest_status' do + include_context 'with some outdated pipelines' + + context 'when no ref is specified' do + let(:latest_status) { described_class.latest_status } + + it 'returns the latest status for the same ref and different sha' do + expect(latest_status).to eq(described_class.latest.status) + expect(latest_status).to eq('failed') + end + end + + context 'when ref is specified' do + let(:latest_status) { described_class.latest_status('ref') } + + it 'returns the latest status for ref and different sha' do + expect(latest_status).to eq(described_class.latest_status('ref')) + expect(latest_status).to eq('failed') + end + end + end + describe '#status' do let!(:build) { create(:ci_build, :created, pipeline: pipeline, name: 'test') } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index 0935fc0561c..74b50d2908d 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -213,23 +213,19 @@ eos end describe '#status' do - context 'without arguments for compound status' do - shared_examples 'giving the status from pipeline' do - it do - expect(commit.status).to eq(Ci::Pipeline.status) - end - end - - context 'with pipelines' do - let!(:pipeline) do - create(:ci_empty_pipeline, project: project, sha: commit.sha) + context 'without ref argument' do + before do + %w[success failed created pending].each do |status| + create(:ci_empty_pipeline, + project: project, + sha: commit.sha, + status: status) end - - it_behaves_like 'giving the status from pipeline' end - context 'without pipelines' do - it_behaves_like 'giving the status from pipeline' + it 'gives compound status from latest pipelines' do + expect(commit.status).to eq(Ci::Pipeline.latest_status) + expect(commit.status).to eq('pending') end end @@ -255,8 +251,9 @@ eos expect(commit.status('fix')).to eq(pipeline_from_fix.status) end - it 'gives compound status if ref is nil' do - expect(commit.status(nil)).to eq(commit.status) + it 'gives compound status from latest pipelines if ref is nil' do + expect(commit.status(nil)).to eq(Ci::Pipeline.latest_status) + expect(commit.status(nil)).to eq('failed') end end end diff --git a/spec/models/concerns/token_authenticatable_spec.rb b/spec/models/concerns/token_authenticatable_spec.rb index eb64f3d0c83..4b0bfa43abf 100644 --- a/spec/models/concerns/token_authenticatable_spec.rb +++ b/spec/models/concerns/token_authenticatable_spec.rb @@ -6,6 +6,7 @@ shared_examples 'TokenAuthenticatable' do it { expect(described_class).to be_private_method_defined(:write_new_token) } it { expect(described_class).to respond_to("find_by_#{token_field}") } it { is_expected.to respond_to("ensure_#{token_field}") } + it { is_expected.to respond_to("set_#{token_field}") } it { is_expected.to respond_to("reset_#{token_field}!") } end end @@ -55,6 +56,12 @@ describe ApplicationSetting, 'TokenAuthenticatable' do end end + describe 'setting new token' do + subject { described_class.new.send("set_#{token_field}", '0123456789') } + + it { is_expected.to eq '0123456789' } + end + describe 'multiple token fields' do before do described_class.send(:add_authentication_token_field, :yet_another_token) diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index c8170164898..97cbb093ed2 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment, models: true do - let(:environment) { create(:environment) } + subject(:environment) { create(:environment) } it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:deployments) } @@ -15,15 +15,11 @@ describe Environment, models: true do it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } - it { is_expected.to validate_length_of(:external_url).is_at_most(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) + it { is_expected.to validate_uniqueness_of(:slug).scoped_to(:project_id) } + it { is_expected.to validate_length_of(:slug).is_at_most(24) } - is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) - end + it { is_expected.to validate_length_of(:external_url).is_at_most(255) } + it { is_expected.to validate_uniqueness_of(:external_url).scoped_to(:project_id) } describe '#nullify_external_url' do it 'replaces a blank url with nil' do @@ -199,4 +195,38 @@ describe Environment, models: true do expect(environment.actions_for('review/master')).to contain_exactly(review_action) end end + + describe '#slug' do + it "is automatically generated" do + expect(environment.slug).not_to be_nil + end + + it "is not regenerated if name changes" do + original_slug = environment.slug + environment.update_attributes!(name: environment.name.reverse) + + expect(environment.slug).to eq(original_slug) + end + end + + describe '#generate_slug' do + SUFFIX = "-[a-z0-9]{6}" + { + "staging-12345678901234567" => "staging-123456789" + SUFFIX, + "9-staging-123456789012345" => "env-9-staging-123" + SUFFIX, + "staging-1234567890123456" => "staging-1234567890123456", + "production" => "production", + "PRODUCTION" => "production" + SUFFIX, + "review/1-foo" => "review-1-foo" + SUFFIX, + "1-foo" => "env-1-foo" + SUFFIX, + "1/foo" => "env-1-foo" + SUFFIX, + "foo-" => "foo" + SUFFIX, + }.each do |name, matcher| + it "returns a slug matching #{matcher}, given #{name}" do + slug = described_class.new(name: name).generate_slug + + expect(slug).to match(/\A#{matcher}\z/) + end + end + end end diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 850b1a3cf1e..7d5ecfbaa64 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -50,9 +50,8 @@ describe Group, models: true do describe 'validations' do it { is_expected.to validate_presence_of :name } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_presence_of :path } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.not_to validate_presence_of :owner } end @@ -273,7 +272,7 @@ describe Group, models: true do end describe 'nested group' do - subject { create(:group, :nested) } + subject { build(:group, :nested) } it { is_expected.to be_valid } it { expect(subject.parent).to be_kind_of(Group) } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 24e216329a9..bb56e44db29 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -22,26 +22,6 @@ 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 let(:project) { build(:empty_project, name: 'sample-project') } let(:issue) { build(:issue, iid: 1, project: project) } diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 8b730be91fd..5da00a8636a 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -205,7 +205,7 @@ describe MergeRequest, models: true do end end - describe "#mr_and_commit_notes" do + describe "#related_notes" do let!(:merge_request) { create(:merge_request) } before do @@ -217,7 +217,7 @@ describe MergeRequest, models: true do it "includes notes for commits" do expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(2) + expect(merge_request.related_notes.count).to eq(2) end it "includes notes for commits from target project as well" do @@ -225,7 +225,7 @@ describe MergeRequest, models: true do project: merge_request.target_project) expect(merge_request.commits).not_to be_empty - expect(merge_request.mr_and_commit_notes.count).to eq(3) + expect(merge_request.related_notes.count).to eq(3) end end @@ -252,7 +252,7 @@ describe MergeRequest, models: true do end end - describe 'detection of issues to be closed' do + describe '#closes_issues' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } @@ -280,14 +280,19 @@ describe MergeRequest, models: true do expect(subject.closes_issues).to be_empty end + end + + describe '#issues_mentioned_but_not_closing' do + it 'detects issues mentioned in description but not closed' do + mentioned_issue = create(:issue, project: subject.project) + + subject.project.team << [subject.author, :developer] + subject.description = "Is related to #{mentioned_issue.to_reference}" - it 'detects issues mentioned in the description' do - issue2 = create(:issue, project: subject.project) - subject.description = "Closes #{issue2.to_reference}" allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.closes_issues).to include(issue2) + expect(subject.issues_mentioned_but_not_closing).to match_array([mentioned_issue]) end end @@ -410,11 +415,17 @@ describe MergeRequest, models: true do .to match("Remove all technical debt\n\n") end - it 'includes its description in the body' do - request = build(:merge_request, description: 'By removing all code') + it 'includes its closed issues in the body' do + issue = create(:issue, project: subject.project) - expect(request.merge_commit_message) - .to match("By removing all code\n\n") + subject.project.team << [subject.author, :developer] + subject.description = "This issue Closes #{issue.to_reference}" + + allow(subject.project).to receive(:default_branch). + and_return(subject.target_branch) + + expect(subject.merge_commit_message) + .to match("Closes #{issue.to_reference}") end it 'includes its reference in the body' do @@ -429,6 +440,20 @@ describe MergeRequest, models: true do expect(request.merge_commit_message).not_to match("Title\n\n\n\n") end + + it 'includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message(include_description: true)) + .to match("By removing all code\n\n") + end + + it 'does not includes its description in the body' do + request = build(:merge_request, description: 'By removing all code') + + expect(request.merge_commit_message) + .not_to match("By removing all code\n\n") + end end describe "#reset_merge_when_build_succeeds" do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 0cc2efae5f9..064f29d2d66 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -24,8 +24,9 @@ describe Milestone, models: true do it { is_expected.to have_many(:issues) } end - let(:milestone) { create(:milestone) } - let(:issue) { create(:issue) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } describe "#title" do @@ -110,8 +111,8 @@ describe Milestone, models: true do describe :items_count do before do - milestone.issues << create(:issue) - milestone.issues << create(:closed_issue) + milestone.issues << create(:issue, project: project) + milestone.issues << create(:closed_issue, project: project) milestone.merge_requests << create(:merge_request) end @@ -126,7 +127,7 @@ describe Milestone, models: true do describe '#total_items_count' do before do - create :closed_issue, milestone: milestone + create :closed_issue, milestone: milestone, project: project create :merge_request, milestone: milestone end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 7f82e85563b..9fd06bb6b23 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -6,20 +6,16 @@ describe Namespace, models: true do it { is_expected.to have_many :projects } it { is_expected.to validate_presence_of(:name) } - it { is_expected.to validate_uniqueness_of(:name) } + it { is_expected.to validate_uniqueness_of(:name).scoped_to(:parent_id) } it { is_expected.to validate_length_of(:name).is_at_most(255) } it { is_expected.to validate_length_of(:description).is_at_most(255) } it { is_expected.to validate_presence_of(:path) } - it { is_expected.to validate_uniqueness_of(:path) } it { is_expected.to validate_length_of(:path).is_at_most(255) } it { is_expected.to validate_presence_of(:owner) } - describe "Mass assignment" do - end - describe "Respond to" do it { is_expected.to respond_to(:human_name) } it { is_expected.to respond_to(:to_param) } @@ -132,4 +128,26 @@ describe Namespace, models: true do it { expect(group.full_path).to eq(group.path) } it { expect(nested_group.full_path).to eq("#{group.path}/#{nested_group.path}") } end + + describe '#full_name' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + + it { expect(group.full_name).to eq(group.name) } + it { expect(nested_group.full_name).to eq("#{group.name} / #{nested_group.name}") } + end + + describe '#parents' do + let(:group) { create(:group) } + let(:nested_group) { create(:group, parent: group) } + let(:deep_nested_group) { create(:group, parent: nested_group) } + let(:very_deep_nested_group) { create(:group, parent: deep_nested_group) } + + it 'returns the correct parents' do + expect(very_deep_nested_group.parents).to eq([group, nested_group, deep_nested_group]) + expect(deep_nested_group.parents).to eq([group, nested_group]) + expect(nested_group.parents).to eq([group]) + expect(group.parents).to eq([]) + end + end end diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 17a15b12dcb..310fecd8a5c 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -162,44 +162,6 @@ describe Note, models: true do end end - describe '.search' do - let(:note) { create(:note_on_issue, note: 'WoW') } - - it 'returns notes with matching content' do - expect(described_class.search(note.note)).to eq([note]) - end - - it 'returns notes with matching content regardless of the casing' do - expect(described_class.search('WOW')).to eq([note]) - end - - context "confidential issues" do - let(:user) { create(:user) } - let(:project) { create(:project) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: user) } - let(:confidential_note) { create(:note, note: "Random", noteable: confidential_issue, project: confidential_issue.project) } - - it "returns notes with matching content if user can see the issue" do - expect(described_class.search(confidential_note.note, as_user: user)).to eq([confidential_note]) - end - - it "does not return notes with matching content if user can not see the issue" do - user = create(:user) - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for project members with guest role" do - user = create(:user) - project.team << [user, :guest] - expect(described_class.search(confidential_note.note, as_user: user)).to be_empty - end - - it "does not return notes with matching content for unauthenticated users" do - expect(described_class.search(confidential_note.note)).to be_empty - end - end - end - describe "editable?" do it "returns true" do note = build(:note) diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/chat_message/build_message_spec.rb index 452f4e2782c..b71d153f814 100644 --- a/spec/models/project_services/slack_service/build_message_spec.rb +++ b/spec/models/project_services/chat_message/build_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::BuildMessage do - subject { SlackService::BuildMessage.new(args) } +describe ChatMessage::BuildMessage do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/chat_message/issue_message_spec.rb index 98c36ec088d..ebe0ead4408 100644 --- a/spec/models/project_services/slack_service/issue_message_spec.rb +++ b/spec/models/project_services/chat_message/issue_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::IssueMessage, models: true do - subject { SlackService::IssueMessage.new(args) } +describe ChatMessage::IssueMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index c5c052d9af1..07c414c6ca4 100644 --- a/spec/models/project_services/slack_service/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::MergeMessage, models: true do - subject { SlackService::MergeMessage.new(args) } +describe ChatMessage::MergeMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/chat_message/note_message_spec.rb index 97f818125d3..31936da40a2 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/chat_message/note_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::NoteMessage, models: true do +describe ChatMessage::NoteMessage, models: true do let(:color) { '#345' } before do @@ -36,7 +36,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on commits' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user <url|commented on " \ "commit 5f163b2b> in <somewhere.com|project_name>: " \ "*Added a commit message*") @@ -62,7 +62,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a merge request' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user <url|commented on " \ "merge request !30> in <somewhere.com|project_name>: " \ "*merge request title*") @@ -88,7 +88,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on an issue' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq( "test.user <url|commented on " \ "issue #20> in <somewhere.com|project_name>: " \ @@ -114,7 +114,7 @@ describe SlackService::NoteMessage, models: true do end it 'returns a message regarding notes on a project snippet' do - message = SlackService::NoteMessage.new(@args) + message = described_class.new(@args) expect(message.pretext).to eq("test.user <url|commented on " \ "snippet #5> in <somewhere.com|project_name>: " \ "*snippet title*") diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/chat_message/pipeline_message_spec.rb index 363138a9454..eca71db07b6 100644 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ b/spec/models/project_services/chat_message/pipeline_message_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' -describe SlackService::PipelineMessage do - subject { SlackService::PipelineMessage.new(args) } +describe ChatMessage::PipelineMessage do + subject { described_class.new(args) } + let(:user) { { name: 'hacker' } } let(:args) do { @@ -15,7 +16,7 @@ describe SlackService::PipelineMessage do }, project: { path_with_namespace: 'project_name', web_url: 'example.gitlab.com' }, - user: { name: 'hacker' } + user: user } end @@ -28,9 +29,7 @@ describe SlackService::PipelineMessage do let(:message) { build_message('passed') } it 'returns a message with information about succeeded build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end end @@ -40,16 +39,29 @@ describe SlackService::PipelineMessage do let(:duration) { 10 } it 'returns a message with information about failed build' do - expect(subject.pretext).to be_empty - expect(subject.fallback).to eq(message) - expect(subject.attachments).to eq([text: message, color: color]) + verify_message end + + context 'when triggered by API therefore lacking user' do + let(:user) { nil } + let(:message) { build_message(status, 'API') } + + it 'returns a message stating it is by API' do + verify_message + end + end + end + + def verify_message + expect(subject.pretext).to be_empty + expect(subject.fallback).to eq(message) + expect(subject.attachments).to eq([text: message, color: color]) end - def build_message(status_text = status) + def build_message(status_text = status, name = user[:name]) "<example.gitlab.com|project_name>:" \ " Pipeline <example.gitlab.com/pipelines/123|#123>" \ " of <example.gitlab.com/commits/develop|develop> branch" \ - " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" + " by #{name} #{status_text} in #{duration} #{'second'.pluralize(duration)}" end end diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/chat_message/push_message_spec.rb index 17cd05e24f1..b781c4505db 100644 --- a/spec/models/project_services/slack_service/push_message_spec.rb +++ b/spec/models/project_services/chat_message/push_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe SlackService::PushMessage, models: true do - subject { SlackService::PushMessage.new(args) } +describe ChatMessage::PushMessage, models: true do + subject { described_class.new(args) } let(:args) do { diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/chat_message/wiki_page_message_spec.rb index 093911598b0..94c04dc0865 100644 --- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb +++ b/spec/models/project_services/chat_message/wiki_page_message_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SlackService::WikiPageMessage, models: true do +describe ChatMessage::WikiPageMessage, models: true do subject { described_class.new(args) } let(:args) do diff --git a/spec/models/project_services/chat_notification_service_spec.rb b/spec/models/project_services/chat_notification_service_spec.rb new file mode 100644 index 00000000000..c98e7ee14fd --- /dev/null +++ b/spec/models/project_services/chat_notification_service_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe ChatNotificationService, models: true do + describe "Associations" do + before do + allow(subject).to receive(:activated?).and_return(true) + end + + it { is_expected.to validate_presence_of :webhook } + end +end diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb new file mode 100644 index 00000000000..3603602e41d --- /dev/null +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -0,0 +1,159 @@ +require 'spec_helper' + +describe KubernetesService, models: true do + let(:project) { create(:empty_project) } + + describe "Associations" do + it { is_expected.to belong_to :project } + end + + describe 'Validations' do + context 'when service is active' do + before { subject.active = true } + it { is_expected.to validate_presence_of(:namespace) } + it { is_expected.to validate_presence_of(:api_url) } + it { is_expected.to validate_presence_of(:token) } + + context 'namespace format' do + before do + subject.project = project + subject.api_url = "http://example.com" + subject.token = "test" + end + + { + 'foo' => true, + '1foo' => true, + 'foo1' => true, + 'foo-bar' => true, + '-foo' => false, + 'foo-' => false, + 'a' * 63 => true, + 'a' * 64 => false, + 'a.b' => false, + 'a*b' => false, + }.each do |namespace, validity| + it "should validate #{namespace} as #{validity ? 'valid' : 'invalid'}" do + subject.namespace = namespace + + expect(subject.valid?).to eq(validity) + end + end + end + end + + context 'when service is inactive' do + before { subject.active = false } + it { is_expected.not_to validate_presence_of(:namespace) } + it { is_expected.not_to validate_presence_of(:api_url) } + it { is_expected.not_to validate_presence_of(:token) } + end + end + + describe '#initialize_properties' do + context 'with a project' do + it 'defaults to the project name' do + expect(described_class.new(project: project).namespace).to eq(project.name) + end + end + + context 'without a project' do + it 'leaves the namespace unset' do + expect(described_class.new.namespace).to be_nil + end + end + end + + describe '#test' do + let(:project) { create(:kubernetes_project) } + let(:service) { project.kubernetes_service } + let(:discovery_url) { service.api_url + '/api/v1' } + + # JSON response body from Kubernetes GET /api/v1 request + let(:discovery_response) { { "kind" => "APIResourceList", "groupVersion" => "v1", "resources" => [] }.to_json } + + context 'with path prefix in api_url' do + let(:discovery_url) { 'https://kubernetes.example.com/prefix/api/v1' } + + before do + service.api_url = 'https://kubernetes.example.com/prefix/' + end + + it 'tests with the prefix' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'with custom CA certificate' do + let(:certificate) { "CA PEM DATA" } + before do + service.update_attributes!(ca_pem: certificate) + end + + it 'is added to the certificate store' do + cert = double("certificate") + + expect(OpenSSL::X509::Certificate).to receive(:new).with(certificate).and_return(cert) + expect_any_instance_of(OpenSSL::X509::Store).to receive(:add_cert).with(cert) + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'success' do + it 'reads the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(body: discovery_response) + + expect(service.test[:success]).to be_truthy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + + context 'failure' do + it 'fails to read the discovery endpoint' do + WebMock.stub_request(:get, discovery_url).to_return(status: 404) + + expect(service.test[:success]).to be_falsy + expect(WebMock).to have_requested(:get, discovery_url).once + end + end + end + + describe '#predefined_variables' do + before do + subject.api_url = 'https://kube.domain.com' + subject.token = 'token' + subject.namespace = 'my-project' + subject.ca_pem = 'CA PEM DATA' + end + + it 'sets KUBE_URL' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_URL', value: 'https://kube.domain.com', public: true } + ) + end + + it 'sets KUBE_TOKEN' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_TOKEN', value: 'token', public: false } + ) + end + + it 'sets KUBE_NAMESPACE' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_NAMESPACE', value: 'my-project', public: true } + ) + end + + it 'sets KUBE_CA_PEM' do + expect(subject.predefined_variables).to include( + { key: 'KUBE_CA_PEM', value: 'CA PEM DATA', public: true } + ) + end + end +end diff --git a/spec/models/project_services/mattermost_notification_service_spec.rb b/spec/models/project_services/mattermost_notification_service_spec.rb new file mode 100644 index 00000000000..c01e64b4c8e --- /dev/null +++ b/spec/models/project_services/mattermost_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe MattermostNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_services/slack_notification_service_spec.rb b/spec/models/project_services/slack_notification_service_spec.rb new file mode 100644 index 00000000000..59ddddf7454 --- /dev/null +++ b/spec/models/project_services/slack_notification_service_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe SlackNotificationService, models: true do + it_behaves_like "slack or mattermost" +end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 21ff238841e..ed6b2c6a22b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -22,7 +22,8 @@ describe Project, models: true do it { is_expected.to have_many(:protected_branches).dependent(:destroy) } it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } - it { is_expected.to have_one(:slack_service).dependent(:destroy) } + it { is_expected.to have_one(:slack_notification_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_notification_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } it { is_expected.to have_one(:asana_service).dependent(:destroy) } it { is_expected.to have_many(:boards).dependent(:destroy) } @@ -1696,6 +1697,26 @@ describe Project, models: true do end end + describe '#deployment_variables' do + context 'when project has no deployment service' do + let(:project) { create(:empty_project) } + + it 'returns an empty array' do + expect(project.deployment_variables).to eq [] + end + end + + context 'when project has a deployment service' do + let(:project) { create(:kubernetes_project) } + + it 'returns variables from this service' do + expect(project.deployment_variables).to include( + { key: 'KUBE_TOKEN', value: project.kubernetes_service.token, public: false } + ) + end + end + end + def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb new file mode 100644 index 00000000000..a20ac303a53 --- /dev/null +++ b/spec/policies/group_policy_spec.rb @@ -0,0 +1,108 @@ +require 'spec_helper' + +describe GroupPolicy, models: true do + let(:guest) { create(:user) } + let(:reporter) { create(:user) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:group) { create(:group) } + + let(:master_permissions) do + [ + :create_projects, + :admin_milestones, + :admin_label + ] + end + + let(:owner_permissions) do + [ + :admin_group, + :admin_namespace, + :admin_group_member, + :change_visibility_level + ] + end + + before do + group.add_guest(guest) + group.add_reporter(reporter) + group.add_developer(developer) + group.add_master(master) + group.add_owner(owner) + end + + subject { described_class.abilities(current_user, group).to_set } + + context 'with no user' do + let(:current_user) { nil } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'guests' do + let(:current_user) { guest } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'reporter' do + let(:current_user) { reporter } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'developer' do + let(:current_user) { developer } + + it do + is_expected.to include(:read_group) + is_expected.not_to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'master' do + let(:current_user) { master } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.not_to include(*owner_permissions) + end + end + + context 'owner' do + let(:current_user) { owner } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end + + context 'admin' do + let(:current_user) { admin } + + it do + is_expected.to include(:read_group) + is_expected.to include(*master_permissions) + is_expected.to include(*owner_permissions) + end + end +end diff --git a/spec/requests/api/doorkeeper_access_spec.rb b/spec/requests/api/doorkeeper_access_spec.rb index 5262a623761..bd9ecaf2685 100644 --- a/spec/requests/api/doorkeeper_access_spec.rb +++ b/spec/requests/api/doorkeeper_access_spec.rb @@ -5,7 +5,7 @@ describe API::API, api: true do let!(:user) { create(:user) } let!(:application) { Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) } - let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id } + let!(:token) { Doorkeeper::AccessToken.create! application_id: application.id, resource_owner_id: user.id, scopes: "api" } describe "when unauthenticated" do it "returns authentication success" do diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 126496c43a5..b9d535bc314 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -46,6 +46,7 @@ describe API::Environments, api: true do expect(response).to have_http_status(201) expect(json_response['name']).to eq('mepmep') + expect(json_response['slug']).to eq('mepmep') expect(json_response['external']).to be nil end @@ -60,6 +61,13 @@ describe API::Environments, api: true do expect(response).to have_http_status(400) end + + it 'returns a 400 if slug is specified' do + post api("/projects/#{project.id}/environments", user), name: "foo", slug: "foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + end end context 'a non member' do @@ -86,6 +94,15 @@ describe API::Environments, api: true do expect(json_response['external_url']).to eq(url) end + it "won't allow slug to be changed" do + slug = environment.slug + api_url = api("/projects/#{project.id}/environments/#{environment.id}", user) + put api_url, slug: slug + "-foo" + + expect(response).to have_http_status(400) + expect(json_response["error"]).to eq("slug is automatically generated and cannot be changed") + 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), diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb index a75ba824e85..cdeb965b413 100644 --- a/spec/requests/api/groups_spec.rb +++ b/spec/requests/api/groups_spec.rb @@ -2,13 +2,13 @@ require 'spec_helper' describe API::Groups, api: true do include ApiHelpers + include UploadHelpers let(:user1) { create(:user, can_create_group: false) } let(:user2) { create(:user) } let(:user3) { create(:user) } let(:admin) { create(:admin) } - let(:avatar_file_path) { File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif') } - let!(:group1) { create(:group, avatar: File.open(avatar_file_path)) } + let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) } let!(:group2) { create(:group, :private) } let!(:project1) { create(:project, namespace: group1) } let!(:project2) { create(:project, namespace: group2) } diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb index 4035fd97af5..c3d7ac3eef8 100644 --- a/spec/requests/api/helpers_spec.rb +++ b/spec/requests/api/helpers_spec.rb @@ -1,6 +1,7 @@ require 'spec_helper' describe API::Helpers, api: true do + include API::APIGuard::HelperMethods include API::Helpers include SentryHelper @@ -15,24 +16,24 @@ describe API::Helpers, api: true do def set_env(user_or_token, identifier) clear_env clear_param - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token env[API::Helpers::SUDO_HEADER] = identifier.to_s end def set_param(user_or_token, identifier) clear_env clear_param - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user_or_token.respond_to?(:private_token) ? user_or_token.private_token : user_or_token params[API::Helpers::SUDO_PARAM] = identifier.to_s end def clear_env - env.delete(API::Helpers::PRIVATE_TOKEN_HEADER) + env.delete(API::APIGuard::PRIVATE_TOKEN_HEADER) env.delete(API::Helpers::SUDO_HEADER) end def clear_param - params.delete(API::Helpers::PRIVATE_TOKEN_PARAM) + params.delete(API::APIGuard::PRIVATE_TOKEN_PARAM) params.delete(API::Helpers::SUDO_PARAM) end @@ -94,22 +95,28 @@ describe API::Helpers, api: true do describe "when authenticating using a user's private token" do it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = user.private_token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = user.private_token + expect(current_user).to eq(user) + clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = user.private_token + + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = user.private_token + expect(current_user).to eq(user) end end @@ -117,37 +124,51 @@ describe API::Helpers, api: true do describe "when authenticating using a user's personal access tokens" do let(:personal_access_token) { create(:personal_access_token, user: user) } + before do + allow_any_instance_of(self.class).to receive(:doorkeeper_guard) { false } + end + it "returns nil for an invalid token" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = 'invalid token' - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = 'invalid token' + expect(current_user).to be_nil end it "returns nil for a user without access" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token allow_any_instance_of(Gitlab::UserAccess).to receive(:allowed?).and_return(false) + + expect(current_user).to be_nil + end + + it "returns nil for a token without the appropriate scope" do + personal_access_token = create(:personal_access_token, user: user, scopes: ['read_user']) + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + allow_access_with_scope('write_user') + expect(current_user).to be_nil end it "leaves user as is when sudo not specified" do - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token expect(current_user).to eq(user) clear_env - params[API::Helpers::PRIVATE_TOKEN_PARAM] = personal_access_token.token + params[API::APIGuard::PRIVATE_TOKEN_PARAM] = personal_access_token.token + expect(current_user).to eq(user) end it 'does not allow revoked tokens' do personal_access_token.revoke! - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end it 'does not allow expired tokens' do personal_access_token.update_attributes!(expires_at: 1.day.ago) - env[API::Helpers::PRIVATE_TOKEN_HEADER] = personal_access_token.token - allow_any_instance_of(self.class).to receive(:doorkeeper_guard){ false } + env[API::APIGuard::PRIVATE_TOKEN_HEADER] = personal_access_token.token + expect(current_user).to be_nil end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index c5d67a90abc..8304c408064 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -167,7 +167,7 @@ describe API::Projects, api: true do expect(json_response).to satisfy do |response| response.one? do |entry| entry.has_key?('permissions') && - entry['name'] == project.name && + entry['name'] == project.name && entry['owner']['username'] == user.username end end diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index f1728d61def..d71bb08c218 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -230,7 +230,7 @@ describe 'Git HTTP requests', lib: true do context "when an oauth token is provided" do before do application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user) - @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id) + @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id, scopes: "api") end it "downloads get status 200" do diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb index 69eeb45ed71..661b671301e 100644 --- a/spec/routing/admin_routing_spec.rb +++ b/spec/routing/admin_routing_spec.rb @@ -66,7 +66,8 @@ describe Admin::ProjectsController, "routing" do end it "to #show" do - expect(get("/admin/projects/gitlab")).to route_to('admin/projects#show', namespace_id: 'gitlab') + expect(get("/admin/projects/gitlab/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab', id: 'gitlab-ce') + expect(get("/admin/projects/gitlab/subgroup/gitlab-ce")).to route_to('admin/projects#show', namespace_id: 'gitlab/subgroup', id: 'gitlab-ce') end end @@ -119,3 +120,14 @@ describe Admin::HealthCheckController, "routing" do expect(get("/admin/health_check")).to route_to('admin/health_check#show') end end + +describe Admin::GroupsController, "routing" do + it "to #index" do + expect(get("/admin/groups")).to route_to('admin/groups#index') + end + + it "to #show" do + expect(get("/admin/groups/gitlab")).to route_to('admin/groups#show', id: 'gitlab') + expect(get("/admin/groups/gitlab/subgroup")).to route_to('admin/groups#show', id: 'gitlab/subgroup') + end +end diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index b6e7da841b1..77549db2927 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,10 +80,6 @@ describe 'project routing' do expect(get('/gitlab/gitlabhq/edit')).to route_to('projects#edit', namespace_id: 'gitlab', id: 'gitlabhq') end - it 'to #autocomplete_sources' do - expect(get('/gitlab/gitlabhq/autocomplete_sources')).to route_to('projects#autocomplete_sources', namespace_id: 'gitlab', id: 'gitlabhq') - end - describe 'to #show' do context 'regular name' do it { expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') } @@ -117,6 +113,21 @@ describe 'project routing' do end end + # emojis_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/emojis(.:format) projects/autocomplete_sources#emojis + # members_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/members(.:format) projects/autocomplete_sources#members + # issues_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/issues(.:format) projects/autocomplete_sources#issues + # merge_requests_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/merge_requests(.:format) projects/autocomplete_sources#merge_requests + # labels_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/labels(.:format) projects/autocomplete_sources#labels + # milestones_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/milestones(.:format) projects/autocomplete_sources#milestones + # commands_namespace_project_autocomplete_sources_path GET /:project_id/autocomplete_sources/commands(.:format) projects/autocomplete_sources#commands + describe Projects::AutocompleteSourcesController, 'routing' do + [:emojis, :members, :issues, :merge_requests, :labels, :milestones, :commands].each do |action| + it "to ##{action}" do + expect(get("/gitlab/gitlabhq/autocomplete_sources/#{action}")).to route_to("projects/autocomplete_sources##{action}", namespace_id: 'gitlab', project_id: 'gitlabhq') + end + end + end + # pages_project_wikis GET /:project_id/wikis/pages(.:format) projects/wikis#pages # history_project_wiki GET /:project_id/wikis/:id/history(.:format) projects/wikis#history # project_wikis POST /:project_id/wikis(.:format) projects/wikis#create diff --git a/spec/serializers/analytics_build_entity_spec.rb b/spec/serializers/analytics_build_entity_spec.rb index 6b33fe66a63..86e703a6448 100644 --- a/spec/serializers/analytics_build_entity_spec.rb +++ b/spec/serializers/analytics_build_entity_spec.rb @@ -13,6 +13,14 @@ describe AnalyticsBuildEntity do subject { entity.as_json } + before do + Timecop.freeze + end + + after do + Timecop.return + end + it 'contains the URL' do expect(subject).to include(:url) end diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb new file mode 100644 index 00000000000..87f093ee8ce --- /dev/null +++ b/spec/services/access_token_validation_service_spec.rb @@ -0,0 +1,41 @@ +require 'spec_helper' + +describe AccessTokenValidationService, services: true do + describe ".include_any_scope?" do + it "returns true if the required scope is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api])).to be(true) + end + + it "returns true if more than one of the required scopes is present in the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes is an exact match for the token's scopes" do + token = double("token", scopes: [:api, :read_user, :other_scope]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + end + + it 'returns true if the list of required scopes is blank' do + token = double("token", scopes: []) + + expect(described_class.new(token).include_any_scope?([])).to be(true) + end + + it "returns false if there are no scopes in common between the required scopes and the token scopes" do + token = double("token", scopes: [:api, :read_user]) + + expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + end + end +end diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index 4aadd009f3e..ceaca96e25b 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -210,5 +210,22 @@ describe Ci::CreatePipelineService, services: true do expect(result.manual_actions).not_to be_empty end end + + context 'with environment' do + before do + config = YAML.dump(deploy: { environment: { name: "review/$CI_BUILD_REF_NAME" }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'creates the environment' do + result = execute(ref: 'refs/heads/master', + before: '00000000', + after: project.commit.id, + commits: [{ message: 'some msg' }]) + + expect(result).to be_persisted + expect(Environment.find_by(name: "review/master")).not_to be_nil + end + end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 500d224ff98..eafbea46905 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -376,5 +376,10 @@ describe Issues::UpdateService, services: true do let(:mentionable) { issue } include_examples 'updating mentions', Issues::UpdateService end + + include_examples 'issuable update service' do + let(:open_issuable) { issue } + let(:closed_issuable) { create(:closed_issue, project: project) } + end end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 790ef765f3a..88c786947d3 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -320,5 +320,10 @@ describe MergeRequests::UpdateService, services: true do expect(issue_ids).to be_empty end end + + include_examples 'issuable update service' do + let(:open_issuable) { merge_request } + let(:closed_issuable) { create(:closed_merge_request, source_project: project) } + end end end diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb new file mode 100644 index 00000000000..a3336755773 --- /dev/null +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -0,0 +1,17 @@ +shared_examples 'issuable update service' do + context 'changing state' do + before { expect(project).to receive(:execute_hooks).once } + + context 'to reopened' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'reopen').execute(closed_issuable) + end + end + + context 'to closed' do + it 'executes hooks only once' do + described_class.new(project, user, state_event: 'close').execute(open_issuable) + end + end + end +end diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/support/slack_mattermost_shared_examples.rb index c07a70a8069..56d4965f74d 100644 --- a/spec/models/project_services/slack_service_spec.rb +++ b/spec/support/slack_mattermost_shared_examples.rb @@ -1,7 +1,7 @@ -require 'spec_helper' +Dir[Rails.root.join("app/models/project_services/chat_message/*.rb")].each { |f| require f } -describe SlackService, models: true do - let(:slack) { SlackService.new } +RSpec.shared_examples 'slack or mattermost' do + let(:chat_service) { described_class.new } let(:webhook_url) { 'https://example.gitlab.com/' } describe "Associations" do @@ -24,7 +24,7 @@ describe SlackService, models: true do end end - describe "Execute" do + describe "#execute" do let(:user) { create(:user) } let(:project) { create(:project) } let(:username) { 'slack_username' } @@ -35,7 +35,7 @@ describe SlackService, models: true do end before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, project_id: project.id, service_hook: true, @@ -77,54 +77,55 @@ describe SlackService, models: true do @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create') end - it "calls Slack API for push events" do - slack.execute(push_sample_data) + it "calls Slack/Mattermost API for push events" do + chat_service.execute(push_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for issue events" do - slack.execute(@issues_sample_data) + it "calls Slack/Mattermost API for issue events" do + chat_service.execute(@issues_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for merge requests events" do - slack.execute(@merge_sample_data) + it "calls Slack/Mattermost API for merge requests events" do + chat_service.execute(@merge_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end - it "calls Slack API for wiki page events" do - slack.execute(@wiki_page_sample_data) + it "calls Slack/Mattermost API for wiki page events" do + chat_service.execute(@wiki_page_sample_data) expect(WebMock).to have_requested(:post, webhook_url).once end it 'uses the username as an option for slack when configured' do - allow(slack).to receive(:username).and_return(username) + allow(chat_service).to receive(:username).and_return(username) + expect(Slack::Notifier).to receive(:new). - with(webhook_url, username: username). + with(webhook_url, username: username, channel: chat_service.default_channel). and_return( double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end it 'uses the channel as an option when it is configured' do - allow(slack).to receive(:channel).and_return(channel) + allow(chat_service).to receive(:channel).and_return(channel) expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: channel). and_return( double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end context "event channels" do it "uses the right channel for push event" do - slack.update_attributes(push_channel: "random") + chat_service.update_attributes(push_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -132,11 +133,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(push_sample_data) + chat_service.execute(push_sample_data) end it "uses the right channel for merge request event" do - slack.update_attributes(merge_request_channel: "random") + chat_service.update_attributes(merge_request_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -144,11 +145,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@merge_sample_data) + chat_service.execute(@merge_sample_data) end it "uses the right channel for issue event" do - slack.update_attributes(issue_channel: "random") + chat_service.update_attributes(issue_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -156,11 +157,11 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@issues_sample_data) + chat_service.execute(@issues_sample_data) end it "uses the right channel for wiki event" do - slack.update_attributes(wiki_page_channel: "random") + chat_service.update_attributes(wiki_page_channel: "random") expect(Slack::Notifier).to receive(:new). with(webhook_url, channel: "random"). @@ -168,7 +169,7 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(@wiki_page_sample_data) + chat_service.execute(@wiki_page_sample_data) end context "note event" do @@ -177,7 +178,7 @@ describe SlackService, models: true do end it "uses the right channel" do - slack.update_attributes(note_channel: "random") + chat_service.update_attributes(note_channel: "random") note_data = Gitlab::DataBuilder::Note.build(issue_note, user) @@ -187,7 +188,7 @@ describe SlackService, models: true do double(:slack_service).as_null_object ) - slack.execute(note_data) + chat_service.execute(note_data) end end end @@ -198,7 +199,7 @@ describe SlackService, models: true do let(:project) { create(:project, creator_id: user.id) } before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, project_id: project.id, service_hook: true, @@ -216,9 +217,9 @@ describe SlackService, models: true do note: 'a comment on a commit') end - it "calls Slack API for commit comment events" do + it "calls Slack/Mattermost API for commit comment events" do data = Gitlab::DataBuilder::Note.build(commit_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -232,7 +233,7 @@ describe SlackService, models: true do it "calls Slack API for merge request comment events" do data = Gitlab::DataBuilder::Note.build(merge_request_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -245,7 +246,7 @@ describe SlackService, models: true do it "calls Slack API for issue comment events" do data = Gitlab::DataBuilder::Note.build(issue_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -259,7 +260,7 @@ describe SlackService, models: true do it "calls Slack API for snippet comment events" do data = Gitlab::DataBuilder::Note.build(snippet_note, user) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -277,21 +278,21 @@ describe SlackService, models: true do end before do - allow(slack).to receive_messages( + allow(chat_service).to receive_messages( project: project, service_hook: true, webhook: webhook_url ) end - shared_examples 'call Slack API' do + shared_examples 'call Slack/Mattermost API' do before do WebMock.stub_request(:post, webhook_url) end - it 'calls Slack API for pipeline events' do + it 'calls Slack/Mattermost API for pipeline events' do data = Gitlab::DataBuilder::Pipeline.build(pipeline) - slack.execute(data) + chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once end @@ -300,16 +301,16 @@ describe SlackService, models: true do context 'with failed pipeline' do let(:status) { 'failed' } - it_behaves_like 'call Slack API' + it_behaves_like 'call Slack/Mattermost API' end context 'with succeeded pipeline' do let(:status) { 'success' } context 'with default to notify_only_broken_pipelines' do - it 'does not call Slack API for pipeline events' do + it 'does not call Slack/Mattermost API for pipeline events' do data = Gitlab::DataBuilder::Pipeline.build(pipeline) - result = slack.execute(data) + result = chat_service.execute(data) expect(result).to be_falsy end @@ -317,10 +318,10 @@ describe SlackService, models: true do context 'with setting notify_only_broken_pipelines to false' do before do - slack.notify_only_broken_pipelines = false + chat_service.notify_only_broken_pipelines = false end - it_behaves_like 'call Slack API' + it_behaves_like 'call Slack/Mattermost API' end end end diff --git a/spec/support/upload_helpers.rb b/spec/support/upload_helpers.rb new file mode 100644 index 00000000000..5eead80c935 --- /dev/null +++ b/spec/support/upload_helpers.rb @@ -0,0 +1,16 @@ +require 'fileutils' + +module UploadHelpers + extend self + + def uploaded_image_temp_path + basename = 'banana_sample.gif' + orig_path = File.join(Rails.root, 'spec', 'fixtures', basename) + tmp_path = File.join(Rails.root, 'tmp', 'tests', basename) + # Because we use 'move_to_store' on all uploaders, we create a new + # tempfile on each call: the file we return here will be renamed in most + # cases. + FileUtils.copy(orig_path, tmp_path) + tmp_path + end +end diff --git a/spec/uploaders/attachment_uploader_spec.rb b/spec/uploaders/attachment_uploader_spec.rb new file mode 100644 index 00000000000..6098be5cd45 --- /dev/null +++ b/spec/uploaders/attachment_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AttachmentUploader do + let(:issue) { build(:issue) } + subject { described_class.new(issue) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/avatar_uploader_spec.rb b/spec/uploaders/avatar_uploader_spec.rb new file mode 100644 index 00000000000..1f0e8732587 --- /dev/null +++ b/spec/uploaders/avatar_uploader_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe AvatarUploader do + let(:user) { build(:user) } + subject { described_class.new(user) } + + describe '#move_to_cache' do + it 'is true' do + expect(subject.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(subject.move_to_store).to eq(true) + end + end +end diff --git a/spec/uploaders/file_uploader_spec.rb b/spec/uploaders/file_uploader_spec.rb index e8300abed5d..bc86adfef68 100644 --- a/spec/uploaders/file_uploader_spec.rb +++ b/spec/uploaders/file_uploader_spec.rb @@ -42,4 +42,16 @@ describe FileUploader do expect(@uploader.image_or_video?).to be false end end + + describe '#move_to_cache' do + it 'is true' do + expect(@uploader.move_to_cache).to eq(true) + end + end + + describe '#move_to_store' do + it 'is true' do + expect(@uploader.move_to_store).to eq(true) + end + end end |