diff options
author | Rémy Coutable <remy@rymai.me> | 2016-03-18 23:29:18 +0100 |
---|---|---|
committer | Rémy Coutable <remy@rymai.me> | 2016-03-18 23:29:18 +0100 |
commit | cafa408b2521aa82d856581eb5d78d98114f1ab2 (patch) | |
tree | 5172e0c5555354d0275c6bf830bdd25e2e116d1c /spec/models | |
parent | 0b942541da1dc616cea266dc1f4d517fe81f6e5a (diff) | |
parent | 18fc7c66f4455e757593a60e02a6306decef5a47 (diff) | |
download | gitlab-ce-cafa408b2521aa82d856581eb5d78d98114f1ab2.tar.gz |
Merge remote-tracking branch 'origin/master' into remove-wip
Diffstat (limited to 'spec/models')
28 files changed, 1356 insertions, 178 deletions
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 4799bbaa57c..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 } @@ -31,17 +32,14 @@ RSpec.describe AbuseReport, type: :model do describe '#remove_user' do it 'blocks the user' do - report = build(:abuse_report) - - allow(report.user).to receive(:destroy) - - expect { report.remove_user }.to change { report.user.blocked? }.to(true) + expect { subject.remove_user(deleted_by: user) }.to change { subject.user.blocked? }.to(true) end - it 'removes the user' do - report = build(:abuse_report) + 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) - expect { report.remove_user }.to change { User.count }.by(-1) + subject.remove_user(deleted_by: user) 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/build_spec.rb b/spec/models/build_spec.rb index e3d3d453653..b7457808040 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -9,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 } @@ -19,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 @@ -33,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 @@ -69,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 } @@ -101,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", @@ -122,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') } @@ -172,7 +172,7 @@ describe Ci::Build, models: true do end end - describe :variables do + describe '#variables' do context 'returns variables' do subject { build.variables } @@ -242,7 +242,7 @@ describe Ci::Build, models: true do end end - describe :can_be_served? do + describe '#can_be_served?' do let(:runner) { FactoryGirl.create :ci_runner } before { build.project.runners << runner } @@ -277,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 @@ -312,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 @@ -343,35 +343,7 @@ describe Ci::Build, models: true do end end - describe :artifacts_download_url do - subject { build.artifacts_download_url } - - context 'artifacts file does not exist' do - before { build.update_attributes(artifacts_file: nil) } - it { is_expected.to be_nil } - end - - context 'artifacts file exists' do - let(:build) { create(:ci_build, :artifacts) } - it { is_expected.to_not be_nil } - end - end - - describe :artifacts_browse_url do - subject { build.artifacts_browse_url } - - it "should be nil if artifacts browser is unsupported" do - allow(build).to receive(:artifacts_metadata?).and_return(false) - is_expected.to be_nil - end - - it 'should not be nil if artifacts browser is supported' do - allow(build).to receive(:artifacts_metadata?).and_return(true) - is_expected.to_not be_nil - end - end - - describe :artifacts? do + describe '#artifacts?' do subject { build.artifacts? } context 'artifacts archive does not exist' do @@ -386,7 +358,7 @@ describe Ci::Build, models: true do end - describe :artifacts_metadata? do + describe '#artifacts_metadata?' do subject { build.artifacts_metadata? } context 'artifacts metadata does not exist' do it { is_expected.to be_falsy } @@ -398,7 +370,7 @@ describe Ci::Build, models: true do end end - describe :repo_url do + describe '#repo_url' do let(:build) { FactoryGirl.create :ci_build } let(:project) { build.project } @@ -412,7 +384,7 @@ describe Ci::Build, models: true do it { is_expected.to include(project.web_url[7..-1]) } end - describe :depends_on_builds do + 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' } @@ -444,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) diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb index 4dc309a4255..412842337ba 100644 --- a/spec/models/ci/commit_spec.rb +++ b/spec/models/ci/commit_spec.rb @@ -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 diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index e891838672e..25e9e5eca48 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -132,4 +132,32 @@ describe Ci::Runner, models: true do 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/commit_spec.rb b/spec/models/commit_spec.rb index 253902512c3..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 diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 600089802b2..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,6 +113,48 @@ 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(:data) { issue.to_hook_data(user) } let(:project) { issue.project } 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/event_spec.rb b/spec/models/event_spec.rb index ec2a923f91b..5fe44246738 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -65,6 +65,42 @@ describe Event, models: true do it { expect(@event.author).to eq(@user) } end + 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 + project.team << [member, :developer] + end + + 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 + + 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 + describe '.limit_recent' do let!(:event1) { create(:closed_issue_event) } let!(:event2) { create(:closed_issue_event) } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 3c995053eec..c9245fc9535 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -103,4 +103,30 @@ describe Group, models: true do expect(group.avatar_type).to eq(["only images allowed"]) end end + + 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 groups with a partially matching path' do + expect(described_class.search(group.path[0..2])).to eq([group]) + end + + 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/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/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb index 6ea99952a8f..04bc2dcfb16 100644 --- a/spec/models/hooks/web_hook_spec.rb +++ b/spec/models/hooks/web_hook_spec.rb @@ -52,7 +52,7 @@ describe WebHook, 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' } 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/merge_request_spec.rb b/spec/models/merge_request_spec.rb index c33dda01c4f..f2f07e4ee17 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -80,6 +80,47 @@ 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 '#target_sha' do + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'returns nil' do + expect(subject.target_sha).to be_nil + end + 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}" @@ -144,6 +185,7 @@ describe MergeRequest, models: true do 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 @@ -266,6 +308,82 @@ describe MergeRequest, models: true do end end + describe '#diverged_commits_count' do + let(:project) { create(:project) } + let(:fork_project) { create(:project, forked_from_project: project) } + + context 'when the target branch does not exist anymore' do + subject { create(:merge_request).tap { |mr| mr.update_attribute(:target_branch, 'deleted') } } + + it 'does not crash' do + expect{ subject.diverged_commits_count }.not_to raise_error + end + + it 'returns 0' do + expect(subject.diverged_commits_count).to eq(0) + end + end + + 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 + it_behaves_like 'an editable mentionable' do subject { create(:merge_request) } diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 1b1380ce4e2..72a4ea70228 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -32,6 +32,7 @@ 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 @@ -50,18 +51,17 @@ describe Milestone, models: true do 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 @@ -103,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 @@ -113,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 @@ -182,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 e0b3290e416..3c3a580942a 100644 --- a/spec/models/namespace_spec.rb +++ b/spec/models/namespace_spec.rb @@ -41,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 583937ca748..6b18936edb1 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -24,7 +24,7 @@ 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) } @@ -140,13 +140,19 @@ describe Note, models: true do end end - describe :search do - let!(:note) { create(:note, note: "WoW") } + describe '.search' do + let(:note) { create(:note, note: 'WoW') } - it { expect(Note.search('wow')).to include(note) } + it 'returns notes with matching content' do + expect(described_class.search(note.note)).to eq([note]) + end + + it 'returns notes with matching content regardless of the casing' do + expect(described_class.search('WOW')).to eq([note]) + end 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 @@ -163,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) @@ -220,4 +286,12 @@ describe Note, models: true do 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_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 3ccb627a259..b8b9a455b83 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -68,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 @@ -102,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: '' @@ -524,30 +525,30 @@ describe Project, models: true do 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 @@ -561,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 } @@ -581,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 7b63da005f0..bacb17a8883 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -67,6 +67,50 @@ 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 'returns Master role' do user = create(:user) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 51ae2c04ed0..a57229a4fdf 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -101,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 @@ -148,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 @@ -405,7 +427,7 @@ describe Repository, models: true do end end - describe '#expire_branch_ache' do + 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) } @@ -457,11 +479,38 @@ describe Repository, models: true do end end - describe '#revert_merge' do - it 'should revert the changes' do - repository.revert(user, merge_commit, 'master') + 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 - expect(repository.blob_at_branch('master', 'files/ruby/feature.rb')).not_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 @@ -529,11 +578,12 @@ describe Repository, models: true do end end - describe '#before_create_tag' do + 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_create_tag + repository.before_push_tag end end @@ -547,9 +597,9 @@ describe Repository, models: true do describe '#after_push_commit' do it 'flushes the cache' do - expect(repository).to receive(:expire_cache).with('master') + expect(repository).to receive(:expire_cache).with('master', '123') - repository.after_push_commit('master') + repository.after_push_commit('master', '123') end end @@ -568,4 +618,196 @@ describe Repository, models: true do 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/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/todo_spec.rb b/spec/models/todo_spec.rb index fe9ea7e7d1e..d9b86b9368f 100644 --- a/spec/models/todo_spec.rb +++ b/spec/models/todo_spec.rb @@ -5,19 +5,24 @@ # id :integer not null, primary key # user_id :integer not null # project_id :integer not null -# target_id :integer not null +# target_id :integer # 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 +# note_id :integer +# commit_id :string # require 'spec_helper' describe Todo, models: true do + let(:project) { create(:project) } + let(:commit) { project.commit } + let(:issue) { create(:issue) } + describe 'relationships' do it { is_expected.to belong_to(:author).class_name("User") } it { is_expected.to belong_to(:note) } @@ -33,8 +38,22 @@ describe Todo, models: true do 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(:target_type) } it { is_expected.to validate_presence_of(:user) } + + context 'for commits' do + subject { described_class.new(target_type: 'Commit') } + + it { is_expected.to validate_presence_of(:commit_id) } + it { is_expected.not_to validate_presence_of(:target_id) } + end + + context 'for issuables' do + subject { described_class.new(target: issue) } + + it { is_expected.to validate_presence_of(:target_id) } + it { is_expected.not_to validate_presence_of(:commit_id) } + end end describe '#body' do @@ -55,15 +74,69 @@ describe Todo, models: true do end end - describe '#done!' do + 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') + 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 + expect { todo.done }.not_to raise_error + end + end + + describe '#for_commit?' do + it 'returns true when target is a commit' do + subject.target_type = 'Commit' + expect(subject.for_commit?).to eq true + end + + it 'returns false when target is an issuable' do + subject.target_type = 'Issue' + expect(subject.for_commit?).to eq false + end + end + + describe '#target' do + context 'for commits' do + it 'returns an instance of Commit when exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target).to be_a(Commit) + expect(subject.target).to eq commit + end + + it 'returns nil when does not exists' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = 'xxxx' + + expect(subject.target).to be_nil + end + end + + it 'returns the issuable for issuables' do + subject.target_id = issue.id + subject.target_type = issue.class.name + expect(subject.target).to eq issue + end + end + + describe '#target_reference' do + it 'returns the short commit id for commits' do + subject.project = project + subject.target_type = 'Commit' + subject.commit_id = commit.id + + expect(subject.target_reference).to eq commit.short_id + end + + it 'returns reference for issuables' do + subject.target = issue + expect(subject.target_reference).to eq issue.to_reference end end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 88821dd0dad..0ab7fd88ce6 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -174,24 +174,26 @@ describe User, models: true do end end end - - describe 'avatar' do - it 'only validates when avatar is present' do - user = build(:user, :with_avatar) - - user.avatar_crop_x = nil - user.avatar_crop_y = nil - user.avatar_crop_size = nil - - expect(user).not_to be_valid - end - end end describe "Respond to" 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 @@ -268,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! @@ -276,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 @@ -414,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 @@ -447,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 '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 "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 username regardless of the casing' do + expect(described_class.search(user.username.upcase)).to eq([user]) end end |