diff options
Diffstat (limited to 'spec')
314 files changed, 12323 insertions, 2769 deletions
diff --git a/spec/benchmarks/finders/issues_finder_spec.rb b/spec/benchmarks/finders/issues_finder_spec.rb deleted file mode 100644 index b57a33004a4..00000000000 --- a/spec/benchmarks/finders/issues_finder_spec.rb +++ /dev/null @@ -1,55 +0,0 @@ -require 'spec_helper' - -describe IssuesFinder, benchmark: true do - describe '#execute' do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } - - let(:label1) { create(:label, project: project, title: 'A') } - let(:label2) { create(:label, project: project, title: 'B') } - - before do - 10.times do |n| - issue = create(:issue, author: user, project: project) - - if n > 4 - create(:label_link, label: label1, target: issue) - create(:label_link, label: label2, target: issue) - end - end - end - - describe 'retrieving issues without labels' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: Label::None.title, - state: 'opened') - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(2000) } - end - - describe 'retrieving issues with labels' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: label1.title, - state: 'opened') - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(1000) } - end - - describe 'retrieving issues for a single project' do - let(:finder) do - IssuesFinder.new(user, scope: 'all', label_name: Label::None.title, - state: 'opened', project_id: project.id) - end - - benchmark_subject { finder.execute } - - it { is_expected.to iterate_per_second(2000) } - end - end -end diff --git a/spec/benchmarks/finders/trending_projects_finder_spec.rb b/spec/benchmarks/finders/trending_projects_finder_spec.rb deleted file mode 100644 index 551ce21840d..00000000000 --- a/spec/benchmarks/finders/trending_projects_finder_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe TrendingProjectsFinder, benchmark: true do - describe '#execute' do - let(:finder) { described_class.new } - let(:user) { create(:user) } - - # to_a is used to force actually running the query (instead of just building - # it). - benchmark_subject { finder.execute(user).non_archived.to_a } - - it { is_expected.to iterate_per_second(500) } - end -end diff --git a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb b/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb deleted file mode 100644 index 3855763b200..00000000000 --- a/spec/benchmarks/lib/gitlab/markdown/reference_filter_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -require 'spec_helper' - -describe Banzai::Filter::ReferenceFilter, benchmark: true do - let(:input) do - html = <<-EOF -<p>Hello @alice and @bob, how are you doing today?</p> -<p>This is simple @dummy text to see how the @ReferenceFilter class performs -when @processing HTML.</p> - EOF - - Nokogiri::HTML.fragment(html) - end - - let(:project) { create(:empty_project) } - - let(:filter) { described_class.new(input, project: project) } - - describe '#replace_text_nodes_matching' do - let(:iterations) { 6000 } - - describe 'with identical input and output HTML' do - benchmark_subject do - filter.replace_text_nodes_matching(User.reference_pattern) do |content| - content - end - end - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'with different input and output HTML' do - benchmark_subject do - filter.replace_text_nodes_matching(User.reference_pattern) do |content| - '@eve' - end - end - - it { is_expected.to iterate_per_second(iterations) } - end - end -end diff --git a/spec/benchmarks/models/milestone_spec.rb b/spec/benchmarks/models/milestone_spec.rb deleted file mode 100644 index a94afc4c40d..00000000000 --- a/spec/benchmarks/models/milestone_spec.rb +++ /dev/null @@ -1,17 +0,0 @@ -require 'spec_helper' - -describe Milestone, benchmark: true do - describe '#sort_issues' do - let(:milestone) { create(:milestone) } - - let(:issue1) { create(:issue, milestone: milestone) } - let(:issue2) { create(:issue, milestone: milestone) } - let(:issue3) { create(:issue, milestone: milestone) } - - let(:issue_ids) { [issue3.id, issue2.id, issue1.id] } - - benchmark_subject { milestone.sort_issues(issue_ids) } - - it { is_expected.to iterate_per_second(500) } - end -end diff --git a/spec/benchmarks/models/project_spec.rb b/spec/benchmarks/models/project_spec.rb deleted file mode 100644 index cee0949edc5..00000000000 --- a/spec/benchmarks/models/project_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Project, benchmark: true do - describe '.trending' do - let(:group) { create(:group) } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :public, group: group) } - - let(:iterations) { 500 } - - before do - 2.times do - create(:note_on_commit, project: project1) - end - - create(:note_on_commit, project: project2) - end - - describe 'without an explicit start date' do - benchmark_subject { described_class.trending.to_a } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'with an explicit start date' do - let(:date) { 1.month.ago } - - benchmark_subject { described_class.trending(date).to_a } - - it { is_expected.to iterate_per_second(iterations) } - end - end - - describe '.find_with_namespace' do - let(:group) { create(:group, name: 'sisinmaru') } - let(:project) { create(:project, name: 'maru', namespace: group) } - - describe 'using a capitalized namespace' do - benchmark_subject { described_class.find_with_namespace('sisinmaru/MARU') } - - it { is_expected.to iterate_per_second(600) } - end - - describe 'using a lowercased namespace' do - benchmark_subject { described_class.find_with_namespace('sisinmaru/maru') } - - it { is_expected.to iterate_per_second(600) } - end - end -end diff --git a/spec/benchmarks/models/project_team_spec.rb b/spec/benchmarks/models/project_team_spec.rb deleted file mode 100644 index 8b039ef7317..00000000000 --- a/spec/benchmarks/models/project_team_spec.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'spec_helper' - -describe ProjectTeam, benchmark: true do - describe '#max_member_access' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } - let(:user) { create(:user) } - - before do - project.team << [user, :master] - - 5.times do - project.team << [create(:user), :reporter] - - project.group.add_user(create(:user), :reporter) - end - end - - benchmark_subject { project.team.max_member_access(user.id) } - - it { is_expected.to iterate_per_second(35000) } - end -end diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb deleted file mode 100644 index 1be7a8d3ed9..00000000000 --- a/spec/benchmarks/models/user_spec.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'spec_helper' - -describe User, benchmark: true do - describe '.all' do - before do - 10.times { create(:user) } - end - - benchmark_subject { User.all.to_a } - - it { is_expected.to iterate_per_second(500) } - end - - describe '.by_login' do - before do - %w{Alice Bob Eve}.each do |name| - create(:user, - email: "#{name}@gitlab.com", - username: name, - name: name) - end - end - - # The iteration count is based on the query taking little over 1 ms when - # using PostgreSQL. - let(:iterations) { 900 } - - describe 'using a capitalized username' do - benchmark_subject { User.by_login('Alice') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a lowercase username' do - benchmark_subject { User.by_login('alice') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a capitalized Email address' do - benchmark_subject { User.by_login('Alice@gitlab.com') } - - it { is_expected.to iterate_per_second(iterations) } - end - - describe 'using a lowercase Email address' do - benchmark_subject { User.by_login('alice@gitlab.com') } - - it { is_expected.to iterate_per_second(iterations) } - end - end - - describe '.find_by_any_email' do - let(:user) { create(:user) } - - describe 'using a user with only a single Email address' do - let(:email) { user.email } - - benchmark_subject { User.find_by_any_email(email) } - - it { is_expected.to iterate_per_second(1000) } - end - - describe 'using a user with multiple Email addresses' do - let(:email) { user.emails.first.email } - - benchmark_subject { User.find_by_any_email(email) } - - before do - 10.times do - user.emails.create(email: FFaker::Internet.email) - end - end - - it { is_expected.to iterate_per_second(1000) } - end - end -end diff --git a/spec/benchmarks/services/projects/create_service_spec.rb b/spec/benchmarks/services/projects/create_service_spec.rb deleted file mode 100644 index 25ed48c34fd..00000000000 --- a/spec/benchmarks/services/projects/create_service_spec.rb +++ /dev/null @@ -1,28 +0,0 @@ -require 'spec_helper' - -describe Projects::CreateService, benchmark: true do - describe '#execute' do - let(:user) { create(:user, :admin) } - - let(:group) do - group = create(:group) - - create(:group_member, group: group, user: user) - - group - end - - benchmark_subject do - name = SecureRandom.hex - service = described_class.new(user, - name: name, - path: name, - namespace_id: group.id, - visibility_level: Gitlab::VisibilityLevel::PUBLIC) - - service.execute - end - - it { is_expected.to iterate_per_second(0.5) } - end -end diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb new file mode 100644 index 00000000000..462afb24f08 --- /dev/null +++ b/spec/config/mail_room_spec.rb @@ -0,0 +1,56 @@ +require "spec_helper" + +describe "mail_room.yml" do + let(:config_path) { "config/mail_room.yml" } + let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) } + + context "when incoming email is disabled" do + before do + ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_disabled.yml").to_s + end + + after do + ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + end + + it "contains no configuration" do + expect(configuration[:mailboxes]).to be_nil + end + end + + context "when incoming email is enabled" do + before do + ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = Rails.root.join("spec/fixtures/mail_room_enabled.yml").to_s + end + + after do + ENV["MAIL_ROOM_GITLAB_CONFIG_FILE"] = nil + end + + it "contains the intended configuration" do + expect(configuration[:mailboxes].length).to eq(1) + + mailbox = configuration[:mailboxes].first + + expect(mailbox[:host]).to eq("imap.gmail.com") + expect(mailbox[:port]).to eq(993) + expect(mailbox[:ssl]).to eq(true) + expect(mailbox[:start_tls]).to eq(false) + expect(mailbox[:email]).to eq("gitlab-incoming@gmail.com") + expect(mailbox[:password]).to eq("[REDACTED]") + expect(mailbox[:name]).to eq("inbox") + + redis_config_file = Rails.root.join('config', 'resque.yml') + + redis_url = + if File.exists?(redis_config_file) + YAML.load_file(redis_config_file)[Rails.env] + else + "redis://localhost:6379" + end + + expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url) + expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url) + end + end +end diff --git a/spec/controllers/abuse_reports_controller_spec.rb b/spec/controllers/abuse_reports_controller_spec.rb index 15824a1c67f..80a418feb3e 100644 --- a/spec/controllers/abuse_reports_controller_spec.rb +++ b/spec/controllers/abuse_reports_controller_spec.rb @@ -1,76 +1,46 @@ require 'spec_helper' describe AbuseReportsController do - let(:reporter) { create(:user) } - let(:user) { create(:user) } - let(:message) { "This user is a spammer" } + let(:reporter) { create(:user) } + let(:user) { create(:user) } + let(:attrs) do + attributes_for(:abuse_report) do |hash| + hash[:user_id] = user.id + end + end before do sign_in(reporter) end - describe "POST create" do - context "with admin notification email set" do - let(:admin_email) { "admin@example.com"} - - before(:each) do - stub_application_setting(admin_notification_email: admin_email) + describe 'POST create' do + context 'with valid attributes' do + it 'saves the abuse report' do + expect do + post :create, abuse_report: attrs + end.to change { AbuseReport.count }.by(1) end - it "sends a notification email" do - perform_enqueued_jobs do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - - email = ActionMailer::Base.deliveries.last + it 'calls notify' do + expect_any_instance_of(AbuseReport).to receive(:notify) - expect(email.to).to eq([admin_email]) - expect(email.subject).to include(user.username) - expect(email.text_part.body).to include(message) - end + post :create, abuse_report: attrs end - it "saves the abuse report" do - perform_enqueued_jobs do - expect do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - end.to change { AbuseReport.count }.by(1) - end - end - end + it 'redirects back to the reported user' do + post :create, abuse_report: attrs - context "without admin notification email set" do - before(:each) do - stub_application_setting(admin_notification_email: nil) + expect(response).to redirect_to user end + end - it "does not send a notification email" do - expect do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - end.not_to change { ActionMailer::Base.deliveries.count } - end + context 'with invalid attributes' do + it 'renders new' do + attrs.delete(:user_id) + post :create, abuse_report: attrs - it "saves the abuse report" do - expect do - post :create, - abuse_report: { - user_id: user.id, - message: message - } - end.to change { AbuseReport.count }.by(1) + expect(response).to render_template(:new) end end end - end diff --git a/spec/controllers/admin/identities_controller_spec.rb b/spec/controllers/admin/identities_controller_spec.rb new file mode 100644 index 00000000000..c131d22a30a --- /dev/null +++ b/spec/controllers/admin/identities_controller_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Admin::IdentitiesController do + let(:admin) { create(:admin) } + before { sign_in(admin) } + + describe 'UPDATE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + put :update, user_id: user.username, id: user.ldap_identity.id, identity: { provider: 'twitter' } + end + end + + describe 'DELETE identity' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', extern_uid: 'uid=myuser,ou=people,dc=example,dc=com') } + + it 'repairs ldap blocks' do + expect_any_instance_of(RepairLdapBlockedUserService).to receive(:execute) + + delete :destroy, user_id: user.username, id: user.ldap_identity.id + end + end +end diff --git a/spec/controllers/admin/spam_logs_controller_spec.rb b/spec/controllers/admin/spam_logs_controller_spec.rb new file mode 100644 index 00000000000..b51b303a714 --- /dev/null +++ b/spec/controllers/admin/spam_logs_controller_spec.rb @@ -0,0 +1,37 @@ +require 'spec_helper' + +describe Admin::SpamLogsController do + let(:admin) { create(:admin) } + let(:user) { create(:user) } + let!(:first_spam) { create(:spam_log, user: user) } + let!(:second_spam) { create(:spam_log, user: user) } + + before do + sign_in(admin) + end + + describe '#index' do + it 'lists all spam logs' do + get :index + + expect(response.status).to eq(200) + end + end + + describe '#destroy' do + it 'removes only the spam log when removing log' do + expect { delete :destroy, id: first_spam.id }.to change { SpamLog.count }.by(-1) + expect(User.find(user.id)).to be_truthy + expect(response.status).to eq(200) + end + + it 'removes user and his spam logs when removing the user' do + delete :destroy, id: first_spam.id, remove_user: true + + expect(flash[:notice]).to eq "User #{user.username} was successfully removed." + expect(response.status).to eq(302) + expect(SpamLog.count).to eq(0) + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end +end diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb index 8b7af4d3a0a..5b1f65d7aff 100644 --- a/spec/controllers/admin/users_controller_spec.rb +++ b/spec/controllers/admin/users_controller_spec.rb @@ -34,17 +34,34 @@ describe Admin::UsersController do end describe 'PUT unblock/:id' do - let(:user) { create(:user) } - - before do - user.block + context 'ldap blocked users' do + let(:user) { create(:omniauth_user, provider: 'ldapmain') } + + before do + user.ldap_block + end + + it 'will not unblock user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_truthy + expect(flash[:alert]).to eq 'This user cannot be unlocked manually from GitLab' + end end - it 'unblocks user' do - put :unblock, id: user.username - user.reload - expect(user.blocked?).to be_falsey - expect(flash[:notice]).to eq 'Successfully unblocked' + context 'manually blocked users' do + let(:user) { create(:user) } + + before do + user.block + end + + it 'unblocks user' do + put :unblock, id: user.username + user.reload + expect(user.blocked?).to be_falsey + expect(flash[:notice]).to eq 'Successfully unblocked' + end end end diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 85379a8e984..410b993fdfb 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -21,7 +21,7 @@ describe AutocompleteController do it { expect(body).to be_kind_of(Array) } it { expect(body.size).to eq 1 } - it { expect(body.first["username"]).to eq user.username } + it { expect(body.map { |u| u["username"] }).to include(user.username) } end describe 'GET #users with unknown project' do diff --git a/spec/controllers/blame_controller_spec.rb b/spec/controllers/blame_controller_spec.rb deleted file mode 100644 index 3ad4d5fc0a8..00000000000 --- a/spec/controllers/blame_controller_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'spec_helper' - -describe Projects::BlameController do - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - sign_in(user) - - project.team << [user, :master] - controller.instance_variable_set(:@project, project) - end - - describe "GET show" do - render_views - - before do - get(:show, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - id: id) - end - - context "valid file" do - let(:id) { 'master/files/ruby/popen.rb' } - it { is_expected.to respond_with(:success) } - - it 'groups blames properly' do - blame = assigns(:blame) - # Sanity check a few items - expect(blame.count).to eq(18) - expect(blame[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') - expect(blame[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) - - expect(blame[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') - expect(blame[1][:lines]).to eq(["module Popen", " extend self"]) - - expect(blame[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') - expect(blame[-1][:lines]).to eq([" end", "end"]) - end - end - end -end diff --git a/spec/controllers/branches_controller_spec.rb b/spec/controllers/branches_controller_spec.rb deleted file mode 100644 index 8e06d4bdc77..00000000000 --- a/spec/controllers/branches_controller_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -require 'spec_helper' - -describe Projects::BranchesController do - let(:project) { create(:project) } - let(:user) { create(:user) } - - before do - sign_in(user) - - project.team << [user, :master] - - allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) - allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) - controller.instance_variable_set(:@project, project) - end - - describe "POST create" do - render_views - - before do - post :create, - namespace_id: project.namespace.to_param, - project_id: project.to_param, - branch_name: branch, - ref: ref - end - - context "valid branch name, valid source" do - let(:branch) { "merge_branch" } - let(:ref) { "master" } - it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") - end - end - - context "invalid branch name, valid ref" do - let(:branch) { "<script>alert('merge');</script>" } - let(:ref) { "master" } - it 'redirects' do - expect(subject). - to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") - end - end - - context "valid branch name, invalid ref" do - let(:branch) { "merge_branch" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - end - - context "invalid branch name, invalid ref" do - let(:branch) { "<script>alert('merge');</script>" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - end - - context "valid branch name with encoded slashes" do - let(:branch) { "feature%2Ftest" } - let(:ref) { "<script>alert('ref');</script>" } - it { is_expected.to render_template('new') } - it { project.repository.branch_names.include?('feature/test')} - end - end - - describe "POST destroy" do - render_views - - before do - post :destroy, - format: :js, - id: branch, - namespace_id: project.namespace.to_param, - project_id: project.to_param - end - - context "valid branch name, valid source" do - let(:branch) { "feature" } - - it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } - end - - context "valid branch name with unencoded slashes" do - let(:branch) { "improve/awesome" } - - it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } - end - - context "valid branch name with encoded slashes" do - let(:branch) { "improve%2Fawesome" } - - it { expect(response.status).to eq(200) } - it { expect(subject).to render_template('destroy') } - end - context "invalid branch name, valid ref" do - let(:branch) { "no-branch" } - - it { expect(response.status).to eq(404) } - it { expect(subject).to render_template('destroy') } - end - end -end diff --git a/spec/controllers/ci/projects_controller_spec.rb b/spec/controllers/ci/projects_controller_spec.rb new file mode 100644 index 00000000000..db0748f323f --- /dev/null +++ b/spec/controllers/ci/projects_controller_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe Ci::ProjectsController do + let(:visibility) { :public } + let!(:project) { create(:project, visibility, ci_id: 1) } + let(:ci_id) { project.ci_id } + + ## + # Specs for *deprecated* CI badge + # + describe '#badge' do + shared_examples 'badge provider' do + it 'shows badge' do + expect(response.status).to eq 200 + expect(response.headers) + .to include('Content-Type' => 'image/svg+xml') + end + end + + context 'user not signed in' do + before { get(:badge, id: ci_id) } + + context 'project has no ci_id reference' do + let(:ci_id) { 123 } + + it 'returns 404' do + expect(response.status).to eq 404 + end + end + + context 'project is public' do + let(:visibility) { :public } + it_behaves_like 'badge provider' + end + + context 'project is private' do + let(:visibility) { :private } + it_behaves_like 'badge provider' + end + end + + context 'user signed in' do + let(:user) { create(:user) } + before { sign_in(user) } + before { get(:badge, id: ci_id) } + + context 'private is internal' do + let(:visibility) { :internal } + it_behaves_like 'badge provider' + end + end + end +end diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb index 7793bf1e421..f09e4fcb154 100644 --- a/spec/controllers/commit_controller_spec.rb +++ b/spec/controllers/commit_controller_spec.rb @@ -81,7 +81,7 @@ describe Projects::CommitController do expect(response.body).to start_with("diff --git") # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs)[0].diff.split("\n") + diff_splits = assigns(:diffs).first.diff.split("\n") expect(diff_splits.length).to be <= 2 end end @@ -143,4 +143,53 @@ describe Projects::CommitController do expect(assigns(:tags)).to include("v1.1.0") end end + + describe '#revert' do + context 'when target branch is not provided' do + it 'should render the 404 page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: commit.id) + + expect(response).not_to be_success + expect(response.status).to eq(404) + end + end + + context 'when the revert was successful' do + it 'should redirect to the commits page' do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master') + expect(flash[:notice]).to eq('The commit has been successfully reverted.') + end + end + + context 'when the revert failed' do + before do + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + end + + it 'should redirect to the commit page' do + # Reverting a commit that has been already reverted. + post(:revert, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + target_branch: 'master', + id: commit.id) + + expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, commit.id) + expect(flash[:alert]).to match('Sorry, we cannot revert this commit automatically.') + end + end + end end diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb new file mode 100644 index 00000000000..938e97298b6 --- /dev/null +++ b/spec/controllers/groups_controller_spec.rb @@ -0,0 +1,23 @@ +require 'rails_helper' + +describe GroupsController do + describe 'GET index' do + context 'as a user' do + it 'redirects to Groups Dashboard' do + sign_in(create(:user)) + + get :index + + expect(response).to redirect_to(dashboard_groups_path) + end + end + + context 'as a guest' do + it 'redirects to Explore Groups' do + get :index + + expect(response).to redirect_to(explore_groups_path) + end + end + end +end diff --git a/spec/controllers/profile_keys_controller_spec.rb b/spec/controllers/profiles/keys_controller_spec.rb index b6573f105dc..b6573f105dc 100644 --- a/spec/controllers/profile_keys_controller_spec.rb +++ b/spec/controllers/profiles/keys_controller_spec.rb diff --git a/spec/controllers/projects/blame_controller_spec.rb b/spec/controllers/projects/blame_controller_spec.rb new file mode 100644 index 00000000000..25f06299a29 --- /dev/null +++ b/spec/controllers/projects/blame_controller_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Projects::BlameController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET show" do + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid file" do + let(:id) { 'master/files/ruby/popen.rb' } + it { is_expected.to respond_with(:success) } + end + end +end diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb new file mode 100644 index 00000000000..98ae424ed7c --- /dev/null +++ b/spec/controllers/projects/branches_controller_spec.rb @@ -0,0 +1,134 @@ +require 'spec_helper' + +describe Projects::BranchesController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + + allow(project).to receive(:branches).and_return(['master', 'foo/bar/baz']) + allow(project).to receive(:tags).and_return(['v1.0.0', 'v2.0.0']) + controller.instance_variable_set(:@project, project) + end + + describe "POST create" do + render_views + + context "on creation of a new branch" do + before do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + ref: ref + end + + context "valid branch name, valid source" do + let(:branch) { "merge_branch" } + let(:ref) { "master" } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/merge_branch") + end + end + + context "invalid branch name, valid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "master" } + it 'redirects' do + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/alert('merge');") + end + end + + context "valid branch name, invalid ref" do + let(:branch) { "merge_branch" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + end + + context "invalid branch name, invalid ref" do + let(:branch) { "<script>alert('merge');</script>" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + end + + context "valid branch name with encoded slashes" do + let(:branch) { "feature%2Ftest" } + let(:ref) { "<script>alert('ref');</script>" } + it { is_expected.to render_template('new') } + it { project.repository.branch_names.include?('feature/test') } + end + end + + describe "created from the new branch button on issues" do + let(:branch) { "1-feature-branch" } + let!(:issue) { create(:issue, project: project) } + + + it 'redirects' do + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + + expect(subject). + to redirect_to("/#{project.path_with_namespace}/tree/1-feature-branch") + end + + it 'posts a system note' do + expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, "1-feature-branch") + + post :create, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + branch_name: branch, + issue_iid: issue.iid + end + + end + end + + describe "POST destroy" do + render_views + + before do + post :destroy, + format: :js, + id: branch, + namespace_id: project.namespace.to_param, + project_id: project.to_param + end + + context "valid branch name, valid source" do + let(:branch) { "feature" } + + it { expect(response.status).to eq(200) } + it { expect(subject).to render_template('destroy') } + end + + context "valid branch name with unencoded slashes" do + let(:branch) { "improve/awesome" } + + it { expect(response.status).to eq(200) } + it { expect(subject).to render_template('destroy') } + end + + context "valid branch name with encoded slashes" do + let(:branch) { "improve%2Fawesome" } + + it { expect(response.status).to eq(200) } + it { expect(subject).to render_template('destroy') } + end + context "invalid branch name, valid ref" do + let(:branch) { "no-branch" } + + it { expect(response.status).to eq(404) } + it { expect(subject).to render_template('destroy') } + end + end +end diff --git a/spec/controllers/projects/commit_controller_spec.rb b/spec/controllers/projects/commit_controller_spec.rb new file mode 100644 index 00000000000..438e776ec4b --- /dev/null +++ b/spec/controllers/projects/commit_controller_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Projects::CommitController do + describe 'GET show' do + let(:project) { create(:project) } + + before do + user = create(:user) + project.team << [user, :master] + + sign_in(user) + end + + context 'with valid id' do + it 'responds with 200' do + go id: project.commit.id + + expect(response).to be_ok + end + end + + context 'with invalid id' do + it 'responds with 404' do + go id: project.commit.id.reverse + + expect(response).to be_not_found + end + end + + def go(id:) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end +end diff --git a/spec/controllers/commits_controller_spec.rb b/spec/controllers/projects/commits_controller_spec.rb index 7d8089c4bc6..7d8089c4bc6 100644 --- a/spec/controllers/commits_controller_spec.rb +++ b/spec/controllers/projects/commits_controller_spec.rb diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb index be19f1abc53..788a609ee40 100644 --- a/spec/controllers/projects/compare_controller_spec.rb +++ b/spec/controllers/projects/compare_controller_spec.rb @@ -19,7 +19,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs).length).to be >= 1 + expect(assigns(:diffs).first).to_not be_nil expect(assigns(:commits).length).to be >= 1 end @@ -32,10 +32,10 @@ describe Projects::CompareController do w: 1) expect(response).to be_success - expect(assigns(:diffs).length).to be >= 1 + expect(assigns(:diffs).first).to_not be_nil expect(assigns(:commits).length).to be >= 1 # without whitespace option, there are more than 2 diff_splits - diff_splits = assigns(:diffs)[0].diff.split("\n") + diff_splits = assigns(:diffs).first.diff.split("\n") expect(diff_splits.length).to be <= 2 end @@ -48,7 +48,7 @@ describe Projects::CompareController do to: ref_to) expect(response).to be_success - expect(assigns(:diffs)).to eq([]) + expect(assigns(:diffs).to_a).to eq([]) expect(assigns(:commits)).to eq([]) end diff --git a/spec/controllers/projects/find_file_controller_spec.rb b/spec/controllers/projects/find_file_controller_spec.rb new file mode 100644 index 00000000000..038dfeb8466 --- /dev/null +++ b/spec/controllers/projects/find_file_controller_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +describe Projects::FindFileController do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + sign_in(user) + + project.team << [user, :master] + controller.instance_variable_set(:@project, project) + end + + describe "GET #show" do + # Make sure any errors accessing the tree in our views bubble up to this spec + render_views + + before do + get(:show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id) + end + + context "valid branch" do + let(:id) { 'master' } + it { is_expected.to respond_with(:success) } + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + it { is_expected.to respond_with(:not_found) } + end + end + + describe "GET #list" do + def go(format: 'json') + get :list, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + format: format + end + + context "valid branch" do + let(:id) { 'master' } + it 'returns an array of file path list' do + go + + json = JSON.parse(response.body) + is_expected.to respond_with(:success) + expect(json).not_to eq(nil) + expect(json.length).to be >= 0 + end + end + + context "invalid branch" do + let(:id) { 'invalid-branch' } + + it 'responds with status 404' do + go + is_expected.to respond_with(:not_found) + end + end + end +end diff --git a/spec/controllers/projects/forks_controller_spec.rb b/spec/controllers/projects/forks_controller_spec.rb new file mode 100644 index 00000000000..70ed8f3a62e --- /dev/null +++ b/spec/controllers/projects/forks_controller_spec.rb @@ -0,0 +1,72 @@ +require 'spec_helper' + +describe Projects::ForksController do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:forked_project) { Projects::ForkService.new(project, user).execute } + let(:group) { create(:group, owner: forked_project.creator) } + + describe 'GET index' do + def get_forks + get :index, + namespace_id: project.namespace.to_param, + project_id: project.to_param + end + + context 'when fork is public' do + before { forked_project.update_attribute(:visibility_level, Project::PUBLIC) } + + it 'should be visible for non logged in users' do + get_forks + + expect(assigns[:forks]).to be_present + end + end + + context 'when fork is private' do + before do + forked_project.update_attributes(visibility_level: Project::PRIVATE, group: group) + end + + it 'should not be visible for non logged in users' do + get_forks + + expect(assigns[:forks]).to be_blank + end + + context 'when user is logged in' do + before { sign_in(project.creator) } + + context 'when user is not a Project member neither a group member' do + it 'should not see the Project listed' do + get_forks + + expect(assigns[:forks]).to be_blank + end + end + + context 'when user is a member of the Project' do + before { forked_project.team << [project.creator, :developer] } + + it 'should see the project listed' do + get_forks + + expect(assigns[:forks]).to be_present + end + end + + context 'when user is a member of the Group' do + before { forked_project.group.add_developer(project.creator) } + + it 'should see the project listed' do + get_forks + + expect(assigns[:forks]).to be_present + end + end + + end + end + end + +end diff --git a/spec/controllers/projects/imports_controller_spec.rb b/spec/controllers/projects/imports_controller_spec.rb new file mode 100644 index 00000000000..2acbba469e3 --- /dev/null +++ b/spec/controllers/projects/imports_controller_spec.rb @@ -0,0 +1,121 @@ +require 'spec_helper' + +describe Projects::ImportsController do + let(:user) { create(:user) } + + describe 'GET #show' do + context 'when repository does not exists' do + let(:project) { create(:empty_project) } + + before do + sign_in(user) + project.team << [user, :master] + end + + it 'renders template' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to render_template :show + end + + it 'sets flash.now if params is present' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'Started' } + + expect(flash.now[:notice]).to eq 'Started' + end + end + + context 'when repository exists' do + let(:project) { create(:project_empty_repo, import_url: 'https://github.com/vim/vim.git') } + + before do + sign_in(user) + project.team << [user, :master] + end + + context 'when import is in progress' do + before do + project.update_attribute(:import_status, :started) + end + + it 'renders template' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to render_template :show + end + + it 'sets flash.now if params is present' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: { to: '/', notice_now: 'In progress' } + + expect(flash.now[:notice]).to eq 'In progress' + end + end + + context 'when import failed' do + before do + project.update_attribute(:import_status, :failed) + end + + it 'redirects to new_namespace_project_import_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to redirect_to new_namespace_project_import_path(project.namespace, project) + end + end + + context 'when import finished' do + before do + project.update_attribute(:import_status, :finished) + end + + context 'when project is a fork' do + it 'redirects to namespace_project_path' do + allow_any_instance_of(Project).to receive(:forked?).and_return(true) + + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(flash[:notice]).to eq 'The project was successfully forked.' + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end + + context 'when project is external' do + it 'redirects to namespace_project_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(flash[:notice]).to eq 'The project was successfully imported.' + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end + + context 'when continue params is present' do + let(:params) do + { + to: namespace_project_path(project.namespace, project), + notice: 'Finished' + } + end + + it 'redirects to params[:to]' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param, continue: params + + expect(flash[:notice]).to eq params[:notice] + expect(response).to redirect_to params[:to] + end + end + end + + context 'when import never happened' do + before do + project.update_attribute(:import_status, :none) + end + + it 'redirects to namespace_project_path' do + get :show, namespace_id: project.namespace.to_param, project_id: project.to_param + + expect(response).to redirect_to namespace_project_path(project.namespace, project) + end + end + end + end +end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 76d56bc989d..2cd81231144 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1,16 +1,16 @@ require('spec_helper') describe Projects::IssuesController do - let(:project) { create(:project) } - let(:user) { create(:user) } - let(:issue) { create(:issue, project: project) } + describe "GET #index" do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } - before do - sign_in(user) - project.team << [user, :developer] - end + before do + sign_in(user) + project.team << [user, :developer] + end - describe "GET #index" do it "returns index" do get :index, namespace_id: project.namespace.path, project_id: project.path @@ -38,6 +38,152 @@ describe Projects::IssuesController do get :index, namespace_id: project.namespace.path, project_id: project.path expect(response.status).to eq(404) end + end + + describe 'Confidential Issues' do + let(:project) { create(:empty_project, :public) } + let(:assignee) { create(:assignee) } + let(:author) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project) } + let!(:unescaped_parameter_value) { create(:issue, :confidential, project: project, author: author) } + let!(:request_forgery_timing_attack) { create(:issue, :confidential, project: project, assignee: assignee) } + + describe 'GET #index' do + it 'should not list confidential issues for guests' do + sign_out(:user) + get_issues + + expect(assigns(:issues)).to eq [issue] + end + + it 'should not list confidential issues for non project members' do + sign_in(non_member) + get_issues + + expect(assigns(:issues)).to eq [issue] + end + + it 'should list confidential issues for author' do + sign_in(author) + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).not_to include request_forgery_timing_attack + end + + it 'should list confidential issues for assignee' do + sign_in(assignee) + get_issues + + expect(assigns(:issues)).not_to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + it 'should list confidential issues for project members' do + sign_in(member) + project.team << [member, :developer] + + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + it 'should list confidential issues for admin' do + sign_in(admin) + get_issues + + expect(assigns(:issues)).to include unescaped_parameter_value + expect(assigns(:issues)).to include request_forgery_timing_attack + end + + def get_issues + get :index, + namespace_id: project.namespace.to_param, + project_id: project.to_param + end + end + shared_examples_for 'restricted action' do |http_status| + it 'returns 404 for guests' do + sign_out :user + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status :not_found + end + + it 'returns 404 for non project members' do + sign_in(non_member) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status :not_found + end + + it "returns #{http_status[:success]} for author" do + sign_in(author) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for assignee" do + sign_in(assignee) + go(id: request_forgery_timing_attack.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for project members" do + sign_in(member) + project.team << [member, :developer] + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + + it "returns #{http_status[:success]} for admin" do + sign_in(admin) + go(id: unescaped_parameter_value.to_param) + + expect(response).to have_http_status http_status[:success] + end + end + + describe 'GET #show' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :show, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end + + describe 'GET #edit' do + it_behaves_like 'restricted action', success: 200 + + def go(id:) + get :edit, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id + end + end + + describe 'PUT #update' do + it_behaves_like 'restricted action', success: 302 + + def go(id:) + put :update, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: id, + issue: { title: 'New title' } + end + end end end diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 6aaec224f6e..e82fe26c7a6 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -123,6 +123,40 @@ describe Projects::MergeRequestsController do end end + describe 'GET #index' do + def get_merge_requests + get :index, + namespace_id: project.namespace.to_param, + project_id: project.to_param, + state: 'opened' + end + + context 'when filtering by opened state' do + + context 'with opened merge requests' do + it 'should list those merge requests' do + get_merge_requests + + expect(assigns(:merge_requests)).to include(merge_request) + end + end + + context 'with reopened merge requests' do + before do + merge_request.close! + merge_request.reopen! + end + + it 'should list those merge requests' do + get_merge_requests + + expect(assigns(:merge_requests)).to include(merge_request) + end + end + + end + end + describe 'GET diffs' do def go(format: 'html') get :diffs, @@ -188,7 +222,7 @@ describe Projects::MergeRequestsController do expect(response).to render_template('diffs') end end - + context 'as json' do it 'renders the diffs template to a string' do go format: 'json' @@ -199,6 +233,32 @@ describe Projects::MergeRequestsController do end end + describe 'GET diffs with view' do + def go(extra_params = {}) + params = { + namespace_id: project.namespace.to_param, + project_id: project.to_param, + id: merge_request.iid + } + + get :diffs, params.merge(extra_params) + end + + it 'saves the preferred diff view in a cookie' do + go view: 'parallel' + + expect(response.cookies['diff_view']).to eq('parallel') + end + + it 'assigns :view param based on cookie' do + request.cookies['diff_view'] = 'parallel' + + go + + expect(controller.params[:view]).to eq 'parallel' + end + end + describe 'GET commits' do def go(format: 'html') get :commits, diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb index 18a30033ed8..0ddbec9eac2 100644 --- a/spec/controllers/projects/repositories_controller_spec.rb +++ b/spec/controllers/projects/repositories_controller_spec.rb @@ -2,35 +2,41 @@ require "spec_helper" describe Projects::RepositoriesController do let(:project) { create(:project) } - let(:user) { create(:user) } describe "GET archive" do - before do - sign_in(user) - project.team << [user, :developer] + context 'as a guest' do + it 'responds with redirect in correct format' do + get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip" - allow(ArchiveRepositoryService).to receive(:new).and_return(service) - end - - let(:service) { ArchiveRepositoryService.new(project, "master", "zip") } - - it "executes ArchiveRepositoryService" do - expect(ArchiveRepositoryService).to receive(:new).with(project, "master", "zip") - expect(service).to receive(:execute) - - get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + expect(response.content_type).to start_with 'text/html' + expect(response).to be_redirect + end end - context "when the service raises an error" do + context 'as a user' do + let(:user) { create(:user) } before do - allow(service).to receive(:execute).and_raise("Archive failed") + project.team << [user, :developer] + sign_in(user) end + it "uses Gitlab::Workhorse" do + expect(Gitlab::Workhorse).to receive(:send_git_archive).with(project, "master", "zip") - it "renders Not Found" do get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" + end + + context "when the service raises an error" do + + before do + allow(Gitlab::Workhorse).to receive(:send_git_archive).and_raise("Archive failed") + end + + it "renders Not Found" do + get :archive, namespace_id: project.namespace.path, project_id: project.path, ref: "master", format: "zip" - expect(response.status).to eq(404) + expect(response.status).to eq(404) + end end end end diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 665526fde93..1893e946f5c 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -9,19 +9,6 @@ describe ProjectsController do describe "GET show" do - context "when requested by `go get`" do - render_views - - it "renders the go-import meta tag" do - get :show, "go-get" => "1", namespace_id: "bogus_namespace", id: "bogus_project" - - expect(response.body).to include("name='go-import'") - - content = "localhost/bogus_namespace/bogus_project git http://localhost/bogus_namespace/bogus_project.git" - expect(response.body).to include("content='#{content}'") - end - end - context "rendering default project view" do render_views @@ -86,6 +73,14 @@ describe ProjectsController do end end end + + context "when the url contains .atom" do + let(:public_project_with_dot_atom) { build(:project, :public, name: 'my.atom', path: 'my.atom') } + + it 'expect an error creating the project' do + expect(public_project_with_dot_atom).not_to be_valid + end + end end describe "#destroy" do diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb new file mode 100644 index 00000000000..9ced397bd4a --- /dev/null +++ b/spec/controllers/sent_notifications_controller_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +describe SentNotificationsController, type: :controller do + let(:user) { create(:user) } + let(:issue) { create(:issue, author: user) } + let(:sent_notification) { create(:sent_notification, noteable: issue) } + + describe 'GET #unsubscribe' do + it 'returns a 404 when calling without existing id' do + get(:unsubscribe, id: '0' * 32) + + expect(response.status).to be 404 + end + + context 'calling with id' do + it 'shows a flash message to the user' do + get(:unsubscribe, id: sent_notification.reply_key) + + expect(response.status).to be 302 + + expect(response).to redirect_to new_user_session_path + expect(controller).to set_flash[:notice].to(/unsubscribed/).now + end + end + end +end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 104a5f50143..7337ff58be1 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -41,6 +41,7 @@ describe UsersController do end describe 'GET #calendar' do + it 'renders calendar' do sign_in(user) @@ -48,6 +49,23 @@ describe UsersController do expect(response).to render_template('calendar') end + + context 'forked project' do + let!(:project) { create(:project) } + let!(:forked_project) { Projects::ForkService.new(project, user).execute } + + before do + sign_in(user) + project.team << [user, :developer] + EventCreateService.new.push(project, user, []) + EventCreateService.new.push(forked_project, user, []) + end + + it 'includes forked projects' do + get :calendar, username: user.username + expect(assigns(:contributions_calendar).projects.count).to eq(2) + end + end end describe 'GET #calendar_activities' do diff --git a/spec/factories.rb b/spec/factories.rb deleted file mode 100644 index d6b4efa9a03..00000000000 --- a/spec/factories.rb +++ /dev/null @@ -1,215 +0,0 @@ -include ActionDispatch::TestProcess - -FactoryGirl.define do - sequence :sentence, aliases: [:title, :content] do - FFaker::Lorem.sentence - end - - sequence :name do - FFaker::Name.name - end - - sequence :file_name do - FFaker::Internet.user_name - end - - sequence(:url) { FFaker::Internet.uri('http') } - - factory :user, aliases: [:author, :assignee, :owner, :creator] do - email { FFaker::Internet.email } - name - sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } - password "12345678" - confirmed_at { Time.now } - confirmation_token { nil } - can_create_group true - - trait :admin do - admin true - end - - trait :two_factor do - before(:create) do |user| - user.two_factor_enabled = true - user.otp_secret = User.generate_otp_secret(32) - user.generate_otp_backup_codes! - end - end - - factory :omniauth_user do - ignore do - extern_uid '123456' - provider 'ldapmain' - end - - after(:create) do |user, evaluator| - user.identities << create( - :identity, - provider: evaluator.provider, - extern_uid: evaluator.extern_uid - ) - end - end - - factory :admin, traits: [:admin] - end - - factory :group do - sequence(:name) { |n| "group#{n}" } - path { name.downcase.gsub(/\s/, '_') } - type 'Group' - end - - factory :namespace do - sequence(:name) { |n| "namespace#{n}" } - path { name.downcase.gsub(/\s/, '_') } - owner - end - - factory :project_member do - user - project - access_level { ProjectMember::MASTER } - end - - factory :issue do - title - author - project - - trait :closed do - state :closed - end - - trait :reopened do - state :reopened - end - - factory :closed_issue, traits: [:closed] - factory :reopened_issue, traits: [:reopened] - end - - factory :event do - factory :closed_issue_event do - project - action { Event::CLOSED } - target factory: :closed_issue - author factory: :user - end - end - - factory :key do - title - key do - "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" - end - - factory :deploy_key, class: 'DeployKey' do - end - - factory :personal_key do - user - end - - factory :another_key do - key do - "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" - end - - factory :another_deploy_key, class: 'DeployKey' do - end - end - end - - factory :email do - user - email do - FFaker::Internet.email('alias') - end - - factory :another_email do - email do - FFaker::Internet.email('another.alias') - end - end - end - - factory :milestone do - title - project - - trait :closed do - state :closed - end - - factory :closed_milestone, traits: [:closed] - end - - factory :system_hook do - url - end - - factory :project_hook do - url - end - - factory :project_snippet do - project - author - title - content - file_name - end - - factory :personal_snippet do - author - title - content - file_name - - trait :public do - visibility_level Gitlab::VisibilityLevel::PUBLIC - end - - trait :internal do - visibility_level Gitlab::VisibilityLevel::INTERNAL - end - - trait :private do - visibility_level Gitlab::VisibilityLevel::PRIVATE - end - end - - factory :snippet do - author - title - content - file_name - end - - factory :protected_branch do - name - project - end - - factory :service do - type "" - title "GitLab CI" - project - end - - factory :service_hook do - url - service - end - - factory :deploy_keys_project do - deploy_key - project - end - - factory :identity do - provider 'ldapmain' - extern_uid 'my-ldap-id' - end -end diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb index 8d287ded292..d0e8c778518 100644 --- a/spec/factories/abuse_reports.rb +++ b/spec/factories/abuse_reports.rb @@ -10,8 +10,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :abuse_report do reporter factory: :user diff --git a/spec/factories/appearances.rb b/spec/factories/appearances.rb new file mode 100644 index 00000000000..cf2a2b76bcb --- /dev/null +++ b/spec/factories/appearances.rb @@ -0,0 +1,8 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :appearance do + title "MepMep" + description "This is my Community Edition instance" + end +end diff --git a/spec/factories/broadcast_messages.rb b/spec/factories/broadcast_messages.rb index ea0039d39e6..373ca75467e 100644 --- a/spec/factories/broadcast_messages.rb +++ b/spec/factories/broadcast_messages.rb @@ -6,22 +6,26 @@ # message :text not null # starts_at :datetime # ends_at :datetime -# alert_type :integer # created_at :datetime # updated_at :datetime # color :string(255) # font :string(255) # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :broadcast_message do message "MyText" - starts_at "2013-11-12 13:43:25" - ends_at "2013-11-12 13:43:25" - alert_type 1 - color "#555555" - font "#BBBBBB" + starts_at Date.today + ends_at Date.tomorrow + + trait :expired do + starts_at 5.days.ago + ends_at 3.days.ago + end + + trait :future do + starts_at 5.days.from_now + ends_at 6.days.from_now + end end end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f76e826f138..cd49e559b7d 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -1,35 +1,11 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - -# Read about factories at https://github.com/thoughtbot/factory_girl +include ActionDispatch::TestProcess FactoryGirl.define do factory :ci_build, class: Ci::Build do name 'test' ref 'master' tag false + created_at 'Di 29. Okt 09:50:00 CET 2013' started_at 'Di 29. Okt 09:51:28 CET 2013' finished_at 'Di 29. Okt 09:53:28 CET 2013' commands 'ls -a' @@ -42,6 +18,30 @@ FactoryGirl.define do commit factory: :ci_commit + trait :success do + status 'success' + end + + trait :failed do + status 'failed' + end + + trait :canceled do + status 'canceled' + end + + trait :running do + status 'running' + end + + trait :pending do + status 'pending' + end + + trait :allowed_to_fail do + allow_failure true + end + after(:build) do |build, evaluator| build.project = build.commit.project end @@ -54,5 +54,29 @@ FactoryGirl.define do factory :ci_build_tag do tag true end + + factory :ci_build_with_coverage do + coverage 99.9 + end + + trait :trace do + after(:create) do |build, evaluator| + build.trace = 'BUILD TRACE' + end + end + + trait :artifacts do + after(:create) do |build, _| + build.artifacts_file = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), + 'application/zip') + + build.artifacts_metadata = + fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'), + 'application/x-gzip') + + build.save! + end + end end end diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb index b42cafa518a..645cd7ae766 100644 --- a/spec/factories/ci/commits.rb +++ b/spec/factories/ci/commits.rb @@ -16,7 +16,6 @@ # gl_project_id :integer # -# Read about factories at https://github.com/thoughtbot/factory_girl FactoryGirl.define do factory :ci_empty_commit, class: Ci::Commit do sha '97de212e80737a608d939f648d959671fb0a0142' diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb index 008d1c5d961..83fccad679f 100644 --- a/spec/factories/ci/runner_projects.rb +++ b/spec/factories/ci/runner_projects.rb @@ -9,8 +9,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_runner_project, class: Ci::RunnerProject do runner_id 1 diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb index db759eca9ac..5b645fab32e 100644 --- a/spec/factories/ci/runners.rb +++ b/spec/factories/ci/runners.rb @@ -17,22 +17,18 @@ # architecture :string(255) # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_runner, class: Ci::Runner do sequence :description do |n| "My runner#{n}" end - platform "darwin" + platform "darwin" + is_shared false + active true - factory :ci_shared_runner do + trait :shared do is_shared true end - - factory :ci_specific_runner do - is_shared false - end end end diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb index db053c610cd..6d47d05f8ad 100644 --- a/spec/factories/ci/trigger_requests.rb +++ b/spec/factories/ci/trigger_requests.rb @@ -1,8 +1,8 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_trigger_request, class: Ci::TriggerRequest do factory :ci_trigger_request_with_variables do + trigger factory: :ci_trigger + variables do { TRIGGER_KEY: 'TRIGGER_VALUE' diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb index fd3afdb1ec2..a27b04424e5 100644 --- a/spec/factories/ci/triggers.rb +++ b/spec/factories/ci/triggers.rb @@ -1,5 +1,3 @@ -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :ci_trigger_without_token, class: Ci::Trigger do factory :ci_trigger do diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb new file mode 100644 index 00000000000..856a8e725eb --- /dev/null +++ b/spec/factories/ci/variables.rb @@ -0,0 +1,20 @@ +# == Schema Information +# +# Table name: ci_variables +# +# id :integer not null, primary key +# project_id :integer not null +# key :string(255) +# value :text +# encrypted_value :text +# encrypted_value_salt :string(255) +# encrypted_value_iv :string(255) +# gl_project_id :integer +# + +FactoryGirl.define do + factory :ci_variable, class: Ci::Variable do + sequence(:key) { |n| "VARIABLE_#{n}" } + value 'VARIABLE_VALUE' + end +end diff --git a/spec/factories/commit_statuses.rb b/spec/factories/commit_statuses.rb index 8898b71e2a3..b7c2b32cb13 100644 --- a/spec/factories/commit_statuses.rb +++ b/spec/factories/commit_statuses.rb @@ -1,11 +1,15 @@ FactoryGirl.define do factory :commit_status, class: CommitStatus do - started_at 'Di 29. Okt 09:51:28 CET 2013' - finished_at 'Di 29. Okt 09:53:28 CET 2013' name 'default' status 'success' description 'commit status' commit factory: :ci_commit_with_one_job + started_at 'Tue, 26 Jan 2016 08:21:42 +0100' + finished_at 'Tue, 26 Jan 2016 08:23:42 +0100' + + after(:build) do |build, evaluator| + build.project = build.commit.project + end factory :generic_commit_status, class: GenericCommitStatus do name 'generic' diff --git a/spec/factories/deploy_keys_projects.rb b/spec/factories/deploy_keys_projects.rb new file mode 100644 index 00000000000..27cece487bd --- /dev/null +++ b/spec/factories/deploy_keys_projects.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :deploy_keys_project do + deploy_key + project + end +end diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb new file mode 100644 index 00000000000..9794772ac7d --- /dev/null +++ b/spec/factories/emails.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :email do + user + email { FFaker::Internet.email('alias') } + end +end diff --git a/spec/factories/events.rb b/spec/factories/events.rb new file mode 100644 index 00000000000..90788f30ac9 --- /dev/null +++ b/spec/factories/events.rb @@ -0,0 +1,10 @@ +FactoryGirl.define do + factory :event do + factory :closed_issue_event do + project + action { Event::CLOSED } + target factory: :closed_issue + author factory: :user + end + end +end diff --git a/spec/factories/forked_project_links.rb b/spec/factories/forked_project_links.rb index 906e4106b32..252bf2747e1 100644 --- a/spec/factories/forked_project_links.rb +++ b/spec/factories/forked_project_links.rb @@ -9,8 +9,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :forked_project_link do association :forked_to_project, factory: :project diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb new file mode 100644 index 00000000000..4a3a155d7ff --- /dev/null +++ b/spec/factories/groups.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :group do + sequence(:name) { |n| "group#{n}" } + path { name.downcase.gsub(/\s/, '_') } + type 'Group' + end +end diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb new file mode 100644 index 00000000000..26ef6f18698 --- /dev/null +++ b/spec/factories/identities.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :identity do + provider 'ldapmain' + extern_uid 'my-ldap-id' + end +end diff --git a/spec/factories/issues.rb b/spec/factories/issues.rb new file mode 100644 index 00000000000..e72aa9479b7 --- /dev/null +++ b/spec/factories/issues.rb @@ -0,0 +1,22 @@ +FactoryGirl.define do + factory :issue do + title + author + project + + trait :confidential do + confidential true + end + + trait :closed do + state :closed + end + + trait :reopened do + state :reopened + end + + factory :closed_issue, traits: [:closed] + factory :reopened_issue, traits: [:reopened] + end +end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb new file mode 100644 index 00000000000..d69c5b38d0a --- /dev/null +++ b/spec/factories/keys.rb @@ -0,0 +1,24 @@ +FactoryGirl.define do + factory :key do + title + key do + "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" + end + + factory :deploy_key, class: 'DeployKey' do + end + + factory :personal_key do + user + end + + factory :another_key do + key do + "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDmTillFzNTrrGgwaCKaSj+QCz81E6jBc/s9av0+3b1Hwfxgkqjl4nAK/OD2NjgyrONDTDfR8cRN4eAAy6nY8GLkOyYBDyuc5nTMqs5z3yVuTwf3koGm/YQQCmo91psZ2BgDFTor8SVEE5Mm1D1k3JDMhDFxzzrOtRYFPci9lskTJaBjpqWZ4E9rDTD2q/QZntCqbC3wE9uSemRQB5f8kik7vD/AD8VQXuzKladrZKkzkONCPWsXDspUitjM8HkQdOf0PsYn1CMUC1xKYbCxkg5TkEosIwGv6CoEArUrdu/4+10LVslq494mAvEItywzrluCLCnwELfW+h/m8UHoVhZ" + end + + factory :another_deploy_key, class: 'DeployKey' do + end + end + end +end diff --git a/spec/factories/label_links.rb b/spec/factories/label_links.rb index bd304b5db6b..2939d4307c5 100644 --- a/spec/factories/label_links.rb +++ b/spec/factories/label_links.rb @@ -10,8 +10,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :label_link do label diff --git a/spec/factories/labels.rb b/spec/factories/labels.rb index 8b12ee11af5..ea2be8928d5 100644 --- a/spec/factories/labels.rb +++ b/spec/factories/labels.rb @@ -11,11 +11,9 @@ # template :boolean default(FALSE) # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :label do - title "Bug" + sequence(:title) { |n| "label#{n}" } color "#990000" project end diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb index 2da107ba24b..327858ce435 100644 --- a/spec/factories/lfs_objects.rb +++ b/spec/factories/lfs_objects.rb @@ -10,7 +10,7 @@ # file :string(255) # -# Read about factories at https://github.com/thoughtbot/factory_girl +include ActionDispatch::TestProcess FactoryGirl.define do factory :lfs_object do diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb index 3772236a77a..50b45843c99 100644 --- a/spec/factories/lfs_objects_projects.rb +++ b/spec/factories/lfs_objects_projects.rb @@ -9,8 +9,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :lfs_objects_project do lfs_object diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb index 5b4d7f41bc4..e281e2f227b 100644 --- a/spec/factories/merge_requests.rb +++ b/spec/factories/merge_requests.rb @@ -2,25 +2,29 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer +# merge_commit_sha :string # FactoryGirl.define do @@ -47,11 +51,20 @@ FactoryGirl.define do trait :with_diffs do end + trait :without_diffs do + source_branch "improve/awesome" + target_branch "master" + end + trait :conflict do source_branch "feature_conflict" target_branch "feature" end + trait :merged do + state :merged + end + trait :closed do state :closed end @@ -65,11 +78,22 @@ FactoryGirl.define do target_branch "master" end + trait :rebased do + source_branch "markdown" + target_branch "improve/awesome" + end + + trait :diverged do + source_branch "feature" + target_branch "master" + end + trait :merge_when_build_succeeds do merge_when_build_succeeds true merge_user author end + factory :merged_merge_request, traits: [:merged] factory :closed_merge_request, traits: [:closed] factory :reopened_merge_request, traits: [:reopened] factory :merge_request_with_diffs, traits: [:with_diffs] diff --git a/spec/factories/milestones.rb b/spec/factories/milestones.rb new file mode 100644 index 00000000000..e9e85962fe4 --- /dev/null +++ b/spec/factories/milestones.rb @@ -0,0 +1,12 @@ +FactoryGirl.define do + factory :milestone do + title + project + + trait :closed do + state :closed + end + + factory :closed_milestone, traits: [:closed] + end +end diff --git a/spec/factories/namespaces.rb b/spec/factories/namespaces.rb new file mode 100644 index 00000000000..1b1fc4ce80d --- /dev/null +++ b/spec/factories/namespaces.rb @@ -0,0 +1,7 @@ +FactoryGirl.define do + factory :namespace do + sequence(:name) { |n| "namespace#{n}" } + path { name.downcase.gsub(/\s/, '_') } + owner + end +end diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb index 35a20adeef3..e5dcb159014 100644 --- a/spec/factories/notes.rb +++ b/spec/factories/notes.rb @@ -21,6 +21,8 @@ require_relative '../support/repo_helpers' +include ActionDispatch::TestProcess + FactoryGirl.define do factory :note do project @@ -34,6 +36,8 @@ FactoryGirl.define do factory :note_on_merge_request_diff, traits: [:on_merge_request, :on_diff] factory :note_on_project_snippet, traits: [:on_project_snippet] factory :system_note, traits: [:system] + factory :downvote_note, traits: [:award, :downvote] + factory :upvote_note, traits: [:award, :upvote] trait :on_commit do project @@ -65,6 +69,18 @@ FactoryGirl.define do system true end + trait :award do + is_award true + end + + trait :downvote do + note "thumbsdown" + end + + trait :upvote do + note "thumbsup" + end + trait :with_attachment do attachment { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") } end diff --git a/spec/factories/personal_snippets.rb b/spec/factories/personal_snippets.rb new file mode 100644 index 00000000000..0f13b2c1020 --- /dev/null +++ b/spec/factories/personal_snippets.rb @@ -0,0 +1,4 @@ +FactoryGirl.define do + factory :personal_snippet, parent: :snippet, class: :PersonalSnippet do + end +end diff --git a/spec/factories/project_group_links.rb b/spec/factories/project_group_links.rb new file mode 100644 index 00000000000..e73cc05f9d7 --- /dev/null +++ b/spec/factories/project_group_links.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :project_group_link do + project + group + end +end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb new file mode 100644 index 00000000000..94dd935a039 --- /dev/null +++ b/spec/factories/project_hooks.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :project_hook do + url { FFaker::Internet.uri('http') } + end +end diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb new file mode 100644 index 00000000000..cf3659ba275 --- /dev/null +++ b/spec/factories/project_members.rb @@ -0,0 +1,27 @@ +FactoryGirl.define do + factory :project_member do + user + project + master + + trait :guest do + access_level ProjectMember::GUEST + end + + trait :reporter do + access_level ProjectMember::REPORTER + end + + trait :developer do + access_level ProjectMember::DEVELOPER + end + + trait :master do + access_level ProjectMember::MASTER + end + + trait :owner do + access_level ProjectMember::OWNER + end + end +end diff --git a/spec/factories/project_snippets.rb b/spec/factories/project_snippets.rb new file mode 100644 index 00000000000..d681a2c8483 --- /dev/null +++ b/spec/factories/project_snippets.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :project_snippet, parent: :snippet, class: :ProjectSnippet do + project + end +end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 112213377ff..c14b99606ba 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # FactoryGirl.define do diff --git a/spec/factories/protected_branches.rb b/spec/factories/protected_branches.rb new file mode 100644 index 00000000000..28ed8078157 --- /dev/null +++ b/spec/factories/protected_branches.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :protected_branch do + name + project + end +end diff --git a/spec/factories/releases.rb b/spec/factories/releases.rb index 43d09b17534..7f331c37256 100644 --- a/spec/factories/releases.rb +++ b/spec/factories/releases.rb @@ -10,8 +10,6 @@ # updated_at :datetime # -# Read about factories at https://github.com/thoughtbot/factory_girl - FactoryGirl.define do factory :release do tag "v1.1.0" diff --git a/spec/factories/sent_notifications.rb b/spec/factories/sent_notifications.rb new file mode 100644 index 00000000000..78eb929c6e7 --- /dev/null +++ b/spec/factories/sent_notifications.rb @@ -0,0 +1,8 @@ +FactoryGirl.define do + factory :sent_notification do + project + recipient factory: :user + noteable factory: :issue + reply_key "0123456789abcdef" * 2 + end +end diff --git a/spec/factories/service_hooks.rb b/spec/factories/service_hooks.rb new file mode 100644 index 00000000000..6dd6af63f3e --- /dev/null +++ b/spec/factories/service_hooks.rb @@ -0,0 +1,6 @@ +FactoryGirl.define do + factory :service_hook do + url { FFaker::Internet.uri('http') } + service + end +end diff --git a/spec/factories/services.rb b/spec/factories/services.rb new file mode 100644 index 00000000000..9de78d68280 --- /dev/null +++ b/spec/factories/services.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :service do + project + end +end diff --git a/spec/factories/snippets.rb b/spec/factories/snippets.rb new file mode 100644 index 00000000000..365f12a0c95 --- /dev/null +++ b/spec/factories/snippets.rb @@ -0,0 +1,28 @@ +FactoryGirl.define do + sequence :title, aliases: [:content] do + FFaker::Lorem.sentence + end + + sequence :file_name do + FFaker::Internet.user_name + end + + factory :snippet do + author + title + content + file_name + + trait :public do + visibility_level Snippet::PUBLIC + end + + trait :internal do + visibility_level Snippet::INTERNAL + end + + trait :private do + visibility_level Snippet::PRIVATE + end + end +end diff --git a/spec/factories/spam_logs.rb b/spec/factories/spam_logs.rb new file mode 100644 index 00000000000..a4f6d291269 --- /dev/null +++ b/spec/factories/spam_logs.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + factory :spam_log do + user + source_ip { FFaker::Internet.ip_v4_address } + noteable_type 'Issue' + title { FFaker::Lorem.sentence } + description { FFaker::Lorem.paragraph(5) } + end +end diff --git a/spec/factories/system_hooks.rb b/spec/factories/system_hooks.rb new file mode 100644 index 00000000000..c786e9cb79b --- /dev/null +++ b/spec/factories/system_hooks.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :system_hook do + url { FFaker::Internet.uri('http') } + end +end diff --git a/spec/factories/todos.rb b/spec/factories/todos.rb new file mode 100644 index 00000000000..bd85b1d798a --- /dev/null +++ b/spec/factories/todos.rb @@ -0,0 +1,34 @@ +# == Schema Information +# +# Table name: todos +# +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# target_id :integer not null +# target_type :string not null +# author_id :integer +# note_id :integer +# action :integer not null +# state :string not null +# created_at :datetime +# updated_at :datetime +# + +FactoryGirl.define do + factory :todo do + project + author + user + target factory: :issue + action { Todo::ASSIGNED } + + trait :assigned do + action { Todo::ASSIGNED } + end + + trait :mentioned do + action { Todo::MENTIONED } + end + end +end diff --git a/spec/factories/users.rb b/spec/factories/users.rb new file mode 100644 index 00000000000..a5c60c51c5b --- /dev/null +++ b/spec/factories/users.rb @@ -0,0 +1,43 @@ +FactoryGirl.define do + sequence(:name) { FFaker::Name.name } + + factory :user, aliases: [:author, :assignee, :recipient, :owner, :creator] do + email { FFaker::Internet.email } + name + sequence(:username) { |n| "#{FFaker::Internet.user_name}#{n}" } + password "12345678" + confirmed_at { Time.now } + confirmation_token { nil } + can_create_group true + + trait :admin do + admin true + end + + trait :two_factor do + before(:create) do |user| + user.two_factor_enabled = true + user.otp_secret = User.generate_otp_secret(32) + user.otp_grace_period_started_at = Time.now + user.generate_otp_backup_codes! + end + end + + factory :omniauth_user do + transient do + extern_uid '123456' + provider 'ldapmain' + end + + after(:create) do |user, evaluator| + user.identities << create( + :identity, + provider: evaluator.provider, + extern_uid: evaluator.extern_uid + ) + end + end + + factory :admin, traits: [:admin] + end +end diff --git a/spec/features/admin/admin_builds_spec.rb b/spec/features/admin/admin_builds_spec.rb index 72764b1629d..2e9851fb442 100644 --- a/spec/features/admin/admin_builds_spec.rb +++ b/spec/features/admin/admin_builds_spec.rb @@ -1,69 +1,98 @@ require 'spec_helper' -describe "Admin Builds" do - let(:commit) { FactoryGirl.create :ci_commit } - let(:build) { FactoryGirl.create :ci_build, commit: commit } - +describe 'Admin Builds' do before do login_as :admin end - describe "GET /admin/builds" do - before do - build - visit admin_builds_path - end - - it { expect(page).to have_content "Running" } - it { expect(page).to have_content build.short_sha } - end + describe 'GET /admin/builds' do + let(:commit) { create(:ci_commit) } - describe "Tabs" do - it "shows all builds" do - FactoryGirl.create :ci_build, commit: commit, status: "pending" - FactoryGirl.create :ci_build, commit: commit, status: "running" - FactoryGirl.create :ci_build, commit: commit, status: "success" - FactoryGirl.create :ci_build, commit: commit, status: "failed" + context 'All tab' do + context 'when have builds' do + it 'shows all builds' do + create(:ci_build, commit: commit, status: :pending) + create(:ci_build, commit: commit, status: :running) + create(:ci_build, commit: commit, status: :success) + create(:ci_build, commit: commit, status: :failed) - visit admin_builds_path + visit admin_builds_path - within ".center-top-menu" do - click_on "All" + expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page.all('.build-link').size).to eq(4) + expect(page).to have_link 'Cancel all' + end end - expect(page.all(".build-link").size).to eq(4) + context 'when have no builds' do + it 'shows a message' do + visit admin_builds_path + + expect(page).to have_selector('.nav-links li.active', text: 'All') + expect(page).to have_content 'No builds to show' + expect(page).not_to have_link 'Cancel all' + end + end end - it "shows finished builds" do - build = FactoryGirl.create :ci_build, commit: commit, status: "pending" - build1 = FactoryGirl.create :ci_build, commit: commit, status: "running" - build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" + context 'Running tab' do + context 'when have running builds' do + it 'shows running builds' do + build1 = create(:ci_build, commit: commit, status: :pending) + build2 = create(:ci_build, commit: commit, status: :success) + build3 = create(:ci_build, commit: commit, status: :failed) + + visit admin_builds_path(scope: :running) + + expect(page).to have_selector('.nav-links li.active', text: 'Running') + expect(page.find('.build-link')).to have_content(build1.id) + expect(page.find('.build-link')).not_to have_content(build2.id) + expect(page.find('.build-link')).not_to have_content(build3.id) + expect(page).to have_link 'Cancel all' + end + end - visit admin_builds_path + context 'when have no builds running' do + it 'shows a message' do + create(:ci_build, commit: commit, status: :success) - within ".center-top-menu" do - click_on "Finished" - end + visit admin_builds_path(scope: :running) - expect(page.find(".build-link")).not_to have_content(build.id) - expect(page.find(".build-link")).not_to have_content(build1.id) - expect(page.find(".build-link")).to have_content(build2.id) + expect(page).to have_selector('.nav-links li.active', text: 'Running') + expect(page).to have_content 'No builds to show' + expect(page).not_to have_link 'Cancel all' + end + end end - it "shows running builds" do - build = FactoryGirl.create :ci_build, commit: commit, status: "pending" - build2 = FactoryGirl.create :ci_build, commit: commit, status: "success" - build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed" + context 'Finished tab' do + context 'when have finished builds' do + it 'shows finished builds' do + build1 = create(:ci_build, commit: commit, status: :pending) + build2 = create(:ci_build, commit: commit, status: :running) + build3 = create(:ci_build, commit: commit, status: :success) + + visit admin_builds_path(scope: :finished) + + expect(page).to have_selector('.nav-links li.active', text: 'Finished') + expect(page.find('.build-link')).not_to have_content(build1.id) + expect(page.find('.build-link')).not_to have_content(build2.id) + expect(page.find('.build-link')).to have_content(build3.id) + expect(page).to have_link 'Cancel all' + end + end - visit admin_builds_path + context 'when have no builds finished' do + it 'shows a message' do + create(:ci_build, commit: commit, status: :running) - within ".center-top-menu" do - click_on "Running" - end + visit admin_builds_path(scope: :finished) - expect(page.find(".build-link")).to have_content(build.id) - expect(page.find(".build-link")).not_to have_content(build2.id) - expect(page.find(".build-link")).not_to have_content(build3.id) + expect(page).to have_selector('.nav-links li.active', text: 'Finished') + expect(page).to have_content 'No builds to show' + expect(page).to have_link 'Cancel all' + end + end end end end diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb index f0031a0a247..6da3a857b3f 100644 --- a/spec/features/builds_spec.rb +++ b/spec/features/builds_spec.rb @@ -8,18 +8,18 @@ describe "Builds" do @commit = FactoryGirl.create :ci_commit @build = FactoryGirl.create :ci_build, commit: @commit @project = @commit.project - @project.team << [@user, :master] + @project.team << [@user, :developer] end describe "GET /:project/builds" do context "Running scope" do before do @build.run! - visit namespace_project_builds_path(@project.namespace, @project) + visit namespace_project_builds_path(@project.namespace, @project, scope: :running) end - it { expect(page).to have_content 'Running' } - it { expect(page).to have_content 'Cancel running' } + it { expect(page).to have_selector('.nav-links li.active', text: 'Running') } + it { expect(page).to have_link 'Cancel running' } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } @@ -31,21 +31,22 @@ describe "Builds" do visit namespace_project_builds_path(@project.namespace, @project, scope: :finished) end + it { expect(page).to have_selector('.nav-links li.active', text: 'Finished') } it { expect(page).to have_content 'No builds to show' } - it { expect(page).to have_content 'Cancel running' } + it { expect(page).to have_link 'Cancel running' } end context "All builds" do before do @project.builds.running_or_pending.each(&:success) - visit namespace_project_builds_path(@project.namespace, @project, scope: :all) + visit namespace_project_builds_path(@project.namespace, @project) end - it { expect(page).to have_content 'All' } + it { expect(page).to have_selector('.nav-links li.active', text: 'All') } it { expect(page).to have_content @build.short_sha } it { expect(page).to have_content @build.ref } it { expect(page).to have_content @build.name } - it { expect(page).to_not have_content 'Cancel running' } + it { expect(page).to_not have_link 'Cancel running' } end end @@ -56,8 +57,12 @@ describe "Builds" do click_link "Cancel running" end - it { expect(page).to have_content 'No builds to show' } - it { expect(page).to_not have_content 'Cancel running' } + it { expect(page).to have_selector('.nav-links li.active', text: 'All') } + it { expect(page).to have_content 'canceled' } + it { expect(page).to have_content @build.short_sha } + it { expect(page).to have_content @build.ref } + it { expect(page).to have_content @build.name } + it { expect(page).to_not have_link 'Cancel running' } end describe "GET /:project/builds/:id" do @@ -75,7 +80,11 @@ describe "Builds" do visit namespace_project_build_path(@project.namespace, @project, @build) end - it { expect(page).to have_content 'Download artifacts' } + it 'has button to download artifacts' do + page.within('.artifacts') do + expect(page).to have_content 'Download' + end + end end end @@ -106,7 +115,7 @@ describe "Builds" do before do @build.update_attributes(artifacts_file: artifacts_file) visit namespace_project_build_path(@project.namespace, @project, @build) - click_link 'Download artifacts' + page.within('.artifacts') { click_link 'Download' } end it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) } diff --git a/spec/features/ci_lint_spec.rb b/spec/features/ci_lint_spec.rb index e6e73e5e67c..30e29d9d552 100644 --- a/spec/features/ci_lint_spec.rb +++ b/spec/features/ci_lint_spec.rb @@ -35,5 +35,13 @@ describe 'CI Lint' do expect(page).to have_content('Error: Please provide content of .gitlab-ci.yml') end end + + describe 'YAML revalidate' do + let(:yaml_content) { 'my yaml content' } + + it 'loads previous YAML content after validation' do + expect(page).to have_field('content', with: 'my yaml content', type: 'textarea') + end + end end end diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb index fe7f07f5b75..dacaa96d760 100644 --- a/spec/features/commits_spec.rb +++ b/spec/features/commits_spec.rb @@ -8,7 +8,6 @@ describe 'Commits' do describe 'CI' do before do login_as :user - project.team << [@user, :master] stub_ci_commit_to_return_yaml_file end @@ -16,83 +15,149 @@ describe 'Commits' do FactoryGirl.create :ci_commit, project: project, sha: project.commit.sha end - let!(:build) { FactoryGirl.create :ci_build, commit: commit } + context 'commit status is Generic Commit Status' do + let!(:status) { FactoryGirl.create :generic_commit_status, commit: commit } - describe 'Project commits' do before do - visit namespace_project_commits_path(project.namespace, project, :master) + project.team << [@user, :reporter] end - it 'should show build status' do - page.within("//li[@id='commit-#{commit.short_sha}']") do - expect(page).to have_css(".ci-status-link") + describe 'Commit builds' do + before do + visit ci_status_path(commit) end - end - end - describe 'Commit builds' do - before do - visit ci_status_path(commit) - end + it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.sha[0..7] } - it { expect(page).to have_content commit.git_commit_message } - it { expect(page).to have_content commit.git_author_name } + it 'contains generic commit status build' do + page.within('.table-holder') do + expect(page).to have_content "##{status.id}" # build id + expect(page).to have_content 'generic' # build name + end + end + end end - context 'Download artifacts' do + context 'commit status is Ci Build' do + let!(:build) { FactoryGirl.create :ci_build, commit: commit } let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } - before do - build.update_attributes(artifacts_file: artifacts_file) - end + context 'when logged as developer' do + before do + project.team << [@user, :developer] + end - it do - visit ci_status_path(commit) - click_on 'Download artifacts' - expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) - end - end + describe 'Project commits' do + before do + visit namespace_project_commits_path(project.namespace, project, :master) + end - describe 'Cancel all builds' do - it 'cancels commit' do - visit ci_status_path(commit) - click_on 'Cancel running' - expect(page).to have_content 'canceled' - end - end + it 'should show build status' do + page.within("//li[@id='commit-#{commit.short_sha}']") do + expect(page).to have_css(".ci-status-link") + end + end + end + + describe 'Commit builds' do + before do + visit ci_status_path(commit) + end + + it { expect(page).to have_content commit.sha[0..7] } + it { expect(page).to have_content commit.git_commit_message } + it { expect(page).to have_content commit.git_author_name } + end - describe 'Cancel build' do - it 'cancels build' do - visit ci_status_path(commit) - click_on 'Cancel' - expect(page).to have_content 'canceled' + context 'Download artifacts' do + before do + build.update_attributes(artifacts_file: artifacts_file) + end + + it do + visit ci_status_path(commit) + click_on 'Download artifacts' + expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) + end + end + + describe 'Cancel all builds' do + it 'cancels commit' do + visit ci_status_path(commit) + click_on 'Cancel running' + expect(page).to have_content 'canceled' + end + end + + describe 'Cancel build' do + it 'cancels build' do + visit ci_status_path(commit) + click_on 'Cancel' + expect(page).to have_content 'canceled' + end + end + + describe '.gitlab-ci.yml not found warning' do + context 'ci builds enabled' do + it "does not show warning" do + visit ci_status_path(commit) + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + + it 'shows warning' do + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + expect(page).to have_content '.gitlab-ci.yml not found in this commit' + end + end + + context 'ci builds disabled' do + before do + stub_ci_builds_disabled + stub_ci_commit_yaml_file(nil) + visit ci_status_path(commit) + end + + it 'does not show warning' do + expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + end + end + end end - end - describe '.gitlab-ci.yml not found warning' do - context 'ci builds enabled' do - it "does not show warning" do + context "when logged as reporter" do + before do + project.team << [@user, :reporter] + build.update_attributes(artifacts_file: artifacts_file) visit ci_status_path(commit) - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' end - it 'shows warning' do - stub_ci_commit_yaml_file(nil) - visit ci_status_path(commit) - expect(page).to have_content '.gitlab-ci.yml not found in this commit' + it do + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') end end - context 'ci builds disabled' do + context 'when accessing internal project with disallowed access' do before do - stub_ci_builds_disabled - stub_ci_commit_yaml_file(nil) + project.update( + visibility_level: Gitlab::VisibilityLevel::INTERNAL, + public_builds: false) + build.update_attributes(artifacts_file: artifacts_file) visit ci_status_path(commit) end - it 'does not show warning' do - expect(page).not_to have_content '.gitlab-ci.yml not found in this commit' + it do + expect(page).to have_content commit.sha[0..7] + expect(page).to have_content commit.git_commit_message + expect(page).to have_content commit.git_author_name + expect(page).to_not have_link('Download artifacts') + expect(page).to_not have_link('Cancel running') + expect(page).to_not have_link('Retry failed') end end end diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb index 38c8d343ce3..f6e33f651c4 100644 --- a/spec/features/issues/filter_by_milestone_spec.rb +++ b/spec/features/issues/filter_by_milestone_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Issue filtering by Milestone', feature: true do - include Select2Helper - let(:project) { create(:project, :public) } let(:milestone) { create(:milestone, project: project) } @@ -13,7 +11,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(Milestone::None.title) - expect(page).to have_css('.title', count: 1) + expect(page).to have_css('.issue .title', count: 1) end scenario 'filters by a specific Milestone', js: true do @@ -23,7 +21,7 @@ feature 'Issue filtering by Milestone', feature: true do visit_issues(project) filter_by_milestone(milestone.title) - expect(page).to have_css('.title', count: 1) + expect(page).to have_css('.issue .title', count: 1) end def visit_issues(project) @@ -31,6 +29,9 @@ feature 'Issue filtering by Milestone', feature: true do end def filter_by_milestone(title) - select2(title, from: '#milestone_title') + find(".js-milestone-select").click + sleep 0.5 + find(".milestone-filter a", text: title).click + sleep 1 end end diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb new file mode 100644 index 00000000000..9219b767547 --- /dev/null +++ b/spec/features/issues/new_branch_button_spec.rb @@ -0,0 +1,49 @@ +require 'rails_helper' + +feature 'Start new branch from an issue', feature: true do + let!(:project) { create(:project) } + let!(:issue) { create(:issue, project: project) } + let!(:user) { create(:user)} + + context "for team members" do + before do + project.team << [user, :master] + login_as(user) + end + + it 'shown the new branch button', js: false do + visit namespace_project_issue_path(project.namespace, project, issue) + + expect(page).to have_link "New Branch" + end + + context "when there is a referenced merge request" do + let(:note) do + create(:note, :on_issue, :system, project: project, + note: "mentioned in !#{referenced_mr.iid}") + end + let(:referenced_mr) do + create(:merge_request, :simple, source_project: project, target_project: project, + description: "Fixes ##{issue.iid}", author: user) + end + + before do + issue.notes << note + + visit namespace_project_issue_path(project.namespace, project, issue) + end + + it "hides the new branch button", js: true do + expect(page).not_to have_link "New Branch" + expect(page).to have_content /1 Related Merge Request/ + end + end + end + + context "for visiters" do + it 'no button is shown', js: false do + visit namespace_project_issue_path(project.namespace, project, issue) + expect(page).not_to have_link "New Branch" + end + end +end diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb index a2fb3e4c75d..e844e681ebf 100644 --- a/spec/features/issues_spec.rb +++ b/spec/features/issues_spec.rb @@ -127,15 +127,15 @@ describe 'Issues', feature: true do it 'sorts by newest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_recently_created) - expect(first_issue).to include('foo') - expect(last_issue).to include('baz') + expect(first_issue).to include('baz') + expect(last_issue).to include('foo') end it 'sorts by oldest' do visit namespace_project_issues_path(project.namespace, project, sort: sort_value_oldest_created) - expect(first_issue).to include('baz') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('baz') end it 'sorts by most recently updated' do @@ -190,8 +190,8 @@ describe 'Issues', feature: true do sort: sort_value_oldest_created, assignee_id: user2.id) - expect(first_issue).to include('bar') - expect(last_issue).to include('foo') + expect(first_issue).to include('foo') + expect(last_issue).to include('bar') expect(page).not_to have_content 'baz' end end diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb index 2451e56fe7c..4433ef2d6f1 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/login_spec.rb @@ -1,6 +1,32 @@ require 'spec_helper' feature 'Login', feature: true do + describe 'initial login after setup' do + it 'allows the initial admin to create a password' do + # This behavior is dependent on there only being one user + User.delete_all + + user = create(:admin, password_automatically_set: true) + + visit root_path + expect(current_path).to eq edit_user_password_path + expect(page).to have_content('Please create a password for your new account.') + + fill_in 'user_password', with: 'password' + fill_in 'user_password_confirmation', with: 'password' + click_button 'Change your password' + + expect(current_path).to eq new_user_session_path + expect(page).to have_content(I18n.t('devise.passwords.updated_not_active')) + + fill_in 'user_login', with: user.username + fill_in 'user_password', with: 'password' + click_button 'Sign in' + + expect(current_path).to eq root_path + end + end + describe 'with two-factor authentication' do context 'with valid username/password' do let(:user) { create(:user, :two_factor) } @@ -112,10 +138,10 @@ feature 'Login', feature: true do context 'within the grace period' do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account until') + expect(page).to have_content('You must enable Two-factor Authentication for your account before') end - it 'two-factor configuration is skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path click_link 'Configure it later' @@ -128,10 +154,10 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end - it 'two-factor configuration is not skippable' do + it 'disallows skipping two-factor configuration' do expect(current_path).to eq new_profile_two_factor_auth_path expect(page).not_to have_link('Configure it later') end @@ -146,7 +172,7 @@ feature 'Login', feature: true do it 'redirects to two-factor configuration page' do expect(current_path).to eq new_profile_two_factor_auth_path - expect(page).to have_content('You must configure Two-Factor Authentication in your account.') + expect(page).to have_content('You must enable Two-factor Authentication for your account.') end end end diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb index fdd8cf07b12..12fd8d37210 100644 --- a/spec/features/markdown_spec.rb +++ b/spec/features/markdown_spec.rb @@ -175,13 +175,15 @@ describe 'GitLab Markdown', feature: true do end end - context 'default pipeline' do - before(:all) do - @feat = MarkdownFeature.new + before(:all) do + @feat = MarkdownFeature.new - # `markdown` helper expects a `@project` variable - @project = @feat.project + # `markdown` helper expects a `@project` variable + @project = @feat.project + end + context 'default pipeline' do + before(:all) do @html = markdown(@feat.raw_markdown) end @@ -212,6 +214,7 @@ describe 'GitLab Markdown', feature: true do expect(doc).to reference_commit_ranges expect(doc).to reference_commits expect(doc).to reference_labels + expect(doc).to reference_milestones end end @@ -220,6 +223,57 @@ describe 'GitLab Markdown', feature: true do end end + context 'wiki pipeline' do + before do + @project_wiki = @feat.project_wiki + + file = Gollum::File.new(@project_wiki.wiki) + expect(file).to receive(:path).and_return('images/example.jpg') + expect(@project_wiki).to receive(:find_file).with('images/example.jpg').and_return(file) + + @html = markdown(@feat.raw_markdown, { pipeline: :wiki, project_wiki: @project_wiki }) + end + + it_behaves_like 'all pipelines' + + it 'includes RelativeLinkFilter' do + expect(doc).not_to parse_relative_links + end + + it 'includes EmojiFilter' do + expect(doc).to parse_emoji + end + + it 'includes TableOfContentsFilter' do + expect(doc).to create_header_links + end + + it 'includes AutolinkFilter' do + expect(doc).to create_autolinks + end + + it 'includes all reference filters' do + aggregate_failures do + expect(doc).to reference_users + expect(doc).to reference_issues + expect(doc).to reference_merge_requests + expect(doc).to reference_snippets + expect(doc).to reference_commit_ranges + expect(doc).to reference_commits + expect(doc).to reference_labels + expect(doc).to reference_milestones + end + end + + it 'includes TaskListFilter' do + expect(doc).to parse_task_lists + end + + it 'includes GollumTagsFilter' do + expect(doc).to parse_gollum_tags + end + end + # Fake a `current_user` helper def current_user @feat.user diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb index f70214e1122..b76e4c74c79 100644 --- a/spec/features/merge_requests/filter_by_milestone_spec.rb +++ b/spec/features/merge_requests/filter_by_milestone_spec.rb @@ -1,8 +1,6 @@ require 'rails_helper' feature 'Merge Request filtering by Milestone', feature: true do - include Select2Helper - let(:project) { create(:project, :public) } let(:milestone) { create(:milestone, project: project) } @@ -31,6 +29,7 @@ feature 'Merge Request filtering by Milestone', feature: true do end def filter_by_milestone(title) - select2(title, from: '#milestone_title') + find(".js-milestone-select").click + find(".milestone-filter a", text: title).click end end diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb index f0fc6916c4d..d9a8058efd9 100644 --- a/spec/features/notes_on_merge_requests_spec.rb +++ b/spec/features/notes_on_merge_requests_spec.rb @@ -22,7 +22,7 @@ describe 'Comments', feature: true do it 'should be valid' do is_expected.to have_css('.js-main-target-form', visible: true, count: 1) expect(find('.js-main-target-form input[type=submit]').value). - to eq('Add Comment') + to eq('Comment') page.within('.js-main-target-form') do expect(page).not_to have_link('Cancel') end @@ -49,7 +49,7 @@ describe 'Comments', feature: true do page.within('.js-main-target-form') do fill_in 'note[note]', with: 'This is awsome!' find('.js-md-preview-button').click - click_button 'Add Comment' + click_button 'Comment' end end @@ -167,7 +167,7 @@ describe 'Comments', feature: true do end it 'should be removed when canceled' do - page.within(".diff-file form[rel$='#{line_code}']") do + page.within(".diff-file form[id$='#{line_code}']") do find('.js-close-discussion-note-form').trigger('click') end @@ -202,7 +202,7 @@ describe 'Comments', feature: true do before do page.within("tr[id='#{line_code_2}'] + .js-temp-notes-holder") do fill_in 'note[note]', with: 'Another comment on line 10' - click_button('Add Comment') + click_button('Comment') end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index 74b148f5d17..ed97b6cb577 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -80,8 +80,29 @@ feature 'Project', feature: true do visit namespace_project_path(project.namespace, project) end - it { expect(page).to have_content('You have Master access to this project.') } - it { expect(page).to have_link('Leave this project') } + it 'click project-settings and find leave project' do + find('#project-settings-button').click + expect(page).to have_link('Leave Project') + end + end + + describe 'project title' do + include WaitForAjax + + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + + before do + login_with(user) + project.team.add_user(user, Gitlab::Access::MASTER) + visit namespace_project_path(project.namespace, project) + end + + it 'click toggle and show dropdown', js: true do + find('.js-projects-dropdown-toggle').click + wait_for_ajax + expect(page).to have_css('.select2-results li', count: 1) + end end def remove_with_confirm(button_text, confirm_with) diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index d97831aae14..e8886e7edf9 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -17,10 +17,10 @@ describe "Runners" do @project3 = FactoryGirl.create :empty_project @project3.team << [user, :developer] - @shared_runner = FactoryGirl.create :ci_shared_runner - @specific_runner = FactoryGirl.create :ci_specific_runner - @specific_runner2 = FactoryGirl.create :ci_specific_runner - @specific_runner3 = FactoryGirl.create :ci_specific_runner + @shared_runner = FactoryGirl.create :ci_runner, :shared + @specific_runner = FactoryGirl.create :ci_runner + @specific_runner2 = FactoryGirl.create :ci_runner + @specific_runner3 = FactoryGirl.create :ci_runner @project.runners << @specific_runner @project2.runners << @specific_runner2 @project3.runners << @specific_runner3 @@ -84,7 +84,7 @@ describe "Runners" do before do @project = FactoryGirl.create :empty_project @project.team << [user, :master] - @specific_runner = FactoryGirl.create :ci_specific_runner + @specific_runner = FactoryGirl.create :ci_runner @project.runners << @specific_runner end diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb index 57563add74c..f88c591d897 100644 --- a/spec/features/security/project/internal_access_spec.rb +++ b/spec/features/security/project/internal_access_spec.rb @@ -8,10 +8,12 @@ describe "Internal Project Access", feature: true do let(:master) { create(:user) } let(:guest) { create(:user) } let(:reporter) { create(:user) } + let(:external_team_member) { create(:user, external: true) } before do # full access project.team << [master, :master] + project.team << [external_team_member, :master] # readonly project.team << [reporter, :reporter] @@ -34,6 +36,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -45,6 +49,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -56,6 +62,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -67,6 +75,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -78,6 +88,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -89,22 +101,23 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/blob" do - before do - commit = project.repository.commit - path = '.gitignore' - @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) - end + let(:commit) { project.repository.commit } + subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } - it { expect(@blob_path).to be_allowed_for master } - it { expect(@blob_path).to be_allowed_for reporter } - it { expect(@blob_path).to be_allowed_for :admin } - it { expect(@blob_path).to be_allowed_for guest } - it { expect(@blob_path).to be_allowed_for :user } - it { expect(@blob_path).to be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/edit" do @@ -115,6 +128,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -126,6 +141,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -137,6 +154,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -149,6 +168,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -160,6 +181,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -171,6 +194,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -182,6 +207,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -193,6 +220,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -209,6 +238,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -225,6 +256,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -236,6 +269,8 @@ describe "Internal Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb index a1e111c6cab..19f287ce7a4 100644 --- a/spec/features/security/project/private_access_spec.rb +++ b/spec/features/security/project/private_access_spec.rb @@ -8,10 +8,12 @@ describe "Private Project Access", feature: true do let(:master) { create(:user) } let(:guest) { create(:user) } let(:reporter) { create(:user) } + let(:external_team_member) { create(:user, external: true) } before do # full access project.team << [master, :master] + project.team << [external_team_member, :master] # readonly project.team << [reporter, :reporter] @@ -34,6 +36,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -45,6 +49,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -56,6 +62,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -67,6 +75,7 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -78,6 +87,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -89,22 +100,23 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/blob" do - before do - commit = project.repository.commit - path = '.gitignore' - @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) - end + let(:commit) { project.repository.commit } + subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore'))} - it { expect(@blob_path).to be_allowed_for master } - it { expect(@blob_path).to be_allowed_for reporter } - it { expect(@blob_path).to be_allowed_for :admin } - it { expect(@blob_path).to be_denied_for guest } - it { expect(@blob_path).to be_denied_for :user } - it { expect(@blob_path).to be_denied_for :visitor } + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } + it { is_expected.to be_denied_for :visitor } end describe "GET /:project_path/edit" do @@ -115,6 +127,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -126,6 +140,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -137,6 +153,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -149,6 +167,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -160,6 +180,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -171,6 +193,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -187,6 +211,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -203,6 +229,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end @@ -214,6 +242,8 @@ describe "Private Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_allowed_for external_team_member } it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb index 655d2c8b7d9..4e135076367 100644 --- a/spec/features/security/project/public_access_spec.rb +++ b/spec/features/security/project/public_access_spec.rb @@ -38,6 +38,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -49,6 +50,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -60,6 +62,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -71,6 +74,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -82,6 +86,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -93,22 +98,79 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end - describe "GET /:project_path/blob" do - before do - commit = project.repository.commit - path = '.gitignore' - @blob_path = namespace_project_blob_path(project.namespace, project, File.join(commit.id, path)) + describe "GET /:project_path/builds" do + subject { namespace_project_builds_path(project.namespace, project) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } + end + + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + end + + describe "GET /:project_path/builds/:id" do + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit) } + subject { namespace_project_build_path(project.namespace, project, build.id) } + + context "when allowed for public" do + before { project.update(public_builds: true) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } + it { is_expected.to be_allowed_for :visitor } end - it { expect(@blob_path).to be_allowed_for master } - it { expect(@blob_path).to be_allowed_for reporter } - it { expect(@blob_path).to be_allowed_for :admin } - it { expect(@blob_path).to be_allowed_for guest } - it { expect(@blob_path).to be_allowed_for :user } - it { expect(@blob_path).to be_allowed_for :visitor } + context "when disallowed for public" do + before { project.update(public_builds: false) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_denied_for guest } + it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } + it { is_expected.to be_denied_for :visitor } + end + end + + describe "GET /:project_path/blob" do + let(:commit) { project.repository.commit } + + subject { namespace_project_blob_path(project.namespace, project, File.join(commit.id, '.gitignore')) } + + it { is_expected.to be_allowed_for master } + it { is_expected.to be_allowed_for reporter } + it { is_expected.to be_allowed_for :admin } + it { is_expected.to be_allowed_for guest } + it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :visitor } end describe "GET /:project_path/edit" do @@ -119,6 +181,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end @@ -130,6 +193,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end @@ -141,6 +205,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -153,6 +218,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end @@ -164,6 +230,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -175,6 +242,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end @@ -186,6 +254,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -197,6 +266,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end @@ -213,6 +283,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -229,6 +300,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_allowed_for guest } it { is_expected.to be_allowed_for :user } + it { is_expected.to be_allowed_for :external } it { is_expected.to be_allowed_for :visitor } end @@ -240,6 +312,7 @@ describe "Public Project Access", feature: true do it { is_expected.to be_allowed_for :admin } it { is_expected.to be_denied_for guest } it { is_expected.to be_denied_for :user } + it { is_expected.to be_denied_for :external } it { is_expected.to be_denied_for :visitor } end end diff --git a/spec/finders/groups_finder_spec.rb b/spec/finders/groups_finder_spec.rb deleted file mode 100644 index 4f6a000822e..00000000000 --- a/spec/finders/groups_finder_spec.rb +++ /dev/null @@ -1,48 +0,0 @@ -require 'spec_helper' - -describe GroupsFinder do - describe '#execute' do - let(:user) { create(:user) } - - let(:group1) { create(:group) } - let(:group2) { create(:group) } - let(:group3) { create(:group) } - let(:group4) { create(:group, public: true) } - - let!(:public_project) { create(:project, :public, group: group1) } - let!(:internal_project) { create(:project, :internal, group: group2) } - let!(:private_project) { create(:project, :private, group: group3) } - - let(:finder) { described_class.new } - - describe 'with a user' do - subject { finder.execute(user) } - - describe 'when the user is not a member of any groups' do - it { is_expected.to eq([group4, group2, group1]) } - end - - describe 'when the user is a member of a group' do - before do - group3.add_user(user, Gitlab::Access::DEVELOPER) - end - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - - describe 'when the user is a member of a private project' do - before do - private_project.team.add_user(user, Gitlab::Access::DEVELOPER) - end - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - end - - describe 'without a user' do - subject { finder.execute } - - it { is_expected.to eq([group4, group1]) } - end - end -end diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb deleted file mode 100644 index 2d9068cc720..00000000000 --- a/spec/finders/joined_groups_finder_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'spec_helper' - -describe JoinedGroupsFinder do - describe '#execute' do - let(:source_user) { create(:user) } - let(:current_user) { create(:user) } - - let(:group1) { create(:group) } - let(:group2) { create(:group) } - let(:group3) { create(:group) } - let(:group4) { create(:group, public: true) } - - let!(:public_project) { create(:project, :public, group: group1) } - let!(:internal_project) { create(:project, :internal, group: group2) } - let!(:private_project) { create(:project, :private, group: group3) } - - let(:finder) { described_class.new(source_user) } - - before do - [group1, group2, group3, group4].each do |group| - group.add_user(source_user, Gitlab::Access::MASTER) - end - end - - describe 'with a current user' do - describe 'when the current user has access to the projects of the source user' do - before do - private_project.team.add_user(current_user, Gitlab::Access::DEVELOPER) - end - - subject { finder.execute(current_user) } - - it { is_expected.to eq([group4, group3, group2, group1]) } - end - - describe 'when the current user does not have access to the projects of the source user' do - subject { finder.execute(current_user) } - - it { is_expected.to eq([group4, group2, group1]) } - end - end - - describe 'without a current user' do - subject { finder.execute } - - it { is_expected.to eq([group4, group1]) } - end - end -end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index f32641ef0f6..fae0da9d898 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -17,6 +17,10 @@ describe ProjectsFinder do create(:project, :public, group: group, name: 'C', path: 'C') end + let!(:shared_project) do + create(:project, :private, name: 'D', path: 'D') + end + let(:finder) { described_class.new } describe 'without a group' do @@ -56,7 +60,35 @@ describe ProjectsFinder do describe 'with a user' do subject { finder.execute(user, group: group) } - it { is_expected.to eq([public_project, internal_project]) } + describe 'without shared projects' do + it { is_expected.to eq([public_project, internal_project]) } + end + + describe 'with shared projects and group membership' do + before do + group.add_user(user, Gitlab::Access::DEVELOPER) + + shared_project.project_group_links. + create(group_access: Gitlab::Access::MASTER, group: group) + end + + it do + is_expected.to eq([shared_project, public_project, internal_project]) + end + end + + describe 'with shared projects and project membership' do + before do + shared_project.team.add_user(user, Gitlab::Access::DEVELOPER) + + shared_project.project_group_links. + create(group_access: Gitlab::Access::MASTER, group: group) + end + + it do + is_expected.to eq([shared_project, public_project, internal_project]) + end + end end end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 1b4ffc2d717..7fdc5e5d7aa 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -5,15 +5,14 @@ describe SnippetsFinder do let(:user1) { create :user } let(:group) { create :group } - let(:project1) { create(:empty_project, :public, group: group) } - let(:project2) { create(:empty_project, :private, group: group) } - + let(:project1) { create(:empty_project, :public, group: group) } + let(:project2) { create(:empty_project, :private, group: group) } context ':all filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC) + @snippet1 = create(:personal_snippet, :private) + @snippet2 = create(:personal_snippet, :internal) + @snippet3 = create(:personal_snippet, :public) end it "returns all private and internal snippets" do @@ -31,9 +30,9 @@ describe SnippetsFinder do context ':by_user filter' do before do - @snippet1 = create(:personal_snippet, visibility_level: Snippet::PRIVATE, author: user) - @snippet2 = create(:personal_snippet, visibility_level: Snippet::INTERNAL, author: user) - @snippet3 = create(:personal_snippet, visibility_level: Snippet::PUBLIC, author: user) + @snippet1 = create(:personal_snippet, :private, author: user) + @snippet2 = create(:personal_snippet, :internal, author: user) + @snippet3 = create(:personal_snippet, :public, author: user) end it "returns all public and internal snippets" do @@ -75,9 +74,9 @@ describe SnippetsFinder do context 'by_project filter' do before do - @snippet1 = create(:project_snippet, visibility_level: Snippet::PRIVATE, project: project1) - @snippet2 = create(:project_snippet, visibility_level: Snippet::INTERNAL, project: project1) - @snippet3 = create(:project_snippet, visibility_level: Snippet::PUBLIC, project: project1) + @snippet1 = create(:project_snippet, :private, project: project1) + @snippet2 = create(:project_snippet, :internal, project: project1) + @snippet3 = create(:project_snippet, :public, project: project1) end it "returns public snippets for unauthorized user" do @@ -93,7 +92,7 @@ describe SnippetsFinder do end it "returns all snippets for project members" do - project1.team << [user, :developer] + project1.team << [user, :developer] snippets = SnippetsFinder.new.execute(user, filter: :by_project, project: project1) expect(snippets).to include(@snippet1, @snippet2, @snippet3) end diff --git a/spec/fixtures/ci_build_artifacts.zip b/spec/fixtures/ci_build_artifacts.zip Binary files differnew file mode 100644 index 00000000000..dae976d918e --- /dev/null +++ b/spec/fixtures/ci_build_artifacts.zip diff --git a/spec/fixtures/ci_build_artifacts_metadata.gz b/spec/fixtures/ci_build_artifacts_metadata.gz Binary files differnew file mode 100644 index 00000000000..e6d17e4595d --- /dev/null +++ b/spec/fixtures/ci_build_artifacts_metadata.gz diff --git a/spec/fixtures/logo_sample.svg b/spec/fixtures/logo_sample.svg new file mode 100644 index 00000000000..883e7e6cf92 --- /dev/null +++ b/spec/fixtures/logo_sample.svg @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg width="210px" height="210px" viewBox="0 0 210 210" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns"> + <!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch --> + <title>Slice 1</title> + <desc>Created with Sketch.</desc> + <script>alert('FAIL')</script> + <defs></defs> + <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage"> + <g id="logo" sketch:type="MSLayerGroup" transform="translate(0.000000, 10.000000)"> + <g id="Page-1" sketch:type="MSShapeGroup"> + <g id="Fill-1-+-Group-24"> + <g id="Group-24"> + <g id="Group"> + <path d="M105.0614,193.655 L105.0614,193.655 L143.7014,74.734 L66.4214,74.734 L105.0614,193.655 L105.0614,193.655 Z" id="Fill-4" fill="#E24329" class="tanuki-shape"></path> + <path d="M105.0614,193.6548 L66.4214,74.7338 L12.2684,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-8" fill="#FC6D26" class="tanuki-shape"></path> + <path d="M12.2685,74.7341 L12.2685,74.7341 L0.5265,110.8731 C-0.5445,114.1691 0.6285,117.7801 3.4325,119.8171 L105.0615,193.6551 L12.2685,74.7341 L12.2685,74.7341 Z" id="Fill-12" fill="#FCA326" class="tanuki-shape"></path> + <path d="M12.2685,74.7342 L66.4215,74.7342 L43.1485,3.1092 C41.9515,-0.5768 36.7375,-0.5758 35.5405,3.1092 L12.2685,74.7342 L12.2685,74.7342 Z" id="Fill-16" fill="#E24329" class="tanuki-shape"></path> + <path d="M105.0614,193.6548 L143.7014,74.7338 L197.8544,74.7338 L105.0614,193.6548 L105.0614,193.6548 Z" id="Fill-18" fill="#FC6D26" class="tanuki-shape"></path> + <path d="M197.8544,74.7341 L197.8544,74.7341 L209.5964,110.8731 C210.6674,114.1691 209.4944,117.7801 206.6904,119.8171 L105.0614,193.6551 L197.8544,74.7341 L197.8544,74.7341 Z" id="Fill-20" fill="#FCA326" class="tanuki-shape"></path> + <path d="M197.8544,74.7342 L143.7014,74.7342 L166.9744,3.1092 C168.1714,-0.5768 173.3854,-0.5758 174.5824,3.1092 L197.8544,74.7342 L197.8544,74.7342 Z" id="Fill-22" fill="#E24329" class="tanuki-shape"></path> + </g> + </g> + </g> + </g> + </g> + </g> +</svg> diff --git a/spec/fixtures/mail_room_disabled.yml b/spec/fixtures/mail_room_disabled.yml new file mode 100644 index 00000000000..97f8cff051f --- /dev/null +++ b/spec/fixtures/mail_room_disabled.yml @@ -0,0 +1,11 @@ +test: + incoming_email: + enabled: false + address: "gitlab-incoming+%{key}@gmail.com" + user: "gitlab-incoming@gmail.com" + password: "[REDACTED]" + host: "imap.gmail.com" + port: 993 + ssl: true + start_tls: false + mailbox: "inbox" diff --git a/spec/fixtures/mail_room_enabled.yml b/spec/fixtures/mail_room_enabled.yml new file mode 100644 index 00000000000..9c94649244d --- /dev/null +++ b/spec/fixtures/mail_room_enabled.yml @@ -0,0 +1,11 @@ +test: + incoming_email: + enabled: true + address: "gitlab-incoming+%{key}@gmail.com" + user: "gitlab-incoming@gmail.com" + password: "[REDACTED]" + host: "imap.gmail.com" + port: 993 + ssl: true + start_tls: false + mailbox: "inbox" diff --git a/spec/fixtures/markdown.md.erb b/spec/fixtures/markdown.md.erb index e8dfc5c0eb1..1772cc3f6a4 100644 --- a/spec/fixtures/markdown.md.erb +++ b/spec/fixtures/markdown.md.erb @@ -209,11 +209,18 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - Label by ID: <%= simple_label.to_reference %> - Label by name: <%= Label.reference_prefix %><%= simple_label.name %> -- Label by name in quotes: <%= label.to_reference(:name) %> +- Label by name in quotes: <%= label.to_reference(format: :name) %> - Ignored in code: `<%= simple_label.to_reference %>` - Ignored in links: [Link to <%= simple_label.to_reference %>](#label-link) - Link to label by reference: [Label](<%= label.to_reference %>) +#### MilestoneReferenceFilter + +- Milestone: <%= milestone.to_reference %> +- Milestone in another project: <%= xmilestone.to_reference(project) %> +- Ignored in code: `<%= milestone.to_reference %>` +- Link to milestone by URL: [Milestone](<%= urls.namespace_project_milestone_url(milestone.project.namespace, milestone.project, milestone) %>) + ### Task Lists - [ ] Incomplete task 1 @@ -223,3 +230,12 @@ References should be parseable even inside _<%= merge_request.to_reference %>_ e - [ ] Incomplete sub-task 2 - [x] Complete sub-task 1 - [X] Complete task 2 + +#### Gollum Tags + +- [[linked-resource]] +- [[link-text|linked-resource]] +- [[http://example.com]] +- [[link-text|http://example.com/pdfs/gollum.pdf]] +- [[images/example.jpg]] +- [[http://example.com/images/example.jpg]] diff --git a/spec/fixtures/parallel_diff_result.yml b/spec/fixtures/parallel_diff_result.yml new file mode 100644 index 00000000000..a8b7907d4ba --- /dev/null +++ b/spec/fixtures/parallel_diff_result.yml @@ -0,0 +1,324 @@ +--- +- :left: + :type: match + :number: 6 + :text: "@@ -6,12 +6,18 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :right: + :type: match + :number: 6 + :text: "@@ -6,12 +6,18 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- :left: + :type: + :number: 6 + :text: |2 + <span id="LC6" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 + :right: + :type: + :number: 6 + :text: |2 + <span id="LC6" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6 +- :left: + :type: + :number: 7 + :text: |2 + <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 + :right: + :type: + :number: 7 + :text: |2 + <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7 +- :left: + :type: + :number: 8 + :text: |2 + <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 + :right: + :type: + :number: 8 + :text: |2 + <span id="LC8" class="line"> <span class="k">unless</span> <span class="n">cmd</span><span class="p">.</span><span class="nf">is_a?</span><span class="p">(</span><span class="no">Array</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8 +- :left: + :type: old + :number: 9 + :text: | + -<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9 + :right: + :type: new + :number: 9 + :text: | + +<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9 +- :left: + :type: + :number: 10 + :text: |2 + <span id="LC10" class="line"> <span class="k">end</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 + :right: + :type: + :number: 10 + :text: |2 + <span id="LC10" class="line"> <span class="k">end</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10 +- :left: + :type: + :number: 11 + :text: |2 + <span id="LC11" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 + :right: + :type: + :number: 11 + :text: |2 + <span id="LC11" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11 +- :left: + :type: + :number: 12 + :text: |2 + <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 + :right: + :type: + :number: 12 + :text: |2 + <span id="LC12" class="line"> <span class="n">path</span> <span class="o">||=</span> <span class="no">Dir</span><span class="p">.</span><span class="nf">pwd</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12 +- :left: + :type: old + :number: 13 + :text: | + -<span id="LC13" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span> <span class="p">}</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13 + :right: + :type: old + :number: + :text: '' + :line_code: +- :left: + :type: old + :number: 14 + :text: | + -<span id="LC14" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">chdir: </span><span class="n">path</span> <span class="p">}</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13 + :right: + :type: new + :number: 13 + :text: | + +<span id="LC13" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 + :right: + :type: new + :number: 14 + :text: | + +<span id="LC14" class="line"> <span class="n">vars</span> <span class="o">=</span> <span class="p">{</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 + :right: + :type: new + :number: 15 + :text: | + +<span id="LC15" class="line"> <span class="s2">"PWD"</span> <span class="o">=></span> <span class="n">path</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 + :right: + :type: new + :number: 16 + :text: | + +<span id="LC16" class="line"> <span class="p">}</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 + :right: + :type: new + :number: 17 + :text: | + +<span id="LC17" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 + :right: + :type: new + :number: 18 + :text: | + +<span id="LC18" class="line"> <span class="n">options</span> <span class="o">=</span> <span class="p">{</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 + :right: + :type: new + :number: 19 + :text: | + +<span id="LC19" class="line"> <span class="ss">chdir: </span><span class="n">path</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 + :right: + :type: new + :number: 20 + :text: | + +<span id="LC20" class="line"> <span class="p">}</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20 +- :left: + :type: + :number: 15 + :text: |2 + <span id="LC21" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 + :right: + :type: + :number: 21 + :text: |2 + <span id="LC21" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21 +- :left: + :type: + :number: 16 + :text: |2 + <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 + :right: + :type: + :number: 22 + :text: |2 + <span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22 +- :left: + :type: + :number: 17 + :text: |2 + <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 + :right: + :type: + :number: 23 + :text: |2 + <span id="LC23" class="line"> <span class="no">FileUtils</span><span class="p">.</span><span class="nf">mkdir_p</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23 +- :left: + :type: match + :number: 19 + :text: "@@ -19,6 +25,7 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :right: + :type: match + :number: 25 + :text: "@@ -19,6 +25,7 @@ module Popen" + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- :left: + :type: + :number: 19 + :text: |2 + <span id="LC25" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 + :right: + :type: + :number: 25 + :text: |2 + <span id="LC25" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25 +- :left: + :type: + :number: 20 + :text: |2 + <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 + :right: + :type: + :number: 26 + :text: |2 + <span id="LC26" class="line"> <span class="vi">@cmd_output</span> <span class="o">=</span> <span class="s2">""</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26 +- :left: + :type: + :number: 21 + :text: |2 + <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 + :right: + :type: + :number: 27 + :text: |2 + <span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27 +- :left: + :type: + :number: + :text: '' + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 + :right: + :type: new + :number: 28 + :text: | + +<span id="LC28" class="line"></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28 +- :left: + :type: + :number: 22 + :text: |2 + <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 + :right: + :type: + :number: 29 + :text: |2 + <span id="LC29" class="line"> <span class="no">Open3</span><span class="p">.</span><span class="nf">popen3</span><span class="p">(</span><span class="n">vars</span><span class="p">,</span> <span class="o">*</span><span class="n">cmd</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">stdin</span><span class="p">,</span> <span class="n">stdout</span><span class="p">,</span> <span class="n">stderr</span><span class="p">,</span> <span class="n">wait_thr</span><span class="o">|</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29 +- :left: + :type: + :number: 23 + :text: |2 + <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 + :right: + :type: + :number: 30 + :text: |2 + <span id="LC30" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stdout</span><span class="p">.</span><span class="nf">read</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30 +- :left: + :type: + :number: 24 + :text: |2 + <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 + :right: + :type: + :number: 31 + :text: |2 + <span id="LC31" class="line"> <span class="vi">@cmd_output</span> <span class="o"><<</span> <span class="n">stderr</span><span class="p">.</span><span class="nf">read</span></span> + :line_code: 2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31 diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 68527c3a4f8..f6c1005d265 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -240,7 +240,7 @@ describe ApplicationHelper do describe 'time_ago_with_tooltip' do def element(*arguments) Time.zone = 'UTC' - time = Time.zone.parse('2015-07-02 08:00') + time = Time.zone.parse('2015-07-02 08:23') element = helper.time_ago_with_tooltip(time, *arguments) Nokogiri::HTML::DocumentFragment.parse(element).first_element_child @@ -251,15 +251,15 @@ describe ApplicationHelper do end it 'includes the date string' do - expect(element.text).to eq '2015-07-02 08:00:00 UTC' + expect(element.text).to eq '2015-07-02 08:23:00 UTC' end it 'has a datetime attribute' do - expect(element.attr('datetime')).to eq '2015-07-02T08:00:00Z' + expect(element.attr('datetime')).to eq '2015-07-02T08:23:00Z' end it 'has a formatted title attribute' do - expect(element.attr('title')).to eq 'Jul 02, 2015 8:00am' + expect(element.attr('title')).to eq 'Jul 2, 2015 8:23am' end it 'includes a default js-timeago class' do @@ -285,10 +285,18 @@ describe ApplicationHelper do it 'allows the script tag to be excluded' do expect(element(skip_js: true)).not_to include 'script' end + + it 'converts to Time' do + expect { helper.time_ago_with_tooltip(Date.today) }.not_to raise_error + end end describe 'render_markup' do let(:content) { 'Noël' } + let(:user) { create(:user) } + before do + allow(helper).to receive(:current_user).and_return(user) + end it 'should preserve encoding' do expect(content.encoding.name).to eq('UTF-8') diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index b8bba36439a..87849230dbe 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -1,22 +1,22 @@ require 'spec_helper' describe BlobHelper do - describe 'highlight' do - let(:blob_name) { 'test.lisp' } - let(:no_context_content) { ":type \"assem\"))" } - let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } - let(:split_content) { blob_content.split("\n") } - let(:multiline_content) do - %q( - def test(input): - """This is line 1 of a multi-line comment. - This is line 2. - """ - ) - end + let(:blob_name) { 'test.lisp' } + let(:no_context_content) { ":type \"assem\"))" } + let(:blob_content) { "(make-pathname :defaults name\n#{no_context_content}" } + let(:split_content) { blob_content.split("\n") } + let(:multiline_content) do + %q( + def test(input): + """This is line 1 of a multi-line comment. + This is line 2. + """ + ) + end + describe '#highlight' do it 'should return plaintext for unknown lexer context' do - result = highlight(blob_name, no_context_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, no_context_content, nowrap: true) expect(result).to eq('<span id="LC1" class="line">:type "assem"))</span>') end @@ -24,28 +24,17 @@ describe BlobHelper do expected = %Q[<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span> <span id="LC2" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>] - expect(highlight(blob_name, blob_content, nowrap: true, continue: false)).to eq(expected) - end - - it 'should highlight continued blocks' do - # Both lines have LC1 as ID since formatter doesn't support continue at the moment - expected = [ - '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>', - '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>' - ] - - result = split_content.map{ |content| highlight(blob_name, content, nowrap: true, continue: true) } - expect(result).to eq(expected) + expect(helper.highlight(blob_name, blob_content, nowrap: true)).to eq(expected) end it 'should highlight multi-line comments' do - result = highlight(blob_name, multiline_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, multiline_content, nowrap: true) html = Nokogiri::HTML(result) lines = html.search('.s') expect(lines.count).to eq(3) expect(lines[0].text).to eq('"""This is line 1 of a multi-line comment.') - expect(lines[1].text).to eq(' This is line 2.') - expect(lines[2].text).to eq(' """') + expect(lines[1].text).to eq(' This is line 2.') + expect(lines[2].text).to eq(' """') end context 'diff highlighting' do @@ -59,9 +48,23 @@ describe BlobHelper do end it 'should highlight each line properly' do - result = highlight(blob_name, blob_content, nowrap: true, continue: false) + result = helper.highlight(blob_name, blob_content, nowrap: true) expect(result).to eq(expected) end end end + + describe "#highlighter" do + it 'should highlight continued blocks' do + # Both lines have LC1 as ID since formatter doesn't support continue at the moment + expected = [ + '<span id="LC1" class="line"><span class="p">(</span><span class="nb">make-pathname</span> <span class="ss">:defaults</span> <span class="nv">name</span></span>', + '<span id="LC1" class="line"><span class="ss">:type</span> <span class="s">"assem"</span><span class="p">))</span></span>' + ] + + highlighter = helper.highlighter(blob_name, blob_content, nowrap: true) + result = split_content.map{ |content| highlighter.highlight(content) } + expect(result).to eq(expected) + end + end end diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb index c7c6f45d144..157cc4665a2 100644 --- a/spec/helpers/broadcast_messages_helper_spec.rb +++ b/spec/helpers/broadcast_messages_helper_spec.rb @@ -1,22 +1,60 @@ require 'spec_helper' describe BroadcastMessagesHelper do - describe 'broadcast_styling' do - let(:broadcast_message) { double(color: '', font: '') } + describe 'broadcast_message' do + it 'returns nil when no current message' do + expect(helper.broadcast_message(nil)).to be_nil + end + + it 'includes the current message' do + current = double(message: 'Current Message') + + allow(helper).to receive(:broadcast_message_style).and_return(nil) + + expect(helper.broadcast_message(current)).to include 'Current Message' + end + + it 'includes custom style' do + current = double(message: 'Current Message') + + allow(helper).to receive(:broadcast_message_style).and_return('foo') + + expect(helper.broadcast_message(current)).to include 'style="foo"' + end + end + + describe 'broadcast_message_style' do + it 'defaults to no style' do + broadcast_message = spy + + expect(helper.broadcast_message_style(broadcast_message)).to eq '' + end + + it 'allows custom style' do + broadcast_message = double(color: '#f2dede', font: '#b94a48') + + expect(helper.broadcast_message_style(broadcast_message)). + to match('background-color: #f2dede; color: #b94a48') + end + end + + describe 'broadcast_message_status' do + it 'returns Active' do + message = build(:broadcast_message) + + expect(helper.broadcast_message_status(message)).to eq 'Active' + end + + it 'returns Expired' do + message = build(:broadcast_message, :expired) - context "default style" do - it "should have no style" do - expect(broadcast_styling(broadcast_message)).to eq '' - end + expect(helper.broadcast_message_status(message)).to eq 'Expired' end - context "customized style" do - let(:broadcast_message) { double(color: "#f2dede", font: '#b94a48') } + it 'returns Pending' do + message = build(:broadcast_message, :future) - it "should have a customized style" do - expect(broadcast_styling(broadcast_message)). - to match('background-color: #f2dede; color: #b94a48') - end + expect(helper.broadcast_message_status(message)).to eq 'Pending' end end end diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 7c96a74e581..982c113e84b 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -4,10 +4,12 @@ describe DiffHelper do include RepoHelpers let(:project) { create(:project) } + let(:repository) { project.repository } let(:commit) { project.commit(sample_commit.id) } let(:diffs) { commit.diffs } let(:diff) { diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff) } + let(:diff_refs) { [commit.parent, commit] } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } describe 'diff_hard_limit_enabled?' do it 'should return true if param is provided' do @@ -20,79 +22,14 @@ describe DiffHelper do end end - describe 'allowed_diff_size' do + describe 'diff_options' do it 'should return hard limit for a diff if force diff is true' do allow(controller).to receive(:params) { { force_show_diff: true } } - expect(allowed_diff_size).to eq(1000) + expect(diff_options).to include(Commit.max_diff_options) end it 'should return safe limit for a diff if force diff is false' do - expect(allowed_diff_size).to eq(100) - end - end - - describe 'allowed_diff_lines' do - it 'should return hard limit for number of lines in a diff if force diff is true' do - allow(controller).to receive(:params) { { force_show_diff: true } } - expect(allowed_diff_lines).to eq(50000) - end - - it 'should return safe limit for numbers of lines a diff if force diff is false' do - expect(allowed_diff_lines).to eq(5000) - end - end - - describe 'safe_diff_files' do - it 'should return all files from a commit that is smaller than safe limits' do - expect(safe_diff_files(diffs).length).to eq(2) - end - - it 'should return only the first file if the diff line count in the 2nd file takes the total beyond safe limits' do - allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs).length).to eq(1) - end - - it 'should return all files from a commit that is beyond safe limit for numbers of lines if force diff is true' do - allow(controller).to receive(:params) { { force_show_diff: true } } - allow(diffs[1].diff).to receive(:lines).and_return([""] * 4999) #simulate 4999 lines - expect(safe_diff_files(diffs).length).to eq(2) - end - - it 'should return only the first file if the diff line count in the 2nd file takes the total beyond hard limits' do - allow(controller).to receive(:params) { { force_show_diff: true } } - allow(diffs[1].diff).to receive(:lines).and_return([""] * 49999) #simulate 49999 lines - expect(safe_diff_files(diffs).length).to eq(1) - end - - it 'should return only a safe number of file diffs if a commit touches more files than the safe limits' do - large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs).length).to eq(100) - end - - it 'should return all file diffs if a commit touches more files than the safe limits but force diff is true' do - allow(controller).to receive(:params) { { force_show_diff: true } } - large_diffs = diffs * 100 #simulate 200 diffs - expect(safe_diff_files(large_diffs).length).to eq(200) - end - - it 'should return a limited file diffs if a commit touches more files than the hard limits and force diff is true' do - allow(controller).to receive(:params) { { force_show_diff: true } } - very_large_diffs = diffs * 1000 #simulate 2000 diffs - expect(safe_diff_files(very_large_diffs).length).to eq(1000) - end - end - - describe 'parallel_diff' do - it 'should return an array of arrays containing the parsed diff' do - expect(parallel_diff(diff_file, 0)). - to match_array(parallel_diff_result_array) - end - end - - describe 'generate_line_code' do - it 'should generate correct line code' do - expect(generate_line_code(diff_file.file_path, diff_file.diff_lines.first)). - to eq('2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6') + expect(diff_options).not_to include(:max_lines, :max_files) end end @@ -116,8 +53,7 @@ describe DiffHelper do end end - describe 'diff_line_content' do - + describe '#diff_line_content' do it 'should return non breaking space when line is empty' do expect(diff_line_content(nil)).to eq(' ') end @@ -126,39 +62,21 @@ describe DiffHelper do expect(diff_line_content(diff_file.diff_lines.first.text)). to eq('@@ -6,12 +6,18 @@ module Popen') expect(diff_line_content(diff_file.diff_lines.first.type)).to eq('match') - expect(diff_line_content(diff_file.diff_lines.first.new_pos)).to eq(6) + expect(diff_file.diff_lines.first.new_pos).to eq(6) end end - def parallel_diff_result_array - [ - ["match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", "match", 6, "@@ -6,12 +6,18 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"], - [nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6", nil, 6, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_6_6"], [nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7", nil, 7, " def popen(cmd, path=nil)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"], - [nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8", nil, 8, " unless cmd.is_a?(Array)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_8_8"], - ["old", 9, "- raise <span class='idiff'></span>"System commands must be given as an array of strings"", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9", "new", 9, "+ raise <span class='idiff'>RuntimeError, </span>"System commands must be given as an array of strings"", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"], - [nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10", nil, 10, " end", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_10"], - [nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11", nil, 11, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_11_11"], - [nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12", nil, 12, " path ||= Dir.pwd", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_12_12"], - ["old", 13, "- vars = { "PWD" => path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_13_13", "old", nil, " ", nil], - ["old", 14, "- options = { chdir: path }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_13", "new", 13, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_13"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14", "new", 14, "+ vars = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_14"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15", "new", 15, "+ "PWD" => path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16", "new", 16, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_16"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17", "new", 17, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_17"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18", "new", 18, "+ options = {", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_18"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19", "new", 19, "+ chdir: path", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_19"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20", "new", 20, "+ }", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_20"], - [nil, 15, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21", nil, 21, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_21"], - [nil, 16, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22", nil, 22, " unless File.directory?(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_16_22"], - [nil, 17, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23", nil, 23, " FileUtils.mkdir_p(path)", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_17_23"], - ["match", 19, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", "match", 25, "@@ -19,6 +25,7 @@ module Popen", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"], - [nil, 19, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25", nil, 25, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_19_25"], - [nil, 20, " @cmd_output = """, "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26", nil, 26, " @cmd_output = """, "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_20_26"], - [nil, 21, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27", nil, 27, " @cmd_status = 0", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_21_27"], - [nil, nil, " ", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28", "new", 28, "+", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_28"], - [nil, 22, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29", nil, 29, " Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_22_29"], - [nil, 23, " @cmd_output << stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30", nil, 30, " @cmd_output << stdout.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_23_30"], - [nil, 24, " @cmd_output << stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31", nil, 31, " @cmd_output << stderr.read", "2f6fcd96b88b36ce98c38da085c795a27d92a3dd_24_31"] - ] + describe "#mark_inline_diffs" do + let(:old_line) { %{abc 'def'} } + let(:new_line) { %{abc "def"} } + + it "returns strings with marked inline diffs" do + marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) + + expect(marked_old_line).to eq("abc <span class='idiff left right'>'def'</span>") + expect(marked_old_line).to be_html_safe + expect(marked_new_line).to eq("abc <span class='idiff left right'>"def"</span>") + expect(marked_new_line).to be_html_safe + end end end diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb index 762ec25c4f5..9adcd916ced 100644 --- a/spec/helpers/gitlab_markdown_helper_spec.rb +++ b/spec/helpers/gitlab_markdown_helper_spec.rb @@ -113,7 +113,7 @@ describe GitlabMarkdownHelper do it 'should replace commit message with emoji to link' do actual = link_to_gfm(':book:Book', '/foo') expect(actual). - to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/emoji/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) + to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>) end end @@ -121,12 +121,13 @@ describe GitlabMarkdownHelper do before do @wiki = double('WikiPage') allow(@wiki).to receive(:content).and_return('wiki content') + helper.instance_variable_set(:@project_wiki, @wiki) end - it "should use GitLab Flavored Markdown for markdown files" do + it "should use Wiki pipeline for markdown files" do allow(@wiki).to receive(:format).and_return(:markdown) - expect(helper).to receive(:markdown).with('wiki content') + expect(helper).to receive(:markdown).with('wiki content', pipeline: :wiki, project_wiki: @wiki) helper.render_wiki_content(@wiki) end diff --git a/spec/helpers/labels_helper_spec.rb b/spec/helpers/labels_helper_spec.rb index 0c8d06b7059..4f129eca183 100644 --- a/spec/helpers/labels_helper_spec.rb +++ b/spec/helpers/labels_helper_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe LabelsHelper do describe 'link_to_label' do let(:project) { create(:empty_project) } - let(:label) { create(:label, project: project) } + let(:label) { create(:label, project: project) } context 'with @project set' do before do @@ -11,34 +11,31 @@ describe LabelsHelper do end it 'uses the instance variable' do - expect(label).not_to receive(:project) - link_to_label(label) + expect(link_to_label(label)).to match %r{<a href="/#{@project.to_reference}/issues\?label_name=#{label.name}">.*</a>} end end context 'without @project set' do it "uses the label's project" do - expect(label).to receive(:project).and_return(project) - link_to_label(label) + expect(link_to_label(label)).to match %r{<a href="/#{label.project.to_reference}/issues\?label_name=#{label.name}">.*</a>} end end - context 'with a named project argument' do - it 'uses the provided project' do - arg = double('project') - expect(arg).to receive(:namespace).and_return('foo') - expect(arg).to receive(:to_param).and_return('foo') + context 'with a project argument' do + let(:another_project) { double('project', namespace: 'foo3', to_param: 'bar3') } - link_to_label(label, project: arg) + it 'links to merge requests page' do + expect(link_to_label(label, project: another_project)).to match %r{<a href="/foo3/bar3/issues\?label_name=#{label.name}">.*</a>} end + end - it 'takes precedence over other types' do - @project = project - expect(@project).not_to receive(:namespace) - expect(label).not_to receive(:project) - - arg = double('project', namespace: 'foo', to_param: 'foo') - link_to_label(label, project: arg) + context 'with a type argument' do + ['issue', :issue, 'merge_request', :merge_request].each do |type| + context "set to #{type}" do + it 'links to correct page' do + expect(link_to_label(label, type: type)).to match %r{<a href="/#{label.project.to_reference}/#{type.to_s.pluralize}\?label_name=#{label.name}">.*</a>} + end + end end end @@ -66,5 +63,10 @@ describe LabelsHelper do it 'uses dark text on light backgrounds' do expect(text_color_for_bg('#EEEEEE')).to eq('#333333') end + + it 'supports RGB triplets' do + expect(text_color_for_bg('#FFF')).to eq '#333333' + expect(text_color_for_bg('#000')).to eq '#FFFFFF' + end end end diff --git a/spec/helpers/page_layout_helper_spec.rb b/spec/helpers/page_layout_helper_spec.rb index fd7107779f6..cf632f594c7 100644 --- a/spec/helpers/page_layout_helper_spec.rb +++ b/spec/helpers/page_layout_helper_spec.rb @@ -2,10 +2,8 @@ require 'rails_helper' describe PageLayoutHelper do describe 'page_description' do - it 'defaults to value returned by page_description_default helper' do - allow(helper).to receive(:page_description_default).and_return('Foo') - - expect(helper.page_description).to eq 'Foo' + it 'defaults to nil' do + expect(helper.page_description).to eq nil end it 'returns the last-pushed description' do @@ -42,58 +40,32 @@ describe PageLayoutHelper do end end - describe 'page_description_default' do - it 'uses Project description when available' do - project = double(description: 'Project Description') - helper.instance_variable_set(:@project, project) - - expect(helper.page_description_default).to eq 'Project Description' - end - - it 'uses brand_title when Project description is nil' do - project = double(description: nil) - helper.instance_variable_set(:@project, project) - - expect(helper).to receive(:brand_title).and_return('Brand Title') - expect(helper.page_description_default).to eq 'Brand Title' - end - - it 'falls back to brand_title' do - allow(helper).to receive(:brand_title).and_return('Brand Title') - - expect(helper.page_description_default).to eq 'Brand Title' - end - end - describe 'page_image' do it 'defaults to the GitLab logo' do expect(helper.page_image).to end_with 'assets/gitlab_logo.png' end - context 'with @project' do - it 'uses Project avatar if available' do - project = double(avatar_url: 'http://example.com/uploads/avatar.png') - helper.instance_variable_set(:@project, project) + %w(project user group).each do |type| + context "with @#{type} assigned" do + it "uses #{type.titlecase} avatar if available" do + object = double(avatar_url: 'http://example.com/uploads/avatar.png') + assign(type, object) - expect(helper.page_image).to eq project.avatar_url - end + expect(helper.page_image).to eq object.avatar_url + end - it 'falls back to the default' do - project = double(avatar_url: nil) - helper.instance_variable_set(:@project, project) + it 'falls back to the default when avatar_url is nil' do + object = double(avatar_url: nil) + assign(type, object) - expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end end - end - - context 'with @user' do - it 'delegates to avatar_icon helper' do - user = double('User') - helper.instance_variable_set(:@user, user) - - expect(helper).to receive(:avatar_icon).with(user) - helper.page_image + context "with no assignments" do + it 'falls back to the default' do + expect(helper.page_image).to end_with 'assets/gitlab_logo.png' + end end end end diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb index ebe9c29d91c..601b6915e27 100644 --- a/spec/helpers/search_helper_spec.rb +++ b/spec/helpers/search_helper_spec.rb @@ -42,9 +42,9 @@ describe SearchHelper do expect(search_autocomplete_opts(project.name).size).to eq(1) end - it "includes the public group" do - group = create(:group, public: true) - expect(search_autocomplete_opts(group.name).size).to eq(1) + it "should not include the public group" do + group = create(:group) + expect(search_autocomplete_opts(group.name).size).to eq(0) end context "with a current project" do diff --git a/spec/helpers/visibility_level_helper_spec.rb b/spec/helpers/visibility_level_helper_spec.rb index aafc24397a9..cd7596a763d 100644 --- a/spec/helpers/visibility_level_helper_spec.rb +++ b/spec/helpers/visibility_level_helper_spec.rb @@ -58,7 +58,7 @@ describe VisibilityLevelHelper do describe "skip_level?" do describe "forks" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } let(:fork_project) { create(:forked_project_with_submodules) } before do @@ -74,7 +74,7 @@ describe VisibilityLevelHelper do end describe "non-forked project" do - let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:project) { create(:project, :internal) } it "skips levels" do expect(skip_level?(project, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey @@ -84,7 +84,7 @@ describe VisibilityLevelHelper do end describe "Snippet" do - let(:snippet) { create(:snippet, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + let(:snippet) { create(:snippet, :internal) } it "skips levels" do expect(skip_level?(snippet, Gitlab::VisibilityLevel::PUBLIC)).to be_falsey diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb new file mode 100644 index 00000000000..e58f2c80e95 --- /dev/null +++ b/spec/initializers/settings_spec.rb @@ -0,0 +1,44 @@ +require_relative '../../config/initializers/1_settings' + +describe Settings, lib: true do + + describe '#host_without_www' do + context 'URL with protocol' do + it 'returns the host' do + expect(Settings.host_without_www('http://foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('http://www.foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('http://secure.foo.com')).to eq 'secure.foo.com' + expect(Settings.host_without_www('http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + + expect(Settings.host_without_www('https://foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('https://www.foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('https://secure.foo.com')).to eq 'secure.foo.com' + expect(Settings.host_without_www('https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'secure.gravatar.com' + end + end + + context 'URL without protocol' do + it 'returns the host' do + expect(Settings.host_without_www('foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('www.foo.com')).to eq 'foo.com' + expect(Settings.host_without_www('secure.foo.com')).to eq 'secure.foo.com' + expect(Settings.host_without_www('www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + end + + context 'URL with user/port' do + it 'returns the host' do + expect(Settings.host_without_www('bob:pass@foo.com:8080')).to eq 'foo.com' + expect(Settings.host_without_www('bob:pass@www.foo.com:8080')).to eq 'foo.com' + expect(Settings.host_without_www('bob:pass@secure.foo.com:8080')).to eq 'secure.foo.com' + expect(Settings.host_without_www('bob:pass@www.gravatar.com:8080/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + + expect(Settings.host_without_www('http://bob:pass@foo.com:8080')).to eq 'foo.com' + expect(Settings.host_without_www('http://bob:pass@www.foo.com:8080')).to eq 'foo.com' + expect(Settings.host_without_www('http://bob:pass@secure.foo.com:8080')).to eq 'secure.foo.com' + expect(Settings.host_without_www('http://bob:pass@www.gravatar.com:8080/avatar/%{hash}?s=%{size}&d=identicon')).to eq 'gravatar.com' + end + end + end + end + +end diff --git a/spec/javascripts/behaviors/autosize_spec.js.coffee b/spec/javascripts/behaviors/autosize_spec.js.coffee new file mode 100644 index 00000000000..7fc1d19c35f --- /dev/null +++ b/spec/javascripts/behaviors/autosize_spec.js.coffee @@ -0,0 +1,11 @@ +#= require behaviors/autosize + +describe 'Autosize behavior', -> + beforeEach -> + fixture.set('<textarea class="js-autosize" style="resize: vertical"></textarea>') + + it 'does not overwrite the resize property', -> + load() + expect($('textarea')).toHaveCss(resize: 'vertical') + + load = -> $(document).trigger('page:load') diff --git a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml index b80a28a33ea..e3788bee813 100644 --- a/spec/javascripts/fixtures/behaviors/quick_submit.html.haml +++ b/spec/javascripts/fixtures/behaviors/quick_submit.html.haml @@ -1,6 +1,6 @@ -%form{ action: '/foo' } - %input.js-quick-submit{ type: 'text' } - %textarea.js-quick-submit +%form.js-quick-submit{ action: '/foo' } + %input{ type: 'text' } + %textarea %input{ type: 'submit'} Submit %button.btn{ type: 'submit' } Submit diff --git a/spec/javascripts/fixtures/project_title.html.haml b/spec/javascripts/fixtures/project_title.html.haml new file mode 100644 index 00000000000..e5850b62659 --- /dev/null +++ b/spec/javascripts/fixtures/project_title.html.haml @@ -0,0 +1,7 @@ +%h1.title + %a + GitLab Org + %a.project-item-select-holder{href: "/gitlab-org/gitlab-test"} + GitLab Test + %input#project_path.project-item-select.js-projects-dropdown.ajax-project-select{type: "hidden", name: "project_path", "data-include-groups" => "false"} + %i.fa.chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json new file mode 100644 index 00000000000..84e8d0ba1e4 --- /dev/null +++ b/spec/javascripts/fixtures/projects.json @@ -0,0 +1 @@ +[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}] diff --git a/spec/javascripts/fixtures/zen_mode.html.haml b/spec/javascripts/fixtures/zen_mode.html.haml index e867e4de2b9..1701652c61e 100644 --- a/spec/javascripts/fixtures/zen_mode.html.haml +++ b/spec/javascripts/fixtures/zen_mode.html.haml @@ -1,9 +1,8 @@ .zennable - %input#zen-toggle-comment.zen-toggle-comment{ tabindex: '-1', type: 'checkbox' } .zen-backdrop - %textarea#note_note.js-gfm-input.markdown-area{placeholder: 'Leave a comment'} - %a.zen-enter-link{tabindex: '-1'} + %textarea#note_note.js-gfm-input.markdown-area + %a.js-zen-enter(tabindex="-1" href="#") %i.fa.fa-expand - Edit in fullscreen - %a.zen-leave-link + Edit in fullscreen + %a.js-zen-leave(tabindex="-1" href="#") %i.fa.fa-compress diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee index 7e67c778861..86ba9dd8e96 100644 --- a/spec/javascripts/issue_spec.js.coffee +++ b/spec/javascripts/issue_spec.js.coffee @@ -26,10 +26,10 @@ describe 'reopen/close issue', -> fixture.load('issues_show.html') @issue = new Issue() it 'closes an issue', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://gitlab.com/issues/6/close') - obj.success saved: true + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/close') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -44,12 +44,12 @@ describe 'reopen/close issue', -> expect($('div.status-box-closed')).toBeVisible() expect($('div.status-box-open')).toBeHidden() - it 'fails to closes an issue with success:false', -> + it 'fails to close an issue with success:false', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://goesnowhere.nothing/whereami') - obj.success saved: false + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.success saved: false $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -69,10 +69,10 @@ describe 'reopen/close issue', -> it 'fails to closes an issue with HTTP error', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://goesnowhere.nothing/whereami') - obj.error() + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://goesnowhere.nothing/whereami') + req.error() $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') @@ -91,10 +91,10 @@ describe 'reopen/close issue', -> expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.') it 'reopens an issue', -> - $.ajax = (obj) -> - expect(obj.type).toBe('PUT') - expect(obj.url).toBe('http://gitlab.com/issues/6/reopen') - obj.success saved: true + spyOn(jQuery, 'ajax').and.callFake (req) -> + expect(req.type).toBe('PUT') + expect(req.url).toBe('http://gitlab.com/issues/6/reopen') + req.success saved: true $btnClose = $('a.btn-close') $btnReopen = $('a.btn-reopen') diff --git a/spec/javascripts/project_title_spec.js.coffee b/spec/javascripts/project_title_spec.js.coffee new file mode 100644 index 00000000000..47c7b7febe3 --- /dev/null +++ b/spec/javascripts/project_title_spec.js.coffee @@ -0,0 +1,46 @@ +#= require select2 +#= require api +#= require project_select +#= require project + +window.gon = {} +window.gon.api_version = 'v3' + +describe 'Project Title', -> + fixture.preload('project_title.html') + fixture.preload('projects.json') + + beforeEach -> + fixture.load('project_title.html') + @project = new Project() + + spyOn(@project, 'changeProject').and.callFake (url) -> + window.current_project_url = url + + describe 'project list', -> + beforeEach => + @projects_data = fixture.load('projects.json')[0] + + spyOn(jQuery, 'ajax').and.callFake (req) => + expect(req.url).toBe('/api/v3/projects.json') + d = $.Deferred() + d.resolve @projects_data + d.promise() + + it 'to show on toggle click', => + $('.js-projects-dropdown-toggle').click() + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(true) + expect($('.ajax-project-dropdown li').length).toBe(@projects_data.length) + + it 'hide dropdown', -> + $("#select2-drop-mask").click() + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) + + it 'change project when clicking item', -> + $('.js-projects-dropdown-toggle').click() + $('.ajax-project-dropdown li:nth-child(2)').trigger('mouseup') + + expect($('.title .select2-container').hasClass('select2-dropdown-open')).toBe(false) + expect(window.current_project_url).toBe('http://localhost:3000/h5bp/html5-boilerplate') diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee index 4cb3836755f..b790fce01ed 100644 --- a/spec/javascripts/zen_mode_spec.js.coffee +++ b/spec/javascripts/zen_mode_spec.js.coffee @@ -15,14 +15,6 @@ describe 'ZenMode', -> # Set this manually because we can't actually scroll the window @zen.scroll_position = 456 - # Ohmmmmmmm - enterZen = -> - $('.zen-toggle-comment').prop('checked', true).trigger('change') - - # Wh- what was that?! - exitZen = -> - $('.zen-toggle-comment').prop('checked', false).trigger('change') - describe 'on enter', -> it 'pauses Mousetrap', -> spyOn(Mousetrap, 'pause') @@ -35,16 +27,14 @@ describe 'ZenMode', -> expect('textarea').not.toHaveAttr('style') describe 'in use', -> - beforeEach -> - enterZen() + beforeEach -> enterZen() it 'exits on Escape', -> - $(document).trigger(jQuery.Event('keydown', {keyCode: 27})) - expect($('.zen-toggle-comment').prop('checked')).toBe(false) + escapeKeydown() + expect($('.zen-backdrop')).not.toHaveClass('fullscreen') describe 'on exit', -> - beforeEach -> - enterZen() + beforeEach -> enterZen() it 'unpauses Mousetrap', -> spyOn(Mousetrap, 'unpause') @@ -52,6 +42,10 @@ describe 'ZenMode', -> expect(Mousetrap.unpause).toHaveBeenCalled() it 'restores the scroll position', -> - spyOn(@zen, 'restoreScroll') + spyOn(@zen, 'scrollTo') exitZen() - expect(@zen.restoreScroll).toHaveBeenCalledWith(456) + expect(@zen.scrollTo).toHaveBeenCalled() + +enterZen = -> $('a.js-zen-enter').click() # Ohmmmmmmm +exitZen = -> $('a.js-zen-leave').click() +escapeKeydown = -> $('textarea').trigger($.Event('keydown', {keyCode: 27})) diff --git a/spec/lib/banzai/filter/commit_reference_filter_spec.rb b/spec/lib/banzai/filter/commit_reference_filter_spec.rb index 473534ba68a..63a32d9d455 100644 --- a/spec/lib/banzai/filter/commit_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/commit_reference_filter_spec.rb @@ -21,7 +21,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do let(:reference) { commit.id } # Let's test a variety of commit SHA sizes just to be paranoid - [6, 8, 12, 18, 20, 32, 40].each do |size| + [7, 8, 12, 18, 20, 32, 40].each do |size| it "links to a valid reference of #{size} characters" do doc = reference_filter("See #{reference[0...size]}") @@ -35,7 +35,7 @@ describe Banzai::Filter::CommitReferenceFilter, lib: true do doc = reference_filter("See #{commit.id}") expect(doc.text).to eq "See #{commit.short_id}" - doc = reference_filter("See #{commit.id[0...6]}") + doc = reference_filter("See #{commit.id[0...7]}") expect(doc.text).to eq "See #{commit.short_id}" end diff --git a/spec/lib/banzai/filter/emoji_filter_spec.rb b/spec/lib/banzai/filter/emoji_filter_spec.rb index cf314058158..b5b38cf0c8c 100644 --- a/spec/lib/banzai/filter/emoji_filter_spec.rb +++ b/spec/lib/banzai/filter/emoji_filter_spec.rb @@ -14,7 +14,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'replaces supported emoji' do doc = filter('<p>:heart:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/2764.png' + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/2764.png' end it 'ignores unsupported emoji' do @@ -25,7 +25,7 @@ describe Banzai::Filter::EmojiFilter, lib: true do it 'correctly encodes the URL' do doc = filter('<p>:+1:</p>') - expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/emoji/1F44D.png' + expect(doc.css('img').first.attr('src')).to eq 'https://foo.com/assets/1F44D.png' end it 'matches at the start of a string' do diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb new file mode 100644 index 00000000000..5e23c5c319a --- /dev/null +++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Banzai::Filter::GollumTagsFilter, lib: true do + include FilterSpecHelper + + let(:project) { create(:project) } + let(:user) { double } + let(:project_wiki) { ProjectWiki.new(project, user) } + + describe 'validation' do + it 'ensure that a :project_wiki key exists in context' do + expect { filter("See [[images/image.jpg]]", {}) }.to raise_error ArgumentError, "Missing context keys for Banzai::Filter::GollumTagsFilter: :project_wiki" + end + end + + context 'linking internal images' do + it 'creates img tag if image exists' do + file = Gollum::File.new(project_wiki.wiki) + expect(file).to receive(:path).and_return('images/image.jpg') + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(file) + + tag = '[[images/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg" + end + + it 'does not creates img tag if image does not exist' do + expect(project_wiki).to receive(:find_file).with('images/image.jpg').and_return(nil) + + tag = '[[images/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.css('img').size).to eq 0 + end + end + + context 'linking external images' do + it 'creates img tag for valid URL' do + tag = '[[http://example.com/image.jpg]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg" + end + + it 'does not creates img tag for invalid URL' do + tag = '[[http://example.com/image.pdf]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.css('img').size).to eq 0 + end + end + + context 'linking external resources' do + it "the created link's text will be equal to the resource's text" do + tag = '[[http://example.com]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'http://example.com' + expect(doc.at_css('a')['href']).to eq 'http://example.com' + end + + it "the created link's text will be link-text" do + tag = '[[link-text|http://example.com/pdfs/gollum.pdf]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'link-text' + expect(doc.at_css('a')['href']).to eq 'http://example.com/pdfs/gollum.pdf' + end + end + + context 'linking internal resources' do + it "the created link's text will be equal to the resource's text" do + tag = '[[wiki-slug]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'wiki-slug' + expect(doc.at_css('a')['href']).to eq 'wiki-slug' + end + + it "the created link's text will be link-text" do + tag = '[[link-text|wiki-slug]]' + doc = filter("See #{tag}", project_wiki: project_wiki) + + expect(doc.at_css('a').text).to eq 'link-text' + expect(doc.at_css('a')['href']).to eq 'wiki-slug' + end + end + + context 'table of contents' do + it 'replaces [[<em>TOC</em>]] with ToC result' do + doc = described_class.call("<p>[[<em>TOC</em>]]</p>", { project_wiki: project_wiki }, { toc: "FOO" }) + + expect(doc.to_html).to eq("FOO") + end + + it 'handles an empty ToC result' do + input = "<p>[[<em>TOC</em>]]</p>" + doc = described_class.call(input, project_wiki: project_wiki) + + expect(doc.to_html).to eq '' + end + end +end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index b46ccc47605..e2d21f53b7e 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -111,7 +111,7 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do context 'String-based multi-word references in quotes' do let(:label) { create(:label, name: 'gfm references', project: project) } - let(:reference) { label.to_reference(:name) } + let(:reference) { label.to_reference(format: :name) } it 'links to a valid reference' do doc = reference_filter("See #{reference}") @@ -176,4 +176,29 @@ describe Banzai::Filter::LabelReferenceFilter, lib: true do expect(result[:references][:label]).to eq [label] end end + + describe 'cross project label references' do + let(:another_project) { create(:empty_project, :public) } + let(:project_name) { another_project.name_with_namespace } + let(:label) { create(:label, project: another_project, color: '#00ff00') } + let(:reference) { label.to_reference(project) } + + let!(:result) { reference_filter("See #{reference}") } + + it 'points to referenced project issues page' do + expect(result.css('a').first.attr('href')) + .to eq urls.namespace_project_issues_url(another_project.namespace, + another_project, + label_name: label.name) + end + + it 'has valid color' do + expect(result.css('a span').first.attr('style')) + .to match /background-color: #00ff00/ + end + + it 'contains cross project content' do + expect(result.css('a').first.text).to eq "#{label.name} in #{project_name}" + end + end end diff --git a/spec/lib/banzai/filter/milestone_reference_filter_spec.rb b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb new file mode 100644 index 00000000000..ebf3d7489b5 --- /dev/null +++ b/spec/lib/banzai/filter/milestone_reference_filter_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Banzai::Filter::MilestoneReferenceFilter, lib: true do + include FilterSpecHelper + + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + + it 'requires project context' do + expect { described_class.call('') }.to raise_error(ArgumentError, /:project/) + end + + %w(pre code a style).each do |elem| + it "ignores valid references contained inside '#{elem}' element" do + exp = act = "<#{elem}>milestone #{milestone.to_reference}</#{elem}>" + expect(reference_filter(act).to_html).to eq exp + end + end + + context 'internal reference' do + # Convert the Markdown link to only the URL, since these tests aren't run through the regular Markdown pipeline. + # Milestone reference behavior in the full Markdown pipeline is tested elsewhere. + let(:reference) { milestone.to_reference.gsub(/\[([^\]]+)\]\(([^)]+)\)/, '\2') } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')).to eq urls. + namespace_project_milestone_url(project.namespace, project, milestone) + end + + it 'links with adjacent text' do + doc = reference_filter("milestone (#{reference}.)") + expect(doc.to_html).to match(/\(<a.+>#{Regexp.escape(milestone.title)}<\/a>\.\)/) + end + + it 'includes a title attribute' do + doc = reference_filter("milestone #{reference}") + expect(doc.css('a').first.attr('title')).to eq "Milestone: #{milestone.title}" + end + + it 'escapes the title attribute' do + milestone.update_attribute(:title, %{"></a>whatever<a title="}) + + doc = reference_filter("milestone #{reference}") + expect(doc.text).to eq "milestone #{milestone.title}" + end + + it 'includes default classes' do + doc = reference_filter("milestone #{reference}") + expect(doc.css('a').first.attr('class')).to eq 'gfm gfm-milestone' + end + + it 'includes a data-project attribute' do + doc = reference_filter("milestone #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-project') + expect(link.attr('data-project')).to eq project.id.to_s + end + + it 'includes a data-milestone attribute' do + doc = reference_filter("See #{reference}") + link = doc.css('a').first + + expect(link).to have_attribute('data-milestone') + expect(link.attr('data-milestone')).to eq milestone.id.to_s + end + + it 'adds to the results hash' do + result = reference_pipeline_result("milestone #{reference}") + expect(result[:references][:milestone]).to eq [milestone] + end + end +end diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index e9bb388e361..9acf6304bcb 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -44,8 +44,78 @@ describe Banzai::Filter::RedactorFilter, lib: true do end end - context "for user references" do + context 'with data-issue' do + context 'for confidential issues' do + it 'removes references for non project members' do + non_member = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: non_member) + + expect(doc.css('a').length).to eq 0 + end + + it 'allows references for author' do + author = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project, author: author) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: author) + + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for assignee' do + assignee = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project, assignee: assignee) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: assignee) + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for project members' do + member = create(:user) + project = create(:empty_project, :public) + project.team << [member, :developer] + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: member) + + expect(doc.css('a').length).to eq 1 + end + + it 'allows references for admin' do + admin = create(:admin) + project = create(:empty_project, :public) + issue = create(:issue, :confidential, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: admin) + + expect(doc.css('a').length).to eq 1 + end + end + + it 'allows references for non confidential issues' do + user = create(:user) + project = create(:empty_project, :public) + issue = create(:issue, project: project) + + link = reference_link(project: project.id, issue: issue.id, reference_filter: 'IssueReferenceFilter') + doc = filter(link, current_user: user) + + expect(doc.css('a').length).to eq 1 + end + end + + context "for user references" do context 'with data-group' do it 'removes unpermitted Group references' do user = create(:user) diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb index 0b3e5ecbc9f..0e6685f0ffb 100644 --- a/spec/lib/banzai/filter/relative_link_filter_spec.rb +++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb @@ -92,6 +92,14 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do to eq "/#{project_path}/blob/#{ref}/doc/api/README.md" end + it 'rebuilds relative URL for a file in the repository root' do + relative_link = link('../README.md') + doc = filter(relative_link, requested_path: 'doc/some-file.md') + + expect(doc.at_css('a')['href']). + to eq "/#{project_path}/blob/#{ref}/README.md" + end + it 'rebuilds relative URL for a file in the repo with an anchor' do doc = filter(link('README.md#section')) expect(doc.at_css('a')['href']). diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb index 760d60a4190..27ce312b11c 100644 --- a/spec/lib/banzai/filter/sanitization_filter_spec.rb +++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb @@ -75,6 +75,11 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(filter(act).to_html).to eq exp end + it 'allows `abbr` elements' do + exp = act = %q{<abbr title="HyperText Markup Language">HTML</abbr>} + expect(filter(act).to_html).to eq exp + end + it 'removes `rel` attribute from `a` elements' do act = %q{<a href="#" rel="nofollow">Link</a>} exp = %q{<a href="#">Link</a>} @@ -144,20 +149,54 @@ describe Banzai::Filter::SanitizationFilter, lib: true do output: '<a href="java"></a>' }, + 'protocol-based JS injection: invalid URL char' => { + input: '<img src=java\script:alert("XSS")>', + output: '<img>' + }, + 'protocol-based JS injection: spaces and entities' => { input: '<a href="  javascript:alert(\'XSS\');">foo</a>', output: '<a href="">foo</a>' }, + + 'protocol whitespace' => { + input: '<a href=" http://example.com/"></a>', + output: '<a href="http://example.com/"></a>' + } } protocols.each do |name, data| - it "handles #{name}" do + it "disallows #{name}" do doc = filter(data[:input]) expect(doc.to_html).to eq data[:output] end end + it 'disallows data links' do + input = '<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K">XSS</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>XSS</a>' + end + + it 'disallows vbscript links' do + input = '<a href="vbscript:alert(document.domain)">XSS</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>XSS</a>' + end + + it 'disallows invalid URIs' do + expect(Addressable::URI).to receive(:parse).with('foo://example.com'). + and_raise(Addressable::URI::InvalidURIError) + + input = '<a href="foo://example.com">Foo</a>' + output = filter(input) + + expect(output.to_html).to eq '<a>Foo</a>' + end + it 'allows non-standard anchor schemes' do exp = %q{<a href="irc://irc.freenode.net/git">IRC</a>} act = filter(exp) @@ -172,26 +211,4 @@ describe Banzai::Filter::SanitizationFilter, lib: true do expect(act.to_html).to eq exp end end - - context 'when inline_sanitization is true' do - it 'uses a stricter whitelist' do - doc = filter('<h1>Description</h1>', inline_sanitization: true) - expect(doc.to_html.strip).to eq 'Description' - end - - %w(pre code img ol ul li).each do |elem| - it "removes '#{elem}' elements" do - act = "<#{elem}>Description</#{elem}>" - expect(filter(act, inline_sanitization: true).to_html.strip). - to eq 'Description' - end - end - - %w(b i strong em a ins del sup sub p).each do |elem| - it "still allows '#{elem}' elements" do - exp = act = "<#{elem}>Description</#{elem}>" - expect(filter(act, inline_sanitization: true).to_html).to eq exp - end - end - end end diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb index f2e3a44478d..569cbc885c7 100644 --- a/spec/lib/banzai/filter/task_list_filter_spec.rb +++ b/spec/lib/banzai/filter/task_list_filter_spec.rb @@ -7,4 +7,10 @@ describe Banzai::Filter::TaskListFilter, lib: true do exp = act = %(<ul><li>Item</li></ul>) expect(filter(act).to_html).to eq exp end + + it 'applies `task-list` to single-item task lists' do + act = filter('<ul><li>[ ] Task 1</li></ul>') + + expect(act.to_html).to start_with '<ul class="task-list">' + end end diff --git a/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb new file mode 100644 index 00000000000..fe70eada7eb --- /dev/null +++ b/spec/lib/banzai/filter/yaml_front_matter_filter_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Banzai::Filter::YamlFrontMatterFilter, lib: true do + include FilterSpecHelper + + it 'allows for `encoding:` before the frontmatter' do + content = <<-MD.strip_heredoc + # encoding: UTF-8 + --- + foo: foo + --- + + # Header + + Content + MD + + output = filter(content) + + expect(output).not_to match 'encoding' + end + + it 'converts YAML frontmatter to a fenced code block' do + content = <<-MD.strip_heredoc + --- + bar: :bar_symbol + --- + + # Header + + Content + MD + + output = filter(content) + + aggregate_failures do + expect(output).not_to include '---' + expect(output).to include "```yaml\nbar: :bar_symbol\n```" + end + end + + context 'on content without frontmatter' do + it 'returns the content unmodified' do + content = <<-MD.strip_heredoc + # This is some Markdown + + It has no YAML frontmatter to parse. + MD + + expect(filter(content)).to eq content + end + end +end diff --git a/spec/lib/banzai/filter_array_spec.rb b/spec/lib/banzai/filter_array_spec.rb new file mode 100644 index 00000000000..ea84005e7f8 --- /dev/null +++ b/spec/lib/banzai/filter_array_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Banzai::FilterArray do + describe '#insert_after' do + it 'inserts an element after a provided element' do + filters = described_class.new(%w(a b c)) + + filters.insert_after('b', '1') + + expect(filters).to eq %w(a b 1 c) + end + + it 'inserts an element at the end when the provided element does not exist' do + filters = described_class.new(%w(a b c)) + + filters.insert_after('d', '1') + + expect(filters).to eq %w(a b c 1) + end + end + + describe '#insert_before' do + it 'inserts an element before a provided element' do + filters = described_class.new(%w(a b c)) + + filters.insert_before('b', '1') + + expect(filters).to eq %w(a 1 b c) + end + + it 'inserts an element at the beginning when the provided element does not exist' do + filters = described_class.new(%w(a b c)) + + filters.insert_before('d', '1') + + expect(filters).to eq %w(1 a b c) + end + end +end diff --git a/spec/lib/banzai/pipeline/description_pipeline_spec.rb b/spec/lib/banzai/pipeline/description_pipeline_spec.rb new file mode 100644 index 00000000000..76f42071810 --- /dev/null +++ b/spec/lib/banzai/pipeline/description_pipeline_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe Banzai::Pipeline::DescriptionPipeline do + def parse(html) + # When we pass HTML to Redcarpet, it gets wrapped in `p` tags... + # ...except when we pass it pre-wrapped text. Rabble rabble. + unwrap = !html.start_with?('<p>') + + output = described_class.to_html(html, project: spy) + + output.gsub!(%r{\A<p>(.*)</p>(.*)\z}, '\1\2') if unwrap + + output + end + + it 'uses a limited whitelist' do + doc = parse('# Description') + + expect(doc.strip).to eq 'Description' + end + + %w(pre code img ol ul li).each do |elem| + it "removes '#{elem}' elements" do + act = "<#{elem}>Description</#{elem}>" + + expect(parse(act).strip).to eq 'Description' + end + end + + %w(b i strong em a ins del sup sub p).each do |elem| + it "still allows '#{elem}' elements" do + exp = act = "<#{elem}>Description</#{elem}>" + + expect(parse(act).strip).to eq exp + end + end +end diff --git a/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb new file mode 100644 index 00000000000..3e25406e498 --- /dev/null +++ b/spec/lib/banzai/pipeline/wiki_pipeline_spec.rb @@ -0,0 +1,53 @@ +require 'rails_helper' + +describe Banzai::Pipeline::WikiPipeline do + describe 'TableOfContents' do + it 'replaces the tag with the TableOfContentsFilter result' do + markdown = <<-MD.strip_heredoc + [[_TOC_]] + + ## Header + + Foo + MD + + result = described_class.call(markdown, project: spy, project_wiki: double) + + aggregate_failures do + expect(result[:output].text).not_to include '[[' + expect(result[:output].text).not_to include 'TOC' + expect(result[:output].to_html).to include(result[:toc]) + end + end + + it 'is case-sensitive' do + markdown = <<-MD.strip_heredoc + [[_toc_]] + + # Header 1 + + Foo + MD + + output = described_class.to_html(markdown, project: spy, project_wiki: double) + + expect(output).to include('[[<em>toc</em>]]') + end + + it 'handles an empty pipeline result' do + # No Markdown headers in this doc, so `result[:toc]` will be empty + markdown = <<-MD.strip_heredoc + [[_TOC_]] + + Foo + MD + + output = described_class.to_html(markdown, project: spy, project_wiki: double) + + aggregate_failures do + expect(output).not_to include('<ul>') + expect(output).not_to include('[[<em>TOC</em>]]') + end + end + end +end diff --git a/spec/lib/banzai/querying_spec.rb b/spec/lib/banzai/querying_spec.rb new file mode 100644 index 00000000000..27da2a7439c --- /dev/null +++ b/spec/lib/banzai/querying_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +describe Banzai::Querying do + describe '.css' do + it 'optimizes queries for elements with classes' do + document = double(:document) + + expect(document).to receive(:xpath).with(/^descendant::a/) + + described_class.css(document, 'a.gfm') + end + end +end diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb index d15100fc6d8..fab6412d29f 100644 --- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb +++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb @@ -336,7 +336,7 @@ module Ci describe "Caches" do it "returns cache when defined globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, rspec: { script: "rspec" } @@ -348,13 +348,14 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, + key: 'key', ) end it "returns cache when defined in a job" do config = YAML.dump({ rspec: { - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' }, script: "rspec" } }) @@ -365,15 +366,16 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["logs/", "binaries/"], untracked: true, + key: 'key', ) end it "overwrite cache when defined for a job and globally" do config = YAML.dump({ - cache: { paths: ["logs/", "binaries/"], untracked: true }, + cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' }, rspec: { script: "rspec", - cache: { paths: ["test/"], untracked: false }, + cache: { paths: ["test/"], untracked: false, key: 'local' }, } }) @@ -383,6 +385,7 @@ module Ci expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq( paths: ["test/"], untracked: false, + key: 'local', ) end end @@ -394,7 +397,7 @@ module Ci services: ["mysql"], before_script: ["pwd"], rspec: { - artifacts: { paths: ["logs/", "binaries/"], untracked: true }, + artifacts: { paths: ["logs/", "binaries/"], untracked: true, name: "custom_name" }, script: "rspec" } }) @@ -414,6 +417,7 @@ module Ci image: "ruby:2.1", services: ["mysql"], artifacts: { + name: "custom_name", paths: ["logs/", "binaries/"], untracked: true } @@ -424,6 +428,112 @@ module Ci end end + describe "Dependencies" do + let(:config) do + { + build1: { stage: 'build', script: 'test' }, + build2: { stage: 'build', script: 'test' }, + test1: { stage: 'test', script: 'test', dependencies: dependencies }, + test2: { stage: 'test', script: 'test' }, + deploy: { stage: 'test', script: 'test' } + } + end + + subject { GitlabCiYamlProcessor.new(YAML.dump(config)) } + + context 'no dependencies' do + let(:dependencies) { } + + it { expect { subject }.to_not raise_error } + end + + context 'dependencies to builds' do + let(:dependencies) { [:build1, :build2] } + + it { expect { subject }.to_not raise_error } + end + + context 'undefined dependency' do + let(:dependencies) { [:undefined] } + + it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') } + end + + context 'dependencies to deploy' do + let(:dependencies) { [:deploy] } + + it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') } + end + end + + describe "Hidden jobs" do + let(:config) do + YAML.dump({ + '.hidden_job' => { script: 'test' }, + 'normal_job' => { script: 'test' } + }) + end + + let(:config_processor) { GitlabCiYamlProcessor.new(config) } + + subject { config_processor.builds_for_stage_and_ref("test", "master") } + + it "doesn't create jobs that starts with dot" do + expect(subject.size).to eq(1) + expect(subject.first).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :normal_job, + only: nil, + commands: "\ntest", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + end + end + + describe "YAML Alias/Anchor" do + it "is correctly supported for jobs" do + config = <<EOT +job1: &JOBTMPL + script: execute-script-for-job + +job2: *JOBTMPL +EOT + + config_processor = GitlabCiYamlProcessor.new(config) + + expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(2) + expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :job1, + only: nil, + commands: "\nexecute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + expect(config_processor.builds_for_stage_and_ref("test", "master").second).to eq({ + except: nil, + stage: "test", + stage_idx: 1, + name: :job2, + only: nil, + commands: "\nexecute-script-for-job", + tag_list: [], + options: {}, + when: "on_success", + allow_failure: false + }) + end + end + describe "Error handling" do it "fails to parse YAML" do expect{GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError) @@ -587,6 +697,13 @@ module Ci end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: when parameter should be on_success, on_failure or always") end + it "returns errors if job artifacts:name is not an a string" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { name: 1 } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: artifacts:name parameter should be a string") + end + it "returns errors if job artifacts:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", artifacts: { untracked: "string" } } }) expect do @@ -615,6 +732,20 @@ module Ci end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths parameter should be an array of strings") end + it "returns errors if cache:key is not a string" do + config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key parameter should be a string") + end + + it "returns errors if job cache:key is not an a string" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { key: 1 } } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:key parameter should be a string") + end + it "returns errors if job cache:untracked is not an array of strings" do config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", cache: { untracked: "string" } } }) expect do @@ -628,6 +759,13 @@ module Ci GitlabCiYamlProcessor.new(config) end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: cache:paths parameter should be an array of strings") end + + it "returns errors if job dependencies is not an array of strings" do + config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", dependencies: "string" } }) + expect do + GitlabCiYamlProcessor.new(config) + end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: dependencies parameter should be an array of strings") + end end end end diff --git a/spec/lib/ci/status_spec.rb b/spec/lib/ci/status_spec.rb new file mode 100644 index 00000000000..47f3df6e3ce --- /dev/null +++ b/spec/lib/ci/status_spec.rb @@ -0,0 +1,94 @@ +require 'spec_helper' + +describe Ci::Status do + describe '.get_status' do + subject { described_class.get_status(statuses) } + + shared_examples 'build status summary' do + context 'all successful' do + let(:statuses) { Array.new(2) { create(type, status: :success) } } + it { is_expected.to eq 'success' } + end + + context 'at least one failed' do + let(:statuses) do + [create(type, status: :success), create(type, status: :failed)] + end + + it { is_expected.to eq 'failed' } + end + + context 'at least one running' do + let(:statuses) do + [create(type, status: :success), create(type, status: :running)] + end + + it { is_expected.to eq 'running' } + end + + context 'at least one pending' do + let(:statuses) do + [create(type, status: :success), create(type, status: :pending)] + end + + it { is_expected.to eq 'running' } + end + + context 'success and failed but allowed to fail' do + let(:statuses) do + [create(type, status: :success), + create(type, status: :failed, allow_failure: true)] + end + + it { is_expected.to eq 'success' } + end + + context 'one failed but allowed to fail' do + let(:statuses) { [create(type, status: :failed, allow_failure: true)] } + it { is_expected.to eq 'success' } + end + + context 'success and canceled' do + let(:statuses) do + [create(type, status: :success), create(type, status: :canceled)] + end + it { is_expected.to eq 'failed' } + end + + context 'all canceled' do + let(:statuses) do + [create(type, status: :canceled), create(type, status: :canceled)] + end + it { is_expected.to eq 'canceled' } + end + + context 'success and canceled but allowed to fail' do + let(:statuses) do + [create(type, status: :success), + create(type, status: :canceled, allow_failure: true)] + end + + it { is_expected.to eq 'success' } + end + + context 'one finished and second running but allowed to fail' do + let(:statuses) do + [create(type, status: :success), + create(type, status: :running, allow_failure: true)] + end + + it { is_expected.to eq 'running' } + end + end + + context 'ci build statuses' do + let(:type) { :ci_build } + it_behaves_like 'build status summary' + end + + context 'generic commit statuses' do + let(:type) { :generic_commit_status } + it_behaves_like 'build status summary' + end + end +end diff --git a/spec/lib/gitlab/akismet_helper_spec.rb b/spec/lib/gitlab/akismet_helper_spec.rb new file mode 100644 index 00000000000..9858935180a --- /dev/null +++ b/spec/lib/gitlab/akismet_helper_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe Gitlab::AkismetHelper, type: :helper do + let(:project) { create(:project) } + let(:user) { create(:user) } + + before do + allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url)) + current_application_settings.akismet_enabled = true + current_application_settings.akismet_api_key = '12345' + end + + describe '#check_for_spam?' do + it 'returns true for non-member' do + expect(helper.check_for_spam?(project, user)).to eq(true) + end + + it 'returns false for member' do + project.team << [user, :guest] + expect(helper.check_for_spam?(project, user)).to eq(false) + end + end + + describe '#is_spam?' do + it 'returns true for spam' do + environment = { + 'REMOTE_ADDR' => '127.0.0.1', + 'HTTP_USER_AGENT' => 'Test User Agent' + } + + allow_any_instance_of(::Akismet::Client).to receive(:check).and_return([true, true]) + expect(helper.is_spam?(environment, user, 'Is this spam?')).to eq(true) + end + end +end diff --git a/spec/lib/gitlab/asciidoc_spec.rb b/spec/lib/gitlab/asciidoc_spec.rb index 6beb21c6d2b..736bf787208 100644 --- a/spec/lib/gitlab/asciidoc_spec.rb +++ b/spec/lib/gitlab/asciidoc_spec.rb @@ -42,22 +42,6 @@ module Gitlab end end - context "with project in context" do - - let(:context) { { project: create(:project) } } - - it "should filter converted input via HTML pipeline and return result" do - filtered_html = '<b>ASCII</b>' - - allow(Asciidoctor).to receive(:convert).and_return(html) - expect(Banzai).to receive(:render) - .with(html, context.merge(pipeline: :asciidoc)) - .and_return(filtered_html) - - expect( render('foo', context) ).to eql filtered_html - end - end - def render(*args) described_class.render(*args) end diff --git a/spec/lib/gitlab/bitbucket_import/importer_spec.rb b/spec/lib/gitlab/bitbucket_import/importer_spec.rb new file mode 100644 index 00000000000..c413132abe5 --- /dev/null +++ b/spec/lib/gitlab/bitbucket_import/importer_spec.rb @@ -0,0 +1,88 @@ +require 'spec_helper' + +describe Gitlab::BitbucketImport::Importer, lib: true do + before do + Gitlab.config.omniauth.providers << OpenStruct.new(app_id: "asd123", app_secret: "asd123", name: "bitbucket") + end + + let(:statuses) do + [ + "open", + "resolved", + "on hold", + "invalid", + "duplicate", + "wontfix", + "closed" # undocumented status + ] + end + let(:sample_issues_statuses) do + issues = [] + + statuses.map.with_index do |status, index| + issues << { + local_id: index, + status: status, + title: "Issue #{index}", + content: "Some content to issue #{index}" + } + end + + issues + end + + let(:project_identifier) { 'namespace/repo' } + let(:data) do + { + bb_session: { + bitbucket_access_token: "123456", + bitbucket_access_token_secret: "secret" + } + } + end + let(:project) do + create( + :project, + import_source: project_identifier, + import_data: ProjectImportData.new(data: data) + ) + end + let(:importer) { Gitlab::BitbucketImport::Importer.new(project) } + let(:issues_statuses_sample_data) do + { + count: sample_issues_statuses.count, + issues: sample_issues_statuses + } + end + + context 'issues statuses' 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) + + 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) + + 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" + ).to_return( + status: 200, + body: [{ author_info: { username: "username" }, utc_created_on: index }].to_json + ) + end + end + + it 'map statuses to open or closed' do + importer.execute + + expect(project.issues.where(state: "closed").size).to eq(5) + expect(project.issues.where(state: "opened").size).to eq(2) + end + end +end diff --git a/spec/lib/gitlab/blame_spec.rb b/spec/lib/gitlab/blame_spec.rb new file mode 100644 index 00000000000..89245761b6f --- /dev/null +++ b/spec/lib/gitlab/blame_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe Gitlab::Blame, lib: true do + let(:project) { create(:project) } + let(:path) { 'files/ruby/popen.rb' } + let(:commit) { project.commit('master') } + let(:blob) { project.repository.blob_at(commit.id, path) } + + describe "#groups" do + let(:subject) { described_class.new(blob, commit).groups(highlight: false) } + + it 'groups lines properly' do + expect(subject.count).to eq(18) + expect(subject[0][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(subject[0][:lines]).to eq(["require 'fileutils'", "require 'open3'", ""]) + + expect(subject[1][:commit].sha).to eq('874797c3a73b60d2187ed6e2fcabd289ff75171e') + expect(subject[1][:lines]).to eq(["module Popen", " extend self"]) + + expect(subject[-1][:commit].sha).to eq('913c66a37b4a45b9769037c55c2d238bd0942d2e') + expect(subject[-1][:lines]).to eq([" end", "end"]) + end + end +end diff --git a/spec/lib/gitlab/build_data_builder_spec.rb b/spec/lib/gitlab/build_data_builder_spec.rb index 839b30f1ff4..38be9448794 100644 --- a/spec/lib/gitlab/build_data_builder_spec.rb +++ b/spec/lib/gitlab/build_data_builder_spec.rb @@ -14,6 +14,7 @@ describe 'Gitlab::BuildDataBuilder' do it { expect(data[:tag]).to eq(build.tag) } it { expect(data[:build_id]).to eq(build.id) } it { expect(data[:build_status]).to eq(build.status) } + it { expect(data[:build_allow_failure]).to eq(false) } it { expect(data[:project_id]).to eq(build.project.id) } it { expect(data[:project_name]).to eq(build.project.name_with_namespace) } end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb new file mode 100644 index 00000000000..acca0b08bab --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata/entry_spec.rb @@ -0,0 +1,173 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata::Entry do + let(:entries) do + { 'path/' => {}, + 'path/dir_1/' => {}, + 'path/dir_1/file_1' => { size: 10 }, + 'path/dir_1/file_b' => { size: 10 }, + 'path/dir_1/subdir/' => {}, + 'path/dir_1/subdir/subfile' => { size: 10 }, + 'path/second_dir' => {}, + 'path/second_dir/dir_3/file_2' => { size: 10 }, + 'path/second_dir/dir_3/file_3'=> { size: 10 }, + 'another_directory/'=> {}, + 'another_file' => {}, + '/file/with/absolute_path' => {} } + end + + def path(example) + entry(example.metadata[:path]) + end + + def entry(path) + described_class.new(path, entries) + end + + describe '/file/with/absolute_path', path: '/file/with/absolute_path' do + subject { |example| path(example) } + + it { is_expected.to be_file } + it { is_expected.to have_parent } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'absolute_path' } + end + end + + describe 'path/dir_1/', path: 'path/dir_1/' do + subject { |example| path(example) } + it { is_expected.to have_parent } + it { is_expected.to be_directory } + + describe '#basename' do + subject { |example| path(example).basename } + it { is_expected.to eq 'dir_1/' } + end + + describe '#name' do + subject { |example| path(example).name } + it { is_expected.to eq 'dir_1' } + end + + describe '#parent' do + subject { |example| path(example).parent } + it { is_expected.to eq entry('path/') } + end + + describe '#children' do + subject { |example| path(example).children } + + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b'), + entry('path/dir_1/subdir/') + end + end + + describe '#files' do + subject { |example| path(example).files } + + it { is_expected.to all(be_file) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/file_1'), + entry('path/dir_1/file_b') + end + end + + describe '#directories' do + context 'without options' do + subject { |example| path(example).directories } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it { is_expected.to contain_exactly entry('path/dir_1/subdir/') } + end + + context 'with option parent: true' do + subject { |example| path(example).directories(parent: true) } + + it { is_expected.to all(be_directory) } + it { is_expected.to all(be_an_instance_of described_class) } + it do + is_expected.to contain_exactly entry('path/dir_1/subdir/'), + entry('path/') + end + end + + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 2 } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be true } + end + + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be false } + end + + describe '#total_size' do + subject { |example| path(example).total_size } + it { is_expected.to eq(30) } + end + end + end + + describe 'empty path', path: '' do + subject { |example| path(example) } + it { is_expected.to_not have_parent } + + describe '#children' do + subject { |example| path(example).children } + it { expect(subject.count).to eq 3 } + end + + end + + describe 'path/dir_1/subdir/subfile', path: 'path/dir_1/subdir/subfile' do + describe '#nodes' do + subject { |example| path(example).nodes } + it { is_expected.to eq 4 } + end + end + + describe 'non-existent/', path: 'non-existent/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + + describe '#exists?' do + subject { |example| path(example).exists? } + it { is_expected.to be false } + end + end + + describe 'another_directory/', path: 'another_directory/' do + describe '#empty?' do + subject { |example| path(example).empty? } + it { is_expected.to be true } + end + end + + describe '#metadata' do + let(:entries) do + { 'path/' => { name: '/path/' }, + 'path/file1' => { name: '/path/file1' }, + 'path/file2' => { name: '/path/file2' } } + end + + subject do + described_class.new('path/file1', entries).metadata[:name] + end + + it { is_expected.to eq '/path/file1' } + end +end diff --git a/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb new file mode 100644 index 00000000000..eea01f91879 --- /dev/null +++ b/spec/lib/gitlab/ci/build/artifacts/metadata_spec.rb @@ -0,0 +1,97 @@ +require 'spec_helper' + +describe Gitlab::Ci::Build::Artifacts::Metadata do + def metadata(path = '', **opts) + described_class.new(metadata_file_path, path, **opts) + end + + let(:metadata_file_path) do + Rails.root + 'spec/fixtures/ci_build_artifacts_metadata.gz' + end + + context 'metadata file exists' do + describe '#find_entries! empty string' do + subject { metadata('').find_entries! } + + it 'matches correct paths' do + expect(subject.keys).to contain_exactly 'ci_artifacts.txt', + 'other_artifacts_0.1.2/', + 'rails_sample.jpg', + 'tests_encoding/' + end + + it 'matches metadata for every path' do + expect(subject.keys.count).to eq 4 + end + + it 'return Hashes for each metadata' do + expect(subject.values).to all(be_kind_of(Hash)) + end + end + + describe '#find_entries! other_artifacts_0.1.2/' do + subject { metadata('other_artifacts_0.1.2/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/' + end + end + + describe '#find_entries! other_artifacts_0.1.2/another-subdirectory/' do + subject { metadata('other_artifacts_0.1.2/another-subdirectory/').find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + end + + describe '#find_entries! recursively for other_artifacts_0.1.2/' do + subject { metadata('other_artifacts_0.1.2/', recursive: true).find_entries! } + + it 'matches correct paths' do + expect(subject.keys). + to contain_exactly 'other_artifacts_0.1.2/', + 'other_artifacts_0.1.2/doc_sample.txt', + 'other_artifacts_0.1.2/another-subdirectory/', + 'other_artifacts_0.1.2/another-subdirectory/empty_directory/', + 'other_artifacts_0.1.2/another-subdirectory/banana_sample.gif' + end + end + + describe '#to_entry' do + subject { metadata('').to_entry } + it { is_expected.to be_an_instance_of(Gitlab::Ci::Build::Artifacts::Metadata::Entry) } + end + + describe '#full_version' do + subject { metadata('').full_version } + it { is_expected.to eq 'GitLab Build Artifacts Metadata 0.0.1' } + end + + describe '#version' do + subject { metadata('').version } + it { is_expected.to eq '0.0.1' } + end + + describe '#errors' do + subject { metadata('').errors } + it { is_expected.to eq({}) } + end + end + + context 'metadata file does not exist' do + let(:metadata_file_path) { '' } + + describe '#find_entries!' do + it 'raises error' do + expect { metadata.find_entries! }.to raise_error(Errno::ENOENT) + end + end + end +end diff --git a/spec/lib/gitlab/closing_issue_extractor_spec.rb b/spec/lib/gitlab/closing_issue_extractor_spec.rb index 99288da1e43..844fd79c991 100644 --- a/spec/lib/gitlab/closing_issue_extractor_spec.rb +++ b/spec/lib/gitlab/closing_issue_extractor_spec.rb @@ -11,6 +11,7 @@ describe Gitlab::ClosingIssueExtractor, lib: true do subject { described_class.new(project, project.creator) } before do + project.team << [project.creator, :developer] project2.team << [project.creator, :master] end @@ -135,6 +136,17 @@ describe Gitlab::ClosingIssueExtractor, lib: true do message = "resolve #{reference}" expect(subject.closed_by_message(message)).to eq([issue]) end + + context 'with an external issue tracker reference' do + it 'extracts the referenced issue' do + jira_project = create(:jira_project, name: 'JIRA_EXT1') + jira_issue = ExternalIssue.new("#{jira_project.name}-1", project: jira_project) + closing_issue_extractor = described_class.new jira_project + message = "Resolve #{jira_issue.to_reference}" + + expect(closing_issue_extractor.closed_by_message(message)).to eq([jira_issue]) + end + end end context "with a cross-project reference" do diff --git a/spec/lib/gitlab/database_spec.rb b/spec/lib/gitlab/database_spec.rb index 8461e8ce50d..d0a447753b7 100644 --- a/spec/lib/gitlab/database_spec.rb +++ b/spec/lib/gitlab/database_spec.rb @@ -1,5 +1,9 @@ require 'spec_helper' +class MigrationTest + include Gitlab::Database +end + describe Gitlab::Database, lib: true do # These are just simple smoke tests to check if the methods work (regardless # of what they may return). @@ -14,4 +18,52 @@ describe Gitlab::Database, lib: true do it { is_expected.to satisfy { |val| val == true || val == false } } end + + describe '.version' do + context "on mysql" do + it "extracts the version number" do + allow(described_class).to receive(:database_version). + and_return("5.7.12-standard") + + expect(described_class.version).to eq '5.7.12-standard' + end + end + + context "on postgresql" do + it "extracts the version number" do + allow(described_class).to receive(:database_version). + and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") + + expect(described_class.version).to eq '9.4.4' + end + end + end + + describe '#true_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.true_value).to eq "'t'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.true_value).to eq 1 + end + end + + describe '#false_value' do + it 'returns correct value for PostgreSQL' do + expect(described_class).to receive(:postgresql?).and_return(true) + + expect(MigrationTest.new.false_value).to eq "'f'" + end + + it 'returns correct value for MySQL' do + expect(described_class).to receive(:postgresql?).and_return(false) + + expect(MigrationTest.new.false_value).to eq 0 + end + end end diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index c7cdf8691d6..a0cbef6e6a4 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -6,7 +6,7 @@ describe Gitlab::Diff::File, lib: true do let(:project) { create(:project) } let(:commit) { project.commit(sample_commit.id) } let(:diff) { commit.diffs.first } - let(:diff_file) { Gitlab::Diff::File.new(diff) } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } describe :diff_lines do let(:diff_lines) { diff_file.diff_lines } @@ -18,4 +18,18 @@ describe Gitlab::Diff::File, lib: true do describe :mode_changed? do it { expect(diff_file.mode_changed?).to be_falsey } end + + describe '#too_large?' do + it 'returns true for a file that is too large' do + expect(diff).to receive(:too_large?).and_return(true) + + expect(diff_file.too_large?).to eq(true) + end + + it 'returns false for a file that is small enough' do + expect(diff).to receive(:too_large?).and_return(false) + + expect(diff_file.too_large?).to eq(false) + end + end end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb new file mode 100644 index 00000000000..d19bf4ac84b --- /dev/null +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -0,0 +1,77 @@ +require 'spec_helper' + +describe Gitlab::Diff::Highlight, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:commit) { project.commit(sample_commit.id) } + let(:diff) { commit.diffs.first } + let(:diff_file) { Gitlab::Diff::File.new(diff, [commit.parent, commit]) } + + describe '#highlight' do + context "with a diff file" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file).highlight } + + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end + + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end + + it 'highlights and marks unchanged lines' do + code = %Q{ <span id="LC7" class="line"> <span class="k">def</span> <span class="nf">popen</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="n">path</span><span class="o">=</span><span class="kp">nil</span><span class="p">)</span></span>\n} + + expect(subject[2].text).to eq(code) + end + + it 'highlights and marks removed lines' do + code = %Q{-<span id="LC9" class="line"> <span class="k">raise</span> <span class="s2">"System commands must be given as an array of strings"</span></span>\n} + + expect(subject[4].text).to eq(code) + end + + it 'highlights and marks added lines' do + code = %Q{+<span id="LC9" class="line"> <span class="k">raise</span> <span class="no"><span class='idiff left'>RuntimeError</span></span><span class="p"><span class='idiff'>,</span></span><span class='idiff right'> </span><span class="s2">"System commands must be given as an array of strings"</span></span>\n} + + expect(subject[5].text).to eq(code) + end + end + + context "with diff lines" do + let(:subject) { Gitlab::Diff::Highlight.new(diff_file.diff_lines).highlight } + + it 'should return Gitlab::Diff::Line elements' do + expect(subject.first).to be_an_instance_of(Gitlab::Diff::Line) + end + + it 'should not modify "match" lines' do + expect(subject[0].text).to eq('@@ -6,12 +6,18 @@ module Popen') + expect(subject[22].text).to eq('@@ -19,6 +25,7 @@ module Popen') + end + + it 'marks unchanged lines' do + code = %Q{ def popen(cmd, path=nil)} + + expect(subject[2].text).to eq(code) + expect(subject[2].text).not_to be_html_safe + end + + it 'marks removed lines' do + code = %Q{- raise "System commands must be given as an array of strings"} + + expect(subject[4].text).to eq(code) + expect(subject[4].text).not_to be_html_safe + end + + it 'marks added lines' do + code = %Q{+ raise <span class='idiff left right'>RuntimeError, </span>"System commands must be given as an array of strings"} + + expect(subject[5].text).to eq(code) + expect(subject[5].text).to be_html_safe + end + end + end +end diff --git a/spec/lib/gitlab/diff/inline_diff_marker_spec.rb b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb new file mode 100644 index 00000000000..ea5c31011f0 --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_marker_spec.rb @@ -0,0 +1,29 @@ +require 'spec_helper' + +describe Gitlab::Diff::InlineDiffMarker, lib: true do + describe '#inline_diffs' do + + context "when the rich text is html safe" do + let(:raw) { "abc 'def'" } + let(:rich) { %{<span class="abc">abc</span><span class="space"> </span><span class="def">'def'</span>}.html_safe } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw, rich).mark(inline_diffs) } + + it 'marks the inline diffs' do + expect(subject).to eq(%{<span class="abc">ab<span class='idiff left'>c</span></span><span class="space"><span class='idiff'> </span></span><span class="def"><span class='idiff right'>'d</span>ef'</span>}) + expect(subject).to be_html_safe + end + end + + context "when the text text is not html safe" do + let(:raw) { "abc 'def'" } + let(:inline_diffs) { [2..5] } + let(:subject) { Gitlab::Diff::InlineDiffMarker.new(raw).mark(inline_diffs) } + + it 'marks the inline diffs' do + expect(subject).to eq(%{ab<span class='idiff left right'>c 'd</span>ef'}) + expect(subject).to be_html_safe + end + end + end +end diff --git a/spec/lib/gitlab/diff/inline_diff_spec.rb b/spec/lib/gitlab/diff/inline_diff_spec.rb new file mode 100644 index 00000000000..95a993d26cf --- /dev/null +++ b/spec/lib/gitlab/diff/inline_diff_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Gitlab::Diff::InlineDiff, lib: true do + describe '.for_lines' do + let(:diff) do + <<eos + class Test +- def initialize(test = true) ++ def initialize(test = false) + @test = test + end + end +eos + end + + let(:subject) { described_class.for_lines(diff.lines) } + + it 'finds all inline diffs' do + expect(subject[0]).to be_nil + expect(subject[1]).to eq([25..27]) + expect(subject[2]).to eq([25..28]) + expect(subject[3]).to be_nil + expect(subject[4]).to be_nil + expect(subject[5]).to be_nil + end + end + + describe "#inline_diffs" do + let(:old_line) { "XXX def initialize(test = true)" } + let(:new_line) { "YYY def initialize(test = false)" } + let(:subject) { described_class.new(old_line, new_line, offset: 3).inline_diffs } + + it "finds the inline diff" do + old_diffs, new_diffs = subject + + expect(old_diffs).to eq([26..28]) + expect(new_diffs).to eq([26..29]) + end + end +end diff --git a/spec/lib/gitlab/diff/parallel_diff_spec.rb b/spec/lib/gitlab/diff/parallel_diff_spec.rb new file mode 100644 index 00000000000..1c5bbc47120 --- /dev/null +++ b/spec/lib/gitlab/diff/parallel_diff_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::Diff::ParallelDiff, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:commit) { project.commit(sample_commit.id) } + let(:diffs) { commit.diffs } + let(:diff) { diffs.first } + let(:diff_refs) { [commit.parent, commit] } + let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs) } + subject { described_class.new(diff_file) } + + let(:parallel_diff_result_array) { YAML.load_file("#{Rails.root}/spec/fixtures/parallel_diff_result.yml") } + + describe '#parallelize' do + it 'should return an array of arrays containing the parsed diff' do + expect(subject.parallelize).to match_array(parallel_diff_result_array) + end + end +end diff --git a/spec/lib/gitlab/diff/parser_spec.rb b/spec/lib/gitlab/diff/parser_spec.rb index ba577bd28e5..cdff063a9ed 100644 --- a/spec/lib/gitlab/diff/parser_spec.rb +++ b/spec/lib/gitlab/diff/parser_spec.rb @@ -47,7 +47,7 @@ eos end before do - @lines = parser.parse(diff.lines) + @lines = parser.parse(diff.lines).to_a end it { expect(@lines.size).to eq(30) } @@ -86,8 +86,13 @@ eos it { expect(line.type).to eq(nil) } it { expect(line.old_pos).to eq(24) } it { expect(line.new_pos).to eq(31) } - it { expect(line.text).to eq(' @cmd_output << stderr.read') } + it { expect(line.text).to eq(' @cmd_output << stderr.read') } end end end + + context 'when lines is empty' do + it { expect(parser.parse([])).to eq([]) } + it { expect(parser.parse(nil)).to eq([]) } + end end diff --git a/spec/lib/gitlab/email/message/repository_push_spec.rb b/spec/lib/gitlab/email/message/repository_push_spec.rb index 56ae2a8d121..b2d7a799810 100644 --- a/spec/lib/gitlab/email/message/repository_push_spec.rb +++ b/spec/lib/gitlab/email/message/repository_push_spec.rb @@ -72,7 +72,7 @@ describe Gitlab::Email::Message::RepositoryPush do describe '#compare_timeout' do subject { message.compare_timeout } - it { is_expected.to eq compare.timeout } + it { is_expected.to eq compare.diffs.overflow? } end describe '#reverse_compare?' do diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb index b535413bbd4..abe179cd4af 100644 --- a/spec/lib/gitlab/email/receiver_spec.rb +++ b/spec/lib/gitlab/email/receiver_spec.rb @@ -42,7 +42,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the email was auto generated" do let!(:reply_key) { '636ca428858779856c226bb145ef4fad' } let!(:email_raw) { fixture_file("emails/auto_reply.eml") } - + it "raises an AutoGeneratedEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::AutoGeneratedEmailError) end @@ -90,7 +90,7 @@ describe Gitlab::Email::Receiver, lib: true do context "when the reply is blank" do let!(:email_raw) { fixture_file("emails/no_content_reply.eml") } - + it "raises an EmptyEmailError" do expect { receiver.execute }.to raise_error(Gitlab::Email::Receiver::EmptyEmailError) end @@ -107,13 +107,16 @@ describe Gitlab::Email::Receiver, lib: true do end context "when everything is fine" do + let(:markdown) { "![image](uploads/image.png)" } + before do allow_any_instance_of(Gitlab::Email::AttachmentUploader).to receive(:execute).and_return( [ { url: "uploads/image.png", is_image: true, - alt: "image" + alt: "image", + markdown: markdown } ] ) @@ -132,7 +135,7 @@ describe Gitlab::Email::Receiver, lib: true do note = noteable.notes.last - expect(note.note).to include("![image](uploads/image.png)") + expect(note.note).to include(markdown) end end end diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb new file mode 100644 index 00000000000..fbdb7ea34ac --- /dev/null +++ b/spec/lib/gitlab/exclusive_lease_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::ExclusiveLease do + it 'cannot obtain twice before the lease has expired' do + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600) + expect(lease.try_obtain).to eq(true) + expect(lease.try_obtain).to eq(false) + end + + it 'can obtain after the lease has expired' do + timeout = 1 + lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout) + lease.try_obtain # start the lease + sleep(2 * timeout) # lease should have expired now + expect(lease.try_obtain).to eq(true) + end + + def unique_key + SecureRandom.hex(10) + end +end diff --git a/spec/lib/gitlab/github_import/comment_formatter_spec.rb b/spec/lib/gitlab/github_import/comment_formatter_spec.rb new file mode 100644 index 00000000000..a324a82e69f --- /dev/null +++ b/spec/lib/gitlab/github_import/comment_formatter_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::CommentFormatter, lib: true do + let(:project) { create(:project) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2013-04-10T20:09:31Z') } + let(:updated_at) { DateTime.strptime('2014-03-03T18:58:10Z') } + let(:base_data) do + { + body: "I'm having a problem with this.", + user: octocat, + created_at: created_at, + updated_at: updated_at + } + end + + subject(:comment) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when do not reference a portion of the diff' do + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nI'm having a problem with this.", + commit_id: nil, + line_code: nil, + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when on a portion of the diff' do + let(:diff_data) do + { + body: 'Great stuff', + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + diff_hunk: '@@ -16,33 +16,40 @@ public class Connection : IConnection...', + path: 'file1.txt', + position: 1 + } + end + + let(:raw_data) { OpenStruct.new(base_data.merge(diff_data)) } + + it 'returns formatted attributes' do + expected = { + project: project, + note: "*Created by: octocat*\n\nGreat stuff", + commit_id: '6dcb09b5b57875f334f61aebed695e2e4193db5e', + line_code: 'ce1be0ff4065a6e9415095c95f25f47a633cef2b_0_1', + author_id: project.creator_id, + created_at: created_at, + updated_at: updated_at + } + + expect(comment.attributes).to eq(expected) + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(comment.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(comment.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end +end diff --git a/spec/lib/gitlab/github_import/issue_formatter_spec.rb b/spec/lib/gitlab/github_import/issue_formatter_spec.rb new file mode 100644 index 00000000000..fd05428b322 --- /dev/null +++ b/spec/lib/gitlab/github_import/issue_formatter_spec.rb @@ -0,0 +1,139 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::IssueFormatter, lib: true do + let!(:project) { create(:project, namespace: create(:namespace, path: 'octocat')) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + + let(:base_data) do + { + number: 1347, + state: 'open', + title: 'Found a bug', + body: "I'm having a problem with this.", + assignee: nil, + user: octocat, + comments: 0, + pull_request: nil, + created_at: created_at, + updated_at: updated_at, + closed_at: nil + } + end + + subject(:issue) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when issue is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when issue is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + project: project, + title: 'Found a bug', + description: "*Created by: octocat*\n\nI'm having a problem with this.", + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(issue.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assignee_id when is not a GitLab user' do + expect(issue.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assignee_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(issue.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(issue.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#has_comments?' do + context 'when number of comments is greater than zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 1)) } + + it 'returns true' do + expect(issue.has_comments?).to eq true + end + end + + context 'when number of comments is equal to zero' do + let(:raw_data) { OpenStruct.new(base_data.merge(comments: 0)) } + + it 'returns false' do + expect(issue.has_comments?).to eq false + end + end + end + + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(issue.number).to eq 1347 + end + end + + describe '#valid?' do + context 'when mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: OpenStruct.new)) } + + it 'returns false' do + expect(issue.valid?).to eq false + end + end + + context 'when does not mention a pull request' do + let(:raw_data) { OpenStruct.new(base_data.merge(pull_request: nil)) } + + it 'returns true' do + expect(issue.valid?).to eq true + end + end + end +end diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb new file mode 100644 index 00000000000..e49dcb42342 --- /dev/null +++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb @@ -0,0 +1,185 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::PullRequestFormatter, lib: true do + let(:project) { create(:project) } + let(:repository) { OpenStruct.new(id: 1, fork: false) } + let(:source_repo) { repository } + let(:source_branch) { OpenStruct.new(ref: 'feature', repo: source_repo) } + let(:target_repo) { repository } + let(:target_branch) { OpenStruct.new(ref: 'master', repo: target_repo) } + let(:octocat) { OpenStruct.new(id: 123456, login: 'octocat') } + let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') } + let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') } + let(:base_data) do + { + number: 1347, + state: 'open', + title: 'New feature', + body: 'Please pull these awesome changes', + head: source_branch, + base: target_branch, + assignee: nil, + user: octocat, + created_at: created_at, + updated_at: updated_at, + closed_at: nil, + merged_at: nil + } + end + + subject(:pull_request) { described_class.new(project, raw_data)} + + describe '#attributes' do + context 'when pull request is open' do + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'open')) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'opened', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: updated_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is closed' do + let(:closed_at) { DateTime.strptime('2011-01-28T19:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', closed_at: closed_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'closed', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: closed_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when pull request is merged' do + let(:merged_at) { DateTime.strptime('2011-01-28T13:01:12Z') } + let(:raw_data) { OpenStruct.new(base_data.merge(state: 'closed', merged_at: merged_at)) } + + it 'returns formatted attributes' do + expected = { + title: 'New feature', + description: "*Created by: octocat*\n\nPlease pull these awesome changes", + source_project: project, + source_branch: 'feature', + target_project: project, + target_branch: 'master', + state: 'merged', + author_id: project.creator_id, + assignee_id: nil, + created_at: created_at, + updated_at: merged_at + } + + expect(pull_request.attributes).to eq(expected) + end + end + + context 'when it is assigned to someone' do + let(:raw_data) { OpenStruct.new(base_data.merge(assignee: octocat)) } + + it 'returns nil as assignee_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:assignee_id)).to be_nil + end + + it 'returns GitLab user id as assignee_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:assignee_id)).to eq gl_user.id + end + end + + context 'when author is a GitLab user' do + let(:raw_data) { OpenStruct.new(base_data.merge(user: octocat)) } + + it 'returns project#creator_id as author_id when is not a GitLab user' do + expect(pull_request.attributes.fetch(:author_id)).to eq project.creator_id + end + + it 'returns GitLab user id as author_id when is a GitLab user' do + gl_user = create(:omniauth_user, extern_uid: octocat.id, provider: 'github') + + expect(pull_request.attributes.fetch(:author_id)).to eq gl_user.id + end + end + end + + describe '#number' do + let(:raw_data) { OpenStruct.new(base_data.merge(number: 1347)) } + + it 'returns pull request number' do + expect(pull_request.number).to eq 1347 + end + end + + describe '#valid?' do + let(:invalid_branch) { OpenStruct.new(ref: 'invalid-branch') } + + context 'when source, and target repositories are the same' do + context 'and source and target branches exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: target_branch)) } + + it 'returns true' do + expect(pull_request.valid?).to eq true + end + end + + context 'and source branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: invalid_branch, base: target_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + + context 'and target branch doesn not exists' do + let(:raw_data) { OpenStruct.new(base_data.merge(head: source_branch, base: invalid_branch)) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + end + + context 'when source repo is a fork' do + let(:source_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + + context 'when target repo is a fork' do + let(:target_repo) { OpenStruct.new(id: 2, fork: true) } + let(:raw_data) { OpenStruct.new(base_data) } + + it 'returns false' do + expect(pull_request.valid?).to eq false + end + end + end +end diff --git a/spec/lib/gitlab/github_import/wiki_formatter_spec.rb b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb new file mode 100644 index 00000000000..aed2aa39e3a --- /dev/null +++ b/spec/lib/gitlab/github_import/wiki_formatter_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Gitlab::GithubImport::WikiFormatter, lib: true do + let(:project) do + create(:project, namespace: create(:namespace, path: 'gitlabhq'), + import_url: 'https://xxx@github.com/gitlabhq/sample.gitlabhq.git') + end + + subject(:wiki) { described_class.new(project)} + + describe '#path_with_namespace' do + it 'appends .wiki to project path' do + expect(wiki.path_with_namespace).to eq 'gitlabhq/gitlabhq.wiki' + end + end + + describe '#import_url' do + it 'returns URL of the wiki repository' do + expect(wiki.import_url).to eq 'https://xxx@github.com/gitlabhq/sample.gitlabhq.wiki.git' + end + end +end diff --git a/spec/lib/gitlab/highlight_spec.rb b/spec/lib/gitlab/highlight_spec.rb new file mode 100644 index 00000000000..1620eb6c60a --- /dev/null +++ b/spec/lib/gitlab/highlight_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Gitlab::Highlight, lib: true do + include RepoHelpers + + let(:project) { create(:project) } + let(:commit) { project.commit(sample_commit.id) } + + describe '.highlight_lines' do + let(:lines) do + Gitlab::Highlight.highlight_lines(project.repository, commit.id, 'files/ruby/popen.rb') + end + + it 'should properly highlight all the lines' do + expect(lines[4]).to eq(%Q{<span id="LC5" class="line"> <span class="kp">extend</span> <span class="nb">self</span></span>\n}) + expect(lines[21]).to eq(%Q{<span id="LC22" class="line"> <span class="k">unless</span> <span class="no">File</span><span class="p">.</span><span class="nf">directory?</span><span class="p">(</span><span class="n">path</span><span class="p">)</span></span>\n}) + expect(lines[26]).to eq(%Q{<span id="LC27" class="line"> <span class="vi">@cmd_status</span> <span class="o">=</span> <span class="mi">0</span></span>\n}) + end + end + +end diff --git a/spec/lib/gitlab/inline_diff_spec.rb b/spec/lib/gitlab/inline_diff_spec.rb deleted file mode 100644 index c690c195112..00000000000 --- a/spec/lib/gitlab/inline_diff_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'spec_helper' - -describe Gitlab::InlineDiff, lib: true do - describe '#processing' do - let(:diff) do - <<eos ---- a/test.rb -+++ b/test.rb -@@ -1,6 +1,6 @@ - class Test - def cleanup_string(input) - return nil if input.nil? -- input.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip -+ input.to_s.sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip - end - end -eos - end - - let(:expected) do - ["--- a/test.rb\n", - "+++ b/test.rb\n", - "@@ -1,6 +1,6 @@\n", - " class Test\n", - " def cleanup_string(input)\n", - " return nil if input.nil?\n", - "- input.#!idiff-start!##!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", - "+ input.#!idiff-start!#to_s.#!idiff-finish!#sub(/[\\r\\n].+/,'').sub(/\\\\[rn].+/, '').strip\n", - " end\n", - " end\n"] - end - - let(:subject) { Gitlab::InlineDiff.processing(diff.lines) } - - it 'should retain backslashes' do - expect(subject).to eq(expected) - end - end -end diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/ldap/access_spec.rb index a628d0c0157..32a19bf344b 100644 --- a/spec/lib/gitlab/ldap/access_spec.rb +++ b/spec/lib/gitlab/ldap/access_spec.rb @@ -13,64 +13,58 @@ describe Gitlab::LDAP::Access, lib: true do end it { is_expected.to be_falsey } - + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'when the user is found' do before do - allow(Gitlab::LDAP::Person). - to receive(:find_by_dn).and_return(:ldap_user) + allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user) end context 'and the user is disabled via active directory' do before do - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(true) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true) end it { is_expected.to be_falsey } - it "should block user in GitLab" do + it 'should block user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).to be_ldap_blocked end end context 'and has no disabled flag in active diretory' do before do - user.block - - allow(Gitlab::LDAP::Person). - to receive(:disabled_via_active_directory?).and_return(false) + allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false) end it { is_expected.to be_truthy } context 'when auto-created users are blocked' do - before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(true) + user.block end - it "does not unblock user in GitLab" do + it 'does not unblock user in GitLab' do access.allowed? expect(user).to be_blocked + expect(user).not_to be_ldap_blocked # this block is handled by omniauth not by our internal logic end end - context "when auto-created users are not blocked" do - + context 'when auto-created users are not blocked' do before do - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:block_auto_created_users).and_return(false) + user.ldap_block end - it "should unblock user in GitLab" do + it 'should unblock user in GitLab' do access.allowed? expect(user).not_to be_blocked end @@ -80,8 +74,7 @@ describe Gitlab::LDAP::Access, lib: true do context 'without ActiveDirectory enabled' do before do allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true) - allow_any_instance_of(Gitlab::LDAP::Config). - to receive(:active_directory).and_return(false) + allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false) end it { is_expected.to be_truthy } diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb index 1e755259dae..03199a2523e 100644 --- a/spec/lib/gitlab/ldap/user_spec.rb +++ b/spec/lib/gitlab/ldap/user_spec.rb @@ -37,7 +37,7 @@ describe Gitlab::LDAP::User, lib: true do end it "dont marks existing ldap user as changed" do - create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain') + create(:omniauth_user, email: 'john@example.com', extern_uid: 'my-uid', provider: 'ldapmain', ldap_email: true) expect(ldap_user.changed?).to be_falsey end end @@ -110,6 +110,32 @@ describe Gitlab::LDAP::User, lib: true do end end + describe 'updating email' do + context "when LDAP sets an email" do + it "has a real email" do + expect(ldap_user.gl_user.email).to eq(info[:email]) + end + + it "has ldap_email set to true" do + expect(ldap_user.gl_user.ldap_email?).to be(true) + end + end + + context "when LDAP doesn't set an email" do + before do + info.delete(:email) + end + + it "has a temp email" do + expect(ldap_user.gl_user.temp_oauth_email?).to be(true) + end + + it "has ldap_email set to false" do + expect(ldap_user.gl_user.ldap_email?).to be(false) + end + end + end + describe 'blocking' do def configure_block(value) allow_any_instance_of(Gitlab::LDAP::Config). diff --git a/spec/lib/gitlab/metrics/instrumentation_spec.rb b/spec/lib/gitlab/metrics/instrumentation_spec.rb index a7eab9d11cc..ad4290c43bb 100644 --- a/spec/lib/gitlab/metrics/instrumentation_spec.rb +++ b/spec/lib/gitlab/metrics/instrumentation_spec.rb @@ -48,6 +48,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy.foo') @@ -63,6 +66,16 @@ describe Gitlab::Metrics::Instrumentation do @dummy.foo end + + it 'generates a method with the correct arity when using methods without arguments' do + dummy = Class.new do + def self.test; end + end + + described_class.instrument_method(dummy, :test) + + expect(dummy.method(:test).arity).to eq(0) + end end describe 'with metrics disabled' do @@ -102,6 +115,9 @@ describe Gitlab::Metrics::Instrumentation do allow(described_class).to receive(:transaction). and_return(transaction) + expect(transaction).to receive(:increment). + with(:method_duration, a_kind_of(Numeric)) + expect(transaction).to receive(:add_metric). with(described_class::SERIES, an_instance_of(Hash), method: 'Dummy#bar') diff --git a/spec/lib/gitlab/metrics/metric_spec.rb b/spec/lib/gitlab/metrics/metric_spec.rb index aa76315c79c..f718d536130 100644 --- a/spec/lib/gitlab/metrics/metric_spec.rb +++ b/spec/lib/gitlab/metrics/metric_spec.rb @@ -37,9 +37,6 @@ describe Gitlab::Metrics::Metric do it 'includes the tags' do expect(hash[:tags]).to be_an_instance_of(Hash) - - expect(hash[:tags][:hostname]).to be_an_instance_of(String) - expect(hash[:tags][:process_type]).to be_an_instance_of(String) end it 'includes the values' do diff --git a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb b/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb deleted file mode 100644 index 2b681c9fe34..00000000000 --- a/spec/lib/gitlab/metrics/obfuscated_sql_spec.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Metrics::ObfuscatedSQL do - describe '#to_s' do - it 'replaces newlines with a space' do - sql = described_class.new("SELECT x\nFROM y") - - expect(sql.to_s).to eq('SELECT x FROM y') - end - - describe 'using single values' do - it 'replaces a single integer' do - sql = described_class.new('SELECT x FROM y WHERE a = 10') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces a single float' do - sql = described_class.new('SELECT x FROM y WHERE a = 10.5') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces a single quoted string' do - sql = described_class.new("SELECT x FROM y WHERE a = 'foo'") - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - if Gitlab::Database.mysql? - it 'replaces a double quoted string' do - sql = described_class.new('SELECT x FROM y WHERE a = "foo"') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - end - - it 'replaces a single regular expression' do - sql = described_class.new('SELECT x FROM y WHERE a = /foo/') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - - it 'replaces regular expressions using escaped slashes' do - sql = described_class.new('SELECT x FROM y WHERE a = /foo\/bar/') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE a = ?') - end - end - - describe 'using consecutive values' do - it 'replaces multiple integers' do - sql = described_class.new('SELECT x FROM y WHERE z IN (10, 20, 30)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') - end - - it 'replaces multiple floats' do - sql = described_class.new('SELECT x FROM y WHERE z IN (1.5, 2.5, 3.5)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (3 values)') - end - - it 'replaces multiple single quoted strings' do - sql = described_class.new("SELECT x FROM y WHERE z IN ('foo', 'bar')") - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - - if Gitlab::Database.mysql? - it 'replaces multiple double quoted strings' do - sql = described_class.new('SELECT x FROM y WHERE z IN ("foo", "bar")') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - end - - it 'replaces multiple regular expressions' do - sql = described_class.new('SELECT x FROM y WHERE z IN (/foo/, /bar/)') - - expect(sql.to_s).to eq('SELECT x FROM y WHERE z IN (2 values)') - end - end - - if Gitlab::Database.postgresql? - it 'replaces double quotes' do - sql = described_class.new('SELECT "x" FROM "y"') - - expect(sql.to_s).to eq('SELECT x FROM y') - end - end - end -end diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb index a143fe4cfcd..b99be4e1060 100644 --- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb @@ -40,9 +40,9 @@ describe Gitlab::Metrics::RackMiddleware do expect(transaction).to be_an_instance_of(Gitlab::Metrics::Transaction) end - it 'tags the transaction with the request method and URI' do - expect(transaction.tags[:request_method]).to eq('GET') - expect(transaction.tags[:request_uri]).to eq('/foo') + it 'stores the request method and URI in the transaction as values' do + expect(transaction.values[:request_method]).to eq('GET') + expect(transaction.values[:request_uri]).to eq('/foo') end end @@ -57,7 +57,7 @@ describe Gitlab::Metrics::RackMiddleware do middleware.tag_controller(transaction, env) - expect(transaction.tags[:action]).to eq('TestController#show') + expect(transaction.action).to eq('TestController#show') end end end diff --git a/spec/lib/gitlab/metrics/sampler_spec.rb b/spec/lib/gitlab/metrics/sampler_spec.rb index 51a941c48cd..38da77adc9f 100644 --- a/spec/lib/gitlab/metrics/sampler_spec.rb +++ b/spec/lib/gitlab/metrics/sampler_spec.rb @@ -9,7 +9,7 @@ describe Gitlab::Metrics::Sampler do describe '#start' do it 'gathers a sample at a given interval' do - expect(sampler).to receive(:sleep).with(5) + expect(sampler).to receive(:sleep).with(a_kind_of(Numeric)) expect(sampler).to receive(:sample) expect(sampler).to receive(:loop).and_yield @@ -51,8 +51,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:memory_usage). and_return(9000) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('memory_usage', value: 9000). + expect(sampler).to receive(:add_metric). + with(/memory_usage/, value: 9000). and_call_original sampler.sample_memory_usage @@ -64,8 +64,8 @@ describe Gitlab::Metrics::Sampler do expect(Gitlab::Metrics::System).to receive(:file_descriptor_count). and_return(4) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('file_descriptors', value: 4). + expect(sampler).to receive(:add_metric). + with(/file_descriptors/, value: 4). and_call_original sampler.sample_file_descriptors @@ -74,8 +74,8 @@ describe Gitlab::Metrics::Sampler do describe '#sample_objects' do it 'adds a metric containing the amount of allocated objects' do - expect(Gitlab::Metrics::Metric).to receive(:new). - with('object_counts', an_instance_of(Hash), an_instance_of(Hash)). + expect(sampler).to receive(:add_metric). + with(/object_counts/, an_instance_of(Hash), an_instance_of(Hash)). at_least(:once). and_call_original @@ -87,11 +87,53 @@ describe Gitlab::Metrics::Sampler do it 'adds a metric containing garbage collection statistics' do expect(GC::Profiler).to receive(:total_time).and_return(0.24) - expect(Gitlab::Metrics::Metric).to receive(:new). - with('gc_statistics', an_instance_of(Hash)). + expect(sampler).to receive(:add_metric). + with(/gc_statistics/, an_instance_of(Hash)). and_call_original sampler.sample_gc end end + + describe '#add_metric' do + it 'prefixes the series name for a Rails process' do + expect(sampler).to receive(:sidekiq?).and_return(false) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('rails_cats', { value: 10 }, {}). + and_call_original + + sampler.add_metric('cats', value: 10) + end + + it 'prefixes the series name for a Sidekiq process' do + expect(sampler).to receive(:sidekiq?).and_return(true) + + expect(Gitlab::Metrics::Metric).to receive(:new). + with('sidekiq_cats', { value: 10 }, {}). + and_call_original + + sampler.add_metric('cats', value: 10) + end + end + + describe '#sleep_interval' do + it 'returns a Numeric' do + expect(sampler.sleep_interval).to be_a_kind_of(Numeric) + end + + # Testing random behaviour is very hard, so treat this test as a basic smoke + # test instead of a very accurate behaviour/unit test. + it 'does not return the same interval twice in a row' do + last = nil + + 100.times do + interval = sampler.sleep_interval + + expect(interval).to_not eq(last) + + last = interval + end + end + end end diff --git a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb index 5882e7d81c7..e520a968999 100644 --- a/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb +++ b/spec/lib/gitlab/metrics/sidekiq_middleware_spec.rb @@ -5,22 +5,15 @@ describe Gitlab::Metrics::SidekiqMiddleware do describe '#call' do it 'tracks the transaction' do - worker = Class.new.new + worker = double(:worker, class: double(:class, name: 'TestWorker')) + + expect(Gitlab::Metrics::Transaction).to receive(:new). + with('TestWorker#perform'). + and_call_original expect_any_instance_of(Gitlab::Metrics::Transaction).to receive(:finish) middleware.call(worker, 'test', :test) { nil } end end - - describe '#tag_worker' do - it 'adds the worker class and action to the transaction' do - trans = Gitlab::Metrics::Transaction.new - worker = double(:worker, class: double(:class, name: 'TestWorker')) - - expect(trans).to receive(:add_tag).with(:action, 'TestWorker#perform') - - middleware.tag_worker(trans, worker) - end - end end diff --git a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb index c6cd584663f..0695c5ce096 100644 --- a/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/action_view_spec.rb @@ -14,19 +14,15 @@ describe Gitlab::Metrics::Subscribers::ActionView do before do allow(subscriber).to receive(:current_transaction).and_return(transaction) - - allow(Gitlab::Metrics).to receive(:last_relative_application_frame). - and_return(['app/views/x.html.haml', 4]) end describe '#render_template' do it 'tracks rendering of a template' do values = { duration: 2.1 } - tags = { - view: 'app/views/x.html.haml', - file: 'app/views/x.html.haml', - line: 4 - } + tags = { view: 'app/views/x.html.haml' } + + expect(transaction).to receive(:increment). + with(:view_duration, 2.1) expect(transaction).to receive(:add_metric). with(described_class::SERIES, values, tags) diff --git a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb index 05b6cc14716..7bc070a4d09 100644 --- a/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb +++ b/spec/lib/gitlab/metrics/subscribers/active_record_spec.rb @@ -2,31 +2,34 @@ require 'spec_helper' describe Gitlab::Metrics::Subscribers::ActiveRecord do let(:transaction) { Gitlab::Metrics::Transaction.new } - - let(:subscriber) { described_class.new } + let(:subscriber) { described_class.new } let(:event) do double(:event, duration: 0.2, payload: { sql: 'SELECT * FROM users WHERE id = 10' }) end - before do - allow(subscriber).to receive(:current_transaction).and_return(transaction) + describe '#sql' do + describe 'without a current transaction' do + it 'simply returns' do + expect_any_instance_of(Gitlab::Metrics::Transaction). + to_not receive(:increment) - allow(Gitlab::Metrics).to receive(:last_relative_application_frame). - and_return(['app/models/foo.rb', 4]) - end + subscriber.sql(event) + end + end - describe '#sql' do - it 'tracks the execution of a SQL query' do - sql = 'SELECT * FROM users WHERE id = ?' - values = { duration: 0.2 } - tags = { sql: sql, file: 'app/models/foo.rb', line: 4 } + describe 'with a current transaction' do + it 'increments the :sql_duration value' do + expect(subscriber).to receive(:current_transaction). + at_least(:once). + and_return(transaction) - expect(transaction).to receive(:add_metric). - with(described_class::SERIES, values, tags) + expect(transaction).to receive(:increment). + with(:sql_duration, 0.2) - subscriber.sql(event) + subscriber.sql(event) + end end end end diff --git a/spec/lib/gitlab/metrics/transaction_spec.rb b/spec/lib/gitlab/metrics/transaction_spec.rb index 6862fc9e2d1..1d5a51a157e 100644 --- a/spec/lib/gitlab/metrics/transaction_spec.rb +++ b/spec/lib/gitlab/metrics/transaction_spec.rb @@ -11,6 +11,14 @@ describe Gitlab::Metrics::Transaction do end end + describe '#allocated_memory' do + it 'returns the allocated memory in bytes' do + transaction.run { 'a' * 32 } + + expect(transaction.allocated_memory).to be_a_kind_of(Numeric) + end + end + describe '#run' do it 'yields the supplied block' do expect { |b| transaction.run(&b) }.to yield_control @@ -30,14 +38,45 @@ describe Gitlab::Metrics::Transaction do end describe '#add_metric' do - it 'adds a metric tagged with the transaction UUID' do + it 'adds a metric to the transaction' do expect(Gitlab::Metrics::Metric).to receive(:new). - with('foo', { number: 10 }, { transaction_id: transaction.uuid }) + with('rails_foo', { number: 10 }, {}) transaction.add_metric('foo', number: 10) end end + describe '#increment' do + it 'increments a counter' do + transaction.increment(:time, 1) + transaction.increment(:time, 2) + + values = { duration: 0.0, time: 3, allocated_memory: a_kind_of(Numeric) } + + expect(transaction).to receive(:add_metric). + with('transactions', values, {}) + + transaction.track_self + end + end + + describe '#set' do + it 'sets a value' do + transaction.set(:number, 10) + + values = { + duration: 0.0, + number: 10, + allocated_memory: a_kind_of(Numeric) + } + + expect(transaction).to receive(:add_metric). + with('transactions', values, {}) + + transaction.track_self + end + end + describe '#add_tag' do it 'adds a tag' do transaction.add_tag(:foo, 'bar') @@ -57,8 +96,13 @@ describe Gitlab::Metrics::Transaction do describe '#track_self' do it 'adds a metric for the transaction itself' do + values = { + duration: transaction.duration, + allocated_memory: a_kind_of(Numeric) + } + expect(transaction).to receive(:add_metric). - with(described_class::SERIES, { duration: transaction.duration }, {}) + with('transactions', values, {}) transaction.track_self end @@ -73,5 +117,22 @@ describe Gitlab::Metrics::Transaction do transaction.submit end + + it 'adds the action as a tag for every metric' do + transaction.action = 'Foo#bar' + transaction.track_self + + hash = { + series: 'rails_transactions', + tags: { action: 'Foo#bar' }, + values: { duration: 0.0, allocated_memory: a_kind_of(Numeric) }, + timestamp: an_instance_of(Fixnum) + } + + expect(Gitlab::Metrics).to receive(:submit_metrics). + with([hash]) + + transaction.submit + end end end diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb index 6c0682cac4d..0ec8a6dc5cb 100644 --- a/spec/lib/gitlab/metrics_spec.rb +++ b/spec/lib/gitlab/metrics_spec.rb @@ -1,15 +1,9 @@ require 'spec_helper' describe Gitlab::Metrics do - describe '.pool_size' do - it 'returns a Fixnum' do - expect(described_class.pool_size).to be_an_instance_of(Fixnum) - end - end - - describe '.timeout' do - it 'returns a Fixnum' do - expect(described_class.timeout).to be_an_instance_of(Fixnum) + describe '.settings' do + it 'returns a Hash' do + expect(described_class.settings).to be_an_instance_of(Hash) end end @@ -19,21 +13,6 @@ describe Gitlab::Metrics do end end - describe '.hostname' do - it 'returns a String containing the hostname' do - expect(described_class.hostname).to eq(Socket.gethostname) - end - end - - describe '.last_relative_application_frame' do - it 'returns an Array containing a file path and line number' do - file, line = described_class.last_relative_application_frame - - expect(line).to eq(__LINE__ - 2) - expect(file).to eq('spec/lib/gitlab/metrics_spec.rb') - end - end - describe '#submit_metrics' do it 'prepares and writes the metrics to InfluxDB' do connection = double(:connection) diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb new file mode 100644 index 00000000000..117a15264da --- /dev/null +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -0,0 +1,30 @@ +require 'spec_helper' + +describe Gitlab::Middleware::Go, lib: true do + let(:app) { double(:app) } + let(:middleware) { described_class.new(app) } + + describe '#call' do + describe 'when go-get=0' do + it 'skips go-import generation' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=0' } + expect(app).to receive(:call).with(env).and_return('no-go') + middleware.call(env) + end + end + + describe 'when go-get=1' do + it 'returns a document' do + env = { 'rack.input' => '', + 'QUERY_STRING' => 'go-get=1', + 'PATH_INFO' => '/group/project/path' } + resp = middleware.call(env) + expect(resp[0]).to eq(200) + expect(resp[1]['Content-Type']).to eq('text/html') + expected_body = "<!DOCTYPE html><html><head><meta content='localhost/group/project git http://localhost/group/project.git' name='go-import'></head></html>\n" + expect(resp[2].body).to eq([expected_body]) + end + end + end +end diff --git a/spec/lib/gitlab/note_data_builder_spec.rb b/spec/lib/gitlab/note_data_builder_spec.rb index 6cbdae737f4..da652677443 100644 --- a/spec/lib/gitlab/note_data_builder_spec.rb +++ b/spec/lib/gitlab/note_data_builder_spec.rb @@ -16,58 +16,80 @@ describe 'Gitlab::NoteDataBuilder', lib: true do end describe 'When asking for a note on commit' do - let(:note) { create(:note_on_commit) } + let(:note) { create(:note_on_commit, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on commit diff' do - let(:note) { create(:note_on_commit_diff) } + let(:note) { create(:note_on_commit_diff, project: project) } it 'returns the note and commit-specific data' do expect(data).to have_key(:commit) end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on issue' do let(:issue) { create(:issue, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_issue, noteable_id: issue.id) } + let(:note) { create(:note_on_issue, noteable_id: issue.id, project: project) } it 'returns the note and issue-specific data' do expect(data).to have_key(:issue) - expect(data[:issue]).to eq(issue.hook_attrs) + expect(data[:issue].except('updated_at')).to eq(issue.hook_attrs.except('updated_at')) + expect(data[:issue]['updated_at']).to be > issue.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on merge request diff' do let(:merge_request) { create(:merge_request, created_at: fixed_time, updated_at: fixed_time) } - let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id) } + let(:note) { create(:note_on_merge_request_diff, noteable_id: merge_request.id, project: project) } it 'returns the note and merge request diff data' do expect(data).to have_key(:merge_request) - expect(data[:merge_request]).to eq(merge_request.hook_attrs) + expect(data[:merge_request].except('updated_at')).to eq(merge_request.hook_attrs.except('updated_at')) + expect(data[:merge_request]['updated_at']).to be > merge_request.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe 'When asking for a note on project snippet' do let!(:snippet) { create(:project_snippet, created_at: fixed_time, updated_at: fixed_time) } - let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id) } + let!(:note) { create(:note_on_project_snippet, noteable_id: snippet.id, project: project) } it 'returns the note and project snippet data' do expect(data).to have_key(:snippet) - expect(data[:snippet]).to eq(snippet.hook_attrs) + expect(data[:snippet].except('updated_at')).to eq(snippet.hook_attrs.except('updated_at')) + expect(data[:snippet]['updated_at']).to be > snippet.hook_attrs['updated_at'] end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end end diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb index 925bc442a90..3a769acfdc0 100644 --- a/spec/lib/gitlab/o_auth/user_spec.rb +++ b/spec/lib/gitlab/o_auth/user_spec.rb @@ -41,7 +41,20 @@ describe Gitlab::OAuth::User, lib: true do describe 'signup' do shared_examples "to verify compliance with allow_single_sign_on" do - context "with allow_single_sign_on enabled" do + context "with new allow_single_sign_on enabled syntax" do + before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } + + it "creates a user from Omniauth" do + oauth_user.save + + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'twitter' + end + end + + context "with old allow_single_sign_on enabled syntax" do before { stub_omniauth_config(allow_single_sign_on: true) } it "creates a user from Omniauth" do @@ -54,7 +67,14 @@ describe Gitlab::OAuth::User, lib: true do end end - context "with allow_single_sign_on disabled (Default)" do + context "with new allow_single_sign_on disabled syntax" do + before { stub_omniauth_config(allow_single_sign_on: []) } + it "throws an error" do + expect{ oauth_user.save }.to raise_error StandardError + end + end + + context "with old allow_single_sign_on disabled (Default)" do before { stub_omniauth_config(allow_single_sign_on: false) } it "throws an error" do expect{ oauth_user.save }.to raise_error StandardError @@ -135,7 +155,7 @@ describe Gitlab::OAuth::User, lib: true do describe 'blocking' do let(:provider) { 'twitter' } - before { stub_omniauth_config(allow_single_sign_on: true) } + before { stub_omniauth_config(allow_single_sign_on: ['twitter']) } context 'signup with omniauth only' do context 'dont block on create' do diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb index efc2e5f4ef1..db0ff95b4f5 100644 --- a/spec/lib/gitlab/project_search_results_spec.rb +++ b/spec/lib/gitlab/project_search_results_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' describe Gitlab::ProjectSearchResults, lib: true do + let(:user) { create(:user) } let(:project) { create(:project) } let(:query) { 'hello world' } describe 'initialize with empty ref' do - let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, '') } + let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, '') } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to be_nil } @@ -14,10 +15,74 @@ describe Gitlab::ProjectSearchResults, lib: true do describe 'initialize with ref' do let(:ref) { 'refs/heads/test' } - let(:results) { Gitlab::ProjectSearchResults.new(project.id, query, ref) } + let(:results) { Gitlab::ProjectSearchResults.new(user, project, query, ref) } it { expect(results.project).to eq(project) } it { expect(results.repository_ref).to eq(ref) } it { expect(results.query).to eq('hello world') } end + + describe 'confidential issues' do + let(:query) { 'issue' } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + + it 'should not list project confidential issues for non project members' do + results = described_class.new(non_member, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(results.issues_count).to eq 1 + end + + it 'should list project confidential issues for author' do + results = described_class.new(author, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(results.issues_count).to eq 2 + end + + it 'should list project confidential issues for assignee' do + results = described_class.new(assignee, project.id, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 2 + end + + it 'should list project confidential issues for project members' do + project.team << [member, :developer] + + results = described_class.new(member, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 3 + end + + it 'should list all project issues for admin' do + results = described_class.new(admin, project, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(results.issues_count).to eq 3 + end + end end diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb index 3ef61685398..961022b9d12 100644 --- a/spec/lib/gitlab/push_data_builder_spec.rb +++ b/spec/lib/gitlab/push_data_builder_spec.rb @@ -1,34 +1,32 @@ require 'spec_helper' -describe 'Gitlab::PushDataBuilder', lib: true do +describe Gitlab::PushDataBuilder, lib: true do let(:project) { create(:project) } let(:user) { create(:user) } - describe :build_sample do - let(:data) { Gitlab::PushDataBuilder.build_sample(project, user) } + describe '.build_sample' do + let(:data) { described_class.build_sample(project, user) } it { expect(data).to be_a(Hash) } it { expect(data[:before]).to eq('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') } it { expect(data[:after]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') } it { expect(data[:ref]).to eq('refs/heads/master') } it { expect(data[:commits].size).to eq(3) } - it { expect(data[:repository][:git_http_url]).to eq(project.http_url_to_repo) } - it { expect(data[:repository][:git_ssh_url]).to eq(project.ssh_url_to_repo) } - it { expect(data[:repository][:visibility_level]).to eq(project.visibility_level) } it { expect(data[:total_commits_count]).to eq(3) } it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) } it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) } it { expect(data[:commits].first[:removed]).to eq([]) } + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end - describe :build do + describe '.build' do let(:data) do - Gitlab::PushDataBuilder.build(project, - user, - Gitlab::Git::BLANK_SHA, - '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', - 'refs/tags/v1.1.0') + described_class.build(project, user, Gitlab::Git::BLANK_SHA, + '8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b', + 'refs/tags/v1.1.0') end it { expect(data).to be_a(Hash) } @@ -38,5 +36,10 @@ describe 'Gitlab::PushDataBuilder', lib: true do it { expect(data[:ref]).to eq('refs/tags/v1.1.0') } it { expect(data[:commits]).to be_empty } it { expect(data[:total_commits_count]).to be_zero } + + it 'does not raise an error when given nil commits' do + expect { described_class.build(spy, spy, spy, spy, spy, nil) }. + not_to raise_error + end end end diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb index 7d963795e17..65af37e24f1 100644 --- a/spec/lib/gitlab/reference_extractor_spec.rb +++ b/spec/lib/gitlab/reference_extractor_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe Gitlab::ReferenceExtractor, lib: true do let(:project) { create(:project) } + subject { Gitlab::ReferenceExtractor.new(project, project.creator) } it 'accesses valid user objects' do @@ -41,6 +42,7 @@ describe Gitlab::ReferenceExtractor, lib: true do end it 'accesses valid issue objects' do + project.team << [project.creator, :developer] @i0 = create(:issue, project: project) @i1 = create(:issue, project: project) diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb index d67ee423b9b..c51b10bdc69 100644 --- a/spec/lib/gitlab/regex_spec.rb +++ b/spec/lib/gitlab/regex_spec.rb @@ -21,4 +21,12 @@ describe Gitlab::Regex, lib: true do it { expect('Dash – is this').to match(Gitlab::Regex.project_name_regex) } it { expect('?gitlab').not_to match(Gitlab::Regex.project_name_regex) } end + + describe 'file name regex' do + it { expect('foo@bar').to match(Gitlab::Regex.file_name_regex) } + end + + describe 'file path regex' do + it { expect('foo@/bar').to match(Gitlab::Regex.file_path_regex) } + end end diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/saml/user_spec.rb new file mode 100644 index 00000000000..de7cd99d49d --- /dev/null +++ b/spec/lib/gitlab/saml/user_spec.rb @@ -0,0 +1,271 @@ +require 'spec_helper' + +describe Gitlab::Saml::User, lib: true do + let(:saml_user) { described_class.new(auth_hash) } + let(:gl_user) { saml_user.gl_user } + let(:uid) { 'my-uid' } + let(:provider) { 'saml' } + let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) } + let(:info_hash) do + { + name: 'John', + email: 'john@mail.com' + } + end + let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') } + + describe '#save' do + def stub_omniauth_config(messages) + allow(Gitlab.config.omniauth).to receive_messages(messages) + end + + def stub_ldap_config(messages) + allow(Gitlab::LDAP::Config).to receive_messages(messages) + end + + describe 'account exists on server' do + before { stub_omniauth_config({ allow_single_sign_on: ['saml'], auto_link_saml_user: true }) } + context 'and should bind with SAML' do + let!(:existing_user) { create(:user, email: 'john@mail.com', username: 'john') } + it 'adds the SAML identity to the existing user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to eq existing_user + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'saml' + end + end + end + + describe 'no account exists on server' do + shared_examples 'to verify compliance with allow_single_sign_on' do + context 'with allow_single_sign_on enabled' do + before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + + it 'creates a user from SAML' do + saml_user.save + + expect(gl_user).to be_valid + identity = gl_user.identities.first + expect(identity.extern_uid).to eql uid + expect(identity.provider).to eql 'saml' + end + end + + context 'with allow_single_sign_on default (["saml"])' do + before { stub_omniauth_config(allow_single_sign_on: ['saml']) } + it 'should not throw an error' do + expect{ saml_user.save }.not_to raise_error + end + end + + context 'with allow_single_sign_on disabled' do + before { stub_omniauth_config(allow_single_sign_on: false) } + it 'should throw an error' do + expect{ saml_user.save }.to raise_error StandardError + end + end + end + + context 'with auto_link_ldap_user disabled (default)' do + before { stub_omniauth_config({ auto_link_ldap_user: false, auto_link_saml_user: false, allow_single_sign_on: ['saml'] }) } + include_examples 'to verify compliance with allow_single_sign_on' + end + + context 'with auto_link_ldap_user enabled' do + before { stub_omniauth_config({ auto_link_ldap_user: true, auto_link_saml_user: false }) } + + context 'and no LDAP provider defined' do + before { stub_ldap_config(providers: []) } + + include_examples 'to verify compliance with allow_single_sign_on' + end + + context 'and at least one LDAP provider is defined' do + before { stub_ldap_config(providers: %w(ldapmain)) } + + context 'and a corresponding LDAP person' do + before do + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user) + end + + context 'and no account for the LDAP user' do + + it 'creates a user with dual LDAP and SAML identities' do + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql uid + expect(gl_user.email).to eql 'johndoe@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid } + ]) + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + it "adds the omniauth identity to the LDAP account" do + saml_user.save + + expect(gl_user).to be_valid + expect(gl_user.username).to eql 'john' + expect(gl_user.email).to eql 'john@example.com' + expect(gl_user.identities.length).to eql 2 + identities_as_hash = gl_user.identities.map { |id| { provider: id.provider, extern_uid: id.extern_uid } } + expect(identities_as_hash).to match_array([ { provider: 'ldapmain', extern_uid: 'uid=user1,ou=People,dc=example' }, + { provider: 'saml', extern_uid: uid } + ]) + end + end + end + + context 'and no corresponding LDAP person' do + before { allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil) } + + include_examples 'to verify compliance with allow_single_sign_on' + end + end + end + + end + + describe 'blocking' do + before { stub_omniauth_config({ allow_saml_sign_up: true, auto_link_saml_user: true }) } + + context 'signup with SAML only' do + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it 'should not block the user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it 'should block user' do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'signup with linked omniauth and LDAP account' do + before do + stub_omniauth_config(auto_link_ldap_user: true) + allow(ldap_user).to receive(:uid) { uid } + allow(ldap_user).to receive(:username) { uid } + allow(ldap_user).to receive(:email) { ['johndoe@example.com','john2@example.com'] } + allow(ldap_user).to receive(:dn) { 'uid=user1,ou=People,dc=example' } + allow(saml_user).to receive(:ldap_person).and_return(ldap_user) + end + + context "and no account for the LDAP user" do + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).to be_blocked + end + end + end + + context 'and LDAP user has an account already' do + let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: 'uid=user1,ou=People,dc=example', provider: 'ldapmain', username: 'john') } + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + + + context 'sign-in' do + before do + saml_user.save + saml_user.gl_user.activate + end + + context 'dont block on create' do + before { stub_omniauth_config(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create' do + before { stub_omniauth_config(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'dont block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + + context 'block on create (LDAP)' do + before { allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true) } + + it do + saml_user.save + expect(gl_user).to be_valid + expect(gl_user).not_to be_blocked + end + end + end + end + end +end diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb new file mode 100644 index 00000000000..f4afe597e8d --- /dev/null +++ b/spec/lib/gitlab/search_results_spec.rb @@ -0,0 +1,144 @@ +require 'spec_helper' + +describe Gitlab::SearchResults do + let(:user) { create(:user) } + let!(:project) { create(:project, name: 'foo') } + let!(:issue) { create(:issue, project: project, title: 'foo') } + + let!(:merge_request) do + create(:merge_request, source_project: project, title: 'foo') + end + + let!(:milestone) { create(:milestone, project: project, title: 'foo') } + let(:results) { described_class.new(user, Project.all, 'foo') } + + describe '#total_count' do + it 'returns the total amount of search hits' do + expect(results.total_count).to eq(4) + end + end + + describe '#projects_count' do + it 'returns the total amount of projects' do + expect(results.projects_count).to eq(1) + end + end + + describe '#issues_count' do + it 'returns the total amount of issues' do + expect(results.issues_count).to eq(1) + end + end + + describe '#merge_requests_count' do + it 'returns the total amount of merge requests' do + expect(results.merge_requests_count).to eq(1) + end + end + + describe '#milestones_count' do + it 'returns the total amount of milestones' do + expect(results.milestones_count).to eq(1) + end + end + + describe '#empty?' do + it 'returns true when there are no search results' do + allow(results).to receive(:total_count).and_return(0) + + expect(results.empty?).to eq(true) + end + + it 'returns false when there are search results' do + expect(results.empty?).to eq(false) + end + end + + describe 'confidential issues' do + let(:project_1) { create(:empty_project) } + let(:project_2) { create(:empty_project) } + let(:project_3) { create(:empty_project) } + let(:project_4) { create(:empty_project) } + let(:query) { 'issue' } + let(:limit_projects) { Project.where(id: [project_1.id, project_2.id, project_3.id]) } + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let!(:issue) { create(:issue, project: project_1, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project_1, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project_1, assignee: assignee) } + let!(:security_issue_3) { create(:issue, :confidential, project: project_2, title: 'Security issue 3', author: author) } + let!(:security_issue_4) { create(:issue, :confidential, project: project_3, title: 'Security issue 4', assignee: assignee) } + let!(:security_issue_5) { create(:issue, :confidential, project: project_4, title: 'Security issue 5') } + + it 'should not list confidential issues for non project members' do + results = described_class.new(non_member, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(issues).not_to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 1 + end + + it 'should list confidential issues for author' do + results = described_class.new(author, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).not_to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 3 + end + + it 'should list confidential issues for assignee' do + results = described_class.new(assignee, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).not_to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).not_to include security_issue_3 + expect(issues).to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 3 + end + + it 'should list confidential issues for project members' do + project_1.team << [member, :developer] + project_2.team << [member, :developer] + + results = described_class.new(member, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).not_to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 4 + end + + it 'should list all issues for admin' do + results = described_class.new(admin, limit_projects, query) + issues = results.objects('issues') + + expect(issues).to include issue + expect(issues).to include security_issue_1 + expect(issues).to include security_issue_2 + expect(issues).to include security_issue_3 + expect(issues).to include security_issue_4 + expect(issues).not_to include security_issue_5 + expect(results.issues_count).to eq 5 + end + end +end diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb new file mode 100644 index 00000000000..e86b9ef6a63 --- /dev/null +++ b/spec/lib/gitlab/snippet_search_results_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Gitlab::SnippetSearchResults do + let!(:snippet) { create(:snippet, content: 'foo', file_name: 'foo') } + + let(:results) { described_class.new(Snippet.all, 'foo') } + + describe '#total_count' do + it 'returns the total amount of search hits' do + expect(results.total_count).to eq(2) + end + end + + describe '#snippet_titles_count' do + it 'returns the amount of matched snippet titles' do + expect(results.snippet_titles_count).to eq(1) + end + end + + describe '#snippet_blobs_count' do + it 'returns the amount of matched snippet blobs' do + expect(results.snippet_blobs_count).to eq(1) + end + end +end diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb new file mode 100644 index 00000000000..d940bf05061 --- /dev/null +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe Gitlab::Workhorse, lib: true do + let(:project) { create(:project) } + let(:subject) { Gitlab::Workhorse } + + describe "#send_git_archive" do + context "when the repository doesn't have an archive file path" do + before do + allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) + end + + it "raises an error" do + expect { subject.send_git_archive(project, "master", "zip") }.to raise_error(RuntimeError) + end + end + end +end diff --git a/spec/mailers/abuse_report_mailer_spec.rb b/spec/mailers/abuse_report_mailer_spec.rb new file mode 100644 index 00000000000..eb433c38873 --- /dev/null +++ b/spec/mailers/abuse_report_mailer_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +describe AbuseReportMailer do + include EmailSpec::Matchers + + describe '.notify' do + context 'with admin_notification_email set' do + before do + stub_application_setting(admin_notification_email: 'admin@example.com') + end + + it 'sends to the admin_notification_email' do + report = create(:abuse_report) + + mail = described_class.notify(report.id) + + expect(mail).to deliver_to 'admin@example.com' + end + + it 'includes the user in the subject' do + report = create(:abuse_report) + + mail = described_class.notify(report.id) + + expect(mail).to have_subject "#{report.user.name} (#{report.user.username}) was reported for abuse" + end + end + + context 'with no admin_notification_email set' do + it 'returns early' do + stub_application_setting(admin_notification_email: nil) + + expect { described_class.notify(spy).deliver_now }. + not_to change { ActionMailer::Base.deliveries.count } + end + end + end +end diff --git a/spec/mailers/emails/builds_spec.rb b/spec/mailers/emails/builds_spec.rb new file mode 100644 index 00000000000..0df89938e97 --- /dev/null +++ b/spec/mailers/emails/builds_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + + include_context 'gitlab email notification' + + describe 'build notification email' do + let(:build) { create(:ci_build) } + let(:project) { build.project } + + shared_examples 'build email' do + it 'contains name of project' do + is_expected.to have_body_text build.project_name + end + + it 'contains link to project' do + is_expected.to have_body_text namespace_project_path(project.namespace, project) + end + end + + shared_examples 'an email with X-GitLab headers containing build details' do + it 'has X-GitLab-Build* headers' do + is_expected.to have_header 'X-GitLab-Build-Id', /#{build.id}/ + is_expected.to have_header 'X-GitLab-Build-Ref', /#{build.ref}/ + end + end + + describe 'build success' do + subject { Notify.build_success_email(build.id, 'wow@example.com') } + before { build.success } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'success' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build success for/ + end + end + + describe 'build fail' do + subject { Notify.build_fail_email(build.id, 'wow@example.com') } + before { build.drop } + + it_behaves_like 'build email' + it_behaves_like 'an email with X-GitLab headers containing build details' + it_behaves_like 'an email with X-GitLab headers containing project details' + + it 'has header indicating build status' do + is_expected.to have_header 'X-GitLab-Build-Status', 'failed' + end + + it 'has the correct subject' do + is_expected.to have_subject /Build failed for/ + end + end + end +end diff --git a/spec/mailers/emails/profile_spec.rb b/spec/mailers/emails/profile_spec.rb new file mode 100644 index 00000000000..c6758ccad39 --- /dev/null +++ b/spec/mailers/emails/profile_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' +require 'email_spec' +require 'mailers/shared/notify' + +describe Notify do + include EmailSpec::Matchers + include_context 'gitlab email notification' + + describe 'profile notifications' do + describe 'for new users, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } + let(:token) { 'kETLwRaayvigPq_x3SNM' } + + subject { Notify.new_user_email(new_user.id, token) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'contains the password text' do + is_expected.to have_body_text /Click here to set your password/ + end + + it 'includes a link for user to set password' do + params = "reset_password_token=#{token}" + is_expected.to have_body_text( + %r{http://localhost(:\d+)?/users/password/edit\?#{params}} + ) + end + + it 'explains the reset link expiration' do + is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) + is_expected.to have_body_text(new_user_password_url) + is_expected.to have_body_text(/\?user_email=.*%40.*/) + end + end + + describe 'for users that signed up, the email' do + let(:example_site_path) { root_path } + let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } + + subject { Notify.new_user_email(new_user.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'a new user email' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'should not contain the new user\'s password' do + is_expected.not_to have_body_text /password/ + end + end + + describe 'user added ssh key' do + let(:key) { create(:personal_key) } + + subject { Notify.new_ssh_key_email(key.id) } + + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to key.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^SSH key was added to your account$/i + end + + it 'contains the new ssh key title' do + is_expected.to have_body_text /#{key.title}/ + end + + it 'includes a link to ssh keys page' do + is_expected.to have_body_text /#{profile_keys_path}/ + end + + context 'with SSH key that does not exist' do + it { expect { Notify.new_ssh_key_email('foo') }.not_to raise_error } + end + end + + describe 'user added email' do + let(:email) { create(:email) } + + subject { Notify.new_email_email(email.id) } + + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'is sent to the new user' do + is_expected.to deliver_to email.user.email + end + + it 'has the correct subject' do + is_expected.to have_subject /^Email was added to your account$/i + end + + it 'contains the new email address' do + is_expected.to have_body_text /#{email.email}/ + end + + it 'includes a link to emails page' do + is_expected.to have_body_text /#{profile_emails_path}/ + end + end + end +end diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb index 154901a2fbc..f910424d85b 100644 --- a/spec/mailers/notify_spec.rb +++ b/spec/mailers/notify_spec.rb @@ -1,203 +1,13 @@ require 'spec_helper' require 'email_spec' +require 'mailers/shared/notify' describe Notify do include EmailSpec::Helpers include EmailSpec::Matchers include RepoHelpers - new_user_address = 'newguy@example.com' - - let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } - let(:gitlab_sender) { Gitlab.config.gitlab.email_from } - let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } - let(:recipient) { create(:user, email: 'recipient@example.com') } - let(:project) { create(:project) } - let(:build) { create(:ci_build) } - - before(:each) do - ActionMailer::Base.deliveries.clear - email = recipient.emails.create(email: "notifications@example.com") - recipient.update_attribute(:notification_email, email.email) - end - - shared_examples 'a multiple recipients email' do - it 'is sent to the given recipient' do - is_expected.to deliver_to recipient.notification_email - end - end - - shared_examples 'an email sent from GitLab' do - it 'is sent from GitLab' do - sender = subject.header[:from].addrs[0] - expect(sender.display_name).to eq(gitlab_sender_display_name) - expect(sender.address).to eq(gitlab_sender) - end - - it 'has a Reply-To address' do - reply_to = subject.header[:reply_to].addresses - expect(reply_to).to eq([gitlab_sender_reply_to]) - end - end - - shared_examples 'an email starting a new thread' do |message_id_prefix| - it 'has a discussion identifier' do - is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - end - end - - shared_examples 'an answer to an existing thread' do |thread_id_prefix| - it 'has a subject that begins with Re: ' do - is_expected.to have_subject /^Re: / - end - - it 'has headers that reference an existing thread' do - is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ - is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ - end - end - - shared_examples 'a new user email' do |user_email, site_path| - it 'is sent to the new user' do - is_expected.to deliver_to user_email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Account was created for you$/i - end - - it 'contains the new user\'s login name' do - is_expected.to have_body_text /#{user_email}/ - end - - it 'includes a link to the site' do - is_expected.to have_body_text /#{site_path}/ - end - end - - shared_examples 'it should have Gmail Actions links' do - it { is_expected.to have_body_text /ViewAction/ } - end - - shared_examples 'it should not have Gmail Actions links' do - it { is_expected.to_not have_body_text /ViewAction/ } - end - - shared_examples 'it should show Gmail Actions View Issue link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Issue/ } - end - - shared_examples 'it should show Gmail Actions View Merge request link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Merge request/ } - end - - shared_examples 'it should show Gmail Actions View Commit link' do - it_behaves_like 'it should have Gmail Actions links' - - it { is_expected.to have_body_text /View Commit/ } - end - - describe 'for new users, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, created_by_id: 1) } - - token = 'kETLwRaayvigPq_x3SNM' - - subject { Notify.new_user_email(new_user.id, token) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - - it 'contains the password text' do - is_expected.to have_body_text /Click here to set your password/ - end - - it 'includes a link for user to set password' do - params = "reset_password_token=#{token}" - is_expected.to have_body_text( - %r{http://localhost(:\d+)?/users/password/edit\?#{params}} - ) - end - - it 'explains the reset link expiration' do - is_expected.to have_body_text(/This link is valid for \d+ (hours?|days?)/) - is_expected.to have_body_text(new_user_password_url) - is_expected.to have_body_text(/\?user_email=.*%40.*/) - end - end - - - describe 'for users that signed up, the email' do - let(:example_site_path) { root_path } - let(:new_user) { create(:user, email: new_user_address, password: "securePassword") } - - subject { Notify.new_user_email(new_user.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'a new user email', new_user_address - it_behaves_like 'it should not have Gmail Actions links' - - it 'should not contain the new user\'s password' do - is_expected.not_to have_body_text /password/ - end - end - - describe 'user added ssh key' do - let(:key) { create(:personal_key) } - - subject { Notify.new_ssh_key_email(key.id) } - - it_behaves_like 'an email sent from GitLab' - it_behaves_like 'it should not have Gmail Actions links' - - it 'is sent to the new user' do - is_expected.to deliver_to key.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^SSH key was added to your account$/i - end - - it 'contains the new ssh key title' do - is_expected.to have_body_text /#{key.title}/ - end - - it 'includes a link to ssh keys page' do - is_expected.to have_body_text /#{profile_keys_path}/ - end - end - - describe 'user added email' do - let(:email) { create(:email) } - - subject { Notify.new_email_email(email.id) } - - it_behaves_like 'it should not have Gmail Actions links' - - it 'is sent to the new user' do - is_expected.to deliver_to email.user.email - end - - it 'has the correct subject' do - is_expected.to have_subject /^Email was added to your account$/i - end - - it 'contains the new email address' do - is_expected.to have_body_text /#{email.email}/ - end - - it 'includes a link to emails page' do - is_expected.to have_body_text /#{profile_emails_path}/ - end - end + include_context 'gitlab email notification' context 'for a project' do describe 'items that are assignable, the email' do @@ -227,6 +37,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{project.name} \| #{issue.title} \(##{issue.iid}\)/ @@ -235,6 +46,17 @@ describe Notify do it 'contains a link to the new issue' do is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text issue.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do @@ -253,6 +75,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -277,12 +100,41 @@ describe Notify do end end + describe 'that have been relabeled' do + subject { Notify.relabeled_issue_email(recipient.id, issue.id, %w[foo bar baz], current_user.id) } + + it_behaves_like 'a multiple recipients email' + it_behaves_like 'an answer to an existing thread', 'issue' + it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'an email with a labels subscriptions link in its footer' + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has the correct subject' do + is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ + end + + it 'contains the names of the added labels' do + is_expected.to have_body_text /foo, bar, and baz/ + end + + it 'contains a link to the issue' do + is_expected.to have_body_text /#{namespace_project_issue_path project.namespace, project, issue}/ + end + end + describe 'status changed' do let(:status) { 'closed' } subject { Notify.issue_status_changed_email(recipient.id, issue.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -319,6 +171,7 @@ describe Notify do it_behaves_like 'an assignee email' it_behaves_like 'an email starting a new thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -339,12 +192,24 @@ describe Notify do it 'has the correct message-id set' do is_expected.to have_header 'Message-ID', "<merge_request_#{merge_request.id}@#{Gitlab.config.gitlab.host}>" end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text merge_request.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'that are new with a description' do subject { Notify.new_merge_request_email(merge_request_with_description.assignee_id, merge_request_with_description.id) } it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'contains the description' do is_expected.to have_body_text /#{merge_request_with_description.description}/ @@ -357,6 +222,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -381,12 +247,41 @@ describe Notify do end end + describe 'that have been relabeled' do + subject { Notify.relabeled_merge_request_email(recipient.id, merge_request.id, %w[foo bar baz], current_user.id) } + + it_behaves_like 'a multiple recipients email' + it_behaves_like 'an answer to an existing thread', 'merge_request' + it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'a user cannot unsubscribe through footer link' + it_behaves_like 'an email with a labels subscriptions link in its footer' + + it 'is sent as the author' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(current_user.name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has the correct subject' do + is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ + end + + it 'contains the names of the added labels' do + is_expected.to have_body_text /foo, bar, and baz/ + end + + it 'contains a link to the merge request' do + is_expected.to have_body_text /#{namespace_project_merge_request_path project.namespace, project, merge_request}/ + end + end + describe 'status changed' do let(:status) { 'reopened' } subject { Notify.merge_request_status_email(recipient.id, merge_request.id, status, current_user.id) } it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -417,6 +312,7 @@ describe Notify do it_behaves_like 'a multiple recipients email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like "an unsubscribeable thread" it 'is sent as the merge author' do sender = subject.header[:from].addrs[0] @@ -446,6 +342,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Project was moved/ @@ -468,6 +365,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Access to project was granted/ @@ -506,6 +404,21 @@ describe Notify do it 'contains the message from the note' do is_expected.to have_body_text /#{note.note}/ end + + it 'not contains note author' do + is_expected.not_to have_body_text /wrote\:/ + end + + context 'when enabled email_author_in_body' do + before do + allow(current_application_settings).to receive(:email_author_in_body).and_return(true) + end + + it 'contains a link to note author' do + is_expected.to have_body_text note.author_name + is_expected.to have_body_text /wrote\:/ + end + end end describe 'on a commit' do @@ -518,6 +431,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'commit' it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /#{commit.title} \(#{commit.short_id}\)/ @@ -538,6 +452,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'merge_request' it_behaves_like 'it should show Gmail Actions View Merge request link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/ @@ -558,6 +473,7 @@ describe Notify do it_behaves_like 'a note email' it_behaves_like 'an answer to an existing thread', 'issue' it_behaves_like 'it should show Gmail Actions View Issue link' + it_behaves_like 'an unsubscribeable thread' it 'has the correct subject' do is_expected.to have_subject /#{issue.title} \(##{issue.iid}\)/ @@ -579,6 +495,7 @@ describe Notify do it_behaves_like 'an email sent from GitLab' it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" it 'has the correct subject' do is_expected.to have_subject /Access to group was granted/ @@ -607,6 +524,7 @@ describe Notify do subject { ActionMailer::Base.deliveries.last } it_behaves_like 'an email sent from GitLab' + it_behaves_like "a user cannot unsubscribe through footer link" it 'is sent to the new user' do is_expected.to deliver_to 'new-email@mail.com' @@ -629,6 +547,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :create) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -657,6 +578,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :create) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -684,6 +608,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -707,6 +634,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/tags/v1.0', action: :delete) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -734,6 +664,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare, reverse_compare: false, send_from_committer_email: send_from_committer_email) } it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -839,6 +772,9 @@ describe Notify do subject { Notify.repository_push_email(project.id, 'devs@company.name', author_id: user.id, ref: 'refs/heads/master', action: :push, compare: compare) } it_behaves_like 'it should show Gmail Actions View Commit link' + it_behaves_like "a user cannot unsubscribe through footer link" + it_behaves_like 'an email with X-GitLab headers containing project details' + it_behaves_like 'an email that contains a header with author username' it 'is sent as the author' do sender = subject.header[:from].addrs[0] @@ -867,31 +803,4 @@ describe Notify do end end - describe 'build success' do - before { build.success } - - subject { Notify.build_success_email(build.id, 'wow@example.com') } - - it 'has the correct subject' do - should have_subject /Build success for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end - - describe 'build fail' do - before { build.drop } - - subject { Notify.build_fail_email(build.id, 'wow@example.com') } - - it 'has the correct subject' do - should have_subject /Build failed for/ - end - - it 'contains name of project' do - should have_body_text build.project_name - end - end end diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb new file mode 100644 index 00000000000..6019af544d3 --- /dev/null +++ b/spec/mailers/shared/notify.rb @@ -0,0 +1,121 @@ +shared_context 'gitlab email notification' do + let(:gitlab_sender_display_name) { Gitlab.config.gitlab.email_display_name } + let(:gitlab_sender) { Gitlab.config.gitlab.email_from } + let(:gitlab_sender_reply_to) { Gitlab.config.gitlab.email_reply_to } + let(:recipient) { create(:user, email: 'recipient@example.com') } + let(:project) { create(:project) } + let(:new_user_address) { 'newguy@example.com' } + + before do + ActionMailer::Base.deliveries.clear + email = recipient.emails.create(email: "notifications@example.com") + recipient.update_attribute(:notification_email, email.email) + end +end + +shared_examples 'a multiple recipients email' do + it 'is sent to the given recipient' do + is_expected.to deliver_to recipient.notification_email + end +end + +shared_examples 'an email sent from GitLab' do + it 'is sent from GitLab' do + sender = subject.header[:from].addrs[0] + expect(sender.display_name).to eq(gitlab_sender_display_name) + expect(sender.address).to eq(gitlab_sender) + end + + it 'has a Reply-To address' do + reply_to = subject.header[:reply_to].addresses + expect(reply_to).to eq([gitlab_sender_reply_to]) + end +end + +shared_examples 'an email that contains a header with author username' do + it 'has X-GitLab-Author header containing author\'s username' do + is_expected.to have_header 'X-GitLab-Author', user.username + end +end + +shared_examples 'an email with X-GitLab headers containing project details' do + it 'has X-GitLab-Project* headers' do + is_expected.to have_header 'X-GitLab-Project', /#{project.name}/ + is_expected.to have_header 'X-GitLab-Project-Id', /#{project.id}/ + is_expected.to have_header 'X-GitLab-Project-Path', /#{project.path_with_namespace}/ + end +end + +shared_examples 'an email starting a new thread' do |message_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a discussion identifier' do + is_expected.to have_header 'Message-ID', /<#{message_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'an answer to an existing thread' do |thread_id_prefix| + include_examples 'an email with X-GitLab headers containing project details' + + it 'has a subject that begins with Re: ' do + is_expected.to have_subject /^Re: / + end + + it 'has headers that reference an existing thread' do + is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/ + end +end + +shared_examples 'a new user email' do + it 'is sent to the new user' do + is_expected.to deliver_to new_user_address + end + + it 'has the correct subject' do + is_expected.to have_subject /^Account was created for you$/i + end + + it 'contains the new user\'s login name' do + is_expected.to have_body_text /#{new_user_address}/ + end +end + +shared_examples 'it should have Gmail Actions links' do + it { is_expected.to have_body_text /ViewAction/ } +end + +shared_examples 'it should not have Gmail Actions links' do + it { is_expected.to_not have_body_text /ViewAction/ } +end + +shared_examples 'it should show Gmail Actions View Issue link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Issue/ } +end + +shared_examples 'it should show Gmail Actions View Merge request link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Merge request/ } +end + +shared_examples 'it should show Gmail Actions View Commit link' do + it_behaves_like 'it should have Gmail Actions links' + + it { is_expected.to have_body_text /View Commit/ } +end + +shared_examples 'an unsubscribeable thread' do + it { is_expected.to have_body_text /unsubscribe/ } +end + +shared_examples 'a user cannot unsubscribe through footer link' do + it { is_expected.not_to have_body_text /unsubscribe/ } +end + +shared_examples 'an email with a labels subscriptions link in its footer' do + it { is_expected.to have_body_text /label subscriptions/ } +end diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index d45319b25d4..ac12ab6c757 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -13,7 +13,8 @@ require 'rails_helper' RSpec.describe AbuseReport, type: :model do - subject { create(:abuse_report) } + subject { create(:abuse_report) } + let(:user) { create(:user) } it { expect(subject).to be_valid } @@ -26,6 +27,36 @@ RSpec.describe AbuseReport, type: :model do it { is_expected.to validate_presence_of(:reporter) } it { is_expected.to validate_presence_of(:user) } it { is_expected.to validate_presence_of(:message) } - it { is_expected.to validate_uniqueness_of(:user_id) } + it { is_expected.to validate_uniqueness_of(:user_id).with_message('has already been reported') } + end + + describe '#remove_user' do + it 'blocks the user' do + expect { subject.remove_user(deleted_by: user) }.to change { subject.user.blocked? }.to(true) + end + + it 'lets a worker delete the user' do + expect(DeleteUserWorker).to receive(:perform_async).with(user.id, subject.user.id, + delete_solo_owned_groups: true) + + subject.remove_user(deleted_by: user) + end + end + + describe '#notify' do + it 'delivers' do + expect(AbuseReportMailer).to receive(:notify).with(subject.id). + and_return(spy) + + subject.notify + end + + it 'returns early when not persisted' do + report = build(:abuse_report) + + expect(AbuseReportMailer).not_to receive(:notify) + + report.notify + end end end diff --git a/spec/models/appearance_spec.rb b/spec/models/appearance_spec.rb new file mode 100644 index 00000000000..c5658bd26e1 --- /dev/null +++ b/spec/models/appearance_spec.rb @@ -0,0 +1,10 @@ +require 'rails_helper' + +RSpec.describe Appearance, type: :model do + subject { create(:appearance) } + + it { is_expected.to be_valid } + + it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_presence_of(:description) } +end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 35d8220ae54..b1764d7ac09 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -2,32 +2,47 @@ # # Table name: application_settings # -# id :integer not null, primary key -# default_projects_limit :integer -# signup_enabled :boolean -# signin_enabled :boolean -# gravatar_enabled :boolean -# sign_in_text :text -# created_at :datetime -# updated_at :datetime -# home_page_url :string(255) -# default_branch_protection :integer default(2) -# twitter_sharing_enabled :boolean default(TRUE) -# restricted_visibility_levels :text -# version_check_enabled :boolean default(TRUE) -# max_attachment_size :integer default(10), not null -# default_project_visibility :integer -# default_snippet_visibility :integer -# restricted_signup_domains :text -# user_oauth_applications :boolean default(TRUE) -# after_sign_out_path :string(255) -# session_expire_delay :integer default(10080), not null -# import_sources :text -# help_page_text :text -# admin_notification_email :string(255) -# shared_runners_enabled :boolean default(TRUE), not null -# max_artifacts_size :integer default(100), not null -# runners_registration_token :string(255) +# id :integer not null, primary key +# default_projects_limit :integer +# signup_enabled :boolean +# signin_enabled :boolean +# gravatar_enabled :boolean +# sign_in_text :text +# created_at :datetime +# updated_at :datetime +# home_page_url :string(255) +# default_branch_protection :integer default(2) +# twitter_sharing_enabled :boolean default(TRUE) +# restricted_visibility_levels :text +# version_check_enabled :boolean default(TRUE) +# max_attachment_size :integer default(10), not null +# default_project_visibility :integer +# default_snippet_visibility :integer +# restricted_signup_domains :text +# user_oauth_applications :boolean default(TRUE) +# after_sign_out_path :string(255) +# session_expire_delay :integer default(10080), not null +# import_sources :text +# help_page_text :text +# admin_notification_email :string(255) +# shared_runners_enabled :boolean default(TRUE), not null +# max_artifacts_size :integer default(100), not null +# runners_registration_token :string +# require_two_factor_authentication :boolean default(FALSE) +# two_factor_grace_period :integer default(48) +# metrics_enabled :boolean default(FALSE) +# metrics_host :string default("localhost") +# metrics_username :string +# metrics_password :string +# metrics_pool_size :integer default(16) +# metrics_timeout :integer default(10) +# metrics_method_call_threshold :integer default(10) +# recaptcha_enabled :boolean default(FALSE) +# recaptcha_site_key :string +# recaptcha_private_key :string +# metrics_port :integer default(8089) +# sentry_enabled :boolean default(FALSE) +# sentry_dsn :string # require 'spec_helper' @@ -51,6 +66,18 @@ describe ApplicationSetting, models: true do it { is_expected.to allow_value(http).for(:after_sign_out_path) } it { is_expected.to allow_value(https).for(:after_sign_out_path) } it { is_expected.not_to allow_value(ftp).for(:after_sign_out_path) } + + it { is_expected.to validate_presence_of(:max_attachment_size) } + + it do + is_expected.to validate_numericality_of(:max_attachment_size) + .only_integer + .is_greater_than(0) + end + + it_behaves_like 'an object with email-formated attributes', :admin_notification_email do + subject { setting } + end end context 'restricted signup domains' do diff --git a/spec/models/blob_spec.rb b/spec/models/blob_spec.rb new file mode 100644 index 00000000000..78e95c8fac5 --- /dev/null +++ b/spec/models/blob_spec.rb @@ -0,0 +1,81 @@ +require 'rails_helper' + +describe Blob do + describe '.decorate' do + it 'returns NilClass when given nil' do + expect(described_class.decorate(nil)).to be_nil + end + end + + describe '#svg?' do + it 'is falsey when not text' do + git_blob = double(text?: false) + + expect(described_class.decorate(git_blob)).not_to be_svg + end + + it 'is falsey when no language is detected' do + git_blob = double(text?: true, language: nil) + + expect(described_class.decorate(git_blob)).not_to be_svg + end + + it' is falsey when language is not SVG' do + git_blob = double(text?: true, language: double(name: 'XML')) + + expect(described_class.decorate(git_blob)).not_to be_svg + end + + it 'is truthy when language is SVG' do + git_blob = double(text?: true, language: double(name: 'SVG')) + + expect(described_class.decorate(git_blob)).to be_svg + end + end + + describe '#to_partial_path' do + def stubbed_blob(overrides = {}) + overrides.reverse_merge!( + image?: false, + language: nil, + lfs_pointer?: false, + svg?: false, + text?: false + ) + + described_class.decorate(double).tap do |blob| + allow(blob).to receive_messages(overrides) + end + end + + it 'handles LFS pointers' do + blob = stubbed_blob(lfs_pointer?: true) + + expect(blob.to_partial_path).to eq 'download' + end + + it 'handles SVGs' do + blob = stubbed_blob(text?: true, svg?: true) + + expect(blob.to_partial_path).to eq 'image' + end + + it 'handles images' do + blob = stubbed_blob(image?: true) + + expect(blob.to_partial_path).to eq 'image' + end + + it 'handles text' do + blob = stubbed_blob(text?: true) + + expect(blob.to_partial_path).to eq 'text' + end + + it 'defaults to download' do + blob = stubbed_blob + + expect(blob.to_partial_path).to eq 'download' + end + end +end diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index e4cac105110..f6f84db57e6 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -6,7 +6,6 @@ # message :text not null # starts_at :datetime # ends_at :datetime -# alert_type :integer # created_at :datetime # updated_at :datetime # color :string(255) @@ -16,6 +15,8 @@ require 'spec_helper' describe BroadcastMessage, models: true do + include ActiveSupport::Testing::TimeHelpers + subject { create(:broadcast_message) } it { is_expected.to be_valid } @@ -35,20 +36,79 @@ describe BroadcastMessage, models: true do it { is_expected.not_to allow_value('000').for(:font) } end - describe :current do + describe '.current' do it "should return last message if time match" do - broadcast_message = create(:broadcast_message, starts_at: Time.now.yesterday, ends_at: Time.now.tomorrow) - expect(BroadcastMessage.current).to eq(broadcast_message) + message = create(:broadcast_message) + + expect(BroadcastMessage.current).to eq message end it "should return nil if time not come" do - create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days) + create(:broadcast_message, :future) + expect(BroadcastMessage.current).to be_nil end it "should return nil if time has passed" do - create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday) + create(:broadcast_message, :expired) + expect(BroadcastMessage.current).to be_nil end end + + describe '#active?' do + it 'is truthy when started and not ended' do + message = build(:broadcast_message) + + expect(message).to be_active + end + + it 'is falsey when ended' do + message = build(:broadcast_message, :expired) + + expect(message).not_to be_active + end + + it 'is falsey when not started' do + message = build(:broadcast_message, :future) + + expect(message).not_to be_active + end + end + + describe '#started?' do + it 'is truthy when starts_at has passed' do + message = build(:broadcast_message) + + travel_to(3.days.from_now) do + expect(message).to be_started + end + end + + it 'is falsey when starts_at is in the future' do + message = build(:broadcast_message) + + travel_to(3.days.ago) do + expect(message).not_to be_started + end + end + end + + describe '#ended?' do + it 'is truthy when ends_at has passed' do + message = build(:broadcast_message) + + travel_to(3.days.from_now) do + expect(message).to be_ended + end + end + + it 'is falsey when ends_at is in the future' do + message = build(:broadcast_message) + + travel_to(3.days.ago) do + expect(message).not_to be_ended + end + end + end end diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index 1c22e3cb7c4..b7457808040 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1,32 +1,7 @@ -# == Schema Information -# -# Table name: builds -# -# id :integer not null, primary key -# project_id :integer -# status :string(255) -# finished_at :datetime -# trace :text -# created_at :datetime -# updated_at :datetime -# started_at :datetime -# runner_id :integer -# commit_id :integer -# coverage :float -# commands :text -# job_id :integer -# name :string(255) -# deploy :boolean default(FALSE) -# options :text -# allow_failure :boolean default(FALSE), not null -# stage :string(255) -# trigger_request_id :integer -# - require 'spec_helper' describe Ci::Build, models: true do - let(:project) { FactoryGirl.create :empty_project } + let(:project) { FactoryGirl.create :project } let(:commit) { FactoryGirl.create :ci_commit, project: project } let(:build) { FactoryGirl.create :ci_build, commit: commit } @@ -34,7 +9,7 @@ describe Ci::Build, models: true do it { is_expected.to respond_to :trace_html } - describe :first_pending do + describe '#first_pending' do let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday } let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' } before { first; second } @@ -44,7 +19,7 @@ describe Ci::Build, models: true do it('returns with the first pending build') { is_expected.to eq(first) } end - describe :create_from do + describe '#create_from' do before do build.status = 'success' build.save @@ -58,7 +33,7 @@ describe Ci::Build, models: true do end end - describe :ignored? do + describe '#ignored?' do subject { build.ignored? } context 'if build is not allowed to fail' do @@ -94,7 +69,7 @@ describe Ci::Build, models: true do end end - describe :trace do + describe '#trace' do subject { build.trace_html } it { is_expected.to be_empty } @@ -126,7 +101,7 @@ describe Ci::Build, models: true do # it { is_expected.to eq(commit.project.timeout) } # end - describe :options do + describe '#options' do let(:options) do { image: "ruby:2.1", @@ -147,25 +122,25 @@ describe Ci::Build, models: true do # it { is_expected.to eq(project.allow_git_fetch) } # end - describe :project do + describe '#project' do subject { build.project } it { is_expected.to eq(commit.project) } end - describe :project_id do + describe '#project_id' do subject { build.project_id } it { is_expected.to eq(commit.project_id) } end - describe :project_name do + describe '#project_name' do subject { build.project_name } it { is_expected.to eq(project.name) } end - describe :extract_coverage do + describe '#extract_coverage' do context 'valid content & regex' do subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') } @@ -197,7 +172,7 @@ describe Ci::Build, models: true do end end - describe :variables do + describe '#variables' do context 'returns variables' do subject { build.variables } @@ -267,8 +242,8 @@ describe Ci::Build, models: true do end end - describe :can_be_served? do - let(:runner) { FactoryGirl.create :ci_specific_runner } + describe '#can_be_served?' do + let(:runner) { FactoryGirl.create :ci_runner } before { build.project.runners << runner } @@ -302,7 +277,7 @@ describe Ci::Build, models: true do end end - describe :any_runners_online? do + describe '#any_runners_online?' do subject { build.any_runners_online? } context 'when no runners' do @@ -310,7 +285,7 @@ describe Ci::Build, models: true do end context 'if there are runner' do - let(:runner) { FactoryGirl.create :ci_specific_runner } + let(:runner) { FactoryGirl.create :ci_runner } before do build.project.runners << runner @@ -337,8 +312,8 @@ describe Ci::Build, models: true do end end - describe :show_warning? do - subject { build.show_warning? } + describe '#stuck?' do + subject { build.stuck? } %w(pending).each do |state| context "if commit_status.status is #{state}" do @@ -347,7 +322,7 @@ describe Ci::Build, models: true do it { is_expected.to be_truthy } context "and there are specific runner" do - let(:runner) { FactoryGirl.create :ci_specific_runner, contacted_at: 1.second.ago } + let(:runner) { FactoryGirl.create :ci_runner, contacted_at: 1.second.ago } before do build.project.runners << runner @@ -368,22 +343,34 @@ describe Ci::Build, models: true do end end - describe :download_url do - subject { build.download_url } + describe '#artifacts?' do + subject { build.artifacts? } - it "should be nil if artifact doesn't exist" do - build.update_attributes(artifacts_file: nil) - is_expected.to be_nil + context 'artifacts archive does not exist' do + before { build.update_attributes(artifacts_file: nil) } + it { is_expected.to be_falsy } end - it 'should be nil if artifact exist' do - gif = fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') - build.update_attributes(artifacts_file: gif) - is_expected.to_not be_nil + context 'artifacts archive exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } + end + end + + + describe '#artifacts_metadata?' do + subject { build.artifacts_metadata? } + context 'artifacts metadata does not exist' do + it { is_expected.to be_falsy } + end + + context 'artifacts archive is a zip file and metadata exists' do + let(:build) { create(:ci_build, :artifacts) } + it { is_expected.to be_truthy } end end - describe :repo_url do + describe '#repo_url' do let(:build) { FactoryGirl.create :ci_build } let(:project) { build.project } @@ -397,6 +384,30 @@ describe Ci::Build, models: true do it { is_expected.to include(project.web_url[7..-1]) } end + describe '#depends_on_builds' do + let!(:build) { FactoryGirl.create :ci_build, commit: commit, name: 'build', stage_idx: 0, stage: 'build' } + let!(:rspec_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rspec', stage_idx: 1, stage: 'test' } + let!(:rubocop_test) { FactoryGirl.create :ci_build, commit: commit, name: 'rubocop', stage_idx: 1, stage: 'test' } + let!(:staging) { FactoryGirl.create :ci_build, commit: commit, name: 'staging', stage_idx: 2, stage: 'deploy' } + + it 'to have no dependents if this is first build' do + expect(build.depends_on_builds).to be_empty + end + + it 'to have one dependent if this is test' do + expect(rspec_test.depends_on_builds.map(&:id)).to contain_exactly(build.id) + end + + it 'to have all builds from build and test stage if this is last' do + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, rspec_test.id, rubocop_test.id) + end + + it 'to have retried builds instead the original ones' do + retried_rspec = Ci::Build.retry(rspec_test) + expect(staging.depends_on_builds.map(&:id)).to contain_exactly(build.id, retried_rspec.id, rubocop_test.id) + end + end + def create_mr(build, commit, factory: :merge_request, created_at: Time.now) FactoryGirl.create(factory, source_project_id: commit.gl_project_id, @@ -405,7 +416,7 @@ describe Ci::Build, models: true do created_at: created_at) end - describe :merge_request do + describe '#merge_request' do context 'when a MR has a reference to the commit' do before do @merge_request = create_mr(build, commit, factory: :merge_request) @@ -458,6 +469,103 @@ describe Ci::Build, models: true do expect(@build2.merge_request.id).to eq(@merge_request.id) end end + end + + describe 'build erasable' do + shared_examples 'erasable' do + it 'should remove artifact file' do + expect(build.artifacts_file.exists?).to be_falsy + end + + it 'should remove artifact metadata file' do + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'should erase build trace in trace file' do + expect(build.trace).to be_empty + end + + it 'should set erased to true' do + expect(build.erased?).to be true + end + + it 'should set erase date' do + expect(build.erased_at).to_not be_falsy + end + end + + context 'build is not erasable' do + let!(:build) { create(:ci_build) } + + describe '#erase' do + subject { build.erase } + it { is_expected.to be false } + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq false } + end + end + + context 'build is erasable' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + + describe '#erase' do + before { build.erase(erased_by: user) } + + context 'erased by user' do + let!(:user) { create(:user, username: 'eraser') } + + include_examples 'erasable' + + it 'should record user who erased a build' do + expect(build.erased_by).to eq user + end + end + + context 'erased by system' do + let(:user) { nil } + + include_examples 'erasable' + + it 'should not set user who erased a build' do + expect(build.erased_by).to be_nil + end + end + end + + describe '#erasable?' do + subject { build.erasable? } + it { is_expected.to eq true } + end + + describe '#erased?' do + let!(:build) { create(:ci_build, :trace, :success, :artifacts) } + subject { build.erased? } + + context 'build has not been erased' do + it { is_expected.to be false } + end + + context 'build has been erased' do + before { build.erase } + + it { is_expected.to be true } + end + end + + context 'metadata and build trace are not available' do + let!(:build) { create(:ci_build, :success, :artifacts) } + before { build.remove_artifacts_metadata! } + + describe '#erase' do + it 'should not raise error' do + expect { build.erase }.to_not raise_error + end + end + end + end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb new file mode 100644 index 00000000000..36d10636ae9 --- /dev/null +++ b/spec/models/ci/build_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Ci::Build, models: true do + let(:build) { create(:ci_build) } + let(:test_trace) { 'This is a test' } + + describe '#trace' do + it 'obfuscates project runners token' do + allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}") + + expect(build.trace).to eq("Test: xxxxxx") + end + + it 'empty project runners token' do + allow(build).to receive(:raw_trace).and_return(test_trace) + # runners_token can't normally be set to nil + allow(build.project).to receive(:runners_token).and_return(nil) + + expect(build.trace).to eq(test_trace) + end + end +end diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index b193e16e7f8..412842337ba 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -13,7 +13,7 @@ # tag :boolean default(FALSE) # yaml_errors :text # committed_at :datetime -# project_id :integer +# gl_project_id :integer # require 'spec_helper' @@ -32,50 +32,6 @@ describe Ci::Commit, models: true do it { is_expected.to respond_to :git_author_email } it { is_expected.to respond_to :short_sha } - describe :ordered do - let(:project) { FactoryGirl.create :empty_project } - - it 'returns ordered list of commits' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project - expect(project.ci_commits.ordered).to eq([commit2, commit1]) - end - - it 'returns commits ordered by committed_at and id, with nulls last' do - commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, project: project - commit2 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hours.ago, project: project - commit4 = FactoryGirl.create :ci_commit, committed_at: nil, project: project - expect(project.ci_commits.ordered).to eq([commit2, commit4, commit3, commit1]) - end - end - - describe :last_build do - subject { commit.last_build } - before do - @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday - @second = FactoryGirl.create :ci_build, commit: commit - end - - it { is_expected.to be_a(Ci::Build) } - it('returns with the most recently created build') { is_expected.to eq(@second) } - end - - describe :retry do - before do - @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday - @second = FactoryGirl.create :ci_build, commit: commit - end - - it "creates only a new build" do - expect(commit.builds.count(:all)).to eq 2 - expect(commit.statuses.count(:all)).to eq 2 - commit.retry - expect(commit.builds.count(:all)).to eq 3 - expect(commit.statuses.count(:all)).to eq 3 - end - end - describe :valid_commit_sha do context 'commit.sha can not start with 00000000' do before do @@ -247,6 +203,35 @@ describe Ci::Commit, models: true do end end + + context 'custom stage with first job allowed to fail' do + let(:yaml) do + { + stages: ['clean', 'test'], + clean_job: { + stage: 'clean', + allow_failure: true, + script: 'BUILD', + }, + test_job: { + stage: 'test', + script: 'TEST', + }, + } + end + + before do + stub_ci_commit_yaml_file(YAML.dump(yaml)) + create_builds + end + + it 'properly schedules builds' do + expect(commit.builds.pluck(:status)).to contain_exactly('pending') + commit.builds.running_or_pending.each(&:drop) + expect(commit.builds.pluck(:status)).to contain_exactly('pending', 'failed') + end + end + context 'properly creates builds when "when" is defined' do let(:yaml) do { diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb index da8491357a5..000a732db77 100644 --- a/spec/models/ci/runner_project_spec.rb +++ b/spec/models/ci/runner_project_spec.rb @@ -2,11 +2,12 @@ # # Table name: ci_runner_projects # -# id :integer not null, primary key -# runner_id :integer not null -# project_id :integer not null -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# runner_id :integer not null +# project_id :integer +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 232760dfeba..25e9e5eca48 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -39,7 +39,7 @@ describe Ci::Runner, models: true do describe :assign_to do let!(:project) { FactoryGirl.create :empty_project } - let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) } + let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) } before { shared_runner.assign_to(project) } @@ -52,15 +52,15 @@ describe Ci::Runner, models: true do subject { Ci::Runner.online } before do - @runner1 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.year.ago) - @runner2 = FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) + @runner1 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.year.ago) + @runner2 = FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) end it { is_expected.to eq([@runner2])} end describe :online? do - let(:runner) { FactoryGirl.create(:ci_shared_runner) } + let(:runner) { FactoryGirl.create(:ci_runner, :shared) } subject { runner.online? } @@ -84,7 +84,7 @@ describe Ci::Runner, models: true do end describe :status do - let(:runner) { FactoryGirl.create(:ci_shared_runner, contacted_at: 1.second.ago) } + let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) } subject { runner.status } @@ -115,7 +115,7 @@ describe Ci::Runner, models: true do describe "belongs_to_one_project?" do it "returns false if there are two projects runner assigned to" do - runner = FactoryGirl.create(:ci_specific_runner) + runner = FactoryGirl.create(:ci_runner) project = FactoryGirl.create(:empty_project) project1 = FactoryGirl.create(:empty_project) project.runners << runner @@ -125,11 +125,39 @@ describe Ci::Runner, models: true do end it "returns true" do - runner = FactoryGirl.create(:ci_specific_runner) + runner = FactoryGirl.create(:ci_runner) project = FactoryGirl.create(:empty_project) project.runners << runner expect(runner.belongs_to_one_project?).to be_truthy end end + + describe '#search' do + let(:runner) { create(:ci_runner, token: '123abc') } + + it 'returns runners with a matching token' do + expect(described_class.search(runner.token)).to eq([runner]) + end + + it 'returns runners with a partially matching token' do + expect(described_class.search(runner.token[0..2])).to eq([runner]) + end + + it 'returns runners with a matching token regardless of the casing' do + expect(described_class.search(runner.token.upcase)).to eq([runner]) + end + + it 'returns runners with a matching description' do + expect(described_class.search(runner.description)).to eq([runner]) + end + + it 'returns runners with a partially matching description' do + expect(described_class.search(runner.description[0..2])).to eq([runner]) + end + + it 'returns runners with a matching description regardless of the casing' do + expect(described_class.search(runner.description.upcase)).to eq([runner]) + end + end end diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb index cb2f51e2011..159be939300 100644 --- a/spec/models/ci/trigger_spec.rb +++ b/spec/models/ci/trigger_spec.rb @@ -2,12 +2,13 @@ # # Table name: ci_triggers # -# id :integer not null, primary key -# token :string(255) -# project_id :integer not null -# deleted_at :datetime -# created_at :datetime -# updated_at :datetime +# id :integer not null, primary key +# token :string(255) +# project_id :integer +# deleted_at :datetime +# created_at :datetime +# updated_at :datetime +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb index 31b56953a13..71e84091cb7 100644 --- a/spec/models/ci/variable_spec.rb +++ b/spec/models/ci/variable_spec.rb @@ -3,12 +3,13 @@ # Table name: ci_variables # # id :integer not null, primary key -# project_id :integer not null +# project_id :integer # key :string(255) # value :text # encrypted_value :text # encrypted_value_salt :string(255) # encrypted_value_iv :string(255) +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index ecf37b40c58..0e9111c8029 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -86,10 +86,21 @@ eos let(:issue) { create :issue, project: project } let(:other_project) { create :project, :public } let(:other_issue) { create :issue, project: other_project } + let(:commiter) { create :user } + + before do + project.team << [commiter, :developer] + other_project.team << [commiter, :developer] + end it 'detects issues that this commit is marked as closing' do ext_ref = "#{other_project.path_with_namespace}##{other_issue.iid}" - allow(commit).to receive(:safe_message).and_return("Fixes ##{issue.iid} and #{ext_ref}") + + allow(commit).to receive_messages( + safe_message: "Fixes ##{issue.iid} and #{ext_ref}", + committer_email: commiter.email + ) + expect(commit.closes_issues).to include(issue) expect(commit.closes_issues).to include(other_issue) end @@ -118,4 +129,38 @@ eos it { expect(data[:modified]).to eq([".gitmodules"]) } it { expect(data[:removed]).to eq([]) } end + + describe '#reverts_commit?' do + let(:another_commit) { double(:commit, revert_description: "This reverts commit #{commit.sha}") } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + + context 'commit has no description' do + before { allow(commit).to receive(:description?).and_return(false) } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + end + + context "another_commit's description does not revert commit" do + before { allow(commit).to receive(:description).and_return("Foo Bar") } + + it { expect(commit.reverts_commit?(another_commit)).to be_falsy } + end + + context "another_commit's description reverts commit" do + before { allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") } + + it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + end + + context "another_commit's description reverts merged merge request" do + before do + revert_description = "This reverts merge request !foo123" + allow(another_commit).to receive(:revert_description).and_return(revert_description) + allow(commit).to receive(:description).and_return("Foo #{another_commit.revert_description} Bar") + end + + it { expect(commit.reverts_commit?(another_commit)).to be_truthy } + end + end end diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb index b8f901b3433..82c68ff6cb1 100644 --- a/spec/models/commit_status_spec.rb +++ b/spec/models/commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/concerns/case_sensitivity_spec.rb b/spec/models/concerns/case_sensitivity_spec.rb index 25b3f4e50da..92fdc5cd65d 100644 --- a/spec/models/concerns/case_sensitivity_spec.rb +++ b/spec/models/concerns/case_sensitivity_spec.rb @@ -37,7 +37,7 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."bar") = LOWER(:value)}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')).to eq(criteria) + expect(model.iwhere('foo.bar'.to_sym => 'bar')).to eq(criteria) end end @@ -87,8 +87,8 @@ describe CaseSensitivity, models: true do with(%q{LOWER("foo"."baz") = LOWER(:value)}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end @@ -127,7 +127,7 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`bar` = :value}, value: 'bar'). and_return(criteria) - expect(model.iwhere(:'foo.bar' => 'bar')). + expect(model.iwhere('foo.bar'.to_sym => 'bar')). to eq(criteria) end end @@ -178,8 +178,8 @@ describe CaseSensitivity, models: true do with(%q{`foo`.`baz` = :value}, value: 'baz'). and_return(final) - got = model.iwhere(:'foo.bar' => 'bar', - :'foo.baz' => 'baz') + got = model.iwhere('foo.bar'.to_sym => 'bar', + 'foo.baz'.to_sym => 'baz') expect(got).to eq(final) end diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 021d62cdf0c..be29b6d66ff 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -32,9 +32,54 @@ describe Issue, "Issuable" do describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable issue") } - it "matches by title" do + it 'returns notes with a matching title' do + expect(described_class.search(searchable_issue.title)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching title' do expect(described_class.search('able')).to eq([searchable_issue]) end + + it 'returns notes with a matching title regardless of the casing' do + expect(described_class.search(searchable_issue.title.upcase)). + to eq([searchable_issue]) + end + end + + describe ".full_search" do + let!(:searchable_issue) do + create(:issue, title: "Searchable issue", description: 'kittens') + end + + it 'returns notes with a matching title' do + expect(described_class.full_search(searchable_issue.title)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching title' do + expect(described_class.full_search('able')).to eq([searchable_issue]) + end + + it 'returns notes with a matching title regardless of the casing' do + expect(described_class.full_search(searchable_issue.title.upcase)). + to eq([searchable_issue]) + end + + it 'returns notes with a matching description' do + expect(described_class.full_search(searchable_issue.description)). + to eq([searchable_issue]) + end + + it 'returns notes with a partially matching description' do + expect(described_class.full_search(searchable_issue.description)). + to eq([searchable_issue]) + end + + it 'returns notes with a matching description regardless of the casing' do + expect(described_class.full_search(searchable_issue.description.upcase)). + to eq([searchable_issue]) + end end describe "#today?" do @@ -68,18 +113,71 @@ describe Issue, "Issuable" do end end + describe '#subscribed?' do + context 'user is not a participant in the issue' do + before { allow(issue).to receive(:participants).with(user).and_return([]) } + + it 'returns false when no subcription exists' do + expect(issue.subscribed?(user)).to be_falsey + end + + it 'returns true when a subcription exists and subscribed is true' do + issue.subscriptions.create(user: user, subscribed: true) + + expect(issue.subscribed?(user)).to be_truthy + end + + it 'returns false when a subcription exists and subscribed is false' do + issue.subscriptions.create(user: user, subscribed: false) + + expect(issue.subscribed?(user)).to be_falsey + end + end + + context 'user is a participant in the issue' do + before { allow(issue).to receive(:participants).with(user).and_return([user]) } + + it 'returns false when no subcription exists' do + expect(issue.subscribed?(user)).to be_truthy + end + + it 'returns true when a subcription exists and subscribed is true' do + issue.subscriptions.create(user: user, subscribed: true) + + expect(issue.subscribed?(user)).to be_truthy + end + + it 'returns false when a subcription exists and subscribed is false' do + issue.subscriptions.create(user: user, subscribed: false) + + expect(issue.subscribed?(user)).to be_falsey + end + end + end + describe "#to_hook_data" do - let(:hook_data) { issue.to_hook_data(user) } + let(:data) { issue.to_hook_data(user) } + let(:project) { issue.project } + it "returns correct hook data" do - expect(hook_data[:object_kind]).to eq("issue") - expect(hook_data[:user]).to eq(user.hook_attrs) - expect(hook_data[:repository][:name]).to eq(issue.project.name) - expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo) - expect(hook_data[:repository][:description]).to eq(issue.project.description) - expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url) - expect(hook_data[:object_attributes]).to eq(issue.hook_attrs) + expect(data[:object_kind]).to eq("issue") + expect(data[:user]).to eq(user.hook_attrs) + expect(data[:object_attributes]).to eq(issue.hook_attrs) + expect(data).to_not have_key(:assignee) end + + context "issue is assigned" do + before { issue.update_attribute(:assignee, user) } + + it "returns correct hook data" do + expect(data[:object_attributes]['assignee_id']).to eq(user.id) + expect(data[:assignee]).to eq(user.hook_attrs) + end + end + + include_examples 'project hook data' + include_examples 'deprecated repository hook data' end describe '#card_attributes' do diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb index 20f0c561e44..cb33edde820 100644 --- a/spec/models/concerns/mentionable_spec.rb +++ b/spec/models/concerns/mentionable_spec.rb @@ -48,7 +48,8 @@ describe Issue, "Mentionable" do describe '#create_new_cross_references!' do let(:project) { create(:project) } - let(:issues) { create_list(:issue, 2, project: project) } + let(:author) { create(:author) } + let(:issues) { create_list(:issue, 2, project: project, author: author) } context 'before changes are persisted' do it 'ignores pre-existing references' do @@ -91,7 +92,7 @@ describe Issue, "Mentionable" do end def create_issue(description:) - create(:issue, project: project, description: description) + create(:issue, project: project, description: description, author: author) end end end diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb new file mode 100644 index 00000000000..47c3be673c5 --- /dev/null +++ b/spec/models/concerns/milestoneish_spec.rb @@ -0,0 +1,104 @@ +require 'spec_helper' + +describe Milestone, 'Milestoneish' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:project, :public) } + let(:milestone) { create(:milestone, project: project) } + let!(:issue) { create(:issue, project: project, milestone: milestone) } + let!(:security_issue_1) { create(:issue, :confidential, project: project, author: author, milestone: milestone) } + let!(:security_issue_2) { create(:issue, :confidential, project: project, assignee: assignee, milestone: milestone) } + let!(:closed_issue_1) { create(:issue, :closed, project: project, milestone: milestone) } + let!(:closed_issue_2) { create(:issue, :closed, project: project, milestone: milestone) } + let!(:closed_security_issue_1) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) } + let!(:closed_security_issue_2) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) } + let!(:closed_security_issue_3) { create(:issue, :confidential, :closed, project: project, author: author, milestone: milestone) } + let!(:closed_security_issue_4) { create(:issue, :confidential, :closed, project: project, assignee: assignee, milestone: milestone) } + let!(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) } + + before do + project.team << [member, :developer] + end + + describe '#closed_items_count' do + it 'should not count confidential issues for non project members' do + expect(milestone.closed_items_count(non_member)).to eq 2 + end + + it 'should count confidential issues for author' do + expect(milestone.closed_items_count(author)).to eq 4 + end + + it 'should count confidential issues for assignee' do + expect(milestone.closed_items_count(assignee)).to eq 4 + end + + it 'should count confidential issues for project members' do + expect(milestone.closed_items_count(member)).to eq 6 + end + + it 'should count all issues for admin' do + expect(milestone.closed_items_count(admin)).to eq 6 + end + end + + describe '#total_items_count' do + it 'should not count confidential issues for non project members' do + expect(milestone.total_items_count(non_member)).to eq 4 + end + + it 'should count confidential issues for author' do + expect(milestone.total_items_count(author)).to eq 7 + end + + it 'should count confidential issues for assignee' do + expect(milestone.total_items_count(assignee)).to eq 7 + end + + it 'should count confidential issues for project members' do + expect(milestone.total_items_count(member)).to eq 10 + end + + it 'should count all issues for admin' do + expect(milestone.total_items_count(admin)).to eq 10 + end + end + + describe '#complete?' do + it 'returns false when has items opened' do + expect(milestone.complete?(non_member)).to eq false + end + + it 'returns true when all items are closed' do + issue.close + merge_request.close + + expect(milestone.complete?(non_member)).to eq true + end + end + + describe '#percent_complete' do + it 'should not count confidential issues for non project members' do + expect(milestone.percent_complete(non_member)).to eq 50 + end + + it 'should count confidential issues for author' do + expect(milestone.percent_complete(author)).to eq 57 + end + + it 'should count confidential issues for assignee' do + expect(milestone.percent_complete(assignee)).to eq 57 + end + + it 'should count confidential issues for project members' do + expect(milestone.percent_complete(member)).to eq 60 + end + + it 'should count confidential issues for admin' do + expect(milestone.percent_complete(admin)).to eq 60 + end + end +end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb new file mode 100644 index 00000000000..e31fdb0bffb --- /dev/null +++ b/spec/models/concerns/subscribable_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe Subscribable, 'Subscribable' do + let(:resource) { create(:issue) } + let(:user) { create(:user) } + + describe '#subscribed?' do + it 'returns false when no subcription exists' do + expect(resource.subscribed?(user)).to be_falsey + end + + it 'returns true when a subcription exists and subscribed is true' do + resource.subscriptions.create(user: user, subscribed: true) + + expect(resource.subscribed?(user)).to be_truthy + end + + it 'returns false when a subcription exists and subscribed is false' do + resource.subscriptions.create(user: user, subscribed: false) + + expect(resource.subscribed?(user)).to be_falsey + end + end + describe '#subscribers' do + it 'returns [] when no subcribers exists' do + expect(resource.subscribers).to be_empty + end + + it 'returns the subscribed users' do + resource.subscriptions.create(user: user, subscribed: true) + resource.subscriptions.create(user: create(:user), subscribed: false) + + expect(resource.subscribers).to eq [user] + end + end + + describe '#toggle_subscription' do + it 'toggles the current subscription state for the given user' do + expect(resource.subscribed?(user)).to be_falsey + + resource.toggle_subscription(user) + + expect(resource.subscribed?(user)).to be_truthy + end + end + + describe '#unsubscribe' do + it 'unsubscribes the given current user' do + resource.subscriptions.create(user: user, subscribed: true) + expect(resource.subscribed?(user)).to be_truthy + + resource.unsubscribe(user) + + expect(resource.subscribed?(user)).to be_falsey + end + end +end diff --git a/spec/models/email_spec.rb b/spec/models/email_spec.rb new file mode 100644 index 00000000000..a20a6149649 --- /dev/null +++ b/spec/models/email_spec.rb @@ -0,0 +1,22 @@ +# == Schema Information +# +# Table name: emails +# +# id :integer not null, primary key +# user_id :integer not null +# email :string(255) not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Email, models: true do + + describe 'validations' do + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:email) } + end + end + +end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 071582b0282..5fe44246738 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -65,23 +65,38 @@ describe Event, models: true do it { expect(@event.author).to eq(@user) } end - describe '.latest_update_time' do - describe 'when events are present' do - let(:time) { Time.utc(2015, 1, 1) } + describe '#proper?' do + context 'issue event' do + let(:project) { create(:empty_project, :public) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:user) } + let(:admin) { create(:admin) } + let(:event) { Event.new(project: project, action: Event::CREATED, target: issue, author_id: author.id) } before do - create(:closed_issue_event, updated_at: time) - create(:closed_issue_event, updated_at: time + 5) + project.team << [member, :developer] end - it 'returns the latest update time' do - expect(Event.latest_update_time).to eq(time + 5) + context 'for non confidential issues' do + let(:issue) { create(:issue, project: project, author: author, assignee: assignee) } + + it { expect(event.proper?(non_member)).to eq true } + it { expect(event.proper?(author)).to eq true } + it { expect(event.proper?(assignee)).to eq true } + it { expect(event.proper?(member)).to eq true } + it { expect(event.proper?(admin)).to eq true } end - end - describe 'when no events exist' do - it 'returns nil' do - expect(Event.latest_update_time).to be_nil + context 'for confidential issues' do + let(:issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + + it { expect(event.proper?(non_member)).to eq false } + it { expect(event.proper?(author)).to eq true } + it { expect(event.proper?(assignee)).to eq true } + it { expect(event.proper?(member)).to eq true } + it { expect(event.proper?(admin)).to eq true } end end end diff --git a/spec/models/external_issue_spec.rb b/spec/models/external_issue_spec.rb index 6ec6b9037a4..9b144dd1ecc 100644 --- a/spec/models/external_issue_spec.rb +++ b/spec/models/external_issue_spec.rb @@ -10,6 +10,21 @@ describe ExternalIssue, models: true do it { is_expected.to include_module(Referable) } end + describe '.reference_pattern' do + it 'allows underscores in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + + it 'allows numbers in the project name' do + expect(ExternalIssue.reference_pattern.match('EXT3_EXT-1234')[0]).to eq 'EXT3_EXT-1234' + end + + it 'requires the project name to begin with A-Z' do + expect(ExternalIssue.reference_pattern.match('3EXT_EXT-1234')).to eq nil + expect(ExternalIssue.reference_pattern.match('EXT_EXT-1234')[0]).to eq 'EXT_EXT-1234' + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(issue.to_reference).to eq issue.id diff --git a/spec/models/external_wiki_service_spec.rb b/spec/models/external_wiki_service_spec.rb index b198aa77526..d37978720bf 100644 --- a/spec/models/external_wiki_service_spec.rb +++ b/spec/models/external_wiki_service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/generic_commit_status_spec.rb b/spec/models/generic_commit_status_spec.rb index d61c1c96bde..5b0883d8702 100644 --- a/spec/models/generic_commit_status_spec.rb +++ b/spec/models/generic_commit_status_spec.rb @@ -29,6 +29,7 @@ # target_url :string(255) # description :string(255) # artifacts_file :text +# gl_project_id :integer # require 'spec_helper' diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 646f767e6fe..c9245fc9535 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' @@ -38,14 +37,6 @@ describe Group, models: true do it { is_expected.not_to validate_presence_of :owner } end - describe '.public_and_given_groups' do - let!(:public_group) { create(:group, public: true) } - - subject { described_class.public_and_given_groups([group.id]) } - - it { is_expected.to eq([public_group, group]) } - end - describe '.visible_to_user' do let!(:group) { create(:group) } let!(:user) { create(:user) } @@ -113,22 +104,29 @@ describe Group, models: true do end end - describe "public_profile?" do - it "returns true for public group" do - group = create(:group, public: true) - expect(group.public_profile?).to be_truthy + describe '.search' do + it 'returns groups with a matching name' do + expect(described_class.search(group.name)).to eq([group]) + end + + it 'returns groups with a partially matching name' do + expect(described_class.search(group.name[0..2])).to eq([group]) + end + + it 'returns groups with a matching name regardless of the casing' do + expect(described_class.search(group.name.upcase)).to eq([group]) + end + + it 'returns groups with a matching path' do + expect(described_class.search(group.path)).to eq([group]) end - it "returns true for non-public group with public project" do - group = create(:group) - create(:project, :public, group: group) - expect(group.public_profile?).to be_truthy + it 'returns groups with a partially matching path' do + expect(described_class.search(group.path[0..2])).to eq([group]) end - it "returns false for non-public group with no public projects" do - group = create(:group) - create(:project, group: group) - expect(group.public_profile?).to be_falsy + it 'returns groups with a matching path regardless of the casing' do + expect(described_class.search(group.path.upcase)).to eq([group]) end end end diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb index 645ee0b929a..983848392b7 100644 --- a/spec/models/hooks/project_hook_spec.rb +++ b/spec/models/hooks/project_hook_spec.rb @@ -19,6 +19,10 @@ require 'spec_helper' describe ProjectHook, models: true do + describe "Associations" do + it { is_expected.to belong_to :project } + end + describe '.push_hooks' do it 'should return hooks for push events only' do hook = create(:project_hook, push_events: true) diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb index 1455661485b..f800f415bd2 100644 --- a/spec/models/hooks/service_hook_spec.rb +++ b/spec/models/hooks/service_hook_spec.rb @@ -31,7 +31,7 @@ describe ServiceHook, models: true do WebMock.stub_request(:post, @service_hook.url) end - it "POSTs to the web hook URL" do + it "POSTs to the webhook URL" do @service_hook.execute(@data) expect(WebMock).to have_requested(:post, @service_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' } diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb index 138b87a9a06..fd1513cab1b 100644 --- a/spec/models/hooks/system_hook_spec.rb +++ b/spec/models/hooks/system_hook_spec.rb @@ -36,7 +36,7 @@ describe SystemHook, models: true do it "project_destroy hook" do user = create(:user) project = create(:empty_project, namespace: user.namespace) - Projects::DestroyService.new(project, user, {}).execute + Projects::DestroyService.new(project, user, {}).pending_delete! expect(WebMock).to have_requested(:post, @system_hook.url).with( body: /project_destroy/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } @@ -65,7 +65,7 @@ describe SystemHook, models: true do project = create(:project) project.team << [user, :master] expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_add_to_team/, + body: /user_add_to_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end @@ -76,7 +76,7 @@ describe SystemHook, models: true do project.team << [user, :master] project.project_members.destroy_all expect(WebMock).to have_requested(:post, @system_hook.url).with( - body: /user_remove_from_team/, + body: /user_remove_from_team/, headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'System Hook' } ).once end diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 2d90b0793cc..04bc2dcfb16 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -18,20 +18,14 @@ require 'spec_helper' -describe ProjectHook, models: true do - describe "Associations" do - it { is_expected.to belong_to :project } - end - - describe "Mass assignment" do - end - +describe WebHook, models: true do describe "Validations" do it { is_expected.to validate_presence_of(:url) } - context "url format" do + describe 'url' do it { is_expected.to allow_value("http://example.com").for(:url) } - it { is_expected.to allow_value("https://excample.com").for(:url) } + it { is_expected.to allow_value("https://example.com").for(:url) } + it { is_expected.to allow_value(" https://example.com ").for(:url) } it { is_expected.to allow_value("http://test.com/api").for(:url) } it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) } it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) } @@ -39,6 +33,12 @@ describe ProjectHook, models: true do it { is_expected.not_to allow_value("example.com").for(:url) } it { is_expected.not_to allow_value("ftp://example.com").for(:url) } it { is_expected.not_to allow_value("herp-and-derp").for(:url) } + + it 'strips :url before saving it' do + hook = create(:project_hook, url: ' https://example.com ') + + expect(hook.url).to eq('https://example.com') + end end end @@ -52,7 +52,7 @@ describe ProjectHook, models: true do WebMock.stub_request(:post, @project_hook.url) end - it "POSTs to the web hook URL" do + it "POSTs to the webhook URL" do @project_hook.execute(@data, 'push_hooks') expect(WebMock).to have_requested(:post, @project_hook.url).with( headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' } @@ -77,5 +77,17 @@ describe ProjectHook, models: true do expect(@project_hook.execute(@data, 'push_hooks')).to eq([false, 'SSL error']) end + + it "handles 200 status code" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 200, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end + + it "handles 2xx status codes" do + WebMock.stub_request(:post, @project_hook.url).to_return(status: 201, body: "Success") + + expect(@project_hook.execute(@data, 'push_hooks')).to eq([true, 'Success']) + end end end diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb new file mode 100644 index 00000000000..5afe042e154 --- /dev/null +++ b/spec/models/identity_spec.rb @@ -0,0 +1,38 @@ +# == Schema Information +# +# Table name: identities +# +# id :integer not null, primary key +# extern_uid :string(255) +# provider :string(255) +# user_id :integer +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +RSpec.describe Identity, models: true do + + describe 'relations' do + it { is_expected.to belong_to(:user) } + end + + describe 'fields' do + it { is_expected.to respond_to(:provider) } + it { is_expected.to respond_to(:extern_uid) } + end + + describe '#is_ldap?' do + let(:ldap_identity) { create(:identity, provider: 'ldapmain') } + let(:other_identity) { create(:identity, provider: 'twitter') } + + it 'returns true if it is a ldap identity' do + expect(ldap_identity.ldap?).to be_truthy + end + + it 'returns false if it is not a ldap identity' do + expect(other_identity.ldap?).to be_falsey + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 52271c7c8c6..2ccdec1eeff 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -105,6 +105,40 @@ describe Issue, models: true do end end + describe '#referenced_merge_requests' do + it 'returns the referenced merge requests' do + project = create(:project, :public) + + mr1 = create(:merge_request, + source_project: project, + source_branch: 'master', + target_branch: 'feature') + + mr2 = create(:merge_request, + source_project: project, + source_branch: 'feature', + target_branch: 'master') + + issue = create(:issue, description: mr1.to_reference, project: project) + + create(:note_on_issue, + noteable: issue, + note: mr2.to_reference, + project_id: project.id) + + expect(issue.referenced_merge_requests).to eq([mr1, mr2]) + end + end + + describe '#related_branches' do + it "should " do + allow(subject.project.repository).to receive(:branch_names). + and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name]) + + expect(subject.related_branches).to eq [subject.to_branch_name] + end + end + it_behaves_like 'an editable mentionable' do subject { create(:issue) } @@ -115,4 +149,12 @@ describe Issue, models: true do it_behaves_like 'a Taskable' do let(:subject) { create :issue } end + + describe "#to_branch_name" do + let(:issue) { build(:issue, title: 'a' * 30) } + + it "starts with the issue iid" do + expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/ + end + end end diff --git a/spec/models/label_spec.rb b/spec/models/label_spec.rb index 696fbf7e0aa..0614ca1e7c9 100644 --- a/spec/models/label_spec.rb +++ b/spec/models/label_spec.rb @@ -59,18 +59,42 @@ describe Label, models: true do context 'using id' do it 'returns a String reference to the object' do expect(label.to_reference).to eq "~#{label.id}" - expect(label.to_reference(double('project'))).to eq "~#{label.id}" end end context 'using name' do it 'returns a String reference to the object' do - expect(label.to_reference(:name)).to eq %(~"#{label.name}") + expect(label.to_reference(format: :name)).to eq %(~"#{label.name}") end it 'uses id when name contains double quote' do label = create(:label, name: %q{"irony"}) - expect(label.to_reference(:name)).to eq "~#{label.id}" + expect(label.to_reference(format: :name)).to eq "~#{label.id}" + end + end + + context 'using invalid format' do + it 'raises error' do + expect { label.to_reference(format: :invalid) } + .to raise_error StandardError, /Unknown format/ + end + end + + context 'cross project reference' do + let(:project) { create(:project) } + + context 'using name' do + it 'returns cross reference with label name' do + expect(label.to_reference(project, format: :name)) + .to eq %Q(#{label.project.to_reference}~"#{label.name}") + end + end + + context 'using id' do + it 'returns cross reference with label id' do + expect(label.to_reference(project, format: :id)) + .to eq %Q(#{label.project.to_reference}~#{label.id}) + end end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 2aedca20df2..2d8f1cc1ad3 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -31,6 +31,10 @@ describe Member, models: true do it { is_expected.to validate_presence_of(:source) } it { is_expected.to validate_inclusion_of(:access_level).in_array(Gitlab::Access.values) } + it_behaves_like 'an object with email-formated attributes', :invite_email do + subject { build(:project_member) } + end + context "when an invite email is provided" do let(:member) { build(:project_member, invite_email: "user@example.com", user: nil) } @@ -159,7 +163,7 @@ describe Member, models: true do describe "#generate_invite_token" do let!(:member) { create(:project_member, invite_email: "user@example.com", user: nil) } - + it "sets the invite token" do expect { member.generate_invite_token }.to change { member.invite_token} end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index e0653a8327d..654c71b6825 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -2,25 +2,29 @@ # # Table name: merge_requests # -# id :integer not null, primary key -# target_branch :string(255) not null -# source_branch :string(255) not null -# source_project_id :integer not null -# author_id :integer -# assignee_id :integer -# title :string(255) -# created_at :datetime -# updated_at :datetime -# milestone_id :integer -# state :string(255) -# merge_status :string(255) -# target_project_id :integer not null -# iid :integer -# description :text -# position :integer default(0) -# locked_at :datetime -# updated_by_id :integer -# merge_error :string(255) +# id :integer not null, primary key +# target_branch :string(255) not null +# source_branch :string(255) not null +# source_project_id :integer not null +# author_id :integer +# assignee_id :integer +# title :string(255) +# created_at :datetime +# updated_at :datetime +# milestone_id :integer +# state :string(255) +# merge_status :string(255) +# target_project_id :integer not null +# iid :integer +# description :text +# position :integer default(0) +# locked_at :datetime +# updated_by_id :integer +# merge_error :string(255) +# merge_params :text +# merge_when_build_succeeds :boolean default(FALSE), not null +# merge_user_id :integer +# merge_commit_sha :string # require 'spec_helper' @@ -76,6 +80,37 @@ describe MergeRequest, models: true do it { is_expected.to respond_to(:merge_when_build_succeeds) } end + describe '.in_projects' do + it 'returns the merge requests for a set of projects' do + expect(described_class.in_projects(Project.all)).to eq([subject]) + end + end + + describe '#source_sha' do + let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } + + context 'with diffs' do + subject { create(:merge_request, :with_diffs) } + it 'returns the sha of the source branch last commit' do + expect(subject.source_sha).to eq(last_branch_commit.sha) + end + end + + context 'without diffs' do + subject { create(:merge_request, :without_diffs) } + it 'returns the sha of the source branch last commit' do + expect(subject.source_sha).to eq(last_branch_commit.sha) + end + end + + context 'when the merge request is being created' do + subject { build(:merge_request, source_branch: nil, compare_commits: []) } + it 'returns nil' do + expect(subject.source_sha).to be_nil + end + end + end + describe '#to_reference' do it 'returns a String reference to the object' do expect(subject.to_reference).to eq "!#{subject.iid}" @@ -134,11 +169,13 @@ describe MergeRequest, models: true do describe 'detection of issues to be closed' do let(:issue0) { create :issue, project: subject.project } let(:issue1) { create :issue, project: subject.project } - let(:commit0) { double('commit0', closes_issues: [issue0]) } - let(:commit1) { double('commit1', closes_issues: [issue0]) } - let(:commit2) { double('commit2', closes_issues: [issue1]) } + + let(:commit0) { double('commit0', safe_message: "Fixes #{issue0.to_reference}") } + let(:commit1) { double('commit1', safe_message: "Fixes #{issue0.to_reference}") } + let(:commit2) { double('commit2', safe_message: "Fixes #{issue1.to_reference}") } before do + subject.project.team << [subject.author, :developer] allow(subject).to receive(:commits).and_return([commit0, commit1, commit2]) end @@ -146,7 +183,9 @@ describe MergeRequest, models: true do allow(subject.project).to receive(:default_branch). and_return(subject.target_branch) - expect(subject.closes_issues).to eq([issue0, issue1].sort_by(&:id)) + closed = subject.closes_issues + + expect(closed).to include(issue0, issue1) end it 'only lists issues as to be closed if it targets the default branch' do @@ -164,17 +203,6 @@ describe MergeRequest, models: true do expect(subject.closes_issues).to include(issue2) end - - context 'for a project with JIRA integration' do - let(:issue0) { JiraIssue.new('JIRA-123', subject.project) } - let(:issue1) { JiraIssue.new('FOOBAR-4567', subject.project) } - - it 'returns sorted JiraIssues' do - allow(subject.project).to receive_messages(default_branch: subject.target_branch) - - expect(subject.closes_issues).to eq([issue0, issue1]) - end - end end describe "#work_in_progress?" do @@ -193,6 +221,11 @@ describe MergeRequest, models: true do expect(subject).to be_work_in_progress end + it "detects the '[WIP]' prefix" do + subject.title = "[WIP]#{subject.title}" + expect(subject).to be_work_in_progress + end + it "doesn't detect WIP for words starting with WIP" do subject.title = "Wipwap #{subject.title}" expect(subject).not_to be_work_in_progress @@ -231,9 +264,15 @@ describe MergeRequest, models: true do expect(subject.can_remove_source_branch?(user2)).to be_falsey end - it "is can be removed in all other cases" do + it "can be removed if the last commit is the head of the source branch" do + allow(subject.source_project).to receive(:commit).and_return(subject.last_commit) + expect(subject.can_remove_source_branch?(user)).to be_truthy end + + it "cannot be removed if the last commit is not also the head of the source branch" do + expect(subject.can_remove_source_branch?(user)).to be_falsey + end end describe "#reset_merge_when_build_succeeds" do @@ -248,13 +287,86 @@ describe MergeRequest, models: true do end describe "#hook_attrs" do + let(:attrs_hash) { subject.hook_attrs.to_h } + + [:source, :target].each do |key| + describe "#{key} key" do + include_examples 'project hook data', project_key: key do + let(:data) { attrs_hash } + let(:project) { subject.send("#{key}_project") } + end + end + end + it "has all the required keys" do - attrs = subject.hook_attrs - attrs = attrs.to_h - expect(attrs).to include(:source) - expect(attrs).to include(:target) - expect(attrs).to include(:last_commit) - expect(attrs).to include(:work_in_progress) + expect(attrs_hash).to include(:source) + expect(attrs_hash).to include(:target) + expect(attrs_hash).to include(:last_commit) + expect(attrs_hash).to include(:work_in_progress) + end + end + + describe '#diverged_commits_count' do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + + context 'diverged on same repository' do + subject(:merge_request_with_divergence) { create(:merge_request, :diverged, source_project: project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(5) + end + end + + context 'diverged on fork' do + subject(:merge_request_fork_with_divergence) { create(:merge_request, :diverged, source_project: fork_project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(5) + end + end + + context 'rebased on fork' do + subject(:merge_request_rebased) { create(:merge_request, :rebased, source_project: fork_project, target_project: project) } + + it 'counts commits that are on target branch but not on source branch' do + expect(subject.diverged_commits_count).to eq(0) + end + end + + describe 'caching' do + before(:example) do + allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new) + end + + it 'caches the output' do + expect(subject).to receive(:compute_diverged_commits_count). + once. + and_return(2) + + subject.diverged_commits_count + subject.diverged_commits_count + end + + it 'invalidates the cache when the source sha changes' do + expect(subject).to receive(:compute_diverged_commits_count). + twice. + and_return(2) + + subject.diverged_commits_count + allow(subject).to receive(:source_sha).and_return('123abc') + subject.diverged_commits_count + end + + it 'invalidates the cache when the target sha changes' do + expect(subject).to receive(:compute_diverged_commits_count). + twice. + and_return(2) + + subject.diverged_commits_count + allow(subject).to receive(:target_sha).and_return('123abc') + subject.diverged_commits_count + end end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 30a71987d86..72a4ea70228 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -32,22 +32,36 @@ describe Milestone, models: true do let(:milestone) { create(:milestone) } let(:issue) { create(:issue) } + let(:user) { create(:user) } + + describe "unique milestone title per project" do + it "shouldn't accept the same title in a project twice" do + new_milestone = Milestone.new(project: milestone.project, title: milestone.title) + expect(new_milestone).not_to be_valid + end + + it "should accept the same title in another project" do + project = build(:project) + new_milestone = Milestone.new(project: project, title: milestone.title) + + expect(new_milestone).to be_valid + end + end describe "#percent_complete" do it "should not count open issues" do milestone.issues << issue - expect(milestone.percent_complete).to eq(0) + expect(milestone.percent_complete(user)).to eq(0) end it "should count closed issues" do issue.close milestone.issues << issue - expect(milestone.percent_complete).to eq(100) + expect(milestone.percent_complete(user)).to eq(100) end it "should recover from dividing by zero" do - expect(milestone.issues).to receive(:count).and_return(0) - expect(milestone.percent_complete).to eq(0) + expect(milestone.percent_complete(user)).to eq(0) end end @@ -89,7 +103,7 @@ describe Milestone, models: true do ) end - it { expect(milestone.percent_complete).to eq(75) } + it { expect(milestone.percent_complete(user)).to eq(75) } end describe :items_count do @@ -99,24 +113,23 @@ describe Milestone, models: true do milestone.merge_requests << create(:merge_request) end - it { expect(milestone.closed_items_count).to eq(1) } - it { expect(milestone.open_items_count).to eq(2) } - it { expect(milestone.total_items_count).to eq(3) } - it { expect(milestone.is_empty?).to be_falsey } + it { expect(milestone.closed_items_count(user)).to eq(1) } + it { expect(milestone.total_items_count(user)).to eq(3) } + it { expect(milestone.is_empty?(user)).to be_falsey } end describe :can_be_closed? do it { expect(milestone.can_be_closed?).to be_truthy } end - describe :is_empty? do + describe :total_items_count do before do create :closed_issue, milestone: milestone create :merge_request, milestone: milestone end it 'Should return total count of issues and merge requests assigned to milestone' do - expect(milestone.total_items_count).to eq 2 + expect(milestone.total_items_count(user)).to eq 2 end end @@ -168,4 +181,34 @@ describe Milestone, models: true do expect(issue4.position).to eq(42) end end + + describe '.search' do + let(:milestone) { create(:milestone, title: 'foo', description: 'bar') } + + it 'returns milestones with a matching title' do + expect(described_class.search(milestone.title)).to eq([milestone]) + end + + it 'returns milestones with a partially matching title' do + expect(described_class.search(milestone.title[0..2])).to eq([milestone]) + end + + it 'returns milestones with a matching title regardless of the casing' do + expect(described_class.search(milestone.title.upcase)).to eq([milestone]) + end + + it 'returns milestones with a matching description' do + expect(described_class.search(milestone.description)).to eq([milestone]) + end + + it 'returns milestones with a partially matching description' do + expect(described_class.search(milestone.description[0..2])). + to eq([milestone]) + end + + it 'returns milestones with a matching description regardless of the casing' do + expect(described_class.search(milestone.description.upcase)). + to eq([milestone]) + end + end end diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb index 4fa2d2bc4d2..3c3a580942a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -11,7 +11,6 @@ # type :string(255) # description :string(255) default(""), not null # avatar :string(255) -# public :boolean default(FALSE) # require 'spec_helper' @@ -42,13 +41,32 @@ describe Namespace, models: true do it { expect(namespace.human_name).to eq(namespace.owner_name) } end - describe :search do - before do - @namespace = create :namespace + describe '.search' do + let(:namespace) { create(:namespace) } + + it 'returns namespaces with a matching name' do + expect(described_class.search(namespace.name)).to eq([namespace]) + end + + it 'returns namespaces with a partially matching name' do + expect(described_class.search(namespace.name[0..2])).to eq([namespace]) + end + + it 'returns namespaces with a matching name regardless of the casing' do + expect(described_class.search(namespace.name.upcase)).to eq([namespace]) + end + + it 'returns namespaces with a matching path' do + expect(described_class.search(namespace.path)).to eq([namespace]) end - it { expect(Namespace.search(@namespace.path)).to eq([@namespace]) } - it { expect(Namespace.search('unknown')).to eq([]) } + it 'returns namespaces with a partially matching path' do + expect(described_class.search(namespace.path[0..2])).to eq([namespace]) + end + + it 'returns namespaces with a matching path regardless of the casing' do + expect(described_class.search(namespace.path.upcase)).to eq([namespace]) + end end describe :move_dir do diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 593d8f76215..6b18936edb1 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -24,8 +24,10 @@ require 'spec_helper' describe Note, models: true do describe 'associations' do it { is_expected.to belong_to(:project) } - it { is_expected.to belong_to(:noteable) } + it { is_expected.to belong_to(:noteable).touch(true) } it { is_expected.to belong_to(:author).class_name('User') } + + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'validation' do @@ -125,13 +127,32 @@ describe Note, models: true do let(:set_mentionable_text) { ->(txt) { subject.note = txt } } end - describe :search do - let!(:note) { create(:note, note: "WoW") } + describe "#all_references" do + let!(:note1) { create(:note) } + let!(:note2) { create(:note) } + + it "reads the rendered note body from the cache" do + expect(Banzai::Renderer).to receive(:render).with(note1.note, pipeline: :note, cache_key: [note1, "note"], project: note1.project) + expect(Banzai::Renderer).to receive(:render).with(note2.note, pipeline: :note, cache_key: [note2, "note"], project: note2.project) + + note1.all_references + note2.all_references + end + end + + describe '.search' do + let(:note) { create(:note, note: 'WoW') } + + it 'returns notes with matching content' do + expect(described_class.search(note.note)).to eq([note]) + end - it { expect(Note.search('wow')).to include(note) } + it 'returns notes with matching content regardless of the casing' do + expect(described_class.search('WOW')).to eq([note]) + end end - describe :grouped_awards do + describe '.grouped_awards' do before do create :note, note: "smile", is_award: true create :note, note: "smile", is_award: true @@ -148,6 +169,66 @@ describe Note, models: true do end end + describe '#active?' do + it 'is always true when the note has no associated diff' do + note = build(:note) + + expect(note).to receive(:diff).and_return(nil) + + expect(note).to be_active + end + + it 'is never true when the note has no noteable associated' do + note = build(:note) + + expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:noteable).and_return(nil) + + expect(note).not_to be_active + end + + it 'returns the memoized value if defined' do + note = build(:note) + + expect(note).to receive(:diff).and_return(double) + expect(note).to receive(:noteable).and_return(double) + + note.instance_variable_set(:@active, 'foo') + expect(note).not_to receive(:find_noteable_diff) + + expect(note.active?).to eq 'foo' + end + + context 'for a merge request noteable' do + it 'is false when noteable has no matching diff' do + merge = build_stubbed(:merge_request, :simple) + note = build(:note, noteable: merge) + + allow(note).to receive(:diff).and_return(double) + expect(note).to receive(:find_noteable_diff).and_return(nil) + + expect(note).not_to be_active + end + + it 'is true when noteable has a matching diff' do + merge = create(:merge_request, :simple) + + # Generate a real line_code value so we know it will match. We use a + # random line from a random diff just for funsies. + diff = merge.diffs.to_a.sample + line = Gitlab::Diff::Parser.new.parse(diff.diff.each_line).to_a.sample + code = Gitlab::Diff::LineCode.generate(diff.new_path, line.new_pos, line.old_pos) + + # We're persisting in order to trigger the set_diff callback + note = create(:note, noteable: merge, line_code: code) + + # Make sure we don't get a false positive from a guard clause + expect(note).to receive(:find_noteable_diff).and_call_original + expect(note).to be_active + end + end + end + describe "editable?" do it "returns true" do note = build(:note) @@ -164,13 +245,53 @@ describe Note, models: true do expect(note.editable?).to be_falsy end end - + + describe "cross_reference_not_visible_for?" do + let(:private_user) { create(:user) } + let(:private_project) { create(:project, namespace: private_user.namespace).tap { |p| p.team << [private_user, :master] } } + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let(:note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + + it "returns true" do + expect(note.cross_reference_not_visible_for?(ext_issue.author)).to be_truthy + end + + it "returns false" do + expect(note.cross_reference_not_visible_for?(private_user)).to be_falsy + end + end + describe "set_award!" do - let(:issue) { create :issue } + let(:merge_request) { create :merge_request } it "converts aliases to actual name" do - note = create :note, note: ":+1:", noteable: issue + note = create(:note, note: ":+1:", noteable: merge_request) expect(note.reload.note).to eq("thumbsup") end + + it "is not an award emoji when comment is on a diff" do + note = create(:note, note: ":blowfish:", noteable: merge_request, line_code: "11d5d2e667e9da4f7f610f81d86c974b146b13bd_0_2") + note = note.reload + + expect(note.note).to eq(":blowfish:") + expect(note.is_award?).to be_falsy + end + end + + describe 'clear_blank_line_code!' do + it 'clears a blank line code before validation' do + note = build(:note, line_code: ' ') + + expect { note.valid? }.to change(note, :line_code).to(nil) + end end end diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb new file mode 100644 index 00000000000..2fa6715fcaf --- /dev/null +++ b/spec/models/project_group_link_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe ProjectGroupLink do + describe "Associations" do + it { should belong_to(:group) } + it { should belong_to(:project) } + end + + describe "Validation" do + let!(:project_group_link) { create(:project_group_link) } + + it { should validate_presence_of(:project_id) } + it { should validate_uniqueness_of(:group_id).scoped_to(:project_id).with_message(/already shared/) } + it { should validate_presence_of(:group_id) } + it { should validate_presence_of(:group_access) } + end +end diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb index 64bb92fba95..f3d15f3c1ea 100644 --- a/spec/models/project_services/asana_service_spec.rb +++ b/spec/models/project_services/asana_service_spec.rb @@ -40,6 +40,20 @@ describe AsanaService, models: true do let(:user) { create(:user) } let(:project) { create(:project) } + def create_data_for_commits(*messages) + { + object_kind: 'push', + ref: 'master', + user_name: user.name, + commits: messages.map do |m| + { + message: m, + url: 'https://gitlab.com/', + } + end + } + end + before do @asana = AsanaService.new allow(@asana).to receive_messages( @@ -51,16 +65,67 @@ describe AsanaService, models: true do ) end - it 'should call Asana service to created a story' do - expect(Asana::Task).to receive(:find).with('123456').once + it 'should call Asana service to create a story' do + data = create_data_for_commits('Message from commit. related to #123456') + expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}" - @asana.check_commit('related to #123456', 'pushed') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment).with(text: expected_message) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123456').once.and_return(d1) + + @asana.execute(data) end - it 'should call Asana service to created a story and close a task' do - expect(Asana::Task).to receive(:find).with('456789').twice + it 'should call Asana service to create a story and close a task' do + data = create_data_for_commits('fix #456789') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456789').once.and_return(d1) + + @asana.execute(data) + end + + it 'should be able to close via url' do + data = create_data_for_commits('closes https://app.asana.com/19292/956299/42') + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d1) + + @asana.execute(data) + end + + it 'should allow multiple matches per line' do + message = <<-EOF + minor bigfix, refactoring, fixed #123 and Closes #456 work on #789 + ref https://app.asana.com/19292/956299/42 and closing https://app.asana.com/19292/956299/12 + EOF + data = create_data_for_commits(message) + d1 = double('Asana::Task') + expect(d1).to receive(:add_comment) + expect(d1).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '123').once.and_return(d1) + + d2 = double('Asana::Task') + expect(d2).to receive(:add_comment) + expect(d2).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '456').once.and_return(d2) + + d3 = double('Asana::Task') + expect(d3).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '789').once.and_return(d3) + + d4 = double('Asana::Task') + expect(d4).to receive(:add_comment) + expect(Asana::Task).to receive(:find_by_id).with(anything, '42').once.and_return(d4) + + d5 = double('Asana::Task') + expect(d5).to receive(:add_comment) + expect(d5).to receive(:update).with(completed: true) + expect(Asana::Task).to receive(:find_by_id).with(anything, '12').once.and_return(d5) - @asana.check_commit('fix #456789', 'pushed') + @asana.execute(data) end end end diff --git a/spec/models/project_services/builds_email_service_spec.rb b/spec/models/project_services/builds_email_service_spec.rb new file mode 100644 index 00000000000..905379a64e3 --- /dev/null +++ b/spec/models/project_services/builds_email_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe BuildsEmailService do + let(:build) { create(:ci_build) } + let(:data) { Gitlab::BuildDataBuilder.build(build) } + let(:service) { BuildsEmailService.new } + + describe :execute do + it "sends email" do + service.recipients = 'test@gitlab.com' + data[:build_status] = 'failed' + expect(BuildEmailWorker).to receive(:perform_async) + service.execute(data) + end + + it "does not sends email with failed build and allowed_failure on" do + data[:build_status] = 'failed' + data[:build_allow_failure] = true + expect(BuildEmailWorker).not_to receive(:perform_async) + service.execute(data) + end + end +end diff --git a/spec/models/project_snippet_spec.rb b/spec/models/project_snippet_spec.rb index cc92eb0bd9f..e0feb606f78 100644 --- a/spec/models/project_snippet_spec.rb +++ b/spec/models/project_snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 400bdf2d962..b8b9a455b83 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -29,6 +29,13 @@ # import_source :string(255) # commit_count :integer default(0) # import_error :text +# ci_id :integer +# builds_enabled :boolean default(TRUE), not null +# shared_runners_enabled :boolean default(TRUE), not null +# runners_token :string +# build_coverage_regex :string +# build_allow_git_fetch :boolean default(TRUE), not null +# build_timeout :integer default(3600), not null # require 'spec_helper' @@ -61,6 +68,7 @@ describe Project, models: true do it { is_expected.to have_many(:runners) } it { is_expected.to have_many(:variables) } it { is_expected.to have_many(:triggers) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'modules' do @@ -95,7 +103,7 @@ describe Project, models: true do expect(project2.errors[:limit_reached].first).to match(/Your project limit is 0/) end end - + describe 'project token' do it 'should set an random token if none provided' do project = FactoryGirl.create :empty_project, runners_token: '' @@ -512,35 +520,35 @@ describe Project, models: true do describe :any_runners do let(:project) { create(:empty_project, shared_runners_enabled: shared_runners_enabled) } - let(:specific_runner) { create(:ci_specific_runner) } - let(:shared_runner) { create(:ci_shared_runner) } + let(:specific_runner) { create(:ci_runner) } + let(:shared_runner) { create(:ci_runner, :shared) } context 'for shared runners disabled' do let(:shared_runners_enabled) { false } - + it 'there are no runners available' do expect(project.any_runners?).to be_falsey end - + it 'there is a specific runner' do project.runners << specific_runner expect(project.any_runners?).to be_truthy end - + it 'there is a shared runner, but they are prohibited to use' do shared_runner expect(project.any_runners?).to be_falsey end - + it 'checks the presence of specific runner' do project.runners << specific_runner expect(project.any_runners? { |runner| runner == specific_runner }).to be_truthy end end - + context 'for shared runners enabled' do let(:shared_runners_enabled) { true } - + it 'there is a shared runner' do shared_runner expect(project.any_runners?).to be_truthy @@ -554,7 +562,7 @@ describe Project, models: true do end describe '#visibility_level_allowed?' do - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } context 'when checking on non-forked project' do it { expect(project.visibility_level_allowed?(Gitlab::VisibilityLevel::PRIVATE)).to be_truthy } @@ -574,6 +582,142 @@ describe Project, models: true do it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::INTERNAL)).to be_truthy } it { expect(forked_project.visibility_level_allowed?(Gitlab::VisibilityLevel::PUBLIC)).to be_falsey } end + end + + describe '.search' do + let(:project) { create(:project, description: 'kitten mittens') } + + it 'returns projects with a matching name' do + expect(described_class.search(project.name)).to eq([project]) + end + + it 'returns projects with a partially matching name' do + expect(described_class.search(project.name[0..2])).to eq([project]) + end + + it 'returns projects with a matching name regardless of the casing' do + expect(described_class.search(project.name.upcase)).to eq([project]) + end + + it 'returns projects with a matching description' do + expect(described_class.search(project.description)).to eq([project]) + end + + it 'returns projects with a partially matching description' do + expect(described_class.search('kitten')).to eq([project]) + end + + it 'returns projects with a matching description regardless of the casing' do + expect(described_class.search('KITTEN')).to eq([project]) + end + + it 'returns projects with a matching path' do + expect(described_class.search(project.path)).to eq([project]) + end + + it 'returns projects with a partially matching path' do + expect(described_class.search(project.path[0..2])).to eq([project]) + end + + it 'returns projects with a matching path regardless of the casing' do + expect(described_class.search(project.path.upcase)).to eq([project]) + end + + it 'returns projects with a matching namespace name' do + expect(described_class.search(project.namespace.name)).to eq([project]) + end + + it 'returns projects with a partially matching namespace name' do + expect(described_class.search(project.namespace.name[0..2])).to eq([project]) + end + + it 'returns projects with a matching namespace name regardless of the casing' do + expect(described_class.search(project.namespace.name.upcase)).to eq([project]) + end + + it 'returns projects when eager loading namespaces' do + relation = described_class.all.includes(:namespace) + expect(relation.search(project.namespace.name)).to eq([project]) + end + end + + describe '#rename_repo' do + let(:project) { create(:project) } + let(:gitlab_shell) { Gitlab::Shell.new } + + before do + # Project#gitlab_shell returns a new instance of Gitlab::Shell on every + # call. This makes testing a bit easier. + allow(project).to receive(:gitlab_shell).and_return(gitlab_shell) + end + + it 'renames a repository' do + allow(project).to receive(:previous_changes).and_return('path' => ['foo']) + + ns = project.namespace_dir + + expect(gitlab_shell).to receive(:mv_repository). + ordered. + with("#{ns}/foo", "#{ns}/#{project.path}"). + and_return(true) + + expect(gitlab_shell).to receive(:mv_repository). + ordered. + with("#{ns}/foo.wiki", "#{ns}/#{project.path}.wiki"). + and_return(true) + + expect_any_instance_of(SystemHooksService). + to receive(:execute_hooks_for). + with(project, :rename) + + expect_any_instance_of(Gitlab::UploadsTransfer). + to receive(:rename_project). + with('foo', project.path, ns) + + expect(project).to receive(:expire_caches_before_rename) + + project.rename_repo + end + end + + describe '#expire_caches_before_rename' do + let(:project) { create(:project) } + let(:repo) { double(:repo, exists?: true) } + let(:wiki) { double(:wiki, exists?: true) } + + it 'expires the caches of the repository and wiki' do + allow(Repository).to receive(:new). + with('foo', project). + and_return(repo) + + allow(Repository).to receive(:new). + with('foo.wiki', project). + and_return(wiki) + + expect(repo).to receive(:expire_cache) + expect(repo).to receive(:expire_emptiness_caches) + + expect(wiki).to receive(:expire_cache) + expect(wiki).to receive(:expire_emptiness_caches) + + project.expire_caches_before_rename('foo') + end + end + + describe '.search_by_title' do + let(:project) { create(:project, name: 'kittens') } + + it 'returns projects with a matching name' do + expect(described_class.search_by_title(project.name)).to eq([project]) + end + + it 'returns projects with a partially matching name' do + expect(described_class.search_by_title('kitten')).to eq([project]) + end + + it 'returns projects with a matching name regardless of the casing' do + expect(described_class.search_by_title('KITTENS')).to eq([project]) + end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index 5cd5ae327bf..bacb17a8883 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -67,15 +67,69 @@ describe ProjectTeam, models: true do end end + describe :max_invited_level do + let(:group) { create(:group) } + let(:project) { create(:empty_project) } + + before do + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER + ) + + group.add_user(master, Gitlab::Access::MASTER) + group.add_user(reporter, Gitlab::Access::REPORTER) + end + + it { expect(project.team.max_invited_level(master.id)).to eq(Gitlab::Access::DEVELOPER) } + it { expect(project.team.max_invited_level(reporter.id)).to eq(Gitlab::Access::REPORTER) } + it { expect(project.team.max_invited_level(nonmember.id)).to be_nil } + end + + describe :max_member_access do + let(:group) { create(:group) } + let(:project) { create(:empty_project) } + + before do + project.project_group_links.create( + group: group, + group_access: Gitlab::Access::DEVELOPER + ) + + group.add_user(master, Gitlab::Access::MASTER) + group.add_user(reporter, Gitlab::Access::REPORTER) + end + + it { expect(project.team.max_member_access(master.id)).to eq(Gitlab::Access::DEVELOPER) } + it { expect(project.team.max_member_access(reporter.id)).to eq(Gitlab::Access::REPORTER) } + it { expect(project.team.max_member_access(nonmember.id)).to be_nil } + + it "does not have an access" do + project.namespace.update(share_with_group_lock: true) + expect(project.team.max_member_access(master.id)).to be_nil + expect(project.team.max_member_access(reporter.id)).to be_nil + end + end + describe "#human_max_access" do - it "return master role" do - user = create :user - group = create :group - group.add_users([user.id], GroupMember::MASTER) - project = create(:project, namespace: group) - project.team << [user, :guest] - - expect(project.team.human_max_access(user.id)).to eq("Master") + it 'returns Master role' do + user = create(:user) + group = create(:group) + group.add_master(user) + + project = build_stubbed(:empty_project, namespace: group) + + expect(project.team.human_max_access(user.id)).to eq 'Master' + end + + it 'returns Owner role' do + user = create(:user) + group = create(:group) + group.add_owner(user) + + project = build_stubbed(:empty_project, namespace: group) + + expect(project.team.human_max_access(user.id)).to eq 'Owner' end end end diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 876b927eaea..a2085df5bcd 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -36,6 +36,13 @@ describe ProjectWiki, models: true do end end + describe "#wiki_base_path" do + it "returns the wiki base path" do + wiki_base_path = "/#{project.path_with_namespace}/wikis" + expect(subject.wiki_base_path).to eq(wiki_base_path) + end + end + describe "#wiki" do it "contains a Gollum::Wiki instance" do expect(subject.wiki).to be_a Gollum::Wiki diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index afbf62035ac..a57229a4fdf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -5,6 +5,15 @@ describe Repository, models: true do let(:repository) { create(:project).repository } let(:user) { create(:user) } + let(:commit_options) do + author = repository.user_to_committer(user) + { message: 'Test message', committer: author, author: author } + end + let(:merge_commit) do + source_sha = repository.find_branch('feature').target + merge_commit_id = repository.merge(user, source_sha, 'master', commit_options) + repository.commit(merge_commit_id) + end describe :branch_names_contains do subject { repository.branch_names_contains(sample_commit.id) } @@ -92,13 +101,29 @@ describe Repository, models: true do end describe 'parsing result' do - subject { repository.parse_search_result(results.first) } + subject { repository.parse_search_result(search_result) } + let(:search_result) { results.first } it { is_expected.to be_an OpenStruct } it { expect(subject.filename).to eq('CHANGELOG') } + it { expect(subject.basename).to eq('CHANGELOG') } it { expect(subject.ref).to eq('master') } it { expect(subject.startline).to eq(186) } it { expect(subject.data.lines[2]).to eq(" - Feature: Replace teams with group membership\n") } + + context "when filename has extension" do + let(:search_result) { "master:CONTRIBUTE.md:5:- [Contribute to GitLab](#contribute-to-gitlab)\n" } + + it { expect(subject.filename).to eq('CONTRIBUTE.md') } + it { expect(subject.basename).to eq('CONTRIBUTE') } + end + + context "when file under directory" do + let(:search_result) { "master:a/b/c.md:5:a b c\n" } + + it { expect(subject.filename).to eq('a/b/c.md') } + it { expect(subject.basename).to eq('a/b/c') } + end end end @@ -139,6 +164,12 @@ describe Repository, models: true do expect(branch.name).to eq('new_feature') end + + it 'calls the after_create_branch hook' do + expect(repository).to receive(:after_create_branch) + + repository.add_branch(user, 'new_feature', 'master') + end end context 'when pre hooks failed' do @@ -200,13 +231,22 @@ describe Repository, models: true do describe :commit_with_hooks do context 'when pre hooks were successful' do - it 'should run without errors' do - expect_any_instance_of(GitHooksService).to receive(:execute).and_return(true) + before do + expect_any_instance_of(GitHooksService).to receive(:execute). + and_return(true) + end + it 'should run without errors' do expect do repository.commit_with_hooks(user, 'feature') { sample_commit.id } end.not_to raise_error end + + it 'should ensure the autocrlf Git option is set to :input' do + expect(repository).to receive(:update_autocrlf_option) + + repository.commit_with_hooks(user, 'feature') { sample_commit.id } + end end context 'when pre hooks failed' do @@ -219,4 +259,555 @@ describe Repository, models: true do end end end + + describe '#exists?' do + it 'returns true when a repository exists' do + expect(repository.exists?).to eq(true) + end + + it 'returns false when a repository does not exist' do + expect(repository.raw_repository).to receive(:rugged). + and_raise(Gitlab::Git::Repository::NoRepository) + + expect(repository.exists?).to eq(false) + end + + it 'returns false when there is no namespace' do + allow(repository).to receive(:path_with_namespace).and_return(nil) + + expect(repository.exists?).to eq(false) + end + end + + describe '#has_visible_content?' do + subject { repository.has_visible_content? } + + describe 'when there are no branches' do + before do + allow(repository.raw_repository).to receive(:branch_count).and_return(0) + end + + it { is_expected.to eq(false) } + end + + describe 'when there are branches' do + it 'returns true' do + expect(repository.raw_repository).to receive(:branch_count).and_return(3) + + expect(subject).to eq(true) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(3) + + repository.has_visible_content? + repository.has_visible_content? + end + end + end + + describe '#update_autocrlf_option' do + describe 'when autocrlf is not already set to :input' do + before do + repository.raw_repository.autocrlf = true + end + + it 'sets autocrlf to :input' do + repository.update_autocrlf_option + + expect(repository.raw_repository.autocrlf).to eq(:input) + end + end + + describe 'when autocrlf is already set to :input' do + before do + repository.raw_repository.autocrlf = :input + end + + it 'does nothing' do + expect(repository.raw_repository).to_not receive(:autocrlf=). + with(:input) + + repository.update_autocrlf_option + end + end + end + + describe '#empty?' do + let(:empty_repository) { create(:project_empty_repo).repository } + + it 'returns true for an empty repository' do + expect(empty_repository.empty?).to eq(true) + end + + it 'returns false for a non-empty repository' do + expect(repository.empty?).to eq(false) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:empty?). + once. + and_return(false) + + repository.empty? + repository.empty? + end + end + + describe '#root_ref' do + it 'returns a branch name' do + expect(repository.root_ref).to be_an_instance_of(String) + end + + it 'caches the output' do + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('master') + + repository.root_ref + repository.root_ref + end + end + + describe '#expire_cache' do + it 'expires all caches' do + expect(repository).to receive(:expire_branch_cache) + + repository.expire_cache + end + + it 'expires the caches for a specific branch' do + expect(repository).to receive(:expire_branch_cache).with('master') + + repository.expire_cache('master') + end + + it 'expires the emptiness caches for an empty repository' do + expect(repository).to receive(:empty?).and_return(true) + expect(repository).to receive(:expire_emptiness_caches) + + repository.expire_cache + end + + it 'does not expire the emptiness caches for a non-empty repository' do + expect(repository).to receive(:empty?).and_return(false) + expect(repository).to_not receive(:expire_emptiness_caches) + + repository.expire_cache + end + end + + describe '#expire_root_ref_cache' do + it 'expires the root reference cache' do + repository.root_ref + + expect(repository.raw_repository).to receive(:root_ref). + once. + and_return('foo') + + repository.expire_root_ref_cache + + expect(repository.root_ref).to eq('foo') + end + end + + describe '#expire_has_visible_content_cache' do + it 'expires the visible content cache' do + repository.has_visible_content? + + expect(repository.raw_repository).to receive(:branch_count). + once. + and_return(0) + + repository.expire_has_visible_content_cache + + expect(repository.has_visible_content?).to eq(false) + end + end + + describe '#expire_branch_cache' do + # This method is private but we need it for testing purposes. Sadly there's + # no other proper way of testing caching operations. + let(:cache) { repository.send(:cache) } + + it 'expires the cache for all branches' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache + end + + it 'expires the cache for all branches when the root branch is given' do + expect(cache).to receive(:expire). + at_least(repository.branches.length). + times + + repository.expire_branch_cache(repository.root_ref) + end + + it 'expires the cache for a specific branch' do + expect(cache).to receive(:expire).once + + repository.expire_branch_cache('foo') + end + end + + describe '#expire_emptiness_caches' do + let(:cache) { repository.send(:cache) } + + it 'expires the caches' do + expect(cache).to receive(:expire).with(:empty?) + expect(repository).to receive(:expire_has_visible_content_cache) + + repository.expire_emptiness_caches + end + end + + describe :skip_merged_commit do + subject { repository.commits(Gitlab::Git::BRANCH_REF_PREFIX + "'test'", nil, 100, 0, true).map{ |k| k.id } } + + it { is_expected.not_to include('e56497bb5f03a90a51293fc6d516788730953899') } + end + + describe '#merge' do + it 'should merge the code and return the commit id' do + expect(merge_commit).to be_present + expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present + end + end + + describe '#revert' do + let(:new_image_commit) { repository.commit('33f3729a45c02fc67d00adb1b8bca394b0e761d9') } + let(:update_image_commit) { repository.commit('2f63565e7aac07bcdadb654e253078b727143ec4') } + + context 'when there is a conflict' do + it 'should abort the operation' do + expect(repository.revert(user, new_image_commit, 'master')).to eq(false) + end + end + + context 'when commit was already reverted' do + it 'should abort the operation' do + repository.revert(user, update_image_commit, 'master') + + expect(repository.revert(user, update_image_commit, 'master')).to eq(false) + end + end + + context 'when commit can be reverted' do + it 'should revert the changes' do + expect(repository.revert(user, update_image_commit, 'master')).to be_truthy + end + end + + context 'reverting a merge commit' do + it 'should revert the changes' do + merge_commit + expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).to be_present + + repository.revert(user, merge_commit, 'master') + expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_to be_present + end + end + end + + describe '#before_delete' do + describe 'when a repository does not exist' do + before do + allow(repository).to receive(:exists?).and_return(false) + end + + it 'does not flush caches that depend on repository data' do + expect(repository).to_not receive(:expire_cache) + + repository.before_delete + end + + it 'flushes the root ref cache' do + expect(repository).to receive(:expire_root_ref_cache) + + repository.before_delete + end + + it 'flushes the emptiness caches' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.before_delete + end + end + + describe 'when a repository exists' do + before do + allow(repository).to receive(:exists?).and_return(true) + end + + it 'flushes the caches that depend on repository data' do + expect(repository).to receive(:expire_cache) + + repository.before_delete + end + + it 'flushes the root ref cache' do + expect(repository).to receive(:expire_root_ref_cache) + + repository.before_delete + end + + it 'flushes the emptiness caches' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.before_delete + end + end + end + + describe '#before_change_head' do + it 'flushes the branch cache' do + expect(repository).to receive(:expire_branch_cache) + + repository.before_change_head + end + + it 'flushes the root ref cache' do + expect(repository).to receive(:expire_root_ref_cache) + + repository.before_change_head + end + end + + describe '#before_push_tag' do + it 'flushes the cache' do + expect(repository).to receive(:expire_cache) + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_push_tag + end + end + + describe '#after_import' do + it 'flushes the emptiness cachess' do + expect(repository).to receive(:expire_emptiness_caches) + + repository.after_import + end + end + + describe '#after_push_commit' do + it 'flushes the cache' do + expect(repository).to receive(:expire_cache).with('master', '123') + + repository.after_push_commit('master', '123') + end + end + + describe '#after_create_branch' do + it 'flushes the visible content cache' do + expect(repository).to receive(:expire_has_visible_content_cache) + + repository.after_create_branch + end + end + + describe '#after_remove_branch' do + it 'flushes the visible content cache' do + expect(repository).to receive(:expire_has_visible_content_cache) + + repository.after_remove_branch + end + end + + describe "#main_language" do + it 'shows the main language of the project' do + expect(repository.main_language).to eq("Ruby") + end + + it 'returns nil when the repository is empty' do + allow(repository).to receive(:empty?).and_return(true) + + expect(repository.main_language).to be_nil + end + end + + describe '#before_remove_tag' do + it 'flushes the tag cache' do + expect(repository).to receive(:expire_tag_count_cache) + + repository.before_remove_tag + end + end + + describe '#branch_count' do + it 'returns the number of branches' do + expect(repository.branch_count).to be_an_instance_of(Fixnum) + end + end + + describe '#tag_count' do + it 'returns the number of tags' do + expect(repository.tag_count).to be_an_instance_of(Fixnum) + end + end + + describe '#expire_branch_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:branch_count) + + repository.expire_branch_count_cache + end + end + + describe '#expire_tag_count_cache' do + let(:cache) { repository.send(:cache) } + + it 'expires the cache' do + expect(cache).to receive(:expire).with(:tag_count) + + repository.expire_tag_count_cache + end + end + + describe '#add_tag' do + it 'adds a tag' do + expect(repository).to receive(:before_push_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:add_tag). + with(repository.path_with_namespace, '8.5', 'master', 'foo') + + repository.add_tag('8.5', 'master', 'foo') + end + end + + describe '#rm_branch' do + let(:user) { create(:user) } + + it 'removes a branch' do + expect(repository).to receive(:before_remove_branch) + expect(repository).to receive(:after_remove_branch) + + repository.rm_branch(user, 'feature') + end + end + + describe '#rm_tag' do + it 'removes a tag' do + expect(repository).to receive(:before_remove_tag) + + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + with(repository.path_with_namespace, '8.5') + + repository.rm_tag('8.5') + end + end + + describe '#avatar' do + it 'returns the first avatar file found in the repository' do + expect(repository).to receive(:blob_at_branch). + with('master', 'logo.png'). + and_return(true) + + expect(repository.avatar).to eq('logo.png') + end + + it 'caches the output' do + allow(repository).to receive(:blob_at_branch). + with('master', 'logo.png'). + and_return(true) + + expect(repository.avatar).to eq('logo.png') + + expect(repository).to_not receive(:blob_at_branch) + expect(repository.avatar).to eq('logo.png') + end + end + + describe '#expire_avatar_cache' do + let(:cache) { repository.send(:cache) } + + before do + allow(repository).to receive(:cache).and_return(cache) + end + + context 'without a branch or revision' do + it 'flushes the cache' do + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache + end + end + + context 'with a branch' do + it 'does not flush the cache if the branch is not the default branch' do + expect(cache).not_to receive(:expire) + + repository.expire_avatar_cache('cats') + end + + it 'flushes the cache if the branch equals the default branch' do + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache(repository.root_ref) + end + end + + context 'with a branch and revision' do + let(:commit) { double(:commit) } + + before do + allow(repository).to receive(:commit).and_return(commit) + end + + it 'does not flush the cache if the commit does not change any logos' do + diff = double(:diff, new_path: 'test.txt') + + expect(commit).to receive(:diffs).and_return([diff]) + expect(cache).not_to receive(:expire) + + repository.expire_avatar_cache(repository.root_ref, '123') + end + + it 'flushes the cache if the commit changes any of the logos' do + diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) + + expect(commit).to receive(:diffs).and_return([diff]) + expect(cache).to receive(:expire).with(:avatar) + + repository.expire_avatar_cache(repository.root_ref, '123') + end + end + end + + describe '#build_cache' do + let(:cache) { repository.send(:cache) } + + it 'builds the caches if they do not already exist' do + expect(cache).to receive(:exist?). + exactly(repository.cache_keys.length). + times. + and_return(false) + + repository.cache_keys.each do |key| + expect(repository).to receive(key) + end + + repository.build_cache + end + + it 'does not build any caches that already exist' do + expect(cache).to receive(:exist?). + exactly(repository.cache_keys.length). + times. + and_return(true) + + repository.cache_keys.each do |key| + expect(repository).to_not receive(key) + end + + repository.build_cache + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0ca82365b98..173628c08d0 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -16,6 +16,7 @@ # merge_requests_events :boolean default(TRUE) # tag_push_events :boolean default(TRUE) # note_events :boolean default(TRUE), not null +# build_events :boolean default(FALSE), not null # require 'spec_helper' diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index eb2dbbdc5a4..5077ac7b62b 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -10,7 +10,6 @@ # created_at :datetime # updated_at :datetime # file_name :string(255) -# expires_at :datetime # type :string(255) # visibility_level :integer default(0), not null # @@ -60,4 +59,48 @@ describe Snippet, models: true do expect(snippet.to_reference(cross)).to eq "#{project.to_reference}$#{snippet.id}" end end + + describe '.search' do + let(:snippet) { create(:snippet) } + + it 'returns snippets with a matching title' do + expect(described_class.search(snippet.title)).to eq([snippet]) + end + + it 'returns snippets with a partially matching title' do + expect(described_class.search(snippet.title[0..2])).to eq([snippet]) + end + + it 'returns snippets with a matching title regardless of the casing' do + expect(described_class.search(snippet.title.upcase)).to eq([snippet]) + end + + it 'returns snippets with a matching file name' do + expect(described_class.search(snippet.file_name)).to eq([snippet]) + end + + it 'returns snippets with a partially matching file name' do + expect(described_class.search(snippet.file_name[0..2])).to eq([snippet]) + end + + it 'returns snippets with a matching file name regardless of the casing' do + expect(described_class.search(snippet.file_name.upcase)).to eq([snippet]) + end + end + + describe '#search_code' do + let(:snippet) { create(:snippet, content: 'class Foo; end') } + + it 'returns snippets with matching content' do + expect(described_class.search_code(snippet.content)).to eq([snippet]) + end + + it 'returns snippets with partially matching content' do + expect(described_class.search_code('class')).to eq([snippet]) + end + + it 'returns snippets with matching content regardless of the casing' do + expect(described_class.search_code('FOO')).to eq([snippet]) + end + end end diff --git a/spec/models/spam_log_spec.rb b/spec/models/spam_log_spec.rb new file mode 100644 index 00000000000..c4ec7625cb0 --- /dev/null +++ b/spec/models/spam_log_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe SpamLog, models: true do + describe 'associations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:user) } + end + + describe '#remove_user' do + it 'blocks the user' do + spam_log = build(:spam_log) + + expect { spam_log.remove_user }.to change { spam_log.user.blocked? }.to(true) + end + + it 'removes the user' do + spam_log = build(:spam_log) + + expect { spam_log.remove_user }.to change { User.count }.by(-1) + end + end +end diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb new file mode 100644 index 00000000000..fe9ea7e7d1e --- /dev/null +++ b/spec/models/todo_spec.rb @@ -0,0 +1,69 @@ +# == Schema Information +# +# Table name: todos +# +# id :integer not null, primary key +# user_id :integer not null +# project_id :integer not null +# target_id :integer not null +# target_type :string not null +# author_id :integer +# note_id :integer +# action :integer not null +# state :string not null +# created_at :datetime +# updated_at :datetime +# + +require 'spec_helper' + +describe Todo, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:author).class_name("User") } + it { is_expected.to belong_to(:note) } + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:target).touch(true) } + it { is_expected.to belong_to(:user) } + end + + describe 'respond to' do + it { is_expected.to respond_to(:author_name) } + it { is_expected.to respond_to(:author_email) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:action) } + it { is_expected.to validate_presence_of(:target) } + it { is_expected.to validate_presence_of(:user) } + end + + describe '#body' do + before do + subject.target = build(:issue, title: 'Bugfix') + end + + it 'returns target title when note is blank' do + subject.note = nil + + expect(subject.body).to eq 'Bugfix' + end + + it 'returns note when note is present' do + subject.note = build(:note, note: 'quick fix') + + expect(subject.body).to eq 'quick fix' + end + end + + describe '#done!' do + it 'changes state to done' do + todo = create(:todo, state: :pending) + expect { todo.done! }.to change(todo, :state).from('pending').to('done') + end + + it 'does not raise error when is already done' do + todo = create(:todo, state: :done) + expect { todo.done! }.not_to raise_error + end + end +end diff --git a/spec/models/tree_spec.rb b/spec/models/tree_spec.rb new file mode 100644 index 00000000000..0737999e125 --- /dev/null +++ b/spec/models/tree_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Tree, models: true do + let(:repository) { create(:project).repository } + let(:sha) { repository.root_ref } + + subject { described_class.new(repository, '54fcc214') } + + describe '#readme' do + class FakeBlob + attr_reader :name + + def initialize(name) + @name = name + end + + def readme? + name =~ /^readme/i + end + end + + it 'returns nil when repository does not contains a README file' do + files = [FakeBlob.new('file'), FakeBlob.new('license'), FakeBlob.new('copying')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns nil when repository does not contains a previewable README file' do + files = [FakeBlob.new('file'), FakeBlob.new('README.pages'), FakeBlob.new('README.png')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme).to eq nil + end + + it 'returns README when repository contains a previewable README file' do + files = [FakeBlob.new('README.png'), FakeBlob.new('README'), FakeBlob.new('file')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'returns first previewable README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README.md'), FakeBlob.new('README.asciidoc')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + + it 'returns first plain text README when repository contains more than one' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.txt')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README' + end + + it 'prioritizes previewable README file over one in plain text' do + files = [FakeBlob.new('file'), FakeBlob.new('README'), FakeBlob.new('README.md')] + expect(subject).to receive(:blobs).and_return(files) + + expect(subject.readme.name).to eq 'README.md' + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 2f184bbaf92..0ab7fd88ce6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2,62 +2,63 @@ # # Table name: users # -# id :integer not null, primary key -# email :string(255) default(""), not null -# encrypted_password :string(255) default(""), not null -# reset_password_token :string(255) -# reset_password_sent_at :datetime -# remember_created_at :datetime -# sign_in_count :integer default(0) -# current_sign_in_at :datetime -# last_sign_in_at :datetime -# current_sign_in_ip :string(255) -# last_sign_in_ip :string(255) -# created_at :datetime -# updated_at :datetime -# name :string(255) -# admin :boolean default(FALSE), not null -# projects_limit :integer default(10) -# skype :string(255) default(""), not null -# linkedin :string(255) default(""), not null -# twitter :string(255) default(""), not null -# authentication_token :string(255) -# theme_id :integer default(1), not null -# bio :string(255) -# failed_attempts :integer default(0) -# locked_at :datetime -# unlock_token :string(255) -# username :string(255) -# can_create_group :boolean default(TRUE), not null -# can_create_team :boolean default(TRUE), not null -# state :string(255) -# color_scheme_id :integer default(1), not null -# notification_level :integer default(1), not null -# password_expires_at :datetime -# created_by_id :integer -# last_credential_check_at :datetime -# avatar :string(255) -# confirmation_token :string(255) -# confirmed_at :datetime -# confirmation_sent_at :datetime -# unconfirmed_email :string(255) -# hide_no_ssh_key :boolean default(FALSE) -# website_url :string(255) default(""), not null -# notification_email :string(255) -# hide_no_password :boolean default(FALSE) -# password_automatically_set :boolean default(FALSE) -# location :string(255) -# encrypted_otp_secret :string(255) -# encrypted_otp_secret_iv :string(255) -# encrypted_otp_secret_salt :string(255) -# otp_required_for_login :boolean default(FALSE), not null -# otp_backup_codes :text -# public_email :string(255) default(""), not null -# dashboard :integer default(0) -# project_view :integer default(0) -# consumed_timestep :integer -# layout :integer default(0) -# hide_project_limit :boolean default(FALSE) +# id :integer not null, primary key +# email :string(255) default(""), not null +# encrypted_password :string(255) default(""), not null +# reset_password_token :string(255) +# reset_password_sent_at :datetime +# remember_created_at :datetime +# sign_in_count :integer default(0) +# current_sign_in_at :datetime +# last_sign_in_at :datetime +# current_sign_in_ip :string(255) +# last_sign_in_ip :string(255) +# created_at :datetime +# updated_at :datetime +# name :string(255) +# admin :boolean default(FALSE), not null +# projects_limit :integer default(10) +# skype :string(255) default(""), not null +# linkedin :string(255) default(""), not null +# twitter :string(255) default(""), not null +# authentication_token :string(255) +# theme_id :integer default(1), not null +# bio :string(255) +# failed_attempts :integer default(0) +# locked_at :datetime +# username :string(255) +# can_create_group :boolean default(TRUE), not null +# can_create_team :boolean default(TRUE), not null +# state :string(255) +# color_scheme_id :integer default(1), not null +# notification_level :integer default(1), not null +# password_expires_at :datetime +# created_by_id :integer +# last_credential_check_at :datetime +# avatar :string(255) +# confirmation_token :string(255) +# confirmed_at :datetime +# confirmation_sent_at :datetime +# unconfirmed_email :string(255) +# hide_no_ssh_key :boolean default(FALSE) +# website_url :string(255) default(""), not null +# notification_email :string(255) +# hide_no_password :boolean default(FALSE) +# password_automatically_set :boolean default(FALSE) +# location :string(255) +# encrypted_otp_secret :string(255) +# encrypted_otp_secret_iv :string(255) +# encrypted_otp_secret_salt :string(255) +# otp_required_for_login :boolean default(FALSE), not null +# otp_backup_codes :text +# public_email :string(255) default(""), not null +# dashboard :integer default(0) +# project_view :integer default(0) +# consumed_timestep :integer +# layout :integer default(0) +# hide_project_limit :boolean default(FALSE) +# unlock_token :string +# otp_grace_period_started_at :datetime # require 'spec_helper' @@ -90,6 +91,8 @@ describe User, models: true do it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) } it { is_expected.to have_many(:identities).dependent(:destroy) } it { is_expected.to have_one(:abuse_report) } + it { is_expected.to have_many(:spam_logs).dependent(:destroy) } + it { is_expected.to have_many(:todos).dependent(:destroy) } end describe 'validations' do @@ -106,7 +109,7 @@ describe User, models: true do end it 'validates uniqueness' do - expect(subject).to validate_uniqueness_of(:username) + expect(subject).to validate_uniqueness_of(:username).case_insensitive end end @@ -117,37 +120,15 @@ describe User, models: true do it { is_expected.to validate_length_of(:bio).is_within(0..255) } - describe 'email' do - it 'accepts info@example.com' do - user = build(:user, email: 'info@example.com') - expect(user).to be_valid - end - - it 'accepts info+test@example.com' do - user = build(:user, email: 'info+test@example.com') - expect(user).to be_valid - end - - it "accepts o'reilly@example.com" do - user = build(:user, email: "o'reilly@example.com") - expect(user).to be_valid - end - - it 'rejects test@test@example.com' do - user = build(:user, email: 'test@test@example.com') - expect(user).to be_invalid - end - - it 'rejects mailto:test@example.com' do - user = build(:user, email: 'mailto:test@example.com') - expect(user).to be_invalid - end + it_behaves_like 'an object with email-formated attributes', :email do + subject { build(:user) } + end - it "rejects lol!'+=?><#$%^&*()@gmail.com" do - user = build(:user, email: "lol!'+=?><#$%^&*()@gmail.com") - expect(user).to be_invalid - end + it_behaves_like 'an object with email-formated attributes', :public_email, :notification_email do + subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } } + end + describe 'email' do context 'when no signup domains listed' do before { allow(current_application_settings).to receive(:restricted_signup_domains).and_return([]) } it 'accepts any email' do @@ -199,6 +180,20 @@ describe User, models: true do it { is_expected.to respond_to(:is_admin?) } it { is_expected.to respond_to(:name) } it { is_expected.to respond_to(:private_token) } + it { is_expected.to respond_to(:external?) } + end + + describe 'before save hook' do + context 'when saving an external user' do + let(:user) { create(:user) } + let(:external_user) { create(:user, external: true) } + + it "sets other properties aswell" do + expect(external_user.can_create_team).to be_falsey + expect(external_user.can_create_group).to be_falsey + expect(external_user.projects_limit).to be 0 + end + end end describe '#confirm' do @@ -275,6 +270,7 @@ describe User, models: true do expect(user).to be_two_factor_enabled expect(user.encrypted_otp_secret).not_to be_nil expect(user.otp_backup_codes).not_to be_nil + expect(user.otp_grace_period_started_at).not_to be_nil user.disable_two_factor! @@ -283,6 +279,7 @@ describe User, models: true do expect(user.encrypted_otp_secret_iv).to be_nil expect(user.encrypted_otp_secret_salt).to be_nil expect(user.otp_backup_codes).to be_nil + expect(user.otp_grace_period_started_at).to be_nil end end @@ -421,6 +418,7 @@ describe User, models: true do expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit) expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group) expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme) + expect(user.external).to be_falsey end end @@ -454,17 +452,43 @@ describe User, models: true do end end - describe 'search' do - let(:user1) { create(:user, username: 'James', email: 'james@testing.com') } - let(:user2) { create(:user, username: 'jameson', email: 'jameson@example.com') } + describe '.search' do + let(:user) { create(:user) } - it "should be case insensitive" do - expect(User.search(user1.username.upcase).to_a).to eq([user1]) - expect(User.search(user1.username.downcase).to_a).to eq([user1]) - expect(User.search(user2.username.upcase).to_a).to eq([user2]) - expect(User.search(user2.username.downcase).to_a).to eq([user2]) - expect(User.search(user1.username.downcase).to_a.size).to eq(2) - expect(User.search(user2.username.downcase).to_a.size).to eq(1) + it 'returns users with a matching name' do + expect(described_class.search(user.name)).to eq([user]) + end + + it 'returns users with a partially matching name' do + expect(described_class.search(user.name[0..2])).to eq([user]) + end + + it 'returns users with a matching name regardless of the casing' do + expect(described_class.search(user.name.upcase)).to eq([user]) + end + + it 'returns users with a matching Email' do + expect(described_class.search(user.email)).to eq([user]) + end + + it 'returns users with a partially matching Email' do + expect(described_class.search(user.email[0..2])).to eq([user]) + end + + it 'returns users with a matching Email regardless of the casing' do + expect(described_class.search(user.email.upcase)).to eq([user]) + end + + it 'returns users with a matching username' do + expect(described_class.search(user.username)).to eq([user]) + end + + it 'returns users with a partially matching username' do + expect(described_class.search(user.username[0..2])).to eq([user]) + end + + it 'returns users with a matching username regardless of the casing' do + expect(described_class.search(user.username.upcase)).to eq([user]) end end @@ -568,27 +592,39 @@ describe User, models: true do end end - describe :ldap_user? do - it "is true if provider name starts with ldap" do - user = create(:omniauth_user, provider: 'ldapmain') - expect( user.ldap_user? ).to be_truthy - end + context 'ldap synchronized user' do + describe :ldap_user? do + it 'is true if provider name starts with ldap' do + user = create(:omniauth_user, provider: 'ldapmain') + expect(user.ldap_user?).to be_truthy + end + + it 'is false for other providers' do + user = create(:omniauth_user, provider: 'other-provider') + expect(user.ldap_user?).to be_falsey + end - it "is false for other providers" do - user = create(:omniauth_user, provider: 'other-provider') - expect( user.ldap_user? ).to be_falsey + it 'is false if no extern_uid is provided' do + user = create(:omniauth_user, extern_uid: nil) + expect(user.ldap_user?).to be_falsey + end end - it "is false if no extern_uid is provided" do - user = create(:omniauth_user, extern_uid: nil) - expect( user.ldap_user? ).to be_falsey + describe :ldap_identity do + it 'returns ldap identity' do + user = create :omniauth_user + expect(user.ldap_identity.provider).not_to be_empty + end end - end - describe :ldap_identity do - it "returns ldap identity" do - user = create :omniauth_user - expect(user.ldap_identity.provider).not_to be_empty + describe '#ldap_block' do + let(:user) { create(:omniauth_user, provider: 'ldapmain', name: 'John Smith') } + + it 'blocks user flaging the action caming from ldap' do + user.ldap_block + expect(user.blocked?).to be_truthy + expect(user.ldap_blocked?).to be_truthy + end end end diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index c1b03838aa9..ddc49495eda 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -189,6 +189,38 @@ describe WikiPage, models: true do end end + describe '#historical?' do + before do + create_page('Update', 'content') + @page = wiki.find_page('Update') + 3.times { |i| @page.update("content #{i}") } + end + + after do + destroy_page('Update') + end + + it 'returns true when requesting an old version' do + old_version = @page.versions.last.to_s + old_page = wiki.find_page('Update', old_version) + + expect(old_page.historical?).to eq true + end + + it 'returns false when requesting latest version' do + latest_version = @page.versions.first.to_s + latest_page = wiki.find_page('Update', latest_version) + + expect(latest_page.historical?).to eq false + end + + it 'returns false when version is nil' do + latest_page = wiki.find_page('Update', nil) + + expect(latest_page.historical?).to eq false + end + end + private def remove_temp_repo(path) diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb index 36461e84c3a..55582aa53d2 100644 --- a/spec/requests/api/branches_spec.rb +++ b/spec/requests/api/branches_spec.rb @@ -7,8 +7,8 @@ describe API::API, api: true do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } - let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:branch_name) { 'feature' } let!(:branch_sha) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb new file mode 100644 index 00000000000..967c34800d0 --- /dev/null +++ b/spec/requests/api/builds_spec.rb @@ -0,0 +1,244 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:api_user) { user } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:developer) { create(:project_member, :developer, user: user, project: project) } + let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) } + let(:commit) { create(:ci_commit, project: project)} + let(:build) { create(:ci_build, commit: commit) } + + describe 'GET /projects/:id/builds ' do + let(:query) { '' } + + before { get api("/projects/#{project.id}/builds?#{query}", api_user) } + + context 'authorized user' do + it 'should return project builds' do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + + context 'filter project with one scope element' do + let(:query) { 'scope=pending' } + + it do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + end + + context 'filter project with array of scope elements' do + let(:query) { 'scope[0]=pending&scope[1]=running' } + + it do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + end + + context 'respond 400 when scope contains invalid state' do + let(:query) { 'scope[0]=pending&scope[1]=unknown_status' } + + it { expect(response.status).to eq(400) } + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return project builds' do + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/repository/commits/:sha/builds' do + before do + project.ensure_ci_commit(commit.sha) + get api("/projects/#{project.id}/repository/commits/#{commit.sha}/builds", api_user) + end + + context 'authorized user' do + it 'should return project builds for specific commit' do + expect(response.status).to eq(200) + expect(json_response).to be_an Array + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return project builds' do + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/builds/:build_id' do + before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) } + + context 'authorized user' do + it 'should return specific build data' do + expect(response.status).to eq(200) + expect(json_response['name']).to eq('test') + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return specific build data' do + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/builds/:build_id/artifacts' do + before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) } + + context 'build with artifacts' do + let(:build) { create(:ci_build, :artifacts, commit: commit) } + + context 'authorized user' do + let(:download_headers) do + { 'Content-Transfer-Encoding'=>'binary', + 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + end + + it 'should return specific build artifacts' do + expect(response.status).to eq(200) + expect(response.headers).to include(download_headers) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return specific build artifacts' do + expect(response.status).to eq(401) + end + end + end + + it 'should not return build artifacts if not uploaded' do + expect(response.status).to eq(404) + end + end + + describe 'GET /projects/:id/builds/:build_id/trace' do + let(:build) { create(:ci_build, :trace, commit: commit) } + + before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) } + + context 'authorized user' do + it 'should return specific build trace' do + expect(response.status).to eq(200) + expect(response.body).to eq(build.trace) + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not return specific build trace' do + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/builds/:build_id/cancel' do + before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) } + + context 'authorized user' do + context 'user with :update_build persmission' do + it 'should cancel running or pending build' do + expect(response.status).to eq(201) + expect(project.builds.first.status).to eq('canceled') + end + end + + context 'user without :update_build permission' do + let(:api_user) { user2 } + + it 'should not cancel build' do + expect(response.status).to eq(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not cancel build' do + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/builds/:build_id/retry' do + let(:build) { create(:ci_build, :canceled, commit: commit) } + + before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) } + + context 'authorized user' do + context 'user with :update_build permission' do + it 'should retry non-running build' do + expect(response.status).to eq(201) + expect(project.builds.first.status).to eq('canceled') + expect(json_response['status']).to eq('pending') + end + end + + context 'user without :update_build permission' do + let(:api_user) { user2 } + + it 'should not retry build' do + expect(response.status).to eq(403) + end + end + end + + context 'unauthorized user' do + let(:api_user) { nil } + + it 'should not retry build' do + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/builds/:build_id/erase' do + before do + post api("/projects/#{project.id}/builds/#{build.id}/erase", user) + end + + context 'build is erasable' do + let(:build) { create(:ci_build, :trace, :artifacts, :success, project: project, commit: commit) } + + it 'should erase build content' do + expect(response.status).to eq 201 + expect(build.trace).to be_empty + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy + end + + it 'should update build' do + expect(build.reload.erased_at).to be_truthy + expect(build.reload.erased_by).to eq user + end + end + + context 'build is not erasable' do + let(:build) { create(:ci_build, :trace, project: project, commit: commit) } + + it 'should respond with forbidden' do + expect(response.status).to eq 403 + end + end + end +end diff --git a/spec/requests/api/commit_status_spec.rb b/spec/requests/api/commit_status_spec.rb index a28607bd240..429a24109fd 100644 --- a/spec/requests/api/commit_status_spec.rb +++ b/spec/requests/api/commit_status_spec.rb @@ -1,86 +1,126 @@ require 'spec_helper' -describe API::API, api: true do +describe API::CommitStatus, api: true do include ApiHelpers - let(:user) { create(:user) } - let(:user2) { create(:user) } - let!(:project) { create(:project, creator_id: user.id) } - let!(:reporter) { create(:project_member, user: user, project: project, access_level: ProjectMember::REPORTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + + let!(:project) { create(:project) } let(:commit) { project.repository.commit } - let!(:ci_commit) { project.ensure_ci_commit(commit.id) } let(:commit_status) { create(:commit_status, commit: ci_commit) } + let(:guest) { create_user(:guest) } + let(:reporter) { create_user(:reporter) } + let(:developer) { create_user(:developer) } + let(:sha) { commit.id } + describe "GET /projects/:id/repository/commits/:sha/statuses" do - context "reporter user" do - let(:statuses_id) { json_response.map { |status| status['id'] } } - - before do - @status1 = create(:commit_status, commit: ci_commit, status: 'running') - @status2 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'pending') - @status3 = create(:commit_status, commit: ci_commit, name: 'coverage', ref: 'develop', status: 'running', allow_failure: true) - @status4 = create(:commit_status, commit: ci_commit, name: 'coverage', status: 'success') - @status5 = create(:commit_status, commit: ci_commit, ref: 'develop', status: 'success') - @status6 = create(:commit_status, commit: ci_commit, status: 'success') - end + let(:get_url) { "/projects/#{project.id}/repository/commits/#{sha}/statuses" } - it "should return latest commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user) - expect(response.status).to eq(200) + context 'ci commit exists' do + let!(:ci_commit) { project.ensure_ci_commit(commit.id) } - expect(json_response).to be_an Array - expect(statuses_id).to contain_exactly(@status3.id, @status4.id, @status5.id, @status6.id) - json_response.sort_by!{ |status| status['id'] } - expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) + it_behaves_like 'a paginated resources' do + let(:request) { get api(get_url, reporter) } end - it "should return all commit statuses" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?all=1", user) - expect(response.status).to eq(200) + context "reporter user" do + let(:statuses_id) { json_response.map { |status| status['id'] } } - expect(json_response).to be_an Array - expect(statuses_id).to contain_exactly(@status1.id, @status2.id, @status3.id, @status4.id, @status5.id, @status6.id) - end + def create_status(opts = {}) + create(:commit_status, { commit: ci_commit }.merge(opts)) + end - it "should return latest commit statuses for specific ref" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?ref=develop", user) - expect(response.status).to eq(200) + let!(:status1) { create_status(status: 'running') } + let!(:status2) { create_status(name: 'coverage', status: 'pending') } + let!(:status3) { create_status(ref: 'develop', status: 'running', allow_failure: true) } + let!(:status4) { create_status(name: 'coverage', status: 'success') } + let!(:status5) { create_status(name: 'coverage', ref: 'develop', status: 'success') } + let!(:status6) { create_status(status: 'success') } - expect(json_response).to be_an Array - expect(statuses_id).to contain_exactly(@status3.id, @status5.id) + context 'latest commit statuses' do + before { get api(get_url, reporter) } + + it 'returns latest commit statuses' do + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(status3.id, status4.id, status5.id, status6.id) + json_response.sort_by!{ |status| status['id'] } + expect(json_response.map{ |status| status['allow_failure'] }).to eq([true, false, false, false]) + end + end + + context 'all commit statuses' do + before { get api(get_url, reporter), all: 1 } + + it 'returns all commit statuses' do + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(status1.id, status2.id, + status3.id, status4.id, + status5.id, status6.id) + end + end + + context 'latest commit statuses for specific ref' do + before { get api(get_url, reporter), ref: 'develop' } + + it 'returns latest commit statuses for specific ref' do + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(status3.id, status5.id) + end + end + + context 'latest commit statues for specific name' do + before { get api(get_url, reporter), name: 'coverage' } + + it 'return latest commit statuses for specific name' do + expect(response.status).to eq(200) + + expect(json_response).to be_an Array + expect(statuses_id).to contain_exactly(status4.id, status5.id) + end + end end + end - it "should return latest commit statuses for specific name" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses?name=coverage", user) - expect(response.status).to eq(200) + context 'ci commit does not exist' do + before { get api(get_url, reporter) } + it 'returns empty array' do + expect(response.status).to eq 200 expect(json_response).to be_an Array - expect(statuses_id).to contain_exactly(@status3.id, @status4.id) + expect(json_response).to be_empty end end context "guest user" do + before { get api(get_url, guest) } + it "should not return project commits" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses", user2) expect(response.status).to eq(403) end end context "unauthorized user" do + before { get api(get_url) } + it "should not return project commits" do - get api("/projects/#{project.id}/repository/commits/#{commit.id}/statuses") expect(response.status).to eq(401) end end end describe 'POST /projects/:id/statuses/:sha' do - let(:post_url) { "/projects/#{project.id}/statuses/#{commit.id}" } + let(:post_url) { "/projects/#{project.id}/statuses/#{sha}" } - context 'reporter user' do - context 'should create commit status' do - it 'with only required parameters' do - post api(post_url, user), state: 'success' + context 'developer user' do + context 'only required parameters' do + before { post api(post_url, developer), state: 'success' } + + it 'creates commit status' do expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -89,9 +129,17 @@ describe API::API, api: true do expect(json_response['target_url']).to be_nil expect(json_response['description']).to be_nil end + end + + context 'with all optional parameters' do + before do + optional_params = { state: 'success', context: 'coverage', + ref: 'develop', target_url: 'url', description: 'test' } + + post api(post_url, developer), optional_params + end - it 'with all optional parameters' do - post api(post_url, user), state: 'success', context: 'coverage', ref: 'develop', target_url: 'url', description: 'test' + it 'creates commit status' do expect(response.status).to eq(201) expect(json_response['sha']).to eq(commit.id) expect(json_response['status']).to eq('success') @@ -102,36 +150,60 @@ describe API::API, api: true do end end - context 'should not create commit status' do - it 'with invalid state' do - post api(post_url, user), state: 'invalid' + context 'invalid status' do + before { post api(post_url, developer), state: 'invalid' } + + it 'does not create commit status' do expect(response.status).to eq(400) end + end + + context 'request without state' do + before { post api(post_url, developer) } - it 'without state' do - post api(post_url, user) + it 'does not create commit status' do expect(response.status).to eq(400) end + end - it 'invalid commit' do - post api("/projects/#{project.id}/statuses/invalid_sha", user), state: 'running' + context 'invalid commit' do + let(:sha) { 'invalid_sha' } + before { post api(post_url, developer), state: 'running' } + + it 'returns not found error' do expect(response.status).to eq(404) end end end + context 'reporter user' do + before { post api(post_url, reporter) } + + it 'should not create commit status' do + expect(response.status).to eq(403) + end + end + context 'guest user' do + before { post api(post_url, guest) } + it 'should not create commit status' do - post api(post_url, user2) expect(response.status).to eq(403) end end context 'unauthorized user' do + before { post api(post_url) } + it 'should not create commit status' do - post api(post_url) expect(response.status).to eq(401) end end end + + def create_user(access_level_trait) + user = create(:user) + create(:project_member, access_level_trait, user: user, project: project) + user + end end diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 49acc3368f4..7ff21175c1b 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -6,8 +6,8 @@ describe API::API, api: true do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } - let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') } let!(:another_note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'another comment on a commit') } diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb index 3fe7efff5ba..fa94e03ec32 100644 --- a/spec/requests/api/fork_spec.rb +++ b/spec/requests/api/fork_spec.rb @@ -12,7 +12,7 @@ describe API::API, api: true do end let(:project_user2) do - create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) + create(:project_member, :guest, user: user2, project: project) end describe 'POST /projects/fork/:id' do diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index 8d0ae1475c2..22802dd0e05 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -54,6 +54,18 @@ describe API::API, api: true do project.team << [user, :developer] end + context "git push with project.wiki" do + it 'responds with success' do + project_wiki = create(:project, name: 'my.wiki', path: 'my.wiki') + project_wiki.team << [user, :developer] + + push(key, project_wiki) + + expect(response.status).to eq(200) + expect(json_response["status"]).to be_truthy + end + end + context "git pull" do it do pull(key, project) diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 5e65ad18c0e..bb2ab058003 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -3,7 +3,11 @@ require 'spec_helper' describe API::API, api: true do include ApiHelpers let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace ) } + let(:non_member) { create(:user) } + let(:author) { create(:author) } + let(:assignee) { create(:assignee) } + let(:admin) { create(:admin) } + let!(:project) { create(:project, :public, namespace: user.namespace ) } let!(:closed_issue) do create :closed_issue, author: user, @@ -12,6 +16,13 @@ describe API::API, api: true do state: :closed, milestone: milestone end + let!(:confidential_issue) do + create :issue, + :confidential, + project: project, + author: author, + assignee: assignee + end let!(:issue) do create :issue, author: user, @@ -46,10 +57,10 @@ describe API::API, api: true do expect(json_response.first['title']).to eq(issue.title) end - it "should add pagination headers" do - get api("/issues?per_page=3", user) + it "should add pagination headers and keep query params" do + get api("/issues?state=closed&per_page=3", user) expect(response.headers['Link']).to eq( - '<http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3>; rel="last"' + '<http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="first", <http://www.example.com/api/v3/issues?page=1&per_page=3&private_token=%s&state=closed>; rel="last"' % [user.private_token, user.private_token] ) end @@ -123,10 +134,43 @@ describe API::API, api: true do let(:base_url) { "/projects/#{project.id}" } let(:title) { milestone.title } - it "should return project issues" do + it 'should return project issues without confidential issues for non project members' do + get api("#{base_url}/issues", non_member) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(2) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for author' do + get api("#{base_url}/issues", author) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for assignee' do + get api("#{base_url}/issues", assignee) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project issues with confidential issues for project members' do get api("#{base_url}/issues", user) expect(response.status).to eq(200) expect(json_response).to be_an Array + expect(json_response.length).to eq(3) + expect(json_response.first['title']).to eq(issue.title) + end + + it 'should return project confidential issues for admin' do + get api("#{base_url}/issues", admin) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(3) expect(json_response.first['title']).to eq(issue.title) end @@ -206,6 +250,41 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/54321", user) expect(response.status).to eq(404) end + + context 'confidential issues' do + it "should return 404 for non project members" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member) + expect(response.status).to eq(404) + end + + it "should return confidential issue for project members" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", user) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for author" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", author) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for assignee" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", assignee) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + + it "should return confidential issue for admin" do + get api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin) + expect(response.status).to eq(200) + expect(json_response['title']).to eq(confidential_issue.title) + expect(json_response['iid']).to eq(confidential_issue.iid) + end + end end describe "POST /projects/:id/issues" do @@ -241,6 +320,37 @@ describe API::API, api: true do end end + describe 'POST /projects/:id/issues with spam filtering' do + before do + Grape::Endpoint.before_each do |endpoint| + allow(endpoint).to receive(:check_for_spam?).and_return(true) + allow(endpoint).to receive(:is_spam?).and_return(true) + end + end + + let(:params) do + { + title: 'new issue', + description: 'content here', + labels: 'label, label2' + } + end + + it "should not create a new project issue" do + expect { post api("/projects/#{project.id}/issues", user), params }.not_to change(Issue, :count) + expect(response.status).to eq(400) + expect(json_response['message']).to eq({ "error" => "Spam detected" }) + + spam_logs = SpamLog.all + expect(spam_logs.count).to eq(1) + expect(spam_logs[0].title).to eq('new issue') + expect(spam_logs[0].description).to eq('content here') + expect(spam_logs[0].user).to eq(user) + expect(spam_logs[0].noteable_type).to eq('Issue') + expect(spam_logs[0].project_id).to eq(project.id) + end + end + describe "PUT /projects/:id/issues/:issue_id to update only title" do it "should update a project issue" do put api("/projects/#{project.id}/issues/#{issue.id}", user), @@ -263,6 +373,35 @@ describe API::API, api: true do expect(response.status).to eq(400) expect(json_response['message']['labels']['?']['title']).to eq(['is invalid']) end + + context 'confidential issues' do + it "should return 403 for non project members" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", non_member), + title: 'updated title' + expect(response.status).to eq(403) + end + + it "should update a confidential issue for project members" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", user), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + + it "should update a confidential issue for author" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", author), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + + it "should update a confidential issue for admin" do + put api("/projects/#{project.id}/issues/#{confidential_issue.id}", admin), + title: 'updated title' + expect(response.status).to eq(200) + expect(json_response['title']).to eq('updated title') + end + end end describe 'PUT /projects/:id/issues/:issue_id to update labels' do diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e194eb93cf4..4fd1df25568 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -10,6 +10,7 @@ describe API::API, api: true do let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) } let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") } let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") } + let(:milestone) { create(:milestone, title: '1.0.0', project: project) } before do project.team << [user, :reporters] @@ -109,12 +110,13 @@ describe API::API, api: true do end end - describe "GET /projects/:id/merge_request/:merge_request_id" do + describe "GET /projects/:id/merge_requests/:merge_request_id" do it "should return merge_request" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user) expect(response.status).to eq(200) expect(json_response['title']).to eq(merge_request.title) expect(json_response['iid']).to eq(merge_request.iid) + expect(json_response['merge_status']).to eq('can_be_merged') end it 'should return merge_request by iid' do @@ -126,14 +128,14 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999", user) + get api("/projects/#{project.id}/merge_requests/999", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/commits' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/commits' do context 'valid merge request' do - before { get api("/projects/#{project.id}/merge_request/#{merge_request.id}/commits", user) } + before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/commits", user) } let(:commit) { merge_request.commits.first } it { expect(response.status).to eq 200 } @@ -143,20 +145,20 @@ describe API::API, api: true do end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/commits", user) + get api("/projects/#{project.id}/merge_requests/999/commits", user) expect(response.status).to eq(404) end end - describe 'GET /projects/:id/merge_request/:merge_request_id/changes' do + describe 'GET /projects/:id/merge_requests/:merge_request_id/changes' do it 'should return the change information of the merge_request' do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/changes", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/changes", user) expect(response.status).to eq 200 expect(json_response['changes'].size).to eq(merge_request.diffs.size) end it 'returns a 404 when merge_request_id not found' do - get api("/projects/#{project.id}/merge_request/999/changes", user) + get api("/projects/#{project.id}/merge_requests/999/changes", user) expect(response.status).to eq(404) end end @@ -169,10 +171,12 @@ describe API::API, api: true do source_branch: 'feature_conflict', target_branch: 'master', author: user, - labels: 'label, label2' + labels: 'label, label2', + milestone_id: milestone.id expect(response.status).to eq(201) expect(json_response['title']).to eq('Test merge_request') expect(json_response['labels']).to eq(['label', 'label2']) + expect(json_response['milestone']['id']).to eq(milestone.id) end it "should return 422 when source_branch equals target_branch" do @@ -311,19 +315,19 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id to close MR" do + describe "PUT /projects/:id/merge_requests/:merge_request_id to close MR" do it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), state_event: "close" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), state_event: "close" expect(response.status).to eq(200) expect(json_response['state']).to eq('closed') end end - describe "PUT /projects/:id/merge_request/:merge_request_id/merge" do + describe "PUT /projects/:id/merge_requests/:merge_request_id/merge" do let(:ci_commit) { create(:ci_commit_without_jobs) } it "should return merge_request in case of success" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(200) end @@ -332,7 +336,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest). to receive(:can_be_merged?).and_return(false) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(406) expect(json_response['message']).to eq('Branch cannot be merged') @@ -340,14 +344,14 @@ describe API::API, api: true do it "should return 405 if merge_request is not open" do merge_request.close - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end it "should return 405 if merge_request is a work in progress" do merge_request.update_attribute(:title, "WIP: #{merge_request.title}") - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user) expect(response.status).to eq(405) expect(json_response['message']).to eq('405 Method Not Allowed') end @@ -355,7 +359,7 @@ describe API::API, api: true do it "should return 401 if user has no permissions to merge" do user2 = create(:user) project.team << [user2, :reporter] - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user2) + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user2) expect(response.status).to eq(401) expect(json_response['message']).to eq('401 Unauthorized') end @@ -364,7 +368,7 @@ describe API::API, api: true do allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) allow(ci_commit).to receive(:active?).and_return(true) - put api("/projects/#{project.id}/merge_request/#{merge_request.id}/merge", user), merge_when_build_succeeds: true + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}/merge", user), merge_when_build_succeeds: true expect(response.status).to eq(200) expect(json_response['title']).to eq('Test') @@ -372,33 +376,39 @@ describe API::API, api: true do end end - describe "PUT /projects/:id/merge_request/:merge_request_id" do - it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), title: "New title" + describe "PUT /projects/:id/merge_requests/:merge_request_id" do + it "updates title and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: "New title" expect(response.status).to eq(200) expect(json_response['title']).to eq('New title') end - it "should return merge_request" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), description: "New description" + it "updates description and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), description: "New description" expect(response.status).to eq(200) expect(json_response['description']).to eq('New description') end + it "updates milestone_id and returns merge_request" do + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), milestone_id: milestone.id + expect(response.status).to eq(200) + expect(json_response['milestone']['id']).to eq(milestone.id) + end + it "should return 400 when source_branch is specified" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), source_branch: "master", target_branch: "master" expect(response.status).to eq(400) end it "should return merge_request with renamed target_branch" do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", user), target_branch: "wiki" + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), target_branch: "wiki" expect(response.status).to eq(200) expect(json_response['target_branch']).to eq('wiki') end it 'should return 400 on invalid label names' do - put api("/projects/#{project.id}/merge_request/#{merge_request.id}", + put api("/projects/#{project.id}/merge_requests/#{merge_request.id}", user), title: 'new issue', labels: 'label, ?' @@ -407,11 +417,11 @@ describe API::API, api: true do end end - describe "POST /projects/:id/merge_request/:merge_request_id/comments" do + describe "POST /projects/:id/merge_requests/:merge_request_id/comments" do it "should return comment" do original_count = merge_request.notes.size - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user), note: "My comment" + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user), note: "My comment" expect(response.status).to eq(201) expect(json_response['note']).to eq('My comment') expect(json_response['author']['name']).to eq(user.name) @@ -420,20 +430,20 @@ describe API::API, api: true do end it "should return 400 if note is missing" do - post api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + post api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(400) end it "should return 404 if note is attached to non existent merge request" do - post api("/projects/#{project.id}/merge_request/404/comments", user), + post api("/projects/#{project.id}/merge_requests/404/comments", user), note: 'My comment' expect(response.status).to eq(404) end end - describe "GET :id/merge_request/:merge_request_id/comments" do + describe "GET :id/merge_requests/:merge_request_id/comments" do it "should return merge_request comments ordered by created_at" do - get api("/projects/#{project.id}/merge_request/#{merge_request.id}/comments", user) + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/comments", user) expect(response.status).to eq(200) expect(json_response).to be_an Array expect(json_response.length).to eq(2) @@ -443,11 +453,33 @@ describe API::API, api: true do end it "should return a 404 error if merge_request_id not found" do - get api("/projects/#{project.id}/merge_request/999/comments", user) + get api("/projects/#{project.id}/merge_requests/999/comments", user) expect(response.status).to eq(404) end end + describe 'GET :id/merge_requests/:merge_request_id/closes_issues' do + it 'returns the issue that will be closed on merge' do + issue = create(:issue, project: project) + mr = merge_request.tap do |mr| + mr.update_attribute(:description, "Closes #{issue.to_reference(mr.project)}") + end + + get api("/projects/#{project.id}/merge_requests/#{mr.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(1) + expect(json_response.first['id']).to eq(issue.id) + end + + it 'returns an empty array when there are no issues to be closed' do + get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/closes_issues", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(0) + end + end + def mr_with_later_created_and_updated_at_time merge_request merge_request.created_at += 1.hour diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index 8b177af4689..39f9a06fe1b 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -10,9 +10,32 @@ describe API::API, api: true do let!(:issue_note) { create(:note, noteable: issue, project: project, author: user) } let!(:merge_request_note) { create(:note, noteable: merge_request, project: project, author: user) } let!(:snippet_note) { create(:note, noteable: snippet, project: project, author: user) } + + # For testing the cross-reference of a private issue in a public issue + let(:private_user) { create(:user) } + let(:private_project) do + create(:project, namespace: private_user.namespace). + tap { |p| p.team << [private_user, :master] } + end + let(:private_issue) { create(:issue, project: private_project) } + + let(:ext_proj) { create(:project, :public) } + let(:ext_issue) { create(:issue, project: ext_proj) } + + let!(:cross_reference_note) do + create :note, + noteable: ext_issue, project: ext_proj, + note: "mentioned in issue #{private_issue.to_reference(ext_proj)}", + system: true + end + before { project.team << [user, :reporter] } describe "GET /projects/:id/noteable/:noteable_id/notes" do + it_behaves_like 'a paginated resources' do + let(:request) { get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) } + end + context "when noteable is an Issue" do it "should return an array of issue notes" do get api("/projects/#{project.id}/issues/#{issue.id}/notes", user) @@ -25,6 +48,24 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/123/notes", user) expect(response.status).to eq(404) end + + context "that references a private issue" do + it "should return an empty array" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response).to be_empty + end + + context "and current user can view the note" do + it "should return an empty array" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes", private_user) + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.first['body']).to eq(cross_reference_note.note) + end + end + end end context "when noteable is a Snippet" do @@ -68,6 +109,21 @@ describe API::API, api: true do get api("/projects/#{project.id}/issues/#{issue.id}/notes/123", user) expect(response.status).to eq(404) end + + context "that references a private issue" do + it "should return a 404 error" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", user) + expect(response.status).to eq(404) + end + + context "and current user can view the note" do + it "should return an issue note by id" do + get api("/projects/#{ext_proj.id}/issues/#{ext_issue.id}/notes/#{cross_reference_note.id}", private_user) + expect(response.status).to eq(200) + expect(json_response['body']).to eq(cross_reference_note.note) + end + end + end end context "when noteable is a Snippet" do diff --git a/spec/requests/api/project_members_spec.rb b/spec/requests/api/project_members_spec.rb index 6358f6a2a4a..4301588b16a 100644 --- a/spec/requests/api/project_members_spec.rb +++ b/spec/requests/api/project_members_spec.rb @@ -6,8 +6,8 @@ describe API::API, api: true do let(:user2) { create(:user) } let(:user3) { create(:user) } let(:project) { create(:project, creator_id: user.id, namespace: user.namespace) } - let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } describe "GET /projects/:id/members" do before { project_member } diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb new file mode 100644 index 00000000000..3722ddf5a33 --- /dev/null +++ b/spec/requests/api/project_snippets_spec.rb @@ -0,0 +1,18 @@ +require 'rails_helper' + +describe API::API, api: true do + include ApiHelpers + + describe 'GET /projects/:project_id/snippets/:id' do + # TODO (rspeicher): Deprecated; remove in 9.0 + it 'always exposes expires_at as nil' do + admin = create(:admin) + snippet = create(:project_snippet, author: admin) + + get api("/projects/#{snippet.project.id}/snippets/#{snippet.id}", admin) + + expect(json_response).to have_key('expires_at') + expect(json_response['expires_at']).to be_nil + end + end +end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 7f0f9454b10..a6699cdc81c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -12,8 +12,8 @@ describe API::API, api: true do let(:project2) { create(:project, path: 'project2', creator_id: user.id, namespace: user.namespace) } let(:project3) { create(:project, path: 'project3', creator_id: user.id, namespace: user.namespace) } let(:snippet) { create(:project_snippet, author: user, project: project, title: 'example') } - let(:project_member) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let(:project_member2) { create(:project_member, user: user3, project: project, access_level: ProjectMember::DEVELOPER) } + let(:project_member) { create(:project_member, :master, user: user, project: project) } + let(:project_member2) { create(:project_member, :developer, user: user3, project: project) } let(:user4) { create(:user) } let(:project3) do create(:project, @@ -90,6 +90,29 @@ describe API::API, api: true do end end + context 'and using the visibility filter' do + it 'should filter based on private visibility param' do + get api('/projects', user), { visibility: 'private' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PRIVATE).count) + end + + it 'should filter based on internal visibility param' do + get api('/projects', user), { visibility: 'internal' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::INTERNAL).count) + end + + it 'should filter based on public visibility param' do + get api('/projects', user), { visibility: 'public' } + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(json_response.length).to eq(user.namespace.projects.where(visibility_level: Gitlab::VisibilityLevel::PUBLIC).count) + end + end + context 'and using sorting' do before do project2 @@ -353,6 +376,20 @@ describe API::API, api: true do end end + describe "POST /projects/:id/uploads" do + before { project } + + it "uploads the file and returns its info" do + post api("/projects/#{project.id}/uploads", user), file: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") + + expect(response.status).to be(201) + expect(json_response['alt']).to eq("dk") + expect(json_response['url']).to start_with("/uploads/") + expect(json_response['url']).to end_with("/dk.png") + expect(json_response['is_image']).to eq(true) + end + end + describe 'GET /projects/:id' do before { project } before { project_member } @@ -382,6 +419,15 @@ describe API::API, api: true do expect(response.status).to eq(404) end + it 'should handle users with dots' do + dot_user = create(:user, username: 'dot.user') + project = create(:project, creator_id: dot_user.id, namespace: dot_user.namespace) + + get api("/projects/#{dot_user.namespace.name}%2F#{project.path}", dot_user) + expect(response.status).to eq(200) + expect(json_response['name']).to eq(project.name) + end + describe 'permissions' do context 'all projects' do it 'Contains permission information' do @@ -701,6 +747,42 @@ describe API::API, api: true do end end + describe "POST /projects/:id/share" do + let(:group) { create(:group) } + + it "should share project with group" do + expect do + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + end.to change { ProjectGroupLink.count }.by(1) + + expect(response.status).to eq 201 + expect(json_response['group_id']).to eq group.id + expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER + end + + it "should return a 400 error when group id is not given" do + post api("/projects/#{project.id}/share", user), group_access: Gitlab::Access::DEVELOPER + expect(response.status).to eq 400 + end + + it "should return a 400 error when access level is not given" do + post api("/projects/#{project.id}/share", user), group_id: group.id + expect(response.status).to eq 400 + end + + it "should return a 400 error when sharing is disabled" do + project.namespace.update(share_with_group_lock: true) + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER + expect(response.status).to eq 400 + end + + it "should return a 409 error when wrong params passed" do + post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: 1234 + expect(response.status).to eq 409 + expect(json_response['message']).to eq 'Group access is not included in the list' + end + end + describe 'GET /projects/search/:query' do let!(:query) { 'query'} let!(:search) { create(:empty_project, name: query, creator_id: user.id, namespace: user.namespace) } diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 4911cdd9da6..7cf4a01d76b 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -4,12 +4,13 @@ require 'mime/types' describe API::API, api: true do include ApiHelpers include RepoHelpers + include WorkhorseHelpers let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } - let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tree" do context "authorized user" do @@ -91,21 +92,27 @@ describe API::API, api: true do get api("/projects/#{project.id}/repository/archive", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.gz/) end it "should get the archive.zip" do get api("/projects/#{project.id}/repository/archive.zip", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.zip/) end it "should get the archive.tar.bz2" do get api("/projects/#{project.id}/repository/archive.tar.bz2", user) repo_name = project.repository.name.gsub("\.git", "") expect(response.status).to eq(200) - expect(json_response['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) + type, params = workhorse_send_data + expect(type).to eq('git-archive') + expect(params['ArchivePath']).to match(/#{repo_name}\-[^\.]+\.tar.bz2/) end it "should return 404 for invalid sha" do diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb new file mode 100644 index 00000000000..3af61d4b335 --- /dev/null +++ b/spec/requests/api/runners_spec.rb @@ -0,0 +1,464 @@ +require 'spec_helper' + +describe API::Runners, api: true do + include ApiHelpers + + let(:admin) { create(:user, :admin) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + + let(:project) { create(:project, creator_id: user.id) } + let(:project2) { create(:project, creator_id: user.id) } + + let!(:shared_runner) { create(:ci_runner, :shared) } + let!(:unused_specific_runner) { create(:ci_runner) } + + let!(:specific_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + end + end + + let!(:two_projects_runner) do + create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project) + create(:ci_runner_project, runner: runner, project: project2) + end + end + + before do + # Set project access for users + create(:project_member, :master, user: user, project: project) + create(:project_member, :master, user: user, project: project2) + create(:project_member, :reporter, user: user2, project: project) + end + + describe 'GET /runners' do + context 'authorized user' do + it 'should return user available runners' do + get api('/runners', user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should filter runners by scope' do + get api('/runners?scope=active', user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should avoid filtering if scope is invalid' do + get api('/runners?scope=unknown', user) + expect(response.status).to eq(400) + end + end + + context 'unauthorized user' do + it 'should not return runners' do + get api('/runners') + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /runners/all' do + context 'authorized user' do + context 'with admin privileges' do + it 'should return all runners' do + get api('/runners/all', admin) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_truthy + end + end + + context 'without admin privileges' do + it 'should not return runners list' do + get api('/runners/all', user) + + expect(response.status).to eq(403) + end + end + + it 'should filter runners by scope' do + get api('/runners/all?scope=specific', admin) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_falsey + end + + it 'should avoid filtering if scope is invalid' do + get api('/runners?scope=unknown', admin) + expect(response.status).to eq(400) + end + end + + context 'unauthorized user' do + it 'should not return runners' do + get api('/runners') + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it "should return runner's details" do + get api("/runners/#{shared_runner.id}", admin) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(shared_runner.description) + end + end + + context 'when runner is not shared' do + it "should return runner's details" do + get api("/runners/#{specific_runner.id}", admin) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(specific_runner.description) + end + end + + it 'should return 404 if runner does not exists' do + get api('/runners/9999', admin) + + expect(response.status).to eq(404) + end + end + + context "runner project's administrative user" do + context 'when runner is not shared' do + it "should return runner's details" do + get api("/runners/#{specific_runner.id}", user) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(specific_runner.description) + end + end + + context 'when runner is shared' do + it "should return runner's details" do + get api("/runners/#{shared_runner.id}", user) + + expect(response.status).to eq(200) + expect(json_response['description']).to eq(shared_runner.description) + end + end + end + + context 'other authorized user' do + it "should not return runner's details" do + get api("/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not return runner's details" do + get api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'PUT /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'should update runner' do + description = shared_runner.description + active = shared_runner.active + + put api("/runners/#{shared_runner.id}", admin), description: "#{description}_updated", active: !active, + tag_list: ['ruby2.1', 'pgsql', 'mysql'] + shared_runner.reload + + expect(response.status).to eq(200) + expect(shared_runner.description).to eq("#{description}_updated") + expect(shared_runner.active).to eq(!active) + expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql') + end + end + + context 'when runner is not shared' do + it 'should update runner' do + description = specific_runner.description + put api("/runners/#{specific_runner.id}", admin), description: 'test' + specific_runner.reload + + expect(response.status).to eq(200) + expect(specific_runner.description).to eq('test') + expect(specific_runner.description).not_to eq(description) + end + end + + it 'should return 404 if runner does not exists' do + put api('/runners/9999', admin), description: 'test' + + expect(response.status).to eq(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'should not update runner' do + put api("/runners/#{shared_runner.id}", user) + + expect(response.status).to eq(403) + end + end + + context 'when runner is not shared' do + it 'should not update runner without access to it' do + put api("/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + + it 'should update runner with access to it' do + description = specific_runner.description + put api("/runners/#{specific_runner.id}", admin), description: 'test' + specific_runner.reload + + expect(response.status).to eq(200) + expect(specific_runner.description).to eq('test') + expect(specific_runner.description).not_to eq(description) + end + end + end + + context 'unauthorized user' do + it 'should not delete runner' do + put api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /runners/:id' do + context 'admin user' do + context 'when runner is shared' do + it 'should delete runner' do + expect do + delete api("/runners/#{shared_runner.id}", admin) + end.to change{ Ci::Runner.shared.count }.by(-1) + expect(response.status).to eq(200) + end + end + + context 'when runner is not shared' do + it 'should delete unused runner' do + expect do + delete api("/runners/#{unused_specific_runner.id}", admin) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + + it 'should delete used runner' do + expect do + delete api("/runners/#{specific_runner.id}", admin) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + end + + it 'should return 404 if runner does not exists' do + delete api('/runners/9999', admin) + + expect(response.status).to eq(404) + end + end + + context 'authorized user' do + context 'when runner is shared' do + it 'should not delete runner' do + delete api("/runners/#{shared_runner.id}", user) + expect(response.status).to eq(403) + end + end + + context 'when runner is not shared' do + it 'should not delete runner without access to it' do + delete api("/runners/#{specific_runner.id}", user2) + expect(response.status).to eq(403) + end + + it 'should not delete runner with more than one associated project' do + delete api("/runners/#{two_projects_runner.id}", user) + expect(response.status).to eq(403) + end + + it 'should delete runner for one owned project' do + expect do + delete api("/runners/#{specific_runner.id}", user) + end.to change{ Ci::Runner.specific.count }.by(-1) + expect(response.status).to eq(200) + end + end + end + + context 'unauthorized user' do + it 'should not delete runner' do + delete api("/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/runners' do + context 'authorized user with master privileges' do + it "should return project's runners" do + get api("/projects/#{project.id}/runners", user) + shared = json_response.any?{ |r| r['is_shared'] } + + expect(response.status).to eq(200) + expect(json_response).to be_an Array + expect(shared).to be_truthy + end + end + + context 'authorized user without master privileges' do + it "should not return project's runners" do + get api("/projects/#{project.id}/runners", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not return project's runners" do + get api("/projects/#{project.id}/runners") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/runners' do + context 'authorized user' do + it 'should enable specific runner' do + specific_runner2 = create(:ci_runner).tap do |runner| + create(:ci_runner_project, runner: runner, project: project2) + end + + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id + end.to change{ project.runners.count }.by(+1) + expect(response.status).to eq(201) + end + + it 'should avoid changes when enabling already enabled runner' do + expect do + post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id + end.to change{ project.runners.count }.by(0) + expect(response.status).to eq(201) + end + + it 'should not enable shared runner' do + post api("/projects/#{project.id}/runners", user), runner_id: shared_runner.id + + expect(response.status).to eq(403) + end + + context 'user is admin' do + it 'should enable any specific runner' do + expect do + post api("/projects/#{project.id}/runners", admin), runner_id: unused_specific_runner.id + end.to change{ project.runners.count }.by(+1) + expect(response.status).to eq(201) + end + end + + context 'user is not admin' do + it 'should not enable runner without access to' do + post api("/projects/#{project.id}/runners", user), runner_id: unused_specific_runner.id + + expect(response.status).to eq(403) + end + end + + it 'should raise an error when no runner_id param is provided' do + post api("/projects/#{project.id}/runners", admin) + + expect(response.status).to eq(400) + end + end + + context 'authorized user without permissions' do + it 'should not enable runner' do + post api("/projects/#{project.id}/runners", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not enable runner' do + post api("/projects/#{project.id}/runners") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/runners/:runner_id' do + context 'authorized user' do + context 'when runner have more than one associated projects' do + it "should disable project's runner" do + expect do + delete api("/projects/#{project.id}/runners/#{two_projects_runner.id}", user) + end.to change{ project.runners.count }.by(-1) + expect(response.status).to eq(200) + end + end + + context 'when runner have one associated projects' do + it "should not disable project's runner" do + expect do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user) + end.to change{ project.runners.count }.by(0) + expect(response.status).to eq(403) + end + end + + it 'should return 404 is runner is not found' do + delete api("/projects/#{project.id}/runners/9999", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user without permissions' do + it "should not disable project's runner" do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it "should not disable project's runner" do + delete api("/projects/#{project.id}/runners/#{specific_runner.id}") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb index 17f2643fd45..a15be07ed57 100644 --- a/spec/requests/api/tags_spec.rb +++ b/spec/requests/api/tags_spec.rb @@ -8,8 +8,8 @@ describe API::API, api: true do let(:user) { create(:user) } let(:user2) { create(:user) } let!(:project) { create(:project, creator_id: user.id) } - let!(:master) { create(:project_member, user: user, project: project, access_level: ProjectMember::MASTER) } - let!(:guest) { create(:project_member, user: user2, project: project, access_level: ProjectMember::GUEST) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:guest) { create(:project_member, :guest, user: user2, project: project) } describe "GET /projects/:id/repository/tags" do let(:tag_name) { project.repository.tag_names.sort.reverse.first } @@ -65,6 +65,27 @@ describe API::API, api: true do end end + describe 'DELETE /projects/:id/repository/tags/:tag_name' do + let(:tag_name) { project.repository.tag_names.sort.reverse.first } + + before do + allow_any_instance_of(Repository).to receive(:rm_tag).and_return(true) + end + + context 'delete tag' do + it 'should delete an existing tag' do + delete api("/projects/#{project.id}/repository/tags/#{tag_name}", user) + expect(response.status).to eq(200) + expect(json_response['tag_name']).to eq(tag_name) + end + + it 'should raise 404 if the tag does not exist' do + delete api("/projects/#{project.id}/repository/tags/foobar", user) + expect(response.status).to eq(404) + end + end + end + context 'annotated tag' do it 'should create a new annotated tag' do # Identity must be set in .gitconfig to create annotated tag. diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb index 314bd7ddc59..0510b77a39b 100644 --- a/spec/requests/api/triggers_spec.rb +++ b/spec/requests/api/triggers_spec.rb @@ -3,11 +3,19 @@ require 'spec_helper' describe API::API do include ApiHelpers + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:trigger_token) { 'secure_token' } + let!(:trigger_token_2) { 'secure_token_2' } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } + let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) } + let!(:trigger2) { create(:ci_trigger, project: project, token: trigger_token_2) } + let!(:trigger_request) { create(:ci_trigger_request, trigger: trigger, created_at: '2015-01-01 12:13:14') } + describe 'POST /projects/:project_id/trigger' do - let!(:trigger_token) { 'secure token' } - let!(:project) { FactoryGirl.create(:project) } - let!(:project2) { FactoryGirl.create(:empty_project) } - let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) } + let!(:project2) { create(:empty_project) } let(:options) do { token: trigger_token @@ -77,4 +85,127 @@ describe API::API do end end end + + describe 'GET /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should return list of triggers' do + get api("/projects/#{project.id}/triggers", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + expect(json_response[0]).to have_key('token') + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should return trigger details' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Hash) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + get api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not return triggers list' do + get api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/triggers' do + context 'authenticated user with valid permissions' do + it 'should create trigger' do + expect do + post api("/projects/#{project.id}/triggers", user) + end.to change{project.triggers.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response).to be_a(Hash) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not create trigger' do + post api("/projects/#{project.id}/triggers") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/triggers/:token' do + context 'authenticated user with valid permissions' do + it 'should delete trigger' do + expect do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user) + end.to change{project.triggers.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should respond with 404 Not Found if requesting non-existing trigger' do + delete api("/projects/#{project.id}/triggers/abcdef012345", user) + + expect(response.status).to eq(404) + end + end + + context 'authenticated user with invalid permissions' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthenticated user' do + it 'should not delete trigger' do + delete api("/projects/#{project.id}/triggers/#{trigger.token}") + + expect(response.status).to eq(401) + end + end + end end diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 4f278551d07..679227bf881 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -8,6 +8,8 @@ describe API::API, api: true do let(:key) { create(:key, user: user) } let(:email) { create(:email, user: user) } let(:omniauth_user) { create(:omniauth_user) } + let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') } + let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } describe "GET /users" do context "when unauthenticated" do @@ -45,6 +47,8 @@ describe API::API, api: true do expect(json_response.first.keys).to include 'identities' expect(json_response.first.keys).to include 'can_create_project' expect(json_response.first.keys).to include 'two_factor_enabled' + expect(json_response.first.keys).to include 'last_sign_in_at' + expect(json_response.first.keys).to include 'confirmed_at' end end end @@ -116,6 +120,26 @@ describe API::API, api: true do expect(response.status).to eq(201) end + it 'creates non-external users by default' do + post api("/users", admin), attributes_for(:user) + expect(response.status).to eq(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + expect(new_user).not_to eq nil + expect(new_user.external).to be_falsy + end + + it 'should allow an external user to be created' do + post api("/users", admin), attributes_for(:user, external: true) + expect(response.status).to eq(201) + + user_id = json_response['id'] + new_user = User.find(user_id) + expect(new_user).not_to eq nil + expect(new_user.external).to be_truthy + end + it "should not create user with invalid email" do post api('/users', admin), email: 'invalid email', @@ -258,6 +282,13 @@ describe API::API, api: true do expect(user.reload.admin).to eq(true) end + it "should update external status" do + put api("/users/#{user.id}", admin), { external: true } + expect(response.status).to eq 200 + expect(json_response['external']).to eq(true) + expect(user.reload.external?).to be_truthy + end + it "should not update admin status" do put api("/users/#{admin_user.id}", admin), { can_create_group: false } expect(response.status).to eq(200) @@ -783,6 +814,12 @@ describe API::API, api: true do expect(user.reload.state).to eq('blocked') end + it 'should not re-block ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/block", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') + end + it 'should not be available for non admin users' do put api("/users/#{user.id}/block", user) expect(response.status).to eq(403) @@ -797,7 +834,9 @@ describe API::API, api: true do end describe 'PUT /user/:id/unblock' do + let(:blocked_user) { create(:user, state: 'blocked') } before { admin } + it 'should unblock existing user' do put api("/users/#{user.id}/unblock", admin) expect(response.status).to eq(200) @@ -805,12 +844,15 @@ describe API::API, api: true do end it 'should unblock a blocked user' do - put api("/users/#{user.id}/block", admin) + put api("/users/#{blocked_user.id}/unblock", admin) expect(response.status).to eq(200) - expect(user.reload.state).to eq('blocked') - put api("/users/#{user.id}/unblock", admin) - expect(response.status).to eq(200) - expect(user.reload.state).to eq('active') + expect(blocked_user.reload.state).to eq('active') + end + + it 'should not unblock ldap blocked users' do + put api("/users/#{ldap_blocked_user.id}/unblock", admin) + expect(response.status).to eq(403) + expect(ldap_blocked_user.reload.state).to eq('ldap_blocked') end it 'should not be available for non admin users' do diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb new file mode 100644 index 00000000000..b1e1053d037 --- /dev/null +++ b/spec/requests/api/variables_spec.rb @@ -0,0 +1,182 @@ +require 'spec_helper' + +describe API::API, api: true do + include ApiHelpers + + let(:user) { create(:user) } + let(:user2) { create(:user) } + let!(:project) { create(:project, creator_id: user.id) } + let!(:master) { create(:project_member, :master, user: user, project: project) } + let!(:developer) { create(:project_member, :developer, user: user2, project: project) } + let!(:variable) { create(:ci_variable, project: project) } + + describe 'GET /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should return project variables' do + get api("/projects/#{project.id}/variables", user) + + expect(response.status).to eq(200) + expect(json_response).to be_a(Array) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variables' do + get api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'GET /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user) + + expect(response.status).to eq(200) + expect(json_response['value']).to eq(variable.value) + end + + it 'should respond with 404 Not Found if requesting non-existing variable' do + get api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not return project variable details' do + get api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'POST /projects/:id/variables' do + context 'authorized user with proper permissions' do + it 'should create variable' do + expect do + post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2' + end.to change{project.variables.count}.by(1) + + expect(response.status).to eq(201) + expect(json_response['key']).to eq('TEST_VARIABLE_2') + expect(json_response['value']).to eq('VALUE_2') + end + + it 'should not allow to duplicate variable key' do + expect do + post api("/projects/#{project.id}/variables", user), key: variable.key, value: 'VALUE_2' + end.to change{project.variables.count}.by(0) + + expect(response.status).to eq(400) + end + end + + context 'authorized user with invalid permissions' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not create variable' do + post api("/projects/#{project.id}/variables") + + expect(response.status).to eq(401) + end + end + end + + describe 'PUT /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should update variable data' do + initial_variable = project.variables.first + value_before = initial_variable.value + + put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP' + + updated_variable = project.variables.first + + expect(response.status).to eq(200) + expect(value_before).to eq(variable.value) + expect(updated_variable.value).to eq('VALUE_1_UP') + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + put api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not update variable' do + put api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end + + describe 'DELETE /projects/:id/variables/:key' do + context 'authorized user with proper permissions' do + it 'should delete variable' do + expect do + delete api("/projects/#{project.id}/variables/#{variable.key}", user) + end.to change{project.variables.count}.by(-1) + expect(response.status).to eq(200) + end + + it 'should responde with 404 Not Found if requesting non-existing variable' do + delete api("/projects/#{project.id}/variables/non_existing_variable", user) + + expect(response.status).to eq(404) + end + end + + context 'authorized user with invalid permissions' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}", user2) + + expect(response.status).to eq(403) + end + end + + context 'unauthorized user' do + it 'should not delete variable' do + delete api("/projects/#{project.id}/variables/#{variable.key}") + + expect(response.status).to eq(401) + end + end + end +end diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb index c27e87c4acc..57d7eb927fd 100644 --- a/spec/requests/ci/api/builds_spec.rb +++ b/spec/requests/ci/api/builds_spec.rb @@ -101,31 +101,66 @@ describe Ci::API::API do { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false }, ]) end + + it "returns dependent builds" do + commit = FactoryGirl.create(:ci_commit, project: project) + commit.create_builds('master', false, nil, nil) + commit.builds.where(stage: 'test').each(&:success) + + post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin } + + expect(response.status).to eq(201) + expect(json_response["depends_on_builds"].count).to eq(2) + expect(json_response["depends_on_builds"][0]["name"]).to eq("rspec") + end + + %w(name version revision platform architecture).each do |param| + context "updates runner #{param}" do + let(:value) { "#{param}_value" } + + subject { runner.read_attribute(param.to_sym) } + + it do + post ci_api("/builds/register"), token: runner.token, info: { param => value } + expect(response.status).to eq(404) + runner.reload + is_expected.to eq(value) + end + end + end end describe "PUT /builds/:id" do - let(:commit) { FactoryGirl.create(:ci_commit, project: project)} - let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:commit) {create(:ci_commit, project: project)} + let(:build) { create(:ci_build, :trace, commit: commit, runner_id: runner.id) } - it "should update a running build" do + before do build.run! put ci_api("/builds/#{build.id}"), token: runner.token + end + + it "should update a running build" do expect(response.status).to eq(200) end - it 'Should not override trace information when no trace is given' do - build.run! - build.update!(trace: 'hello_world') - put ci_api("/builds/#{build.id}"), token: runner.token - expect(build.reload.trace).to eq 'hello_world' + it 'should not override trace information when no trace is given' do + expect(build.reload.trace).to eq 'BUILD TRACE' + end + + context 'build has been erased' do + let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) } + + it 'should respond with forbidden' do + expect(response.status).to eq 403 + end end end context "Artifacts" do let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') } - let(:commit) { FactoryGirl.create(:ci_commit, project: project) } - let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) } + let(:commit) { create(:ci_commit, project: project) } + let(:build) { create(:ci_build, commit: commit, runner_id: runner.id) } let(:authorize_url) { ci_api("/builds/#{build.id}/artifacts/authorize") } let(:post_url) { ci_api("/builds/#{build.id}/artifacts") } let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") } @@ -133,12 +168,10 @@ describe Ci::API::API do let(:headers) { { "GitLab-Workhorse" => "1.0" } } let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) } + before { build.run! } + describe "POST /builds/:id/artifacts/authorize" do context "should authorize posting artifact to running build" do - before do - build.run! - end - it "using token as parameter" do post authorize_url, { token: build.token }, headers expect(response.status).to eq(200) @@ -153,10 +186,6 @@ describe Ci::API::API do end context "should fail to post too large artifact" do - before do - build.run! - end - it "using token as parameter" do stub_application_setting(max_artifacts_size: 0) post authorize_url, { token: build.token, filesize: 100 }, headers @@ -170,26 +199,32 @@ describe Ci::API::API do end end - context "should get denied" do - it do - post authorize_url, { token: 'invalid', filesize: 100 } + context 'authorization token is invalid' do + before { post authorize_url, { token: 'invalid', filesize: 100 } } + + it 'should respond with forbidden' do expect(response.status).to eq(403) end end end describe "POST /builds/:id/artifacts" do - context "Disable sanitizer" do + context "disable sanitizer" do before do # by configuring this path we allow to pass temp file from any path allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return('/') end - context "should post artifact to running build" do - before do - build.run! + context 'build has been erased' do + let(:build) { create(:ci_build, erased_at: Time.now) } + before { upload_artifacts(file_upload, headers_with_token) } + + it 'should respond with forbidden' do + expect(response.status).to eq 403 end + end + context "should post artifact to running build" do it "uses regual file post" do upload_artifacts(file_upload, headers_with_token, false) expect(response.status).to eq(201) @@ -210,55 +245,83 @@ describe Ci::API::API do end end - context "should fail to post too large artifact" do + context 'should post artifacts file and metadata file' do + let!(:artifacts) { file_upload } + let!(:metadata) { file_upload2 } + + let(:stored_artifacts_file) { build.reload.artifacts_file.file } + let(:stored_metadata_file) { build.reload.artifacts_metadata.file } + before do - build.run! + post(post_url, post_data, headers_with_token) + end + + context 'post data accelerated by workhorse is correct' do + let(:post_data) do + { 'file.path' => artifacts.path, + 'file.name' => artifacts.original_filename, + 'metadata.path' => metadata.path, + 'metadata.name' => metadata.original_filename } + end + + it 'stores artifacts and artifacts metadata' do + expect(response.status).to eq(201) + expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename) + expect(stored_metadata_file.original_filename).to eq(metadata.original_filename) + end + end + + context 'no artifacts file in post data' do + let(:post_data) do + { 'metadata' => metadata } + end + + it 'is expected to respond with bad request' do + expect(response.status).to eq(400) + end + + it 'does not store metadata' do + expect(stored_metadata_file).to be_nil + end end + end - it do + context "artifacts file is too large" do + it "should fail to post too large artifact" do stub_application_setting(max_artifacts_size: 0) upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(413) end end - context "should fail to post artifacts without file" do - before do - build.run! - end - - it do + context "artifacts post request does not contain file" do + it "should fail to post artifacts without file" do post post_url, {}, headers_with_token expect(response.status).to eq(400) end end - context "should fail to post artifacts without GitLab-Workhorse" do - before do - build.run! - end - - it do + context 'GitLab Workhorse is not configured' do + it "should fail to post artifacts without GitLab-Workhorse" do post post_url, { token: build.token }, {} expect(response.status).to eq(403) end end end - context "should fail to post artifacts for outside of tmp path" do + context "artifacts are being stored outside of tmp path" do before do # by configuring this path we allow to pass file from @tmpdir only # but all temporary files are stored in system tmp directory @tmpdir = Dir.mktmpdir allow(ArtifactUploader).to receive(:artifacts_upload_path).and_return(@tmpdir) - build.run! end after do FileUtils.remove_entry @tmpdir end - it do + it "should fail to post artifacts for outside of tmp path" do upload_artifacts(file_upload, headers_with_token) expect(response.status).to eq(400) end @@ -276,33 +339,37 @@ describe Ci::API::API do end end - describe "DELETE /builds/:id/artifacts" do - before do - build.run! - post delete_url, token: build.token, file: file_upload - end + describe 'DELETE /builds/:id/artifacts' do + let(:build) { create(:ci_build, :artifacts) } + before { delete delete_url, token: build.token } - it "should delete artifact build" do - build.success - delete delete_url, token: build.token + it 'should remove build artifacts' do expect(response.status).to eq(200) + expect(build.artifacts_file.exists?).to be_falsy + expect(build.artifacts_metadata.exists?).to be_falsy end end - describe "GET /builds/:id/artifacts" do - before do - build.run! - end + describe 'GET /builds/:id/artifacts' do + before { get get_url, token: build.token } - it "should download artifact" do - build.update_attributes(artifacts_file: file_upload) - get get_url, token: build.token - expect(response.status).to eq(200) + context 'build has artifacts' do + let(:build) { create(:ci_build, :artifacts) } + let(:download_headers) do + { 'Content-Transfer-Encoding'=>'binary', + 'Content-Disposition'=>'attachment; filename=ci_build_artifacts.zip' } + end + + it 'should download artifact' do + expect(response.status).to eq(200) + expect(response.headers).to include download_headers + end end - it "should fail to download if no artifact uploaded" do - get get_url, token: build.token - expect(response.status).to eq(404) + context 'build does not has artifacts' do + it 'should respond with not found' do + expect(response.status).to eq(404) + end end end end diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb index 5942aa7a1b5..db8189ffb79 100644 --- a/spec/requests/ci/api/runners_spec.rb +++ b/spec/requests/ci/api/runners_spec.rb @@ -51,6 +51,20 @@ describe Ci::API::API do expect(response.status).to eq(400) end + + %w(name version revision platform architecture).each do |param| + context "creates runner with #{param} saved" do + let(:value) { "#{param}_value" } + + subject { Ci::Runner.first.read_attribute(param.to_sym) } + + it do + post ci_api("/runners/register"), token: registration_token, info: { param => value } + expect(response.status).to eq(201) + is_expected.to eq(value) + end + end + end end describe "DELETE /runners/delete" do diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 82f62a8709c..538f44e4f3f 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -80,6 +80,7 @@ describe ProjectsController, 'routing' do it 'to #show' do expect(get('/gitlab/gitlabhq')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq') + expect(get('/gitlab/gitlabhq.keys')).to route_to('projects#show', namespace_id: 'gitlab', id: 'gitlabhq.keys') end it 'to #update' do @@ -320,12 +321,12 @@ describe Projects::HooksController, 'routing' do end end -# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /[[:alnum:]]{6,40}/, project_id: /[^\/]+/} +# project_commit GET /:project_id/commit/:id(.:format) commit#show {id: /\h{7,40}/, project_id: /[^\/]+/} describe Projects::CommitController, 'routing' do it 'to #show' do - expect(get('/gitlab/gitlabhq/commit/4246fb')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb') - expect(get('/gitlab/gitlabhq/commit/4246fb.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'diff') - expect(get('/gitlab/gitlabhq/commit/4246fb.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fb', format: 'patch') + expect(get('/gitlab/gitlabhq/commit/4246fbd')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd') + expect(get('/gitlab/gitlabhq/commit/4246fbd.diff')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'diff') + expect(get('/gitlab/gitlabhq/commit/4246fbd.patch')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd', format: 'patch') expect(get('/gitlab/gitlabhq/commit/4246fbd13872934f72a8fd0d6fb1317b47b59cb5')).to route_to('projects/commit#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '4246fbd13872934f72a8fd0d6fb1317b47b59cb5') end end @@ -434,6 +435,18 @@ describe Projects::TreeController, 'routing' do end end +# project_find_file GET /:namespace_id/:project_id/find_file/*id(.:format) projects/find_file#show {:id=>/.+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/html/} +# project_files GET /:namespace_id/:project_id/files/*id(.:format) projects/find_file#list {:id=>/(?:[^.]|\.(?!json$))+/, :namespace_id=>/[a-zA-Z.0-9_\-]+/, :project_id=>/[a-zA-Z.0-9_\-]+(?<!\.atom)/, :format=>/json/} +describe Projects::FindFileController, 'routing' do + it 'to #show' do + expect(get('/gitlab/gitlabhq/find_file/master')).to route_to('projects/find_file#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master') + end + + it 'to #list' do + expect(get('/gitlab/gitlabhq/files/master.json')).to route_to('projects/find_file#list', namespace_id: 'gitlab', project_id: 'gitlabhq', id: 'master', format: 'json') + end +end + describe Projects::BlobController, 'routing' do it 'to #edit' do expect(get('/gitlab/gitlabhq/edit/master/app/models/project.rb')).to( @@ -483,11 +496,11 @@ end describe Projects::ForksController, 'routing' do it 'to #new' do - expect(get('/gitlab/gitlabhq/fork/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(get('/gitlab/gitlabhq/forks/new')).to route_to('projects/forks#new', namespace_id: 'gitlab', project_id: 'gitlabhq') end it 'to #create' do - expect(post('/gitlab/gitlabhq/fork')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') + expect(post('/gitlab/gitlabhq/forks')).to route_to('projects/forks#create', namespace_id: 'gitlab', project_id: 'gitlabhq') end end diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index dfa18f69e05..1527eddfa48 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -137,7 +137,6 @@ end # keys GET /keys(.:format) keys#index # POST /keys(.:format) keys#create -# new_key GET /keys/new(.:format) keys#new # edit_key GET /keys/:id/edit(.:format) keys#edit # key GET /keys/:id(.:format) keys#show # PUT /keys/:id(.:format) keys#update @@ -151,10 +150,6 @@ describe Profiles::KeysController, "routing" do expect(post("/profile/keys")).to route_to('profiles/keys#create') end - it "to #new" do - expect(get("/profile/keys/new")).to route_to('profiles/keys#new') - end - it "to #edit" do expect(get("/profile/keys/1/edit")).to route_to('profiles/keys#edit', id: '1') end diff --git a/spec/services/archive_repository_service_spec.rb b/spec/services/archive_repository_service_spec.rb deleted file mode 100644 index bd871605c66..00000000000 --- a/spec/services/archive_repository_service_spec.rb +++ /dev/null @@ -1,25 +0,0 @@ -require 'spec_helper' - -describe ArchiveRepositoryService, services: true do - let(:project) { create(:project) } - subject { ArchiveRepositoryService.new(project, "master", "zip") } - - describe "#execute" do - it "cleans old archives" do - expect(RepositoryArchiveCacheWorker).to receive(:perform_async) - - subject.execute(timeout: 0.0) - end - - context "when the repository doesn't have an archive file path" do - before do - allow(project.repository).to receive(:archive_metadata).and_return(Hash.new) - end - - it "raises an error" do - expect { subject.execute(timeout: 0.0) }.to raise_error(RuntimeError) - end - end - - end -end diff --git a/spec/services/ci/create_builds_service_spec.rb b/spec/services/ci/create_builds_service_spec.rb new file mode 100644 index 00000000000..1fca3628686 --- /dev/null +++ b/spec/services/ci/create_builds_service_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Ci::CreateBuildsService, services: true do + let(:commit) { create(:ci_commit) } + let(:user) { create(:user) } + + describe '#execute' do + # Using stubbed .gitlab-ci.yml created in commit factory + # + + subject do + described_class.new.execute(commit, 'test', 'master', nil, user, nil, status) + end + + context 'next builds available' do + let(:status) { 'success' } + + it { is_expected.to be_an_instance_of Array } + it { is_expected.to all(be_an_instance_of Ci::Build) } + end + + context 'builds skipped' do + let(:status) { 'skipped' } + + it { is_expected.to be_empty } + end + end +end diff --git a/spec/services/delete_tag_service_spec.rb b/spec/services/delete_tag_service_spec.rb new file mode 100644 index 00000000000..5b7ba521812 --- /dev/null +++ b/spec/services/delete_tag_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe DeleteTagService, services: true do + let(:project) { create(:project) } + let(:repository) { project.repository } + let(:user) { create(:user) } + let(:service) { described_class.new(project, user) } + + let(:tag) { double(:tag, name: '8.5', target: 'abc123') } + + describe '#execute' do + before do + allow(repository).to receive(:find_tag).and_return(tag) + end + + it 'removes the tag' do + expect_any_instance_of(Gitlab::Shell).to receive(:rm_tag). + and_return(true) + + expect(repository).to receive(:before_remove_tag) + expect(service).to receive(:success) + + service.execute('8.5') + end + end +end diff --git a/spec/services/delete_user_service_spec.rb b/spec/services/delete_user_service_spec.rb new file mode 100644 index 00000000000..a65938fa03b --- /dev/null +++ b/spec/services/delete_user_service_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe DeleteUserService, services: true do + describe "Deletes a user and all their personal projects" do + let!(:user) { create(:user) } + let!(:current_user) { create(:user) } + let!(:namespace) { create(:namespace, owner: user) } + let!(:project) { create(:project, namespace: namespace) } + + context 'no options are given' do + it 'deletes the user' do + DeleteUserService.new(current_user).execute(user) + + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'will delete the project in the near future' do + expect_any_instance_of(Projects::DestroyService).to receive(:pending_delete!).once + + DeleteUserService.new(current_user).execute(user) + end + end + + context "solo owned groups present" do + let(:solo_owned) { create(:group) } + let(:member) { create(:group_member) } + let(:user) { member.user } + + before do + solo_owned.group_members = [member] + DeleteUserService.new(current_user).execute(user) + end + + it 'does not delete the user' do + expect(User.find(user.id)).to eq user + end + end + + context "deletions with solo owned groups" do + let(:solo_owned) { create(:group) } + let(:member) { create(:group_member) } + let(:user) { member.user } + + before do + solo_owned.group_members = [member] + DeleteUserService.new(current_user).execute(user, delete_solo_owned_groups: true) + end + + it 'deletes solo owned groups' do + expect { Project.find(solo_owned.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'deletes the user' do + expect { User.find(user.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + end +end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index c1080ef190a..8490a729e51 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -5,7 +5,6 @@ describe GitPushService, services: true do let(:user) { create :user } let(:project) { create :project } - let(:service) { GitPushService.new } before do @blankrev = Gitlab::Git::BLANK_SHA @@ -15,34 +14,70 @@ describe GitPushService, services: true do end describe 'Push branches' do + + let(:oldrev) { @oldrev } + let(:newrev) { @newrev } + + subject do + execute_service(project, user, oldrev, newrev, @ref ) + end + context 'new branch' do - subject do - service.execute(project, user, @blankrev, @newrev, @ref) - end + + let(:oldrev) { @blankrev } it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache). + with('master', newrev) + + subject + end + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end end context 'existing branch' do - subject do - service.execute(project, user, @oldrev, @newrev, @ref) - end it { is_expected.to be_truthy } + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache). + with('master', newrev) + + subject + end end context 'rm branch' do - subject do - service.execute(project, user, @oldrev, @blankrev, @ref) - end + + let(:newrev) { @blankrev } it { is_expected.to be_truthy } + + it 'flushes the visible content cache' do + expect(project.repository).to receive(:expire_has_visible_content_cache) + + subject + end + + it 'flushes general cached data' do + expect(project.repository).to receive(:expire_cache). + with('master', newrev) + + subject + end end end describe "Git Push Data" do before do - service.execute(project, user, @oldrev, @newrev, @ref) + service = execute_service(project, user, @oldrev, @newrev, @ref ) @push_data = service.push_data @commit = project.commit(@newrev) end @@ -104,31 +139,49 @@ describe GitPushService, services: true do describe "Push Event" do before do - service.execute(project, user, @oldrev, @newrev, @ref) + service = execute_service(project, user, @oldrev, @newrev, @ref ) @event = Event.last + @push_data = service.push_data end it { expect(@event).not_to be_nil } it { expect(@event.project).to eq(project) } it { expect(@event.action).to eq(Event::PUSHED) } - it { expect(@event.data).to eq(service.push_data) } + it { expect(@event.data).to eq(@push_data) } context "Updates merge requests" do it "when pushing a new branch for the first time" do expect(project).to receive(:update_merge_requests). with(@blankrev, 'newrev', 'refs/heads/master', user) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + end + end + end + + describe "Updates main language" do + + context "before push" do + it { expect(project.main_language).to eq(nil) } + end + + context "after push" do + before do + @service = execute_service(project, user, @oldrev, @newrev, @ref) end + + it { expect(@service.update_main_language).to eq(true) } + it { expect(project.main_language).to eq("Ruby") } end end - describe "Web Hooks" do - context "execute web hooks" do + + describe "Webhooks" do + context "execute webhooks" do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: false }) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing a branch for the first time with default branch protection disabled" do @@ -137,7 +190,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).not_to receive(:create) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing a branch for the first time with default branch protection set to 'developers can push'" do @@ -146,12 +199,12 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") expect(project.protected_branches).to receive(:create).with({ name: "master", developers_can_push: true }) - service.execute(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) end it "when pushing new commits to existing branch" do expect(project).to receive(:execute_hooks) - service.execute(project, user, 'oldrev', 'newrev', 'refs/heads/master') + execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master' ) end end end @@ -162,19 +215,23 @@ describe GitPushService, services: true do let(:commit) { project.commit } before do + project.team << [commit_author, :developer] + project.team << [user, :developer] + allow(commit).to receive_messages( safe_message: "this commit \n mentions #{issue.to_reference}", references: [issue], author_name: commit_author.name, author_email: commit_author.email ) + allow(project.repository).to receive(:commits_between).and_return([commit]) end it "creates a note if a pushed commit mentions an issue" do expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "only creates a cross-reference note if one doesn't already exist" do @@ -182,7 +239,7 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "defaults to the pushing user if the commit's author is not known" do @@ -192,7 +249,7 @@ describe GitPushService, services: true do ) expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "finds references in the first push to a non-default branch" do @@ -201,7 +258,7 @@ describe GitPushService, services: true do expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - service.execute(project, user, @blankrev, @newrev, 'refs/heads/other') + execute_service(project, user, @blankrev, @newrev, 'refs/heads/other' ) end end @@ -221,22 +278,24 @@ describe GitPushService, services: true do allow(project.repository).to receive(:commits_between). and_return([closing_commit]) + + project.team << [commit_author, :master] end context "to default branches" do it "closes issues" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) end it "doesn't close issues when external issue tracker is in use" do @@ -244,7 +303,7 @@ describe GitPushService, services: true do # The push still shouldn't create cross-reference notes. expect do - service.execute(project, user, @oldrev, @newrev, 'refs/heads/hurf') + execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) end.not_to change { Note.where(project_id: project.id, system: true).count } end end @@ -257,16 +316,15 @@ describe GitPushService, services: true do it "creates cross-reference notes" do expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author) - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) end it "doesn't close issues" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(Issue.find(issue.id)).to be_opened end end - # EE-only tests context "for jira issue tracker" do include JiraServiceHelper @@ -298,7 +356,7 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\nrelated to JIRA-1" } it "should initiate one api call to jira server to mention the issue" do - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: /mentioned this issue in/ @@ -316,7 +374,7 @@ describe GitPushService, services: true do } }.to_json - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_transition_url).with( body: transition_body ).once @@ -327,7 +385,7 @@ describe GitPushService, services: true do body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json - service.execute(project, user, @oldrev, @newrev, @ref) + execute_service(project, commit_author, @oldrev, @newrev, @ref ) expect(WebMock).to have_requested(:post, jira_api_comment_url).with( body: comment_body ).once @@ -346,7 +404,52 @@ describe GitPushService, services: true do end it 'push to first branch updates HEAD' do - service.execute(project, user, @blankrev, @newrev, new_ref) + execute_service(project, user, @blankrev, @newrev, new_ref ) + end + end + + describe "housekeeping" do + let(:housekeeping) { Projects::HousekeepingService.new(project) } + + before do + allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping) end + + it 'does not perform housekeeping when not needed' do + expect(housekeeping).not_to receive(:execute) + + execute_service(project, user, @oldrev, @newrev, @ref) + end + + context 'when housekeeping is needed' do + before do + allow(housekeeping).to receive(:needed?).and_return(true) + end + + it 'performs housekeeping' do + expect(housekeeping).to receive(:execute) + + execute_service(project, user, @oldrev, @newrev, @ref) + end + + it 'does not raise an exception' do + allow(housekeeping).to receive(:try_obtain_lease).and_return(false) + + execute_service(project, user, @oldrev, @newrev, @ref) + end + end + + + it 'increments the push counter' do + expect(housekeeping).to receive(:increment!) + + execute_service(project, user, @oldrev, @newrev, @ref) + end + end + + def execute_service(project, user, oldrev, newrev, ref) + service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) + service.execute + service end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index b982274c529..cc780587e74 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -78,8 +78,8 @@ describe GitTagPushService, services: true do end end - describe "Web Hooks" do - context "execute web hooks" do + describe "Webhooks" do + context "execute webhooks" do it "when pushing tags" do expect(project).to receive(:execute_hooks) service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0') diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 3a8daf28f5e..62b25709a5d 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -5,6 +5,7 @@ describe Issues::CloseService, services: true do let(:user2) { create(:user) } let(:issue) { create(:issue, assignee: user2) } let(:project) { issue.project } + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } before do project.team << [user, :master] @@ -32,6 +33,10 @@ describe Issues::CloseService, services: true do note = @issue.notes.last expect(note.note).to include "Status changed to closed" end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end end context "external issue tracker" do @@ -42,6 +47,7 @@ describe Issues::CloseService, services: true do it { expect(@issue).to be_valid } it { expect(@issue).to be_opened } + it { expect(todo.reload).to be_pending } end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 2148d091a57..5e7915db7e1 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -3,14 +3,18 @@ require 'spec_helper' describe Issues::CreateService, services: true do let(:project) { create(:empty_project) } let(:user) { create(:user) } + let(:assignee) { create(:user) } describe :execute do - context "valid params" do + context 'valid params' do before do project.team << [user, :master] + project.team << [assignee, :master] + opts = { title: 'Awesome issue', - description: 'please fix' + description: 'please fix', + assignee: assignee } @issue = Issues::CreateService.new(project, user, opts).execute @@ -18,6 +22,21 @@ describe Issues::CreateService, services: true do it { expect(@issue).to be_valid } it { expect(@issue.title).to eq('Awesome issue') } + it { expect(@issue.assignee).to eq assignee } + + it 'creates a pending todo for new assignee' do + attributes = { + project: project, + author: user, + user: assignee, + target_id: @issue.id, + target_type: @issue.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end end end end diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 87da0e9618b..4ffe753fef5 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -6,6 +6,7 @@ describe Issues::UpdateService, services: true do let(:user3) { create(:user) } let(:issue) { create(:issue, title: 'Old title', assignee_id: user3.id) } let(:label) { create(:label) } + let(:label2) { create(:label) } let(:project) { issue.project } before do @@ -48,7 +49,7 @@ describe Issues::UpdateService, services: true do it { expect(@issue.assignee).to eq(user2) } it { expect(@issue).to be_closed } it { expect(@issue.labels.count).to eq(1) } - it { expect(@issue.labels.first.title).to eq('Bug') } + it { expect(@issue.labels.first.title).to eq(label.name) } it 'should send email to user2 about assign of new issue and email to user3 about issue unassignment' do deliveries = ActionMailer::Base.deliveries @@ -80,6 +81,116 @@ describe Issues::UpdateService, services: true do end end + context 'todos' do + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + + context 'when the title change' do + before do + update_issue({ title: 'New title' }) + end + + it 'marks pending todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when the description change' do + before do + update_issue({ description: 'Also please fix' }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when is reassigned' do + before do + update_issue({ assignee: user2 }) + end + + it 'marks previous assignee todos as done' do + expect(todo.reload.done?).to eq true + end + + it 'creates a todo for new assignee' do + attributes = { + project: project, + author: user, + user: user2, + target_id: issue.id, + target_type: issue.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + + context 'when the milestone change' do + before do + update_issue({ milestone: create(:milestone) }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + + context 'when the labels change' do + before do + update_issue({ label_ids: [label.id] }) + end + + it 'marks todos as done' do + expect(todo.reload.done?).to eq true + end + end + end + + context 'when the issue is relabeled' do + let!(:non_subscriber) { create(:user) } + let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } } + + it 'sends notifications for subscribers of newly added labels' do + opts = { label_ids: [label.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + + context 'when issue has the `label` label' do + before { issue.labels << label } + + it 'does not send notifications for existing labels' do + opts = { label_ids: [label.id, label2.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + should_not_email(subscriber) + should_not_email(non_subscriber) + end + + it 'does not send notifications for removed labels' do + opts = { label_ids: [label2.id] } + + perform_enqueued_jobs do + @issue = Issues::UpdateService.new(project, user, opts).execute(issue) + end + + should_not_email(subscriber) + should_not_email(non_subscriber) + end + end + end + context 'when Issue has tasks' do before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) } @@ -144,6 +255,5 @@ describe Issues::UpdateService, services: true do end end end - end end diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 50d0c288790..8443a00e70c 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -5,6 +5,7 @@ describe MergeRequests::CloseService, services: true do let(:user2) { create(:user) } let(:merge_request) { create(:merge_request, assignee: user2) } let(:project) { merge_request.project } + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } before do project.team << [user, :master] @@ -41,6 +42,10 @@ describe MergeRequests::CloseService, services: true do note = @merge_request.notes.last expect(note.note).to include 'Status changed to closed' end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end end end end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index be8f1676eeb..120f4d6a669 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe MergeRequests::CreateService, services: true do let(:project) { create(:project) } let(:user) { create(:user) } + let(:assignee) { create(:user) } describe :execute do context 'valid params' do @@ -14,10 +15,12 @@ describe MergeRequests::CreateService, services: true do target_branch: 'master' } end + let(:service) { MergeRequests::CreateService.new(project, user, opts) } before do project.team << [user, :master] + project.team << [assignee, :developer] allow(service).to receive(:execute_hooks) @merge_request = service.execute @@ -25,10 +28,49 @@ describe MergeRequests::CreateService, services: true do it { expect(@merge_request).to be_valid } it { expect(@merge_request.title).to eq('Awesome merge_request') } + it { expect(@merge_request.assignee).to be_nil } it 'should execute hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) end + + it 'does not creates todos' do + attributes = { + project: project, + target_id: @merge_request.id, + target_type: @merge_request.class.name + } + + expect(Todo.where(attributes).count).to be_zero + end + + context 'when merge request is assigned to someone' do + let(:opts) do + { + title: 'Awesome merge_request', + description: 'please fix', + source_branch: 'feature', + target_branch: 'master', + assignee: assignee + } + end + + it { expect(@merge_request.assignee).to eq assignee } + + it 'creates a todo for new assignee' do + attributes = { + project: project, + author: user, + user: assignee, + target_id: @merge_request.id, + target_type: @merge_request.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end end end end diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb index 449cecaa789..52a302e0e1a 100644 --- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb @@ -6,7 +6,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do let(:mr_merge_if_green_enabled) do create(:merge_request, merge_when_build_succeeds: true, merge_user: user, - source_branch: "source_branch", target_branch: project.default_branch, + source_branch: "master", target_branch: 'feature', source_project: project, target_project: project, state: "opened") end @@ -54,14 +54,84 @@ describe MergeRequests::MergeWhenBuildSucceedsService do end describe "#trigger" do - let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } + context 'build with ref' do + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } - it "merges all merge requests with merge when build succeeds enabled" do - allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) - allow(ci_commit).to receive(:success?).and_return(true) + it "merges all merge requests with merge when build succeeds enabled" do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) + + expect(MergeWorker).to receive(:perform_async) + service.trigger(build) + end + end + + context 'triggered by an old build' do + let(:old_build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } + let(:build) { create(:ci_build, ref: mr_merge_if_green_enabled.source_branch, status: "success") } + + it "merges all merge requests with merge when build succeeds enabled" do + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) + allow(old_build).to receive(:sha).and_return('1234abcdef') + + expect(MergeWorker).to_not receive(:perform_async) + service.trigger(old_build) + end + end + + context 'commit status without ref' do + let(:commit_status) { create(:generic_commit_status, status: 'success') } + + before { mr_merge_if_green_enabled } + + it "doesn't merge a requests for status on other branch" do + allow(project.repository).to receive(:branch_names_contains).with(commit_status.sha).and_return([]) + + expect(MergeWorker).to_not receive(:perform_async) + service.trigger(commit_status) + end + + it 'discovers branches and merges all merge requests when status is success' do + allow(project.repository).to receive(:branch_names_contains). + with(commit_status.sha).and_return([mr_merge_if_green_enabled.source_branch]) + allow(ci_commit).to receive(:success?).and_return(true) + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_return(ci_commit) + allow(ci_commit).to receive(:success?).and_return(true) + + expect(MergeWorker).to receive(:perform_async) + service.trigger(commit_status) + end + end - expect(MergeWorker).to receive(:perform_async) - service.trigger(build) + context 'properly handles multiple stages' do + let(:ref) { mr_merge_if_green_enabled.source_branch } + let(:build) { create(:ci_build, commit: ci_commit, ref: ref, name: 'build', stage: 'build') } + let(:test) { create(:ci_build, commit: ci_commit, ref: ref, name: 'test', stage: 'test') } + + before do + # This behavior of MergeRequest: we instantiate a new object + allow_any_instance_of(MergeRequest).to receive(:ci_commit).and_wrap_original do + Ci::Commit.find(ci_commit.id) + end + + # We create test after the build + allow(ci_commit).to receive(:create_next_builds).and_wrap_original do + test + end + end + + it "doesn't merge if some stages failed" do + expect(MergeWorker).to_not receive(:perform_async) + build.success + test.drop + end + + it 'merge when all stages succeeded' do + expect(MergeWorker).to receive(:perform_async) + build.success + test.success + end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 450250ba032..fea8182bd30 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -79,7 +79,7 @@ describe MergeRequests::RefreshService, services: true do it { expect(@merge_request.notes.last.note).to include('changed to merged') } it { expect(@merge_request).to be_merged } - it { expect(@merge_request.diffs.length).to be > 0 } + it { expect(@merge_request.diffs.size).to be > 0 } it { expect(@fork_merge_request).to be_merged } it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') } end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 2e9e6e0870d..cb8cff2fa8c 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -7,6 +7,7 @@ describe MergeRequests::UpdateService, services: true do let(:merge_request) { create(:merge_request, :simple, title: 'Old title', assignee_id: user3.id) } let(:project) { merge_request.project } let(:label) { create(:label) } + let(:label2) { create(:label) } before do project.team << [user, :master] @@ -53,7 +54,7 @@ describe MergeRequests::UpdateService, services: true do it { expect(@merge_request.assignee).to eq(user2) } it { expect(@merge_request).to be_closed } it { expect(@merge_request.labels.count).to eq(1) } - it { expect(@merge_request.labels.first.title).to eq('Bug') } + it { expect(@merge_request.labels.first.title).to eq(label.name) } it { expect(@merge_request.target_branch).to eq('target') } it 'should execute hooks with update action' do @@ -98,6 +99,126 @@ describe MergeRequests::UpdateService, services: true do end end + context 'todos' do + let!(:pending_todo) { create(:todo, :assigned, user: user, project: project, target: merge_request, author: user2) } + + context 'when the title change' do + before do + update_merge_request({ title: 'New title' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the description change' do + before do + update_merge_request({ description: 'Also please fix' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when is reassigned' do + before do + update_merge_request({ assignee: user2 }) + end + + it 'marks previous assignee pending todos as done' do + expect(pending_todo.reload).to be_done + end + + it 'creates a pending todo for new assignee' do + attributes = { + project: project, + author: user, + user: user2, + target_id: merge_request.id, + target_type: merge_request.class.name, + action: Todo::ASSIGNED, + state: :pending + } + + expect(Todo.where(attributes).count).to eq 1 + end + end + + context 'when the milestone change' do + before do + update_merge_request({ milestone: create(:milestone) }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the labels change' do + before do + update_merge_request({ label_ids: [label.id] }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + + context 'when the target branch change' do + before do + update_merge_request({ target_branch: 'target' }) + end + + it 'marks pending todos as done' do + expect(pending_todo.reload).to be_done + end + end + end + + context 'when the issue is relabeled' do + let!(:non_subscriber) { create(:user) } + let!(:subscriber) { create(:user).tap { |u| label.toggle_subscription(u) } } + + it 'sends notifications for subscribers of newly added labels' do + opts = { label_ids: [label.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + should_email(subscriber) + should_not_email(non_subscriber) + end + + context 'when issue has the `label` label' do + before { merge_request.labels << label } + + it 'does not send notifications for existing labels' do + opts = { label_ids: [label.id, label2.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + should_not_email(subscriber) + should_not_email(non_subscriber) + end + + it 'does not send notifications for removed labels' do + opts = { label_ids: [label2.id] } + + perform_enqueued_jobs do + @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + end + + should_not_email(subscriber) + should_not_email(non_subscriber) + end + end + end + context 'when MergeRequest has tasks' do before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } @@ -130,6 +251,5 @@ describe MergeRequests::UpdateService, services: true do end end end - end end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index a797a2fe4aa..ff23f13e1cb 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -14,9 +14,7 @@ describe Notes::CreateService, services: true do noteable_type: 'Issue', noteable_id: issue.id } - - expect(project).to receive(:execute_hooks) - expect(project).to receive(:execute_services) + @note = Notes::CreateService.new(project, user, opts).execute end diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb new file mode 100644 index 00000000000..d4c50f824c1 --- /dev/null +++ b/spec/services/notes/post_process_service_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Notes::PostProcessService, services: true do + let(:project) { create(:empty_project) } + let(:issue) { create(:issue, project: project) } + let(:user) { create(:user) } + + describe :execute do + before do + project.team << [user, :master] + note_opts = { + note: 'Awesome comment', + noteable_type: 'Issue', + noteable_id: issue.id + } + + @note = Notes::CreateService.new(project, user, note_opts).execute + end + + it do + expect(project).to receive(:execute_hooks) + expect(project).to receive(:execute_services) + + Notes::PostProcessService.new(@note).execute + end + end +end diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb new file mode 100644 index 00000000000..dde4bde7dc2 --- /dev/null +++ b/spec/services/notes/update_service_spec.rb @@ -0,0 +1,45 @@ +require 'spec_helper' + +describe Notes::UpdateService, services: true do + let(:project) { create(:empty_project) } + let(:user) { create(:user) } + let(:user2) { create(:user) } + let(:issue) { create(:issue, project: project) } + let(:note) { create(:note, project: project, noteable: issue, author: user, note: 'Old note') } + + before do + project.team << [user, :master] + project.team << [user2, :developer] + end + + describe '#execute' do + def update_note(opts) + @note = Notes::UpdateService.new(project, user, opts).execute(note) + @note.reload + end + + context 'todos' do + let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } + + context 'when the note change' do + before do + update_note({ note: 'New note' }) + end + + it 'marks todos as done' do + expect(todo.reload).to be_done + end + end + + context 'when the note does not change' do + before do + update_note({ note: 'Old note' }) + end + + it 'keep todos' do + expect(todo.reload).to be_pending + end + end + end + end +end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index c103752198d..b5407397c1d 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -52,6 +52,9 @@ describe NotificationService, services: true do it do add_users_with_subscription(note.project, issue) + # Ensure create SentNotification by noteable = issue 6 times, not noteable = note + expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times + ActionMailer::Base.deliveries.clear notification.new_note(note) @@ -61,6 +64,7 @@ describe NotificationService, services: true do should_email(note.noteable.assignee) should_email(@u_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_email(@subscribed_participant) should_not_email(note.author) should_not_email(@u_participating) @@ -220,10 +224,19 @@ describe NotificationService, services: true do should_not_email(issue.assignee) end + + it "emails subscribers of the issue's labels" do + subscriber = create(:user) + label = create(:label, issues: [issue]) + label.toggle_subscription(subscriber) + notification.new_issue(issue, @u_disabled) + + should_email(subscriber) + end end describe :reassigned_issue do - it 'should email new assignee' do + it 'emails new assignee' do notification.reassigned_issue(issue, @u_disabled) should_email(issue.assignee) @@ -234,6 +247,91 @@ describe NotificationService, services: true do should_not_email(@u_participating) should_not_email(@u_disabled) end + + it 'emails previous assignee even if he has the "on mention" notif level' do + issue.update_attribute(:assignee, @u_mentioned) + issue.update_attributes(assignee: @u_watcher) + notification.reassigned_issue(issue, @u_disabled) + + should_email(@u_mentioned) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + end + + it 'emails new assignee even if he has the "on mention" notif level' do + issue.update_attributes(assignee: @u_mentioned) + notification.reassigned_issue(issue, @u_disabled) + + expect(issue.assignee).to be @u_mentioned + should_email(issue.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + end + + it 'emails new assignee' do + issue.update_attribute(:assignee, @u_mentioned) + notification.reassigned_issue(issue, @u_disabled) + + expect(issue.assignee).to be @u_mentioned + should_email(issue.assignee) + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + end + + it 'does not email new assignee if they are the current user' do + issue.update_attribute(:assignee, @u_mentioned) + notification.reassigned_issue(issue, @u_mentioned) + + expect(issue.assignee).to be @u_mentioned + should_email(@u_watcher) + should_email(@u_participant_mentioned) + should_email(@subscriber) + should_not_email(issue.assignee) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(@u_disabled) + end + end + + describe '#relabeled_issue' do + let(:label) { create(:label, issues: [issue]) } + let(:label2) { create(:label) } + let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } } + let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } } + + it "emails subscribers of the issue's added labels only" do + notification.relabeled_issue(issue, [label2], @u_disabled) + + should_not_email(subscriber_to_label) + should_email(subscriber_to_label2) + end + + it "doesn't send email to anyone but subscribers of the given labels" do + notification.relabeled_issue(issue, [label2], @u_disabled) + + should_not_email(issue.assignee) + should_not_email(issue.author) + should_not_email(@u_watcher) + should_not_email(@u_participant_mentioned) + should_not_email(@subscriber) + should_not_email(@watcher_and_subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(subscriber_to_label) + should_email(subscriber_to_label2) + end end describe :close_issue do @@ -245,6 +343,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -260,6 +359,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) end @@ -282,10 +382,20 @@ describe NotificationService, services: true do should_email(merge_request.assignee) should_email(@u_watcher) + should_email(@watcher_and_subscriber) should_email(@u_participant_mentioned) should_not_email(@u_participating) should_not_email(@u_disabled) end + + it "emails subscribers of the merge request's labels" do + subscriber = create(:user) + label = create(:label, merge_requests: [merge_request]) + label.toggle_subscription(subscriber) + notification.new_merge_request(merge_request, @u_disabled) + + should_email(subscriber) + end end describe :reassigned_merge_request do @@ -296,12 +406,42 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) end end + describe :relabel_merge_request do + let(:label) { create(:label, merge_requests: [merge_request]) } + let(:label2) { create(:label) } + let!(:subscriber_to_label) { create(:user).tap { |u| label.toggle_subscription(u) } } + let!(:subscriber_to_label2) { create(:user).tap { |u| label2.toggle_subscription(u) } } + + it "emails subscribers of the merge request's added labels only" do + notification.relabeled_merge_request(merge_request, [label2], @u_disabled) + + should_not_email(subscriber_to_label) + should_email(subscriber_to_label2) + end + + it "doesn't send email to anyone but subscribers of the given labels" do + notification.relabeled_merge_request(merge_request, [label2], @u_disabled) + + should_not_email(merge_request.assignee) + should_not_email(merge_request.author) + should_not_email(@u_watcher) + should_not_email(@u_participant_mentioned) + should_not_email(@subscriber) + should_not_email(@watcher_and_subscriber) + should_not_email(@unsubscriber) + should_not_email(@u_participating) + should_not_email(subscriber_to_label) + should_email(subscriber_to_label2) + end + end + describe :closed_merge_request do it do notification.close_mr(merge_request, @u_disabled) @@ -310,6 +450,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -324,6 +465,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -338,6 +480,7 @@ describe NotificationService, services: true do should_email(@u_watcher) should_email(@u_participant_mentioned) should_email(@subscriber) + should_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -387,25 +530,17 @@ describe NotificationService, services: true do @subscriber = create :user @unsubscriber = create :user @subscribed_participant = create(:user, username: 'subscribed_participant', notification_level: Notification::N_PARTICIPATING) + @watcher_and_subscriber = create(:user, notification_level: Notification::N_WATCH) project.team << [@subscribed_participant, :master] project.team << [@subscriber, :master] project.team << [@unsubscriber, :master] + project.team << [@watcher_and_subscriber, :master] issuable.subscriptions.create(user: @subscriber, subscribed: true) issuable.subscriptions.create(user: @subscribed_participant, subscribed: true) issuable.subscriptions.create(user: @unsubscriber, subscribed: false) - end - - def sent_to_user?(user) - ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1 - end - - def should_email(user) - expect(sent_to_user?(user)).to be_truthy - end - - def should_not_email(user) - expect(sent_to_user?(user)).to be_falsey + # Make the watcher a subscriber to detect dupes + issuable.subscriptions.create(user: @watcher_and_subscriber, subscribed: true) end end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb new file mode 100644 index 00000000000..6108c26a78b --- /dev/null +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -0,0 +1,79 @@ +require 'spec_helper' + +describe Projects::AutocompleteService, services: true do + describe '#issues' do + describe 'confidential issues' do + let(:author) { create(:user) } + let(:assignee) { create(:user) } + let(:non_member) { create(:user) } + let(:member) { create(:user) } + let(:admin) { create(:admin) } + let(:project) { create(:empty_project, :public) } + let!(:issue) { create(:issue, project: project, title: 'Issue 1') } + let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + + it 'should not list project confidential issues for guests' do + autocomplete = described_class.new(project, nil) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 1 + end + + it 'should not list project confidential issues for non project members' do + autocomplete = described_class.new(project, non_member) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 1 + end + + it 'should list project confidential issues for author' do + autocomplete = described_class.new(project, author) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).not_to include security_issue_2.iid + expect(issues.count).to eq 2 + end + + it 'should list project confidential issues for assignee' do + autocomplete = described_class.new(project, assignee) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).not_to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 2 + end + + it 'should list project confidential issues for project members' do + project.team << [member, :developer] + + autocomplete = described_class.new(project, member) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 3 + end + + it 'should list all project issues for admin' do + autocomplete = described_class.new(project, admin) + issues = autocomplete.issues.map(&:iid) + + expect(issues).to include issue.iid + expect(issues).to include security_issue_1.iid + expect(issues).to include security_issue_2.iid + expect(issues.count).to eq 3 + end + end + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 5d0b18558b1..e43903dbd3c 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -32,6 +32,7 @@ describe Projects::CreateService, services: true do it { expect(@project).to be_valid } it { expect(@project.owner).to eq(@user) } + it { expect(@project.team.masters).to include(@user) } it { expect(@project.namespace).to eq(@user.namespace) } end diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index 5ceed5af9a5..f252e2c5902 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -33,12 +33,12 @@ describe Projects::DownloadService, services: true do @link_to_file = download_file(@project, url) end - it { expect(@link_to_file).to have_key('alt') } - it { expect(@link_to_file).to have_key('url') } - it { expect(@link_to_file).to have_key('is_image') } - it { expect(@link_to_file['is_image']).to be true } - it { expect(@link_to_file['url']).to match('rails_sample.jpg') } - it { expect(@link_to_file['alt']).to eq('rails_sample') } + it { expect(@link_to_file).to have_key(:alt) } + it { expect(@link_to_file).to have_key(:url) } + it { expect(@link_to_file).to have_key(:is_image) } + it { expect(@link_to_file[:is_image]).to be true } + it { expect(@link_to_file[:url]).to match('rails_sample.jpg') } + it { expect(@link_to_file[:alt]).to eq('rails_sample') } end context 'a txt file' do @@ -47,12 +47,12 @@ describe Projects::DownloadService, services: true do @link_to_file = download_file(@project, url) end - it { expect(@link_to_file).to have_key('alt') } - it { expect(@link_to_file).to have_key('url') } - it { expect(@link_to_file).to have_key('is_image') } - it { expect(@link_to_file['is_image']).to be false } - it { expect(@link_to_file['url']).to match('doc_sample.txt') } - it { expect(@link_to_file['alt']).to eq('doc_sample.txt') } + it { expect(@link_to_file).to have_key(:alt) } + it { expect(@link_to_file).to have_key(:url) } + it { expect(@link_to_file).to have_key(:is_image) } + it { expect(@link_to_file[:is_image]).to be false } + it { expect(@link_to_file[:url]).to match('doc_sample.txt') } + it { expect(@link_to_file[:alt]).to eq('doc_sample.txt') } end end end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb new file mode 100644 index 00000000000..93bf1b81fbe --- /dev/null +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe Projects::HousekeepingService do + subject { Projects::HousekeepingService.new(project) } + let(:project) { create :project } + + describe 'execute' do + before do + project.pushes_since_gc = 3 + project.save! + end + + it 'enqueues a sidekiq job' do + expect(subject).to receive(:try_obtain_lease).and_return(true) + expect(GitlabShellWorker).to receive(:perform_async).with(:gc, project.path_with_namespace) + + subject.execute + expect(project.pushes_since_gc).to eq(0) + end + + it 'does not enqueue a job when no lease can be obtained' do + expect(subject).to receive(:try_obtain_lease).and_return(false) + expect(GitlabShellWorker).not_to receive(:perform_async) + + expect { subject.execute }.to raise_error(Projects::HousekeepingService::LeaseTaken) + expect(project.pushes_since_gc).to eq(0) + end + end + + describe 'needed?' do + it 'when the count is low enough' do + expect(subject.needed?).to eq(false) + end + + it 'when the count is high enough' do + allow(project).to receive(:pushes_since_gc).and_return(10) + expect(subject.needed?).to eq(true) + end + end + + describe 'increment!' do + it 'increments the pushes_since_gc counter' do + expect(project.pushes_since_gc).to eq(0) + subject.increment! + expect(project.pushes_since_gc).to eq(1) + end + end +end diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb new file mode 100644 index 00000000000..04f474c736c --- /dev/null +++ b/spec/services/projects/import_service_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe Projects::ImportService, services: true do + let!(:project) { create(:empty_project) } + let(:user) { project.creator } + + subject { described_class.new(project, user) } + + describe '#execute' do + context 'with unknown url' do + before do + project.import_url = Project::UNKNOWN_IMPORT_URL + end + + it 'succeeds if repository is created successfully' do + expect(project).to receive(:create_repository).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository creation fails' do + expect(project).to receive(:create_repository).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The repository could not be created.' + end + end + + context 'with known url' do + before do + project.import_url = 'https://github.com/vim/vim.git' + end + + it 'succeeds if repository import is successfully' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if repository import fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Failed to import the repository' + end + end + + context 'with valid importer' do + before do + stub_github_omniauth_provider + + project.import_url = 'https://github.com/vim/vim.git' + project.import_type = 'github' + + allow(project).to receive(:import_data).and_return(double.as_null_object) + end + + it 'succeeds if importer succeeds' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + + result = subject.execute + + expect(result[:status]).to eq :success + end + + it 'fails if importer fails' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'The remote data could not be imported.' + end + + it 'fails if importer raise an error' do + expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.path_with_namespace, project.import_url).and_return(true) + expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API')) + + result = subject.execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Github: failed to connect API' + end + end + + def stub_github_omniauth_provider + provider = OpenStruct.new( + name: 'github', + app_id: 'asd123', + app_secret: 'asd123' + ) + + Gitlab.config.omniauth.providers << provider + end + end +end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3c06a890163..e8b9e6b9238 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -102,8 +102,8 @@ describe Projects::UpdateService, services: true do describe :visibility_level do let(:user) { create :user, admin: true } - let(:project) { create :project, visibility_level: Gitlab::VisibilityLevel::INTERNAL } - let(:forked_project) { create :forked_project_with_submodules, visibility_level: Gitlab::VisibilityLevel::INTERNAL } + let(:project) { create(:project, :internal) } + let(:forked_project) { create(:forked_project_with_submodules, :internal) } let(:opts) { {} } before do diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb new file mode 100644 index 00000000000..ce7d1455975 --- /dev/null +++ b/spec/services/repair_ldap_blocked_user_service_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' + +describe RepairLdapBlockedUserService, services: true do + let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } + let(:identity) { user.ldap_identity } + subject(:service) { RepairLdapBlockedUserService.new(user) } + + describe '#execute' do + it 'change to normal block after destroying last ldap identity' do + identity.destroy + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + + it 'change to normal block after changing last ldap identity to another provider' do + identity.update_attribute(:provider, 'twitter') + service.execute + + expect(user.reload).not_to be_ldap_blocked + end + end +end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index febc78d2784..fef211ded50 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -9,37 +9,54 @@ describe SystemHooksService, services: true do let(:group_member) { create(:group_member) } context 'event data' do - it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :email, :user_id) } - it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :email, :user_id) } - it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } - it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } - it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_email, :access_level, :project_visibility) } + it { expect(event_data(user, :create)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } + it { expect(event_data(user, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :email, :user_id, :username) } + it { expect(event_data(project, :create)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project, :destroy)).to include(:event_name, :name, :created_at, :updated_at, :path, :project_id, :owner_name, :owner_email, :project_visibility) } + it { expect(event_data(project_member, :create)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } + it { expect(event_data(project_member, :destroy)).to include(:event_name, :created_at, :updated_at, :project_name, :project_path, :project_path_with_namespace, :project_id, :user_name, :user_username, :user_email, :user_id, :access_level, :project_visibility) } it { expect(event_data(key, :create)).to include(:username, :key, :id) } it { expect(event_data(key, :destroy)).to include(:username, :key, :id) } it do + project.old_path_with_namespace = 'renamed_from_path' + expect(event_data(project, :rename)).to include( + :event_name, :name, :created_at, :updated_at, :path, :project_id, + :owner_name, :owner_email, :project_visibility, + :old_path_with_namespace + ) + end + it do + project.old_path_with_namespace = 'transfered_from_path' + expect(event_data(project, :transfer)).to include( + :event_name, :name, :created_at, :updated_at, :path, :project_id, + :owner_name, :owner_email, :project_visibility, + :old_path_with_namespace + ) + end + + it do expect(event_data(group, :create)).to include( - :event_name, :name, :created_at, :path, :group_id, :owner_name, - :owner_email + :event_name, :name, :created_at, :updated_at, :path, :group_id, + :owner_name, :owner_email ) end it do expect(event_data(group, :destroy)).to include( - :event_name, :name, :created_at, :path, :group_id, :owner_name, - :owner_email + :event_name, :name, :created_at, :updated_at, :path, :group_id, + :owner_name, :owner_email ) end it do expect(event_data(group_member, :create)).to include( - :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, - :user_name, :user_email, :group_access + :event_name, :created_at, :updated_at, :group_name, :group_path, + :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end it do expect(event_data(group_member, :destroy)).to include( - :event_name, :created_at, :group_name, :group_path, :group_id, :user_id, - :user_name, :user_email, :group_access + :event_name, :created_at, :updated_at, :group_name, :group_path, + :group_id, :user_id, :user_username, :user_name, :user_email, :group_access ) end end @@ -49,6 +66,8 @@ describe SystemHooksService, services: true do it { expect(event_name(user, :destroy)).to eq "user_destroy" } it { expect(event_name(project, :create)).to eq "project_create" } it { expect(event_name(project, :destroy)).to eq "project_destroy" } + it { expect(event_name(project, :rename)).to eq "project_rename" } + it { expect(event_name(project, :transfer)).to eq "project_transfer" } it { expect(event_name(project_member, :create)).to eq "user_add_to_team" } it { expect(event_name(project_member, :destroy)).to eq "user_remove_from_team" } it { expect(event_name(key, :create)).to eq 'key_create' } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index c9f828ae2f7..8e6292014d4 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -171,7 +171,7 @@ describe SystemNoteService, services: true do context 'when milestone added' do it 'sets the note text' do - expect(subject.note).to eq "Milestone changed to #{milestone.title}" + expect(subject.note).to eq "Milestone changed to #{milestone.to_reference}" end end @@ -280,6 +280,18 @@ describe SystemNoteService, services: true do end end + describe '.new_issue_branch' do + subject { described_class.new_issue_branch(noteable, project, author, "1-mepmep") } + + it_behaves_like 'a system note' + + context 'when a branch is created from the new branch button' do + it 'sets the note text' do + expect(subject.note).to match /\AStarted branch [`1-mepmep`]/ + end + end + end + describe '.cross_reference' do subject { described_class.cross_reference(noteable, mentioner, author) } @@ -424,6 +436,21 @@ describe SystemNoteService, services: true do to be_falsey end end + + context 'commit with cross-reference from fork' do + let(:author2) { create(:user) } + let(:forked_project) { Projects::ForkService.new(project, author2).execute } + let(:commit2) { forked_project.commit } + + before do + described_class.cross_reference(noteable, commit0, author2) + end + + it 'is true when a fork mentions an external issue' do + expect(described_class.cross_reference_exists?(noteable, commit2)). + to be true + end + end end include JiraServiceHelper @@ -459,8 +486,8 @@ describe SystemNoteService, services: true do describe "existing reference" do before do - message = "[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]." - WebMock.stub_request(:get, jira_api_comment_url).to_return(body: "{\"comments\":[{\"body\":\"#{message}\"}]}") + message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'} + WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]})) end subject { described_class.cross_reference(jira_issue, commit, author) } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb new file mode 100644 index 00000000000..96420acb31d --- /dev/null +++ b/spec/services/todo_service_spec.rb @@ -0,0 +1,274 @@ +require 'spec_helper' + +describe TodoService, services: true do + let(:author) { create(:user) } + let(:john_doe) { create(:user, username: 'john_doe') } + let(:michael) { create(:user, username: 'michael') } + let(:stranger) { create(:user, username: 'stranger') } + let(:project) { create(:project) } + let(:mentions) { [author.to_reference, john_doe.to_reference, michael.to_reference, stranger.to_reference].join(' ') } + let(:service) { described_class.new } + + before do + project.team << [author, :developer] + project.team << [john_doe, :developer] + project.team << [michael, :developer] + end + + describe 'Issues' do + let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: mentions) } + let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } + + describe '#new_issue' do + it 'creates a todo if assigned' do + service.new_issue(issue, author) + + should_create_todo(user: john_doe, target: issue, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + should_not_create_any_todo { service.new_issue(unassigned_issue, author) } + end + + it 'does not create a todo if assignee is the current user' do + should_not_create_any_todo { service.new_issue(unassigned_issue, john_doe) } + end + + it 'creates a todo for each valid mentioned user' do + service.new_issue(issue, author) + + should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + end + end + + describe '#update_issue' do + it 'creates a todo for each valid mentioned user' do + service.update_issue(issue, author) + + should_create_todo(user: michael, target: issue, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: issue, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: issue, action: Todo::MENTIONED) + end + + it 'does not create a todo if user was already mentioned' do + create(:todo, :mentioned, user: michael, project: project, target: issue, author: author) + + expect { service.update_issue(issue, author) }.not_to change(michael.todos, :count) + end + end + + describe '#close_issue' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.close_issue(issue, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#reassigned_issue' do + it 'creates a pending todo for new assignee' do + unassigned_issue.update_attribute(:assignee, john_doe) + service.reassigned_issue(unassigned_issue, author) + + should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + issue.update_attribute(:assignee, nil) + + should_not_create_any_todo { service.reassigned_issue(issue, author) } + end + + it 'does not create a todo if new assignee is the current user' do + unassigned_issue.update_attribute(:assignee, john_doe) + + should_not_create_any_todo { service.reassigned_issue(unassigned_issue, john_doe) } + end + end + + describe '#mark_pending_todos_as_done' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.mark_pending_todos_as_done(issue, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#new_note' do + let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } + let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } + let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } + let(:note_on_project_snippet) { create(:note_on_project_snippet, project: project, author: john_doe, note: mentions) } + let(:award_note) { create(:note, :award, project: project, noteable: issue, author: john_doe, note: 'thumbsup') } + let(:system_note) { create(:system_note, project: project, noteable: issue) } + + it 'mark related pending todos to the noteable for the note author as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) + + service.new_note(note, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + + it 'mark related pending todos to the noteable for the award note author as done' do + service.new_note(award_note, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + + it 'does not mark related pending todos it is a system note' do + service.new_note(system_note, john_doe) + + expect(first_todo.reload).to be_pending + expect(second_todo.reload).to be_pending + end + + it 'creates a todo for each valid mentioned user' do + service.new_note(note, john_doe) + + should_create_todo(user: michael, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_create_todo(user: author, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: john_doe, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + should_not_create_todo(user: stranger, target: issue, author: john_doe, action: Todo::MENTIONED, note: note) + end + + it 'does not create todo when leaving a note on commit' do + should_not_create_any_todo { service.new_note(note_on_commit, john_doe) } + end + + it 'does not create todo when leaving a note on snippet' do + should_not_create_any_todo { service.new_note(note_on_project_snippet, john_doe) } + end + end + end + + describe 'Merge Requests' do + let(:mr_assigned) { create(:merge_request, source_project: project, author: author, assignee: john_doe, description: mentions) } + let(:mr_unassigned) { create(:merge_request, source_project: project, author: author, assignee: nil) } + + describe '#new_merge_request' do + it 'creates a pending todo if assigned' do + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: john_doe, target: mr_assigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + should_not_create_any_todo { service.new_merge_request(mr_unassigned, author) } + end + + it 'does not create a todo if assignee is the current user' do + should_not_create_any_todo { service.new_merge_request(mr_unassigned, john_doe) } + end + + it 'creates a todo for each valid mentioned user' do + service.new_merge_request(mr_assigned, author) + + should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + end + end + + describe '#update_merge_request' do + it 'creates a todo for each valid mentioned user' do + service.update_merge_request(mr_assigned, author) + + should_create_todo(user: michael, target: mr_assigned, action: Todo::MENTIONED) + should_create_todo(user: john_doe, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: author, target: mr_assigned, action: Todo::MENTIONED) + should_not_create_todo(user: stranger, target: mr_assigned, action: Todo::MENTIONED) + end + + it 'does not create a todo if user was already mentioned' do + create(:todo, :mentioned, user: michael, project: project, target: mr_assigned, author: author) + + expect { service.update_merge_request(mr_assigned, author) }.not_to change(michael.todos, :count) + end + end + + describe '#close_merge_request' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + service.close_merge_request(mr_assigned, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + + describe '#reassigned_merge_request' do + it 'creates a pending todo for new assignee' do + mr_unassigned.update_attribute(:assignee, john_doe) + service.reassigned_merge_request(mr_unassigned, author) + + should_create_todo(user: john_doe, target: mr_unassigned, action: Todo::ASSIGNED) + end + + it 'does not create a todo if unassigned' do + mr_assigned.update_attribute(:assignee, nil) + + should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, author) } + end + + it 'does not create a todo if new assignee is the current user' do + mr_assigned.update_attribute(:assignee, john_doe) + + should_not_create_any_todo { service.reassigned_merge_request(mr_assigned, john_doe) } + end + end + + describe '#merge_merge_request' do + it 'marks related pending todos to the target for the user as done' do + first_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + second_todo = create(:todo, :assigned, user: john_doe, project: project, target: mr_assigned, author: author) + service.merge_merge_request(mr_assigned, john_doe) + + expect(first_todo.reload).to be_done + expect(second_todo.reload).to be_done + end + end + end + + def should_create_todo(attributes = {}) + attributes.reverse_merge!( + project: project, + author: author, + state: :pending + ) + + expect(Todo.where(attributes).count).to eq 1 + end + + def should_not_create_todo(attributes = {}) + attributes.reverse_merge!( + project: project, + author: author, + state: :pending + ) + + expect(Todo.where(attributes).count).to eq 0 + end + + def should_not_create_any_todo + expect { yield }.not_to change(Todo, :count) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 0225a0ee53f..596d607f2a1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -14,7 +14,7 @@ require File.expand_path("../../config/environment", __FILE__) require 'rspec/rails' require 'shoulda/matchers' require 'sidekiq/testing/inline' -require 'benchmark/ips' +require 'rspec/retry' # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. @@ -25,16 +25,19 @@ RSpec.configure do |config| config.use_instantiated_fixtures = false config.mock_with :rspec + config.verbose_retry = true + config.display_try_failure_messages = true + config.include Devise::TestHelpers, type: :controller config.include LoginHelpers, type: :feature config.include LoginHelpers, type: :request config.include StubConfiguration + config.include EmailHelpers config.include RelativeUrl, type: feature config.include TestEnv config.include ActiveJob::TestHelper config.include StubGitlabCalls config.include StubGitlabData - config.include BenchmarkMatchers, benchmark: true config.infer_spec_type_from_file_location! config.raise_errors_for_deprecations! @@ -48,4 +51,10 @@ FactoryGirl::SyntaxRunner.class_eval do include RSpec::Mocks::ExampleMethods end +# Work around a Rails 4.2.5.1 issue +# See https://github.com/rspec/rspec-rails/issues/1532 +RSpec::Rails::ViewRendering::EmptyTemplatePathSetDecorator.class_eval do + alias_method :find_all_anywhere, :find_all +end + ActiveRecord::Migration.maintain_test_schema! diff --git a/spec/support/api/pagination_shared_examples.rb b/spec/support/api/pagination_shared_examples.rb new file mode 100644 index 00000000000..352a6eeec79 --- /dev/null +++ b/spec/support/api/pagination_shared_examples.rb @@ -0,0 +1,20 @@ +# Specs for paginated resources. +# +# Requires an API request: +# let(:request) { get api("/projects/#{project.id}/repository/branches", user) } +shared_examples 'a paginated resources' do + before do + # Fires the request + request + end + + it 'has pagination headers' do + expect(response.headers).to include('X-Total') + expect(response.headers).to include('X-Total-Pages') + expect(response.headers).to include('X-Per-Page') + expect(response.headers).to include('X-Page') + expect(response.headers).to include('X-Next-Page') + expect(response.headers).to include('X-Prev-Page') + expect(response.headers).to include('Link') + end +end diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index fed1ab6ee33..e1f90e17cce 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -7,10 +7,10 @@ timeout = (ENV['CI'] || ENV['CI_SERVER']) ? 90 : 10 Capybara.javascript_driver = :poltergeist Capybara.register_driver :poltergeist do |app| - Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout) + Capybara::Poltergeist::Driver.new(app, js_errors: true, timeout: timeout, window_size: [1366, 768]) end -Capybara.default_wait_time = timeout +Capybara.default_max_wait_time = timeout Capybara.ignore_hidden_elements = true unless ENV['CI'] || ENV['CI_SERVER'] @@ -19,3 +19,9 @@ unless ENV['CI'] || ENV['CI_SERVER'] # Keep only the screenshots generated from the last failing test suite Capybara::Screenshot.prune_strategy = :keep_last_run end + +RSpec.configure do |config| + config.before(:suite) do + TestEnv.warm_asset_cache + end +end diff --git a/spec/support/email_format_shared_examples.rb b/spec/support/email_format_shared_examples.rb new file mode 100644 index 00000000000..b924a208e71 --- /dev/null +++ b/spec/support/email_format_shared_examples.rb @@ -0,0 +1,44 @@ +# Specifications for behavior common to all objects with an email attribute. +# Takes a list of email-format attributes and requires: +# - subject { "the object with a attribute= setter" } +# Note: You have access to `email_value` which is the email address value +# being currently tested). + +shared_examples 'an object with email-formated attributes' do |*attributes| + attributes.each do |attribute| + describe "specifically its :#{attribute} attribute" do + %w[ + info@example.com + info+test@example.com + o'reilly@example.com + mailto:test@example.com + lol!'+=?><#$%^&*()@gmail.com + ].each do |valid_email| + context "with a value of '#{valid_email}'" do + let(:email_value) { valid_email } + + it 'is valid' do + subject.send("#{attribute}=", valid_email) + + expect(subject).to be_valid + end + end + end + + %w[ + foobar + test@test@example.com + ].each do |invalid_email| + context "with a value of '#{invalid_email}'" do + let(:email_value) { invalid_email } + + it 'is invalid' do + subject.send("#{attribute}=", invalid_email) + + expect(subject).to be_invalid + end + end + end + end + end +end diff --git a/spec/support/email_helpers.rb b/spec/support/email_helpers.rb new file mode 100644 index 00000000000..a85ab22ce36 --- /dev/null +++ b/spec/support/email_helpers.rb @@ -0,0 +1,13 @@ +module EmailHelpers + def sent_to_user?(user) + ActionMailer::Base.deliveries.map(&:to).flatten.count(user.email) == 1 + end + + def should_email(user) + expect(sent_to_user?(user)).to be_truthy + end + + def should_not_email(user) + expect(sent_to_user?(user)).to be_falsey + end +end diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb index d6e03cbef3d..ef5ea7d626e 100644 --- a/spec/support/filter_spec_helper.rb +++ b/spec/support/filter_spec_helper.rb @@ -67,9 +67,9 @@ module FilterSpecHelper if reference =~ /\A(.+)?.\d+\z/ # Integer-based reference with optional project prefix reference.gsub(/\d+\z/) { |i| i.to_i + 1 } - elsif reference =~ /\A(.+@)?(\h{6,40}\z)/ + elsif reference =~ /\A(.+@)?(\h{7,40}\z)/ # SHA-based reference with optional prefix - reference.gsub(/\h{6,40}\z/) { |v| v.reverse } + reference.gsub(/\h{7,40}\z/) { |v| v.reverse } else reference.gsub(/\w+\z/) { |v| v.reverse } end diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index 3482145404e..a5b256bd3ec 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -36,8 +36,8 @@ staging: script: "cap deploy stating" type: deploy tags: - - capistrano - - debian + - ruby + - mysql except: - stable @@ -47,8 +47,8 @@ production: - cap deploy production - cap notify tags: - - capistrano - - debian + - ruby + - mysql only: - master - /^deploy-.*$/ diff --git a/spec/support/markdown_feature.rb b/spec/support/markdown_feature.rb index d6d3062a197..73c6792b65f 100644 --- a/spec/support/markdown_feature.rb +++ b/spec/support/markdown_feature.rb @@ -28,6 +28,10 @@ class MarkdownFeature end end + def project_wiki + @project_wiki ||= ProjectWiki.new(project, user) + end + def issue @issue ||= create(:issue, project: project) end @@ -59,6 +63,10 @@ class MarkdownFeature @label ||= create(:label, name: 'awaiting feedback', project: project) end + def milestone + @milestone ||= create(:milestone, project: project) + end + # Cross-references ----------------------------------------------------------- def xproject @@ -93,6 +101,10 @@ class MarkdownFeature end end + def xmilestone + @xmilestone ||= create(:milestone, project: xproject) + end + def urls Gitlab::Application.routes.url_helpers end diff --git a/spec/support/matchers/access_matchers.rb b/spec/support/matchers/access_matchers.rb index 558e8b1612f..4e007c777e3 100644 --- a/spec/support/matchers/access_matchers.rb +++ b/spec/support/matchers/access_matchers.rb @@ -15,6 +15,8 @@ module AccessMatchers logout when :admin login_as(create(:admin)) + when :external + login_as(create(:user, external: true)) when User login_as(user) else diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb deleted file mode 100644 index 84f655c2119..00000000000 --- a/spec/support/matchers/benchmark_matchers.rb +++ /dev/null @@ -1,61 +0,0 @@ -module BenchmarkMatchers - extend RSpec::Matchers::DSL - - def self.included(into) - into.extend(ClassMethods) - end - - matcher :iterate_per_second do |min_iterations| - supports_block_expectations - - match do |block| - @max_stddev ||= 30 - - @entry = benchmark(&block) - - expect(@entry.ips).to be >= min_iterations - expect(@entry.stddev_percentage).to be <= @max_stddev - end - - chain :with_maximum_stddev do |value| - @max_stddev = value - end - - description do - "run at least #{min_iterations} iterations per second" - end - - failure_message do - ips = @entry.ips.round(2) - stddev = @entry.stddev_percentage.round(2) - - "expected at least #{min_iterations} iterations per second " \ - "with a maximum stddev of #{@max_stddev}%, instead of " \ - "#{ips} iterations per second with a stddev of #{stddev}%" - end - end - - # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry. - def benchmark(&block) - report = Benchmark.ips(quiet: true) do |bench| - bench.report do - instance_eval(&block) - end - end - - report.entries[0] - end - - module ClassMethods - # Wraps around rspec's subject method so you can write: - # - # benchmark_subject { SomeClass.some_method } - # - # instead of: - # - # subject { -> { SomeClass.some_method } } - def benchmark_subject(&block) - subject { block } - end - end -end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index 7eadcd58c1f..1d52489e804 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -66,6 +66,24 @@ module MarkdownMatchers end end + # GollumTagsFilter + matcher :parse_gollum_tags do + def have_image(src) + have_css("img[src$='#{src}']") + end + + set_default_markdown_messages + + match do |actual| + expect(actual).to have_link('linked-resource', href: 'linked-resource') + expect(actual).to have_link('link-text', href: 'linked-resource') + expect(actual).to have_link('http://example.com', href: 'http://example.com') + expect(actual).to have_link('link-text', href: 'http://example.com/pdfs/gollum.pdf') + expect(actual).to have_image('/gitlabhq/wikis/images/example.jpg') + expect(actual).to have_image('http://example.com/images/example.jpg') + end + end + # UserReferenceFilter matcher :reference_users do set_default_markdown_messages @@ -130,6 +148,15 @@ module MarkdownMatchers end end + # MilestoneReferenceFilter + matcher :reference_milestones do + set_default_markdown_messages + + match do |actual| + expect(actual).to have_selector('a.gfm.gfm-milestone', count: 3) + end + end + # TaskListFilter matcher :parse_task_lists do set_default_markdown_messages diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb index fce91015fd4..e876d44c166 100644 --- a/spec/support/mentionable_shared_examples.rb +++ b/spec/support/mentionable_shared_examples.rb @@ -52,6 +52,8 @@ shared_context 'mentionable context' do end set_mentionable_text.call(ref_string) + + project.team << [author, :developer] end end diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb new file mode 100644 index 00000000000..422083875d7 --- /dev/null +++ b/spec/support/project_hook_data_shared_example.rb @@ -0,0 +1,27 @@ +RSpec.shared_examples 'project hook data' do |project_key: :project| + it 'contains project data' do + expect(data[project_key][:name]).to eq(project.name) + expect(data[project_key][:description]).to eq(project.description) + expect(data[project_key][:web_url]).to eq(project.web_url) + expect(data[project_key][:avatar_url]).to eq(project.avatar_url) + expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo) + expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:namespace]).to eq(project.namespace.name) + expect(data[project_key][:visibility_level]).to eq(project.visibility_level) + expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace) + expect(data[project_key][:default_branch]).to eq(project.default_branch) + expect(data[project_key][:homepage]).to eq(project.web_url) + expect(data[project_key][:url]).to eq(project.url_to_repo) + expect(data[project_key][:ssh_url]).to eq(project.ssh_url_to_repo) + expect(data[project_key][:http_url]).to eq(project.http_url_to_repo) + end +end + +RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project| + it 'contains deprecated repository data' do + expect(data[:repository][:name]).to eq(project.name) + expect(data[:repository][:description]).to eq(project.description) + expect(data[:repository][:url]).to eq(project.url_to_repo) + expect(data[:repository][:homepage]).to eq(project.web_url) + end +end diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb index 4f4743bff6d..0d1bd030f3c 100644 --- a/spec/support/test_env.rb +++ b/spec/support/test_env.rb @@ -146,6 +146,22 @@ module TestEnv FileUtils.chmod_R 0755, target_repo_path end + # When no cached assets exist, manually hit the root path to create them + # + # Otherwise they'd be created by the first test, often timing out and + # causing a transient test failure + def warm_asset_cache + return if warm_asset_cache? + return unless defined?(Capybara) + + Capybara.current_session.driver.visit '/' + end + + def warm_asset_cache? + cache = Rails.root.join(*%w(tmp cache assets test)) + Dir.exist?(cache) && Dir.entries(cache).length > 2 + end + private def factory_repo_path @@ -172,7 +188,6 @@ module TestEnv 'gitlab-test-fork' end - # Prevent developer git configurations from being persisted to test # repositories def git_env diff --git a/spec/support/wait_for_ajax.rb b/spec/support/wait_for_ajax.rb index 692d219e9f1..b90fc112671 100644 --- a/spec/support/wait_for_ajax.rb +++ b/spec/support/wait_for_ajax.rb @@ -1,6 +1,6 @@ module WaitForAjax def wait_for_ajax - Timeout.timeout(Capybara.default_wait_time) do + Timeout.timeout(Capybara.default_max_wait_time) do loop until finished_all_ajax_requests? end end diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb new file mode 100644 index 00000000000..107b6e30924 --- /dev/null +++ b/spec/support/workhorse_helpers.rb @@ -0,0 +1,16 @@ +module WorkhorseHelpers + extend self + + def workhorse_send_data + @_workhorse_send_data ||= begin + header = response.headers[Gitlab::Workhorse::SEND_DATA_HEADER] + split_header = header.split(':') + type = split_header.shift + header = split_header.join(':') + [ + type, + JSON.parse(Base64.urlsafe_decode64(header)), + ] + end + end +end diff --git a/spec/views/devise/shared/_signin_box.html.haml_spec.rb b/spec/views/devise/shared/_signin_box.html.haml_spec.rb new file mode 100644 index 00000000000..05a76ee4bdb --- /dev/null +++ b/spec/views/devise/shared/_signin_box.html.haml_spec.rb @@ -0,0 +1,37 @@ +require 'rails_helper' + +describe 'devise/shared/_signin_box' do + describe 'Crowd form' do + before do + stub_devise + assign(:ldap_servers, []) + end + + it 'is shown when Crowd is enabled' do + enable_crowd + + render + + expect(rendered).to have_selector('#tab-crowd form') + end + + it 'is not shown when Crowd is disabled' do + render + + expect(rendered).not_to have_selector('#tab-crowd') + end + end + + def stub_devise + allow(view).to receive(:devise_mapping).and_return(Devise.mappings[:user]) + allow(view).to receive(:resource).and_return(spy) + allow(view).to receive(:resource_name).and_return(:user) + end + + def enable_crowd + allow(view).to receive(:form_based_providers).and_return([:crowd]) + allow(view).to receive(:crowd_enabled?).and_return(true) + allow(view).to receive(:user_omniauth_authorize_path).with('crowd'). + and_return('/crowd') + end +end diff --git a/spec/workers/delete_user_worker_spec.rb b/spec/workers/delete_user_worker_spec.rb new file mode 100644 index 00000000000..14c56521280 --- /dev/null +++ b/spec/workers/delete_user_worker_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe DeleteUserWorker do + let!(:user) { create(:user) } + let!(:current_user) { create(:user) } + + it "calls the DeleteUserWorker with the params it was given" do + expect_any_instance_of(DeleteUserService).to receive(:execute). + with(user, {}) + + DeleteUserWorker.new.perform(current_user.id, user.id) + end + + it "uses symbolized keys" do + expect_any_instance_of(DeleteUserService).to receive(:execute). + with(user, test: "test") + + DeleteUserWorker.new.perform(current_user.id, user.id, "test" => "test") + end +end diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb index e4151b9bb6a..0265dbe9c66 100644 --- a/spec/workers/post_receive_spec.rb +++ b/spec/workers/post_receive_spec.rb @@ -11,7 +11,7 @@ describe PostReceive do end end - context "web hook" do + context "webhook" do let(:project) { create(:project) } let(:key) { create(:key, user: project.owner) } let(:key_id) { key.shell_id } diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb index dae31992620..172537474ee 100644 --- a/spec/workers/repository_fork_worker_spec.rb +++ b/spec/workers/repository_fork_worker_spec.rb @@ -19,6 +19,18 @@ describe RepositoryForkWorker do fork_project.namespace.path) end + it 'flushes the empty caches' do + expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository). + with(project.path_with_namespace, fork_project.namespace.path). + and_return(true) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches). + and_call_original + + subject.perform(project.id, project.path_with_namespace, + fork_project.namespace.path) + end + it "handles bad fork" do expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false) subject.perform( diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb new file mode 100644 index 00000000000..6739063543b --- /dev/null +++ b/spec/workers/repository_import_worker_spec.rb @@ -0,0 +1,19 @@ +require 'spec_helper' + +describe RepositoryImportWorker do + let(:project) { create(:project) } + + subject { described_class.new } + + describe '#perform' do + it 'imports a project' do + expect_any_instance_of(Projects::ImportService).to receive(:execute). + and_return({ status: :ok }) + + expect_any_instance_of(Repository).to receive(:expire_emptiness_caches) + expect_any_instance_of(Project).to receive(:import_finish) + + subject.perform(project.id) + end + end +end |