diff options
Diffstat (limited to 'spec/models')
30 files changed, 1294 insertions, 400 deletions
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb index 02d6263094a..219db365a91 100644 --- a/spec/models/broadcast_message_spec.rb +++ b/spec/models/broadcast_message_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe BroadcastMessage, models: true do - subject { create(:broadcast_message) } + subject { build(:broadcast_message) } it { is_expected.to be_valid } diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb index ae185de9ca3..ef07f2275b1 100644 --- a/spec/models/build_spec.rb +++ b/spec/models/build_spec.rb @@ -1052,4 +1052,132 @@ describe Ci::Build, models: true do end end end + + describe '#has_environment?' do + subject { build.has_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + it { is_expected.to be_truthy } + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#starts_environment?' do + subject { build.starts_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_truthy } + end + + context 'and start action is defined' do + before do + build.update(options: { environment: { action: 'start' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#stops_environment?' do + subject { build.stops_environment? } + + context 'when environment is defined' do + before do + build.update(environment: 'review') + end + + context 'no action is defined' do + it { is_expected.to be_falsey } + end + + context 'and stop action is defined' do + before do + build.update(options: { environment: { action: 'stop' } } ) + end + + it { is_expected.to be_truthy } + end + end + + context 'when environment is not defined' do + before do + build.update(environment: nil) + end + + it { is_expected.to be_falsey } + end + end + + describe '#last_deployment' do + subject { build.last_deployment } + + context 'when multiple deployments are created' do + let!(:deployment1) { create(:deployment, deployable: build) } + let!(:deployment2) { create(:deployment, deployable: build) } + + it 'returns the latest one' do + is_expected.to eq(deployment2) + end + end + end + + describe '#outdated_deployment?' do + subject { build.outdated_deployment? } + + context 'when build succeeded' do + let(:build) { create(:ci_build, :success) } + let!(:deployment) { create(:deployment, deployable: build) } + + context 'current deployment is latest' do + it { is_expected.to be_falsey } + end + + context 'current deployment is not latest on environment' do + let!(:deployment2) { create(:deployment, environment: deployment.environment) } + + it { is_expected.to be_truthy } + end + end + + context 'when build failed' do + let(:build) { create(:ci_build, :failed) } + + it { is_expected.to be_falsey } + end + end + + describe '#expanded_environment_name' do + subject { build.expanded_environment_name } + + context 'when environment uses variables' do + let(:build) { create(:ci_build, ref: 'master', environment: 'review/$CI_BUILD_REF_NAME') } + + it { is_expected.to eq('review/master') } + end + end end diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb new file mode 100644 index 00000000000..b02971cab82 --- /dev/null +++ b/spec/models/chat_name_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe ChatName, models: true do + subject { create(:chat_name) } + + it { is_expected.to belong_to(:service) } + it { is_expected.to belong_to(:user) } + + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_presence_of(:service) } + it { is_expected.to validate_presence_of(:team_id) } + it { is_expected.to validate_presence_of(:chat_id) } + + it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) } + it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) } +end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index a37a00f461a..a7e90c8a381 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4,6 +4,12 @@ describe Ci::Build, models: true do let(:build) { create(:ci_build) } let(:test_trace) { 'This is a test' } + it { is_expected.to belong_to(:runner) } + it { is_expected.to belong_to(:trigger_request) } + it { is_expected.to belong_to(:erased_by) } + + it { is_expected.to have_many(:deployments) } + describe '#trace' do it 'obfuscates project runners token' do allow(build).to receive(:raw_trace).and_return("Test: #{build.project.runners_token}") diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 71b7628ef10..ea022e03608 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -571,6 +571,9 @@ describe Ci::Pipeline, models: true do context 'with failed pipeline' do before do perform_enqueued_jobs do + create(:ci_build, :failed, pipeline: pipeline) + create(:generic_commit_status, :failed, pipeline: pipeline) + pipeline.drop end end diff --git a/spec/models/concerns/access_requestable_spec.rb b/spec/models/concerns/access_requestable_spec.rb index 96eee0e8bdd..4829ef17a20 100644 --- a/spec/models/concerns/access_requestable_spec.rb +++ b/spec/models/concerns/access_requestable_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe AccessRequestable do describe 'Group' do describe '#request_access' do - let(:group) { create(:group, :public) } + let(:group) { create(:group, :public, :access_requestable) } let(:user) { create(:user) } it { expect(group.request_access(user)).to be_a(GroupMember) } @@ -11,7 +11,7 @@ describe AccessRequestable do end describe '#access_requested?' do - let(:group) { create(:group, :public) } + let(:group) { create(:group, :public, :access_requestable) } let(:user) { create(:user) } before { group.request_access(user) } @@ -22,14 +22,14 @@ describe AccessRequestable do describe 'Project' do describe '#request_access' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, :access_requestable) } let(:user) { create(:user) } it { expect(project.request_access(user)).to be_a(ProjectMember) } end describe '#access_requested?' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, :access_requestable) } let(:user) { create(:user) } before { project.request_access(user) } diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 6e987967ca5..6f84bffe046 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -176,23 +176,25 @@ describe Issue, "Issuable" do end describe '#subscribed?' do + let(:project) { issue.project } + 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 + expect(issue.subscribed?(user, project)).to be_falsey end it 'returns true when a subcription exists and subscribed is true' do - issue.subscriptions.create(user: user, subscribed: true) + issue.subscriptions.create(user: user, project: project, subscribed: true) - expect(issue.subscribed?(user)).to be_truthy + expect(issue.subscribed?(user, project)).to be_truthy end it 'returns false when a subcription exists and subscribed is false' do - issue.subscriptions.create(user: user, subscribed: false) + issue.subscriptions.create(user: user, project: project, subscribed: false) - expect(issue.subscribed?(user)).to be_falsey + expect(issue.subscribed?(user, project)).to be_falsey end end @@ -200,19 +202,19 @@ describe Issue, "Issuable" 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 + expect(issue.subscribed?(user, project)).to be_truthy end it 'returns true when a subcription exists and subscribed is true' do - issue.subscriptions.create(user: user, subscribed: true) + issue.subscriptions.create(user: user, project: project, subscribed: true) - expect(issue.subscribed?(user)).to be_truthy + expect(issue.subscribed?(user, project)).to be_truthy end it 'returns false when a subcription exists and subscribed is false' do - issue.subscriptions.create(user: user, subscribed: false) + issue.subscriptions.create(user: user, project: project, subscribed: false) - expect(issue.subscribed?(user)).to be_falsey + expect(issue.subscribed?(user, project)).to be_falsey end end end diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb index b7e973798a3..0e097559b59 100644 --- a/spec/models/concerns/milestoneish_spec.rb +++ b/spec/models/concerns/milestoneish_spec.rb @@ -115,4 +115,24 @@ describe Milestone, 'Milestoneish' do expect(milestone.percent_complete(admin)).to eq 60 end end + + describe '#elapsed_days' do + it 'shows 0 if no start_date set' do + milestone = build(:milestone) + + expect(milestone.elapsed_days).to eq(0) + end + + it 'shows 0 if start_date is a future' do + milestone = build(:milestone, start_date: Time.now + 2.days) + + expect(milestone.elapsed_days).to eq(0) + end + + it 'shows correct amount of days' do + milestone = build(:milestone, start_date: Time.now - 2.days) + + expect(milestone.elapsed_days).to eq(2) + end + end end diff --git a/spec/models/concerns/subscribable_spec.rb b/spec/models/concerns/subscribable_spec.rb index b7fc5a92497..58f5c164116 100644 --- a/spec/models/concerns/subscribable_spec.rb +++ b/spec/models/concerns/subscribable_spec.rb @@ -1,67 +1,128 @@ require 'spec_helper' describe Subscribable, 'Subscribable' do - let(:resource) { create(:issue) } - let(:user) { create(:user) } + let(:project) { create(:empty_project) } + let(:resource) { create(:issue, project: project) } + let(:user_1) { create(:user) } describe '#subscribed?' do - it 'returns false when no subcription exists' do - expect(resource.subscribed?(user)).to be_falsey - end + context 'without project' do + it 'returns false when no subscription exists' do + expect(resource.subscribed?(user_1)).to be_falsey + end + + it 'returns true when a subcription exists and subscribed is true' do + resource.subscriptions.create(user: user_1, subscribed: true) + + expect(resource.subscribed?(user_1)).to be_truthy + end - it 'returns true when a subcription exists and subscribed is true' do - resource.subscriptions.create(user: user, subscribed: true) + it 'returns false when a subcription exists and subscribed is false' do + resource.subscriptions.create(user: user_1, subscribed: false) - expect(resource.subscribed?(user)).to be_truthy + expect(resource.subscribed?(user_1)).to be_falsey + end end - it 'returns false when a subcription exists and subscribed is false' do - resource.subscriptions.create(user: user, subscribed: false) + context 'with project' do + it 'returns false when no subscription exists' do + expect(resource.subscribed?(user_1, project)).to be_falsey + end + + it 'returns true when a subcription exists and subscribed is true' do + resource.subscriptions.create(user: user_1, project: project, subscribed: true) + + expect(resource.subscribed?(user_1, project)).to be_truthy + end - expect(resource.subscribed?(user)).to be_falsey + it 'returns false when a subcription exists and subscribed is false' do + resource.subscriptions.create(user: user_1, project: project, subscribed: false) + + expect(resource.subscribed?(user_1, project)).to be_falsey + end end end + describe '#subscribers' do it 'returns [] when no subcribers exists' do - expect(resource.subscribers).to be_empty + expect(resource.subscribers(project)).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) + user_2 = create(:user) + resource.subscriptions.create(user: user_1, subscribed: true) + resource.subscriptions.create(user: user_2, project: project, subscribed: true) + resource.subscriptions.create(user: create(:user), project: project, subscribed: false) - expect(resource.subscribers).to eq [user] + expect(resource.subscribers(project)).to contain_exactly(user_1, user_2) end end describe '#toggle_subscription' do - it 'toggles the current subscription state for the given user' do - expect(resource.subscribed?(user)).to be_falsey + context 'without project' do + it 'toggles the current subscription state for the given user' do + expect(resource.subscribed?(user_1)).to be_falsey - resource.toggle_subscription(user) + resource.toggle_subscription(user_1) - expect(resource.subscribed?(user)).to be_truthy + expect(resource.subscribed?(user_1)).to be_truthy + end + end + + context 'with project' do + it 'toggles the current subscription state for the given user' do + expect(resource.subscribed?(user_1, project)).to be_falsey + + resource.toggle_subscription(user_1, project) + + expect(resource.subscribed?(user_1, project)).to be_truthy + end end end describe '#subscribe' do - it 'subscribes the given user' do - expect(resource.subscribed?(user)).to be_falsey + context 'without project' do + it 'subscribes the given user' do + expect(resource.subscribed?(user_1)).to be_falsey + + resource.subscribe(user_1) + + expect(resource.subscribed?(user_1)).to be_truthy + end + end + + context 'with project' do + it 'subscribes the given user' do + expect(resource.subscribed?(user_1, project)).to be_falsey - resource.subscribe(user) + resource.subscribe(user_1, project) - expect(resource.subscribed?(user)).to be_truthy + expect(resource.subscribed?(user_1, project)).to be_truthy + end 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 + context 'without project' do + it 'unsubscribes the given current user' do + resource.subscriptions.create(user: user_1, subscribed: true) + expect(resource.subscribed?(user_1)).to be_truthy + + resource.unsubscribe(user_1) + + expect(resource.subscribed?(user_1)).to be_falsey + end + end + + context 'with project' do + it 'unsubscribes the given current user' do + resource.subscriptions.create(user: user_1, project: project, subscribed: true) + expect(resource.subscribed?(user_1, project)).to be_truthy - resource.unsubscribe(user) + resource.unsubscribe(user_1, project) - expect(resource.subscribed?(user)).to be_falsey + expect(resource.subscribed?(user_1, project)).to be_falsey + end end end end diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index a94e6d0165f..d06665197db 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -9,6 +9,7 @@ describe Environment, models: true do it { is_expected.to delegate_method(:last_deployment).to(:deployments).as(:last) } it { is_expected.to delegate_method(:stop_action).to(:last_deployment) } + it { is_expected.to delegate_method(:manual_actions).to(:last_deployment) } it { is_expected.to validate_presence_of(:name) } it { is_expected.to validate_uniqueness_of(:name).scoped_to(:project_id) } @@ -166,4 +167,36 @@ describe Environment, models: true do end end end + + describe 'recently_updated_on_branch?' do + subject { environment.recently_updated_on_branch?('feature') } + + context 'when last deployment to environment is the most recent one' do + before do + create(:deployment, environment: environment, ref: 'feature') + end + + it { is_expected.to be true } + end + + context 'when last deployment to environment is not the most recent' do + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: environment, ref: 'master') + end + + it { is_expected.to be false } + end + end + + describe '#actions_for' do + let(:deployment) { create(:deployment, environment: environment) } + let(:pipeline) { deployment.deployable.pipeline } + let!(:review_action) { create(:ci_build, :manual, name: 'review-apps', pipeline: pipeline, environment: 'review/$CI_BUILD_REF_NAME' )} + let!(:production_action) { create(:ci_build, :manual, name: 'production', pipeline: pipeline, environment: 'production' )} + + it 'returns a list of actions with matching environment' do + expect(environment.actions_for('review/master')).to contain_exactly(review_action) + end + end end diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb index 29a3af68a9b..b684053cd02 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/event_spec.rb @@ -94,6 +94,7 @@ describe Event, models: true do let(:admin) { create(:admin) } let(:issue) { create(:issue, project: project, author: author, assignee: assignee) } let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + let(:note_on_commit) { create(:note_on_commit, project: project) } let(:note_on_issue) { create(:note_on_issue, noteable: issue, project: project) } let(:note_on_confidential_issue) { create(:note_on_issue, noteable: confidential_issue, project: project) } let(:event) { Event.new(project: project, target: target, author_id: author.id) } @@ -103,6 +104,32 @@ describe Event, models: true do project.team << [guest, :guest] end + context 'commit note event' do + let(:target) { note_on_commit } + + it do + aggregate_failures do + expect(event.visible_to_user?(non_member)).to eq true + expect(event.visible_to_user?(member)).to eq true + expect(event.visible_to_user?(guest)).to eq true + expect(event.visible_to_user?(admin)).to eq true + end + end + + context 'private project' do + let(:project) { create(:empty_project, :private) } + + it do + aggregate_failures do + expect(event.visible_to_user?(non_member)).to eq false + expect(event.visible_to_user?(member)).to eq true + expect(event.visible_to_user?(guest)).to eq false + expect(event.visible_to_user?(admin)).to eq true + end + end + end + end + context 'issue event' do context 'for non confidential issues' do let(:target) { issue } diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 47f89f744cb..1613a586a2c 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Group, models: true do - let!(:group) { create(:group) } + let!(:group) { create(:group, :access_requestable) } describe 'associations' do it { is_expected.to have_many :projects } diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index 300425767ed..89e93dce8c5 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -331,7 +331,7 @@ describe Issue, models: true do end context 'with a user' do - let(:user) { build(:user) } + let(:user) { create(:user) } let(:issue) { build(:issue) } it 'returns true when the issue is readable' do diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb index 7fc6ed1dd54..90731f55470 100644 --- a/spec/models/key_spec.rb +++ b/spec/models/key_spec.rb @@ -19,7 +19,7 @@ describe Key, models: true do describe "#publishable_keys" do it 'replaces SSH key comment with simple identifier of username + hostname' do - expect(build(:key, user: user).publishable_key).to include("#{user.name} (localhost)") + expect(build(:key, user: user).publishable_key).to include("#{user.name} (#{Gitlab.config.gitlab.host})") end end end @@ -71,15 +71,25 @@ describe Key, models: true do context 'callbacks' do it 'adds new key to authorized_file' do - @key = build(:personal_key, id: 7) - expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, @key.shell_id, @key.key) - @key.save + key = build(:personal_key, id: 7) + expect(GitlabShellWorker).to receive(:perform_async).with(:add_key, key.shell_id, key.key) + key.save! end it 'removes key from authorized_file' do - @key = create(:personal_key) - expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, @key.shell_id, @key.key) - @key.destroy + key = create(:personal_key) + expect(GitlabShellWorker).to receive(:perform_async).with(:remove_key, key.shell_id, key.key) + key.destroy + end + end + + describe '#key=' do + let(:valid_key) do + "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt4596k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0= dummy@gitlab.com" + end + + it 'strips white spaces' do + expect(described_class.new(key: " #{valid_key} ").key).to eq(valid_key) end end end diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb index 485121701af..4f7c8a36cb5 100644 --- a/spec/models/member_spec.rb +++ b/spec/models/member_spec.rb @@ -57,7 +57,7 @@ describe Member, models: true do describe 'Scopes & finders' do before do - project = create(:empty_project, :public) + project = create(:empty_project, :public, :access_requestable) group = create(:group) @owner_user = create(:user).tap { |u| group.add_owner(u) } @owner = group.members.find_by(user_id: @owner_user.id) @@ -174,7 +174,7 @@ describe Member, models: true do describe '.add_user' do %w[project group].each do |source_type| context "when source is a #{source_type}" do - let!(:source) { create(source_type, :public) } + let!(:source) { create(source_type, :public, :access_requestable) } let!(:user) { create(:user) } let!(:admin) { create(:admin) } @@ -443,6 +443,16 @@ describe Member, models: true do member.accept_invite!(user) end + + it "refreshes user's authorized projects", truncate: true do + project = member.source + + expect(user.authorized_projects).not_to include(project) + + member.accept_invite!(user) + + expect(user.authorized_projects.reload).to include(project) + end end describe "#decline_invite!" do @@ -468,4 +478,16 @@ describe Member, models: true do expect { member.generate_invite_token }.to change { member.invite_token} end end + + describe "destroying a record", truncate: true do + it "refreshes user's authorized projects" do + project = create(:project, :private) + user = create(:user) + member = project.team << [user, :reporter] + + member.destroy + + expect(user.authorized_projects).not_to include(project) + end + end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index fb032a89d50..58ccd056328 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -856,13 +856,31 @@ describe MergeRequest, models: true do context 'when it is only allowed to merge when build is green' do context 'and a failed pipeline is associated' do before do - pipeline.statuses << create(:commit_status, status: 'failed', project: project) + pipeline.update(status: 'failed') allow(subject).to receive(:pipeline) { pipeline } end it { expect(subject.mergeable_ci_state?).to be_falsey } end + context 'and a successful pipeline is associated' do + before do + pipeline.update(status: 'success') + allow(subject).to receive(:pipeline) { pipeline } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } + end + + context 'and a skipped pipeline is associated' do + before do + pipeline.update(status: 'skipped') + allow(subject).to receive(:pipeline) { pipeline } + end + + it { expect(subject.mergeable_ci_state?).to be_truthy } + end + context 'when no pipeline is associated' do before do allow(subject).to receive(:pipeline) { nil } @@ -919,6 +937,16 @@ describe MergeRequest, models: true do expect(merge_request.mergeable_discussions_state?).to be_falsey end end + + context 'with no discussions' do + before do + merge_request.notes.destroy_all + end + + it 'returns true' do + expect(merge_request.mergeable_discussions_state?).to be_truthy + end + end end context 'when project.only_allow_merge_if_all_discussions_are_resolved == false' do @@ -1180,6 +1208,50 @@ describe MergeRequest, models: true do end end end + + describe "#discussions_to_be_resolved?" do + context "when discussions are not resolvable" do + before do + allow(subject).to receive(:discussions_resolvable?).and_return(false) + end + + it "returns false" do + expect(subject.discussions_to_be_resolved?).to be false + end + end + + context "when discussions are resolvable" do + before do + allow(subject).to receive(:discussions_resolvable?).and_return(true) + + allow(first_discussion).to receive(:resolvable?).and_return(true) + allow(second_discussion).to receive(:resolvable?).and_return(false) + allow(third_discussion).to receive(:resolvable?).and_return(true) + end + + context "when all resolvable discussions are resolved" do + before do + allow(first_discussion).to receive(:resolved?).and_return(true) + allow(third_discussion).to receive(:resolved?).and_return(true) + end + + it "returns false" do + expect(subject.discussions_to_be_resolved?).to be false + end + end + + context "when some resolvable discussions are not resolved" do + before do + allow(first_discussion).to receive(:resolved?).and_return(true) + allow(third_discussion).to receive(:resolved?).and_return(false) + end + + it "returns true" do + expect(subject.discussions_to_be_resolved?).to be true + end + end + end + end end describe '#conflicts_can_be_resolved_in_ui?' do diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 33fe22dd98c..a4bfe851dfb 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -1,11 +1,6 @@ require 'spec_helper' describe Milestone, models: true do - describe "Associations" do - it { is_expected.to belong_to(:project) } - it { is_expected.to have_many(:issues) } - end - describe "Validation" do before do allow(subject).to receive(:set_iid).and_return(false) @@ -13,6 +8,20 @@ describe Milestone, models: true do it { is_expected.to validate_presence_of(:title) } it { is_expected.to validate_presence_of(:project) } + + describe 'start_date' do + it 'adds an error when start_date is greated then due_date' do + milestone = build(:milestone, start_date: Date.tomorrow, due_date: Date.yesterday) + + expect(milestone).not_to be_valid + expect(milestone.errors[:start_date]).to include("Can't be greater than due date") + end + end + end + + describe "Associations" do + it { is_expected.to belong_to(:project) } + it { is_expected.to have_many(:issues) } end let(:milestone) { create(:milestone) } @@ -58,18 +67,6 @@ describe Milestone, models: true do end end - describe "#expires_at" do - it "is nil when due_date is unset" do - milestone.update_attributes(due_date: nil) - expect(milestone.expires_at).to be_nil - end - - it "is not nil when due_date is set" do - milestone.update_attributes(due_date: Date.tomorrow) - expect(milestone.expires_at).to be_present - end - end - describe '#expired?' do context "expired" do before do @@ -88,6 +85,18 @@ describe Milestone, models: true do end end + describe '#upcoming?' do + it 'returns true' do + milestone = build(:milestone, start_date: Time.now + 1.month) + expect(milestone.upcoming?).to be_truthy + end + + it 'returns false' do + milestone = build(:milestone, start_date: Date.today.prev_year) + expect(milestone.upcoming?).to be_falsey + end + end + describe '#percent_complete' do before do allow(milestone).to receive_messages( diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb index c5ff1941378..47397a822c1 100644 --- a/spec/models/project_group_link_spec.rb +++ b/spec/models/project_group_link_spec.rb @@ -14,4 +14,20 @@ describe ProjectGroupLink do it { should validate_presence_of(:group) } it { should validate_presence_of(:group_access) } end + + describe "destroying a record", truncate: true do + it "refreshes group users' authorized projects" do + project = create(:project, :private) + group = create(:group) + reporter = create(:user) + group_users = group.users + + group.add_reporter(reporter) + project.project_group_links.create(group: group) + group_users.each { |user| expect(user.authorized_projects).to include(project) } + + project.project_group_links.destroy_all + group_users.each { |user| expect(user.authorized_projects).not_to include(project) } + end + end end diff --git a/spec/models/project_services/chat_service_spec.rb b/spec/models/project_services/chat_service_spec.rb new file mode 100644 index 00000000000..c6a45a3e1be --- /dev/null +++ b/spec/models/project_services/chat_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe ChatService, models: true do + describe "Associations" do + it { is_expected.to have_many :chat_names } + end + + describe '#valid_token?' do + subject { described_class.new } + + it 'is false as it has no token' do + expect(subject.valid_token?('wer')).to be_falsey + end + end +end diff --git a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb index 652804fb444..9b80f0e7296 100644 --- a/spec/models/project_services/gitlab_issue_tracker_service_spec.rb +++ b/spec/models/project_services/gitlab_issue_tracker_service_spec.rb @@ -35,9 +35,9 @@ describe GitlabIssueTrackerService, models: true do end it 'gives the correct path' do - expect(@service.project_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues") - expect(@service.new_issue_url).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/new") - expect(@service.issue_url(432)).to eq("http://localhost/gitlab/root/#{project.path_with_namespace}/issues/432") + expect(@service.project_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues") + expect(@service.new_issue_url).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/new") + expect(@service.issue_url(432)).to eq("http://#{Gitlab.config.gitlab.host}/gitlab/root/#{project.path_with_namespace}/issues/432") end end diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb index 05ee4a08391..f5da967cd14 100644 --- a/spec/models/project_services/jira_service_spec.rb +++ b/spec/models/project_services/jira_service_spec.rb @@ -69,6 +69,7 @@ describe JiraService, models: true do end describe "Execute" do + let(:custom_base_url) { 'http://custom_url' } let(:user) { create(:user) } let(:project) { create(:project) } let(:merge_request) { create(:merge_request) } @@ -82,20 +83,34 @@ describe JiraService, models: true do url: 'http://jira.example.com', username: 'gitlab_jira_username', password: 'gitlab_jira_password', - project_key: 'GitLabProject' + project_key: 'GitLabProject', + jira_issue_transition_id: "custom-id" ) + # These stubs are needed to test JiraService#close_issue. + # We close the issue then do another request to API to check if it got closed. + # Here is stubbed the API return with a closed and an opened issues. + open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" }) + closed_issue = open_issue.dup + allow(open_issue).to receive(:resolution).and_return(false) + allow(closed_issue).to receive(:resolution).and_return(true) + allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue) + + allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123") + @jira_service.save project_issues_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123' @project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/project/GitLabProject' @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions' @comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment' + @remote_link_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/remotelink' WebMock.stub_request(:get, @project_url) WebMock.stub_request(:get, project_issues_url) WebMock.stub_request(:post, @transitions_url) WebMock.stub_request(:post, @comment_url) + WebMock.stub_request(:post, @remote_link_url) end it "calls JIRA API" do @@ -106,11 +121,44 @@ describe JiraService, models: true do ).once end + # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links + # for more information + it "creates Remote Link reference in JIRA for comment" do + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + # Creates comment + expect(WebMock).to have_requested(:post, @comment_url) + + # Creates Remote Link in JIRA issue fields + expect(WebMock).to have_requested(:post, @remote_link_url).with( + body: hash_including( + GlobalID: "GitLab", + object: { + url: "#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/commit/#{merge_request.diff_head_sha}", + title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.", + icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, + status: { resolved: true, icon: { url16x16: "http://www.openwebgraphics.com/resources/data/1768/16x16_apply.png", title: "Closed" } } + } + ) + ).once + end + + it "does not send comment or remote links to issues already closed" do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true) + + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) + + expect(WebMock).not_to have_requested(:post, @comment_url) + expect(WebMock).not_to have_requested(:post, @remote_link_url) + end + it "references the GitLab commit/merge request" do + stub_config_setting(base_url: custom_base_url) + @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @comment_url).with( - body: /#{Gitlab.config.gitlab.url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ + body: /#{custom_base_url}\/#{project.path_with_namespace}\/commit\/#{merge_request.diff_head_sha}/ ).once end @@ -130,11 +178,10 @@ describe JiraService, models: true do end it "calls the api with jira_issue_transition_id" do - @jira_service.jira_issue_transition_id = 'this-is-a-custom-id' @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project)) expect(WebMock).to have_requested(:post, @transitions_url).with( - body: /this-is-a-custom-id/ + body: /custom-id/ ).once end diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb new file mode 100644 index 00000000000..4a1037e950b --- /dev/null +++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb @@ -0,0 +1,99 @@ +require 'spec_helper' + +describe MattermostSlashCommandsService, models: true do + describe "Associations" do + it { is_expected.to respond_to :token } + end + + describe '#valid_token?' do + subject { described_class.new } + + context 'when the token is empty' do + it 'is false' do + expect(subject.valid_token?('wer')).to be_falsey + end + end + + context 'when there is a token' do + before do + subject.token = '123' + end + + it 'accepts equal tokens' do + expect(subject.valid_token?('123')).to be_truthy + end + end + end + + describe '#trigger' do + subject { described_class.new } + + context 'no token is passed' do + let(:params) { Hash.new } + + it 'returns nil' do + expect(subject.trigger(params)).to be_nil + end + end + + context 'with a token passed' do + let(:project) { create(:empty_project) } + let(:params) { { token: 'token' } } + + before do + allow(subject).to receive(:token).and_return('token') + end + + context 'no user can be found' do + context 'when no url can be generated' do + it 'responds with the authorize url' do + response = subject.trigger(params) + + expect(response[:response_type]).to eq :ephemeral + expect(response[:text]).to start_with ":sweat_smile: Couldn't identify you" + end + end + + context 'when an auth url can be generated' do + let(:params) do + { + team_domain: 'http://domain.tld', + team_id: 'T3423423', + user_id: 'U234234', + user_name: 'mepmep', + token: 'token' + } + end + + let(:service) do + project.create_mattermost_slash_commands_service( + properties: { token: 'token' } + ) + end + + it 'generates the url' do + response = service.trigger(params) + + expect(response[:text]).to start_with(':wave: Hi there!') + end + end + end + + context 'when the user is authenticated' do + let!(:chat_name) { create(:chat_name, service: service) } + let(:service) do + project.create_mattermost_slash_commands_service( + properties: { token: 'token' } + ) + end + let(:params) { { token: 'token', team_id: chat_name.team_id, user_id: chat_name.chat_id } } + + it 'triggers the command' do + expect_any_instance_of(Gitlab::ChatCommands::Command).to receive(:execute) + + service.trigger(params) + end + end + end + end +end diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb index 38cfe4ad3e3..97f818125d3 100644 --- a/spec/models/project_services/slack_service/note_message_spec.rb +++ b/spec/models/project_services/slack_service/note_message_spec.rb @@ -37,8 +37,8 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on commits' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user commented on " \ - "<url|commit 5f163b2b> in <somewhere.com|project_name>: " \ + expect(message.pretext).to eq("test.user <url|commented on " \ + "commit 5f163b2b> in <somewhere.com|project_name>: " \ "*Added a commit message*") expected_attachments = [ { @@ -63,8 +63,8 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on a merge request' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user commented on " \ - "<url|merge request !30> in <somewhere.com|project_name>: " \ + expect(message.pretext).to eq("test.user <url|commented on " \ + "merge request !30> in <somewhere.com|project_name>: " \ "*merge request title*") expected_attachments = [ { @@ -90,8 +90,8 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on an issue' do message = SlackService::NoteMessage.new(@args) expect(message.pretext).to eq( - "test.user commented on " \ - "<url|issue #20> in <somewhere.com|project_name>: " \ + "test.user <url|commented on " \ + "issue #20> in <somewhere.com|project_name>: " \ "*issue title*") expected_attachments = [ { @@ -115,8 +115,8 @@ describe SlackService::NoteMessage, models: true do it 'returns a message regarding notes on a project snippet' do message = SlackService::NoteMessage.new(@args) - expect(message.pretext).to eq("test.user commented on " \ - "<url|snippet #5> in <somewhere.com|project_name>: " \ + expect(message.pretext).to eq("test.user <url|commented on " \ + "snippet #5> in <somewhere.com|project_name>: " \ "*snippet title*") expected_attachments = [ { diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb index babb3909f56..363138a9454 100644 --- a/spec/models/project_services/slack_service/pipeline_message_spec.rb +++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb @@ -15,7 +15,7 @@ describe SlackService::PipelineMessage do }, project: { path_with_namespace: 'project_name', web_url: 'example.gitlab.com' }, - commit: { author_name: 'hacker' } + user: { name: 'hacker' } } end @@ -48,7 +48,7 @@ describe SlackService::PipelineMessage do def build_message(status_text = status) "<example.gitlab.com|project_name>:" \ - " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \ + " Pipeline <example.gitlab.com/pipelines/123|#123>" \ " of <example.gitlab.com/commits/develop|develop> branch" \ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}" end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0810d06b50f..da38254d1bc 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -20,6 +20,7 @@ describe Project, models: true do it { is_expected.to have_many(:deploy_keys) } it { is_expected.to have_many(:hooks).dependent(:destroy) } it { is_expected.to have_many(:protected_branches).dependent(:destroy) } + it { is_expected.to have_many(:chat_services) } it { is_expected.to have_one(:forked_project_link).dependent(:destroy) } it { is_expected.to have_one(:slack_service).dependent(:destroy) } it { is_expected.to have_one(:pushover_service).dependent(:destroy) } @@ -35,6 +36,7 @@ describe Project, models: true do it { is_expected.to have_one(:hipchat_service).dependent(:destroy) } it { is_expected.to have_one(:flowdock_service).dependent(:destroy) } it { is_expected.to have_one(:assembla_service).dependent(:destroy) } + it { is_expected.to have_one(:mattermost_slash_commands_service).dependent(:destroy) } it { is_expected.to have_one(:gemnasium_service).dependent(:destroy) } it { is_expected.to have_one(:buildkite_service).dependent(:destroy) } it { is_expected.to have_one(:bamboo_service).dependent(:destroy) } @@ -76,7 +78,7 @@ describe Project, models: true do end describe '#members & #requesters' do - let(:project) { create(:project, :public) } + let(:project) { create(:empty_project, :public, :access_requestable) } let(:requester) { create(:user) } let(:developer) { create(:user) } before do @@ -241,6 +243,13 @@ describe Project, models: true do it { is_expected.to respond_to(:path_with_namespace) } end + describe 'delegation' do + it { is_expected.to delegate_method(:add_guest).to(:team) } + it { is_expected.to delegate_method(:add_reporter).to(:team) } + it { is_expected.to delegate_method(:add_developer).to(:team) } + it { is_expected.to delegate_method(:add_master).to(:team) } + end + describe '#name_with_namespace' do let(:project) { build_stubbed(:empty_project) } @@ -496,9 +505,6 @@ describe Project, models: true do end it 'returns nil and does not query services when there is no external issue tracker' do - project.build_missing_services - project.reload - expect(project).not_to receive(:services) expect(project.external_issue_tracker).to eq(nil) @@ -506,9 +512,6 @@ describe Project, models: true do it 'retrieves external_issue_tracker querying services and cache it when there is external issue tracker' do ext_project.reload # Factory returns a project with changed attributes - ext_project.build_missing_services - ext_project.reload - expect(ext_project).to receive(:services).once.and_call_original 2.times { expect(ext_project.external_issue_tracker).to be_a_kind_of(RedmineService) } @@ -706,7 +709,7 @@ describe Project, models: true do "/uploads/project/avatar/#{project.id}/uploads/avatar.png" end - it { should eq "http://localhost#{avatar_path}" } + it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } end context 'When avatar file in git' do @@ -718,7 +721,7 @@ describe Project, models: true do "/#{project.namespace.name}/#{project.path}/avatar" end - it { should eq "http://localhost#{avatar_path}" } + it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" } end context 'when git repo is empty' do @@ -1504,55 +1507,6 @@ describe Project, models: true do end end - describe 'authorized_for_user' do - let(:group) { create(:group) } - let(:developer) { create(:user) } - let(:master) { create(:user) } - let(:personal_project) { create(:project, namespace: developer.namespace) } - let(:group_project) { create(:project, namespace: group) } - let(:members_project) { create(:project) } - let(:shared_project) { create(:project) } - - before do - group.add_master(master) - group.add_developer(developer) - - members_project.team << [developer, :developer] - members_project.team << [master, :master] - - create(:project_group_link, project: shared_project, group: group) - end - - it 'returns false for no user' do - expect(personal_project.authorized_for_user?(nil)).to be(false) - end - - it 'returns true for personal projects of the user' do - expect(personal_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects of groups the user is a member of' do - expect(group_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects for which the user is a member of' do - expect(members_project.authorized_for_user?(developer)).to be(true) - end - - it 'returns true for projects shared on a group the user is a member of' do - expect(shared_project.authorized_for_user?(developer)).to be(true) - end - - it 'checks for the correct minimum level access' do - expect(group_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(group_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) - expect(members_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(members_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) - expect(shared_project.authorized_for_user?(developer, Gitlab::Access::MASTER)).to be(false) - expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true) - end - end - describe 'change_head' do let(:project) { create(:project) } @@ -1574,7 +1528,7 @@ describe Project, models: true do end it 'expires the avatar cache' do - expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch) + expect(project.repository).to receive(:expire_avatar_cache) project.change_head(project.default_branch) end @@ -1646,15 +1600,18 @@ describe Project, models: true do end it 'returns environment when with_tags is set' do - expect(project.environments_for('master', project.commit, with_tags: true)).to contain_exactly(environment) + expect(project.environments_for('master', commit: project.commit, with_tags: true)) + .to contain_exactly(environment) end it 'does not return environment when no with_tags is set' do - expect(project.environments_for('master', project.commit)).to be_empty + expect(project.environments_for('master', commit: project.commit)) + .to be_empty end it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', project.commit('feature'))).to be_empty + expect(project.environments_for('master', commit: project.commit('feature'))) + .to be_empty end end @@ -1664,15 +1621,65 @@ describe Project, models: true do end it 'returns environment when ref is set' do - expect(project.environments_for('master', project.commit)).to contain_exactly(environment) + expect(project.environments_for('master', commit: project.commit)) + .to contain_exactly(environment) end it 'does not environment when ref is different' do - expect(project.environments_for('feature', project.commit)).to be_empty + expect(project.environments_for('feature', commit: project.commit)) + .to be_empty end it 'does not return environment when commit is not part of deployment' do - expect(project.environments_for('master', project.commit('feature'))).to be_empty + expect(project.environments_for('master', commit: project.commit('feature'))) + .to be_empty + end + + it 'returns environment when commit constraint is not set' do + expect(project.environments_for('master')) + .to contain_exactly(environment) + end + end + end + + describe '#environments_recently_updated_on_branch' do + let(:project) { create(:project) } + let(:environment) { create(:environment, project: project) } + + context 'when last deployment to environment is the most recent one' do + before do + create(:deployment, environment: environment, ref: 'feature') + end + + it 'finds recently updated environment' do + expect(project.environments_recently_updated_on_branch('feature')) + .to contain_exactly(environment) + end + end + + context 'when last deployment to environment is not the most recent' do + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: environment, ref: 'master') + end + + it 'does not find environment' do + expect(project.environments_recently_updated_on_branch('feature')) + .to be_empty + end + end + + context 'when there are two environments that deploy to the same branch' do + let(:second_environment) { create(:environment, project: project) } + + before do + create(:deployment, environment: environment, ref: 'feature') + create(:deployment, environment: second_environment, ref: 'feature') + end + + it 'finds both environments' do + expect(project.environments_recently_updated_on_branch('feature')) + .to contain_exactly(environment, second_environment) end end end diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb index e0f2dadf189..0475cecaa2d 100644 --- a/spec/models/project_team_spec.rb +++ b/spec/models/project_team_spec.rb @@ -10,9 +10,9 @@ describe ProjectTeam, models: true do let(:project) { create(:empty_project) } before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) end describe 'members collection' do @@ -37,7 +37,7 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } + let!(:project) { create(:empty_project, group: group) } before do group.add_master(master) @@ -47,8 +47,8 @@ describe ProjectTeam, models: true do # If user is a group and a project member - GitLab uses highest permission # So we add group guest as master and add group master as guest # to this project to test highest access - project.team << [guest, :master] - project.team << [master, :guest] + project.add_master(guest) + project.add_guest(master) end describe 'members collection' do @@ -79,14 +79,14 @@ describe ProjectTeam, models: true do it 'returns project members' do user = create(:user) - project.team << [user, :guest] + project.add_guest(user) expect(project.team.members).to contain_exactly(user) end it 'returns project members of a specified level' do user = create(:user) - project.team << [user, :reporter] + project.add_reporter(user) expect(project.team.guests).to be_empty expect(project.team.reporters).to contain_exactly(user) @@ -118,7 +118,7 @@ describe ProjectTeam, models: true do context 'group project' do let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } + let!(:project) { create(:empty_project, group: group) } it 'returns project members' do group_member = create(:group_member, group: group) @@ -137,13 +137,13 @@ describe ProjectTeam, models: true do describe '#find_member' do context 'personal project' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, :access_requestable) } let(:requester) { create(:user) } before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) project.request_access(requester) end @@ -155,7 +155,7 @@ describe ProjectTeam, models: true do end context 'group project' do - let(:group) { create(:group) } + let(:group) { create(:group, :access_requestable) } let(:project) { create(:empty_project, group: group) } let(:requester) { create(:user) } @@ -178,9 +178,9 @@ describe ProjectTeam, models: true do it 'returns Master role' do user = create(:user) group = create(:group) - group.add_master(user) + project = create(:empty_project, namespace: group) - project = build_stubbed(:empty_project, namespace: group) + group.add_master(user) expect(project.team.human_max_access(user.id)).to eq 'Master' end @@ -188,9 +188,9 @@ describe ProjectTeam, models: true do it 'returns Owner role' do user = create(:user) group = create(:group) - group.add_owner(user) + project = create(:empty_project, namespace: group) - project = build_stubbed(:empty_project, namespace: group) + group.add_owner(user) expect(project.team.human_max_access(user.id)).to eq 'Owner' end @@ -200,13 +200,13 @@ describe ProjectTeam, models: true do let(:requester) { create(:user) } context 'personal project' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:empty_project, :public, :access_requestable) } context 'when project is not shared with group' do before do - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(guest) project.request_access(requester) end @@ -243,8 +243,8 @@ describe ProjectTeam, models: true do end context 'group project' do - let(:group) { create(:group) } - let(:project) { create(:empty_project, group: group) } + let(:group) { create(:group, :access_requestable) } + let!(:project) { create(:empty_project, group: group) } before do group.add_master(master) @@ -261,6 +261,57 @@ describe ProjectTeam, models: true do end end + describe '#member?' do + let(:group) { create(:group) } + let(:developer) { create(:user) } + let(:master) { create(:user) } + let(:personal_project) { create(:project, namespace: developer.namespace) } + let(:group_project) { create(:project, namespace: group) } + let(:members_project) { create(:project) } + let(:shared_project) { create(:project) } + + before do + group.add_master(master) + group.add_developer(developer) + + members_project.team << [developer, :developer] + members_project.team << [master, :master] + + create(:project_group_link, project: shared_project, group: group) + end + + it 'returns false for no user' do + expect(personal_project.team.member?(nil)).to be(false) + end + + it 'returns true for personal projects of the user' do + expect(personal_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects of groups the user is a member of' do + expect(group_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects for which the user is a member of' do + expect(members_project.team.member?(developer)).to be(true) + end + + it 'returns true for projects shared on a group the user is a member of' do + expect(shared_project.team.member?(developer)).to be(true) + end + + it 'checks for the correct minimum level access' do + expect(group_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(group_project.team.member?(master, Gitlab::Access::MASTER)).to be(true) + expect(members_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(members_project.team.member?(master, Gitlab::Access::MASTER)).to be(true) + expect(shared_project.team.member?(developer, Gitlab::Access::MASTER)).to be(false) + expect(shared_project.team.member?(master, Gitlab::Access::MASTER)).to be(false) + expect(shared_project.team.member?(developer, Gitlab::Access::DEVELOPER)).to be(true) + expect(shared_project.team.member?(master, Gitlab::Access::DEVELOPER)).to be(true) + end + end + shared_examples_for "#max_member_access_for_users" do |enable_request_store| describe "#max_member_access_for_users" do before do @@ -281,10 +332,10 @@ describe ProjectTeam, models: true do guest = create(:user) project = create(:project) - project.team << [master, :master] - project.team << [reporter, :reporter] - project.team << [promoted_guest, :guest] - project.team << [guest, :guest] + project.add_master(master) + project.add_reporter(reporter) + project.add_guest(promoted_guest) + project.add_guest(guest) group = create(:group) group_developer = create(:user) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index fe26b4ac18c..04afb8ebc98 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -393,33 +393,33 @@ describe Repository, models: true do end end - describe "search_files" do - let(:results) { repository.search_files('feature', 'master') } + describe "search_files_by_content" do + let(:results) { repository.search_files_by_content('feature', 'master') } subject { results } it { is_expected.to be_an Array } it 'regex-escapes the query string' do - results = repository.search_files("test\\", 'master') + results = repository.search_files_by_content("test\\", 'master') expect(results.first).not_to start_with('fatal:') end it 'properly handles an unmatched parenthesis' do - results = repository.search_files("test(", 'master') + results = repository.search_files_by_content("test(", 'master') expect(results.first).not_to start_with('fatal:') end it 'properly handles when query is not present' do - results = repository.search_files('', 'master') + results = repository.search_files_by_content('', 'master') expect(results).to match_array([]) end it 'properly handles query when repo is empty' do repository = create(:empty_project).repository - results = repository.search_files('test', 'master') + results = repository.search_files_by_content('test', 'master') expect(results).to match_array([]) end @@ -432,6 +432,28 @@ describe Repository, models: true do end end + describe "search_files_by_name" do + let(:results) { repository.search_files_by_name('files', 'master') } + + it 'returns result' do + expect(results.first).to eq('files/html/500.html') + end + + it 'properly handles when query is not present' do + results = repository.search_files_by_name('', 'master') + + expect(results).to match_array([]) + end + + it 'properly handles query when repo is empty' do + repository = create(:empty_project).repository + + results = repository.search_files_by_name('test', 'master') + + expect(results).to match_array([]) + end + end + describe '#create_ref' do it 'redirects the call to fetch_ref' do ref, ref_path = '1', '2' @@ -442,11 +464,7 @@ describe Repository, models: true do end end - describe "#changelog" do - before do - repository.send(:cache).expire(:changelog) - end - + describe "#changelog", caching: true do it 'accepts changelog' do expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('changelog')]) @@ -478,17 +496,16 @@ describe Repository, models: true do end end - describe "#license_blob" do + describe "#license_blob", caching: true do before do - repository.send(:cache).expire(:license_blob) repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end it 'handles when HEAD points to non-existent ref' do repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) - rugged = double('rugged') - expect(rugged).to receive(:head_unborn?).and_return(true) - expect(repository).to receive(:rugged).and_return(rugged) + + allow(repository).to receive(:file_on_head). + and_raise(Rugged::ReferenceError) expect(repository.license_blob).to be_nil end @@ -515,22 +532,18 @@ describe Repository, models: true do end end - describe '#license_key' do + describe '#license_key', caching: true do before do - repository.send(:cache).expire(:license_key) repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master') end - it 'handles when HEAD points to non-existent ref' do - repository.commit_file(user, 'LICENSE', 'Copyright!', 'Add LICENSE', 'master', false) - rugged = double('rugged') - expect(rugged).to receive(:head_unborn?).and_return(true) - expect(repository).to receive(:rugged).and_return(rugged) - + it 'returns nil when no license is detected' do expect(repository.license_key).to be_nil end - it 'returns nil when no license is detected' do + it 'returns nil when the repository does not exist' do + expect(repository).to receive(:exists?).and_return(false) + expect(repository.license_key).to be_nil end @@ -547,7 +560,7 @@ describe Repository, models: true do end end - describe "#gitlab_ci_yml" do + describe "#gitlab_ci_yml", caching: true do it 'returns valid file' do files = [TestBlob.new('file'), TestBlob.new('.gitlab-ci.yml'), TestBlob.new('copying')] expect(repository.tree).to receive(:blobs).and_return(files) @@ -561,7 +574,7 @@ describe Repository, models: true do end it 'returns nil for empty repository' do - expect(repository).to receive(:empty?).and_return(true) + allow(repository).to receive(:file_on_head).and_raise(Rugged::ReferenceError) expect(repository.gitlab_ci_yml).to be_nil end end @@ -756,7 +769,6 @@ describe Repository, models: true do expect(repository).not_to receive(:expire_emptiness_caches) expect(repository).to receive(:expire_branches_cache) expect(repository).to receive(:expire_has_visible_content_cache) - expect(repository).to receive(:expire_branch_count_cache) repository.update_branch_with_hooks(user, 'new-feature') { new_rev } end @@ -775,7 +787,6 @@ describe Repository, models: true do expect(empty_repository).to receive(:expire_emptiness_caches) expect(empty_repository).to receive(:expire_branches_cache) expect(empty_repository).to receive(:expire_has_visible_content_cache) - expect(empty_repository).to receive(:expire_branch_count_cache) empty_repository.commit_file(user, 'CHANGELOG', 'Changelog!', 'Updates file content', 'master', false) @@ -789,8 +800,7 @@ describe Repository, models: true do end it 'returns false when a repository does not exist' do - expect(repository.raw_repository).to receive(:rugged). - and_raise(Gitlab::Git::Repository::NoRepository) + allow(repository).to receive(:refs_directory_exists?).and_return(false) expect(repository.exists?).to eq(false) end @@ -894,34 +904,6 @@ describe Repository, models: true do 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).not_to 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 @@ -981,12 +963,23 @@ describe Repository, models: true do describe '#expire_emptiness_caches' do let(:cache) { repository.send(:cache) } - it 'expires the caches' do + it 'expires the caches for an empty repository' do + allow(repository).to receive(:empty?).and_return(true) + expect(cache).to receive(:expire).with(:empty?) expect(repository).to receive(:expire_has_visible_content_cache) repository.expire_emptiness_caches end + + it 'does not expire the cache for a non-empty repository' do + allow(repository).to receive(:empty?).and_return(false) + + expect(cache).not_to receive(:expire).with(:empty?) + expect(repository).not_to receive(:expire_has_visible_content_cache) + + repository.expire_emptiness_caches + end end describe :skip_merged_commit do @@ -1098,24 +1091,12 @@ describe Repository, models: true do repository.before_delete end - it 'flushes the tag count cache' do - expect(repository).to receive(:expire_tag_count_cache) - - repository.before_delete - end - it 'flushes the branches cache' do expect(repository).to receive(:expire_branches_cache) repository.before_delete end - it 'flushes the branch count cache' do - expect(repository).to receive(:expire_branch_count_cache) - - repository.before_delete - end - it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) @@ -1140,36 +1121,18 @@ describe Repository, models: true 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 tags cache' do expect(repository).to receive(:expire_tags_cache) repository.before_delete end - it 'flushes the tag count cache' do - expect(repository).to receive(:expire_tag_count_cache) - - repository.before_delete - end - it 'flushes the branches cache' do expect(repository).to receive(:expire_branches_cache) repository.before_delete end - it 'flushes the branch count cache' do - expect(repository).to receive(:expire_branch_count_cache) - - repository.before_delete - end - it 'flushes the root ref cache' do expect(repository).to receive(:expire_root_ref_cache) @@ -1200,8 +1163,9 @@ describe Repository, models: true 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) + expect(repository).to receive(:expire_statistics_caches) + expect(repository).to receive(:expire_emptiness_caches) + expect(repository).to receive(:expire_tags_cache) repository.before_push_tag end @@ -1218,17 +1182,23 @@ describe Repository, models: true do describe '#after_import' do it 'flushes and builds the cache' do expect(repository).to receive(:expire_content_cache) - expect(repository).to receive(:build_cache) + expect(repository).to receive(:expire_tags_cache) + expect(repository).to receive(:expire_branches_cache) repository.after_import end end describe '#after_push_commit' do - it 'flushes the cache' do - expect(repository).to receive(:expire_cache).with('master', '123') + it 'expires statistics caches' do + expect(repository).to receive(:expire_statistics_caches). + and_call_original + + expect(repository).to receive(:expire_branch_cache). + with('master'). + and_call_original - repository.after_push_commit('master', '123') + repository.after_push_commit('master') end end @@ -1280,7 +1250,8 @@ describe Repository, models: true do describe '#before_remove_tag' do it 'flushes the tag cache' do - expect(repository).to receive(:expire_tag_count_cache) + expect(repository).to receive(:expire_tags_cache).and_call_original + expect(repository).to receive(:expire_statistics_caches).and_call_original repository.before_remove_tag end @@ -1298,23 +1269,23 @@ describe Repository, models: true do end end - describe '#expire_branch_count_cache' do - let(:cache) { repository.send(:cache) } - + describe '#expire_branches_cache' do it 'expires the cache' do - expect(cache).to receive(:expire).with(:branch_count) + expect(repository).to receive(:expire_method_caches). + with(%i(branch_names branch_count)). + and_call_original - repository.expire_branch_count_cache + repository.expire_branches_cache end end - describe '#expire_tag_count_cache' do - let(:cache) { repository.send(:cache) } - + describe '#expire_tags_cache' do it 'expires the cache' do - expect(cache).to receive(:expire).with(:tag_count) + expect(repository).to receive(:expire_method_caches). + with(%i(tag_names tag_count)). + and_call_original - repository.expire_tag_count_cache + repository.expire_tags_cache end end @@ -1332,6 +1303,28 @@ describe Repository, models: true do repository.add_tag(user, '8.5', 'master', 'foo') end + it 'does not create a tag when a pre-hook fails' do + allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) + + expect do + repository.add_tag(user, '8.5', 'master', 'foo') + end.to raise_error(GitHooksService::PreReceiveError) + + repository.expire_tags_cache + expect(repository.find_tag('8.5')).to be_nil + end + + it 'passes tag SHA to hooks' do + spy = GitHooksService.new + allow(GitHooksService).to receive(:new).and_return(spy) + allow(spy).to receive(:execute).and_call_original + + tag = repository.add_tag(user, '8.5', 'master', 'foo') + + expect(spy).to have_received(:execute). + with(anything, anything, anything, tag.target, anything) + end + it 'returns a Gitlab::Git::Tag object' do tag = repository.add_tag(user, '8.5', 'master', 'foo') @@ -1368,180 +1361,316 @@ describe Repository, models: true do describe '#avatar' do it 'returns nil if repo does not exist' do - expect(repository).to receive(:exists?).and_return(false) + expect(repository).to receive(:file_on_head). + and_raise(Rugged::ReferenceError) expect(repository.avatar).to eq(nil) end 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).to receive(:file_on_head). + with(:avatar). + and_return(double(:tree, path: 'logo.png')) 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 receive(:file_on_head). + with(:avatar). + once. + and_return(double(:tree, path: 'logo.png')) - expect(repository).not_to receive(:blob_at_branch) - expect(repository.avatar).to eq('logo.png') + 2.times { expect(repository.avatar).to eq('logo.png') } end end - describe '#expire_avatar_cache' do + describe '#expire_exists_cache' do let(:cache) { repository.send(:cache) } - before do - allow(repository).to receive(:cache).and_return(cache) + it 'expires the cache' do + expect(cache).to receive(:expire).with(:exists?) + + repository.expire_exists_cache + end + end + + describe "#keep_around" do + it "does not fail if we attempt to reference bad commit" do + expect(repository.kept_around?('abc1234')).to be_falsey end - context 'without a branch or revision' do - it 'flushes the cache' do - expect(cache).to receive(:expire).with(:avatar) + it "stores a reference to the specified commit sha so it isn't garbage collected" do + repository.keep_around(sample_commit.id) - repository.expire_avatar_cache - end + expect(repository.kept_around?(sample_commit.id)).to be_truthy 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) + it "attempting to call keep_around on truncated ref does not fail" do + repository.keep_around(sample_commit.id) + ref = repository.send(:keep_around_ref_name, sample_commit.id) + path = File.join(repository.path, ref) + # Corrupt the reference + File.truncate(path, 0) + + expect(repository.kept_around?(sample_commit.id)).to be_falsey + + repository.keep_around(sample_commit.id) - repository.expire_avatar_cache('cats') + expect(repository.kept_around?(sample_commit.id)).to be_falsey + + File.delete(path) + end + end + + describe '#update_ref!' do + it 'can create a ref' do + repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + + expect(repository.find_branch('foobar')).not_to be_nil + end + + it 'raises CommitError when the ref update fails' do + expect do + repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + end.to raise_error(Repository::CommitError) + end + end + + describe '#contribution_guide', caching: true do + it 'returns and caches the output' do + expect(repository).to receive(:file_on_head). + with(:contributing). + and_return(Gitlab::Git::Tree.new(path: 'CONTRIBUTING.md')). + once + + 2.times do + expect(repository.contribution_guide). + to be_an_instance_of(Gitlab::Git::Tree) end + end + end - it 'flushes the cache if the branch equals the default branch' do - expect(cache).to receive(:expire).with(:avatar) + describe '#gitignore', caching: true do + it 'returns and caches the output' do + expect(repository).to receive(:file_on_head). + with(:gitignore). + and_return(Gitlab::Git::Tree.new(path: '.gitignore')). + once - repository.expire_avatar_cache(repository.root_ref) + 2.times do + expect(repository.gitignore).to be_an_instance_of(Gitlab::Git::Tree) end end + end - context 'with a branch and revision' do - let(:commit) { double(:commit) } + describe '#koding_yml', caching: true do + it 'returns and caches the output' do + expect(repository).to receive(:file_on_head). + with(:koding). + and_return(Gitlab::Git::Tree.new(path: '.koding.yml')). + once - before do - allow(repository).to receive(:commit).and_return(commit) + 2.times do + expect(repository.koding_yml).to be_an_instance_of(Gitlab::Git::Tree) end + end + end - it 'does not flush the cache if the commit does not change any logos' do - diff = double(:diff, new_path: 'test.txt') + describe '#readme', caching: true do + context 'with a non-existing repository' do + it 'returns nil' do + expect(repository).to receive(:tree).with(:head).and_return(nil) - expect(commit).to receive(:raw_diffs).and_return([diff]) - expect(cache).not_to receive(:expire) + expect(repository.readme).to be_nil + end + end - repository.expire_avatar_cache(repository.root_ref, '123') + context 'with an existing repository' do + it 'returns the README' do + expect(repository.readme).to be_an_instance_of(Gitlab::Git::Blob) end + end + end - it 'flushes the cache if the commit changes any of the logos' do - diff = double(:diff, new_path: Repository::AVATAR_FILES[0]) + describe '#expire_statistics_caches' do + it 'expires the caches' do + expect(repository).to receive(:expire_method_caches). + with(%i(size commit_count)) - expect(commit).to receive(:raw_diffs).and_return([diff]) - expect(cache).to receive(:expire).with(:avatar) + repository.expire_statistics_caches + end + end - repository.expire_avatar_cache(repository.root_ref, '123') - end + describe '#expire_method_caches' do + it 'expires the caches of the given methods' do + expect_any_instance_of(RepositoryCache).to receive(:expire).with(:readme) + expect_any_instance_of(RepositoryCache).to receive(:expire).with(:gitignore) + + repository.expire_method_caches(%i(readme gitignore)) end end - describe '#expire_exists_cache' do - let(:cache) { repository.send(:cache) } + describe '#expire_all_method_caches' do + it 'expires the caches of all methods' do + expect(repository).to receive(:expire_method_caches). + with(Repository::CACHED_METHODS) + + repository.expire_all_method_caches + end + end + describe '#expire_avatar_cache' do it 'expires the cache' do - expect(cache).to receive(:expire).with(:exists?) + expect(repository).to receive(:expire_method_caches).with(%i(avatar)) - repository.expire_exists_cache + repository.expire_avatar_cache end end - describe '#build_cache' do - let(:cache) { repository.send(:cache) } + describe '#file_on_head' do + context 'with a non-existing repository' do + it 'returns nil' do + expect(repository).to receive(:tree).with(:head).and_return(nil) - it 'builds the caches if they do not already exist' do - cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags + expect(repository.file_on_head(:readme)).to be_nil + end + end - expect(cache).to receive(:exist?). - exactly(cache_keys.length). - times. - and_return(false) + context 'with a repository that has no blobs' do + it 'returns nil' do + expect_any_instance_of(Tree).to receive(:blobs).and_return([]) - cache_keys.each do |key| - expect(repository).to receive(key) + expect(repository.file_on_head(:readme)).to be_nil end + end - repository.build_cache + context 'with an existing repository' do + it 'returns a Gitlab::Git::Tree' do + expect(repository.file_on_head(:readme)). + to be_an_instance_of(Gitlab::Git::Tree) + end end + end - it 'does not build any caches that already exist' do - cache_keys = repository.cache_keys + repository.cache_keys_for_branches_and_tags + describe '#head_tree' do + context 'with an existing repository' do + it 'returns a Tree' do + expect(repository.head_tree).to be_an_instance_of(Tree) + end + end - expect(cache).to receive(:exist?). - exactly(cache_keys.length). - times. - and_return(true) + context 'with a non-existing repository' do + it 'returns nil' do + expect(repository).to receive(:head_commit).and_return(nil) - cache_keys.each do |key| - expect(repository).not_to receive(key) + expect(repository.head_tree).to be_nil end - - repository.build_cache end end - describe "#keep_around" do - it "does not fail if we attempt to reference bad commit" do - expect(repository.kept_around?('abc1234')).to be_falsey - end + describe '#tree' do + context 'using a non-existing repository' do + before do + allow(repository).to receive(:head_commit).and_return(nil) + end - it "stores a reference to the specified commit sha so it isn't garbage collected" do - repository.keep_around(sample_commit.id) + it 'returns nil' do + expect(repository.tree(:head)).to be_nil + end - expect(repository.kept_around?(sample_commit.id)).to be_truthy + it 'returns nil when using a path' do + expect(repository.tree(:head, 'README.md')).to be_nil + end end - it "attempting to call keep_around on truncated ref does not fail" do - repository.keep_around(sample_commit.id) - ref = repository.send(:keep_around_ref_name, sample_commit.id) - path = File.join(repository.path, ref) - # Corrupt the reference - File.truncate(path, 0) + context 'using an existing repository' do + it 'returns a Tree' do + expect(repository.tree(:head)).to be_an_instance_of(Tree) + end + end + end - expect(repository.kept_around?(sample_commit.id)).to be_falsey + describe '#size' do + context 'with a non-existing repository' do + it 'returns 0' do + expect(repository).to receive(:exists?).and_return(false) - repository.keep_around(sample_commit.id) + expect(repository.size).to eq(0.0) + end + end - expect(repository.kept_around?(sample_commit.id)).to be_falsey + context 'with an existing repository' do + it 'returns the repository size as a Float' do + expect(repository.size).to be_an_instance_of(Float) + end + end + end - File.delete(path) + describe '#commit_count' do + context 'with a non-existing repository' do + it 'returns 0' do + expect(repository).to receive(:root_ref).and_return(nil) + + expect(repository.commit_count).to eq(0) + end + end + + context 'with an existing repository' do + it 'returns the commit count' do + expect(repository.commit_count).to be_an_instance_of(Fixnum) + end end end - describe '#update_ref!' do - it 'can create a ref' do - repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) + describe '#cache_method_output', caching: true do + context 'with a non-existing repository' do + let(:value) do + repository.cache_method_output(:cats, fallback: 10) do + raise Rugged::ReferenceError + end + end - expect(repository.find_branch('foobar')).not_to be_nil + it 'returns a fallback value' do + expect(value).to eq(10) + end + + it 'does not cache the data' do + value + + expect(repository.instance_variable_defined?(:@cats)).to eq(false) + expect(repository.send(:cache).exist?(:cats)).to eq(false) + end end - it 'raises CommitError when the ref update fails' do - expect do - repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) - end.to raise_error(Repository::CommitError) + context 'with an existing repository' do + it 'caches the output' do + object = double + + expect(object).to receive(:number).once.and_return(10) + + 2.times do + val = repository.cache_method_output(:cats) { object.number } + + expect(val).to eq(10) + end + + expect(repository.send(:cache).exist?(:cats)).to eq(true) + expect(repository.instance_variable_get(:@cats)).to eq(10) + end end end - describe '#remove_storage_from_path' do - let(:storage_path) { project.repository_storage_path } - let(:project_path) { project.path_with_namespace } - let(:full_path) { File.join(storage_path, project_path) } + describe '#refresh_method_caches' do + it 'refreshes the caches of the given types' do + expect(repository).to receive(:expire_method_caches). + with(%i(readme license_blob license_key)) + + expect(repository).to receive(:readme) + expect(repository).to receive(:license_blob) + expect(repository).to receive(:license_key) - it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) } - it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) } - it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') } + repository.refresh_method_caches(%i(readme license)) + end end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 43937a54b2c..691511cd93f 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -6,9 +6,6 @@ describe Service, models: true do it { is_expected.to have_one :service_hook } end - describe "Mass assignment" do - end - describe "Test Button" do before do @service = Service.new @@ -53,7 +50,7 @@ describe Service, models: true do describe "Template" do describe "for pushover service" do - let(:service_template) do + let!(:service_template) do PushoverService.create( template: true, properties: { @@ -66,13 +63,9 @@ describe Service, models: true do let(:project) { create(:project) } describe 'is prefilled for projects pushover service' do - before do - service_template - project.build_missing_services - end - it "has all fields prefilled" do - service = project.pushover_service + service = project.find_or_initialize_service('pushover') + expect(service.template).to eq(false) expect(service.device).to eq('MyDevice') expect(service.sound).to eq('mic') diff --git a/spec/models/subscription_spec.rb b/spec/models/subscription_spec.rb new file mode 100644 index 00000000000..9ab112bb2ee --- /dev/null +++ b/spec/models/subscription_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe Subscription, models: true do + describe 'relationships' do + it { is_expected.to belong_to(:project) } + it { is_expected.to belong_to(:subscribable) } + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:subscribable) } + it { is_expected.to validate_presence_of(:user) } + + it 'validates uniqueness of project_id scoped to subscribable_id, subscribable_type, and user_id' do + create(:subscription) + + expect(subject).to validate_uniqueness_of(:project_id).scoped_to([:subscribable_id, :subscribable_type, :user_id]) + end + end +end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3b152e15b61..91826e5884d 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -33,11 +33,12 @@ describe User, models: true do it { is_expected.to have_many(:award_emoji).dependent(:destroy) } it { is_expected.to have_many(:builds).dependent(:nullify) } it { is_expected.to have_many(:pipelines).dependent(:nullify) } + it { is_expected.to have_many(:chat_names).dependent(:destroy) } describe '#group_members' do it 'does not include group memberships for which user is a requester' do user = create(:user) - group = create(:group, :public) + group = create(:group, :public, :access_requestable) group.request_access(user) expect(user.group_members).to be_empty @@ -47,7 +48,7 @@ describe User, models: true do describe '#project_members' do it 'does not include project memberships for which user is a requester' do user = create(:user) - project = create(:project, :public) + project = create(:project, :public, :access_requestable) project.request_access(user) expect(user.project_members).to be_empty @@ -490,6 +491,28 @@ describe User, models: true do end end + describe '.without_projects' do + let!(:project) { create(:empty_project, :public, :access_requestable) } + let!(:user) { create(:user) } + let!(:user_without_project) { create(:user) } + let!(:user_without_project2) { create(:user) } + + before do + # add user to project + project.team << [user, :master] + + # create invite to projet + create(:project_member, :developer, project: project, invite_token: '1234', invite_email: 'inviteduser1@example.com') + + # create request to join project + project.request_access(user_without_project2) + end + + it { expect(User.without_projects).not_to include user } + it { expect(User.without_projects).to include user_without_project } + it { expect(User.without_projects).to include user_without_project2 } + end + describe '.not_in_project' do before do User.delete_all @@ -729,6 +752,17 @@ describe User, models: true do end end + describe '.find_by_username' do + it 'returns nil if not found' do + expect(described_class.find_by_username('JohnDoe')).to be_nil + end + + it 'is case-insensitive' do + user = create(:user, username: 'JohnDoe') + expect(described_class.find_by_username('JOHNDOE')).to eq user + end + end + describe '.find_by_username!' do it 'raises RecordNotFound' do expect { described_class.find_by_username!('JohnDoe') }. @@ -1050,7 +1084,7 @@ describe User, models: true do it { is_expected.to eq([private_group]) } end - describe '#authorized_projects' do + describe '#authorized_projects', truncate: true do context 'with a minimum access level' do it 'includes projects for which the user is an owner' do user = create(:user) @@ -1070,6 +1104,80 @@ describe User, models: true do .to contain_exactly(project) end end + + it "includes user's personal projects" do + user = create(:user) + project = create(:project, :private, namespace: user.namespace) + + expect(user.authorized_projects).to include(project) + end + + it "includes personal projects user has been given access to" do + user1 = create(:user) + user2 = create(:user) + project = create(:project, :private, namespace: user1.namespace) + + project.team << [user2, Gitlab::Access::DEVELOPER] + + expect(user2.authorized_projects).to include(project) + end + + it "includes projects of groups user has been added to" do + group = create(:group) + project = create(:project, group: group) + user = create(:user) + + group.add_developer(user) + + expect(user.authorized_projects).to include(project) + end + + it "does not include projects of groups user has been removed from" do + group = create(:group) + project = create(:project, group: group) + user = create(:user) + + member = group.add_developer(user) + expect(user.authorized_projects).to include(project) + + member.destroy + expect(user.authorized_projects).not_to include(project) + end + + it "includes projects shared with user's group" do + user = create(:user) + project = create(:project, :private) + group = create(:group) + + group.add_reporter(user) + project.project_group_links.create(group: group) + + expect(user.authorized_projects).to include(project) + end + + it "does not include destroyed projects user had access to" do + user1 = create(:user) + user2 = create(:user) + project = create(:project, :private, namespace: user1.namespace) + + project.team << [user2, Gitlab::Access::DEVELOPER] + expect(user2.authorized_projects).to include(project) + + project.destroy + expect(user2.authorized_projects).not_to include(project) + end + + it "does not include projects of destroyed groups user had access to" do + group = create(:group) + project = create(:project, namespace: group) + user = create(:user) + + group.add_developer(user) + expect(user.authorized_projects).to include(project) + + group.destroy + expect(user.authorized_projects).not_to include(project) + end end describe '#projects_where_can_admin_issues' do |