...@@ -100,6 +100,8 @@ describe 'Group issues page' do ...@@ -100,6 +100,8 @@ describe 'Group issues page' do
find('.empty-state .js-lazy-loaded') find('.empty-state .js-lazy-loaded')
find('.new-project-item-link').click find('.new-project-item-link').click
find('.select2-input').set(group.name)
page.within('.select2-results') do page.within('.select2-results') do
expect(page).to have_content(project.full_name) expect(page).to have_content(project.full_name)
expect(page).not_to have_content(project_with_issues_disabled.full_name) expect(page).not_to have_content(project_with_issues_disabled.full_name)
... ...
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Autocomplete::MoveToProjectFinder do describe Autocomplete::MoveToProjectFinder do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:no_access_project) { create(:project) } let(:no_access_project) { create(:project) }
let(:guest_project) { create(:project) } let(:guest_project) { create(:project) }
...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do ...@@ -92,6 +92,15 @@ describe Autocomplete::MoveToProjectFinder do
expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a) expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
.to eq([wadus_project]) .to eq([wadus_project])
end end
it 'allows searching by parent namespace' do
group = create(:group)
other_project = create(:project, group: group)
other_project.add_maintainer(user)
expect(described_class.new(user, project_id: project.id, search: group.name).execute.to_a)
.to contain_exactly(other_project)
end
end end
end end
end end
...@@ -123,7 +123,7 @@ describe GroupDescendantsFinder do ...@@ -123,7 +123,7 @@ describe GroupDescendantsFinder do
project = create(:project, namespace: group) project = create(:project, namespace: group)
other_project = create(:project) other_project = create(:project)
other_project.project_group_links.create(group: group, other_project.project_group_links.create(group: group,
group_access: ProjectGroupLink::MASTER) group_access: ProjectGroupLink::MAINTAINER)
expect(finder.execute).to contain_exactly(project) expect(finder.execute).to contain_exactly(project)
end end
... ...
......
...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -6,22 +6,22 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
include AdminModeHelper include AdminModeHelper
describe '#execute' do describe '#execute' do
let(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:group) { create(:group, :public) } let_it_be(:group) { create(:group, :public) }
let!(:private_project) do let_it_be(:private_project) do
create(:project, :private, name: 'A', path: 'A') create(:project, :private, name: 'A', path: 'A')
end end
let!(:internal_project) do let_it_be(:internal_project) do
create(:project, :internal, group: group, name: 'B', path: 'B') create(:project, :internal, group: group, name: 'B', path: 'B')
end end
let!(:public_project) do let_it_be(:public_project) do
create(:project, :public, group: group, name: 'C', path: 'C') create(:project, :public, group: group, name: 'C', path: 'C')
end end
let!(:shared_project) do let_it_be(:shared_project) do
create(:project, :private, name: 'D', path: 'D') create(:project, :private, name: 'D', path: 'D')
end end
...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do ...@@ -139,6 +139,12 @@ describe ProjectsFinder, :do_not_mock_admin_mode do
it { is_expected.to eq([public_project]) } it { is_expected.to eq([public_project]) }
end end
describe 'filter by group name' do
let(:params) { { name: group.name, search_namespaces: true } }
it { is_expected.to eq([public_project, internal_project]) }
end
describe 'filter by archived' do describe 'filter by archived' do
let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') } let!(:archived_project) { create(:project, :public, :archived, name: 'E', path: 'E') }
... ...
......
...@@ -54,7 +54,8 @@ ...@@ -54,7 +54,8 @@
"cached_markdown_version": { "type": "integer" }, "cached_markdown_version": { "type": "integer" },
"human_access": { "type": ["string", "null"] }, "human_access": { "type": ["string", "null"] },
"toggle_award_path": { "type": "string" }, "toggle_award_path": { "type": "string" },
"path": { "type": "string" } "path": { "type": "string" },
"commands_changes": { "type": "object", "additionalProperties": true }
}, },
"required": [ "required": [
"id", "attachment", "author", "created_at", "updated_at", "id", "attachment", "author", "created_at", "updated_at",
... ...
......
...@@ -19,6 +19,7 @@ ...@@ -19,6 +19,7 @@
}, },
"additionalProperties": false "additionalProperties": false
}, },
"commands_changes": { "type": "object", "additionalProperties": true },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"updated_at": { "type": "date" }, "updated_at": { "type": "date" },
"system": { "type": "boolean" }, "system": { "type": "boolean" },
... ...
......
...@@ -35,6 +35,7 @@ describe('Repository parent row component', () => { ...@@ -35,6 +35,7 @@ describe('Repository parent row component', () => {
${'app'} | ${'/-/tree/master/'} ${'app'} | ${'/-/tree/master/'}
${'app/assets'} | ${'/-/tree/master/app'} ${'app/assets'} | ${'/-/tree/master/app'}
${'app/assets#/test'} | ${'/-/tree/master/app/assets%23'} ${'app/assets#/test'} | ${'/-/tree/master/app/assets%23'}
${'app/assets#/test/world'} | ${'/-/tree/master/app/assets%23/test'}
`('renders link in $path to $to', ({ path, to }) => { `('renders link in $path to $to', ({ path, to }) => {
factory(path); factory(path);
... ...
......
...@@ -22,12 +22,22 @@ describe OptionallySearch do ...@@ -22,12 +22,22 @@ describe OptionallySearch do
it 'delegates to the search method' do it 'delegates to the search method' do
expect(model) expect(model)
.to receive(:search) .to receive(:search)
.with('foo') .with('foo', {})
model.optionally_search('foo') model.optionally_search('foo')
end end
end end
context 'when an option is provided' do
it 'delegates to the search method' do
expect(model)
.to receive(:search)
.with('foo', some_option: true)
model.optionally_search('foo', some_option: true)
end
end
context 'when no query is given' do context 'when no query is given' do
it 'returns the current relation' do it 'returns the current relation' do
expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation) expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation)
... ...
......
...@@ -108,6 +108,8 @@ describe Project do ...@@ -108,6 +108,8 @@ describe Project do
it { is_expected.to have_many(:external_pull_requests) } it { is_expected.to have_many(:external_pull_requests) }
it { is_expected.to have_many(:sourced_pipelines) } it { is_expected.to have_many(:sourced_pipelines) }
it { is_expected.to have_many(:source_pipelines) } it { is_expected.to have_many(:source_pipelines) }
it { is_expected.to have_many(:prometheus_alert_events) }
it { is_expected.to have_many(:self_managed_prometheus_alert_events) }
it_behaves_like 'model with repository' do it_behaves_like 'model with repository' do
let_it_be(:container) { create(:project, :repository, path: 'somewhere') } let_it_be(:container) { create(:project, :repository, path: 'somewhere') }
...@@ -1757,7 +1759,7 @@ describe Project do ...@@ -1757,7 +1759,7 @@ describe Project do
expect(described_class.search(project.path.upcase)).to eq([project]) expect(described_class.search(project.path.upcase)).to eq([project])
end end
context 'by full path' do context 'when include_namespace is true' do
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) } let_it_be(:project) { create(:project, group: group) }
...@@ -1767,11 +1769,11 @@ describe Project do ...@@ -1767,11 +1769,11 @@ describe Project do
end end
it 'returns projects that match the group path' do it 'returns projects that match the group path' do
expect(described_class.search(group.path)).to eq([project]) expect(described_class.search(group.path, include_namespace: true)).to eq([project])
end end
it 'returns projects that match the full path' do it 'returns projects that match the full path' do
expect(described_class.search(project.full_path)).to eq([project]) expect(described_class.search(project.full_path, include_namespace: true)).to eq([project])
end end
end end
...@@ -1781,11 +1783,11 @@ describe Project do ...@@ -1781,11 +1783,11 @@ describe Project do
end end
it 'returns no results when searching by group path' do it 'returns no results when searching by group path' do
expect(described_class.search(group.path)).to be_empty expect(described_class.search(group.path, include_namespace: true)).to be_empty
end end
it 'returns no results when searching by full path' do it 'returns no results when searching by full path' do
expect(described_class.search(project.full_path)).to be_empty expect(described_class.search(project.full_path, include_namespace: true)).to be_empty
end end
end end
end end
... ...
......
# frozen_string_literal: true
require 'spec_helper'
describe PrometheusAlertEvent do
subject { build(:prometheus_alert_event) }
let(:alert) { subject.prometheus_alert }
describe 'associations' do
it { is_expected.to belong_to(:prometheus_alert).required }
end
describe 'validations' do
it { is_expected.to be_valid }
it { is_expected.to validate_presence_of(:prometheus_alert).with_message(:required) }
it { is_expected.to validate_uniqueness_of(:payload_key).scoped_to(:prometheus_alert_id) }
it { is_expected.to validate_presence_of(:started_at) }
describe 'payload_key & ended_at' do
context 'absent if firing?' do
subject { build(:prometheus_alert_event) }
it { is_expected.to validate_presence_of(:payload_key) }
it { is_expected.not_to validate_presence_of(:ended_at) }
end
context 'present if resolved?' do
subject { build(:prometheus_alert_event, :resolved) }
it { is_expected.not_to validate_presence_of(:payload_key) }
it { is_expected.to validate_presence_of(:ended_at) }
end
end
end
describe '#title' do
it 'delegates to alert' do
expect(subject.title).to eq(alert.title)
end
end
describe 'prometheus_metric_id' do
it 'delegates to alert' do
expect(subject.prometheus_metric_id).to eq(alert.prometheus_metric_id)
end
end
describe 'transaction' do
describe 'fire' do
let(:started_at) { Time.now }
context 'when status is none' do
subject { build(:prometheus_alert_event, :none) }
it 'fires an event' do
result = subject.fire(started_at)
expect(result).to eq(true)
expect(subject).to be_firing
expect(subject.started_at).to be_like_time(started_at)
end
end
context 'when firing' do
subject { build(:prometheus_alert_event) }
it 'cannot fire again' do
result = subject.fire(started_at)
expect(result).to eq(false)
end
end
end
describe 'resolve' do
let(:ended_at) { Time.now }
context 'when firing' do
subject { build(:prometheus_alert_event) }
it 'resolves an event' do
result = subject.resolve!(ended_at)
expect(result).to eq(true)
expect(subject).to be_resolved
expect(subject.ended_at).to be_like_time(ended_at)
end
end
context 'when resolved' do
subject { build(:prometheus_alert_event, :resolved) }
it 'cannot resolve again' do
result = subject.resolve(ended_at)
expect(result).to eq(false)
end
end
end
end
end
...@@ -140,6 +140,41 @@ describe SnippetRepository do ...@@ -140,6 +140,41 @@ describe SnippetRepository do
let_it_be(:named_snippet) { { file_path: 'fee.txt', content: 'bar', action: :create } } let_it_be(:named_snippet) { { file_path: 'fee.txt', content: 'bar', action: :create } }
let_it_be(:unnamed_snippet) { { file_path: '', content: 'dummy', action: :create } } let_it_be(:unnamed_snippet) { { file_path: '', content: 'dummy', action: :create } }
context 'when existing file has a default name' do
let(:default_name) { 'snippetfile1.txt' }
let(:new_file) { { file_path: '', content: 'bar' } }
let(:existing_file) { { previous_path: default_name, file_path: '', content: 'new_content' } }
before do
expect(blob_at(snippet, default_name)).to be_nil
snippet_repository.multi_files_action(user, [new_file], commit_opts)
expect(blob_at(snippet, default_name)).to be
end
it 'reuses the existing file name' do
snippet_repository.multi_files_action(user, [existing_file], commit_opts)
blob = blob_at(snippet, default_name)
expect(blob.data).to eq existing_file[:content]
end
end
context 'when file name consists of one or several whitespaces' do
let(:default_name) { 'snippetfile1.txt' }
let(:new_file) { { file_path: ' ', content: 'bar' } }
it 'assigns a new name to the file' do
expect(blob_at(snippet, default_name)).to be_nil
snippet_repository.multi_files_action(user, [new_file], commit_opts)
blob = blob_at(snippet, default_name)
expect(blob.data).to eq new_file[:content]
end
end
context 'when some files are not named' do context 'when some files are not named' do
let(:data) { [named_snippet] + Array.new(2) { unnamed_snippet.clone } } let(:data) { [named_snippet] + Array.new(2) { unnamed_snippet.clone } }
... ...
......
...@@ -302,7 +302,7 @@ describe API::Groups do ...@@ -302,7 +302,7 @@ describe API::Groups do
before do before do
group1.add_developer(user2) group1.add_developer(user2)
group3.add_master(user2) group3.add_maintainer(user2)
end end
it 'returns an array of groups the user has at least master access' do it 'returns an array of groups the user has at least master access' do
... ...
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe API::Notes do describe API::Notes do
let(:user) { create(:user) } let!(:user) { create(:user) }
let!(:project) { create(:project, :public, namespace: user.namespace) } let!(:project) { create(:project, :public) }
let(:private_user) { create(:user) } let(:private_user) { create(:user) }
before do before do
...@@ -226,14 +226,56 @@ describe API::Notes do ...@@ -226,14 +226,56 @@ describe API::Notes do
let(:note) { merge_request_note } let(:note) { merge_request_note }
end end
let(:request_body) { 'Hi!' }
let(:request_path) { "/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes" }
subject { post api(request_path, user), params: { body: request_body } }
context 'a command only note' do
let(:assignee) { create(:user) }
let(:request_body) { "/assign #{assignee.to_reference}" }
before do
project.add_developer(assignee)
project.add_developer(user)
end
it 'returns 202 Accepted status' do
subject
expect(response).to have_gitlab_http_status(:accepted)
end
it 'does not actually create a new note' do
expect { subject }.not_to change { Note.where(system: false).count }
end
it 'does however create a system note about the change' do
expect { subject }.to change { Note.system.count }.by(1)
end
it 'applies the commands' do
expect { subject }.to change { merge_request.reset.assignees }
end
it 'reports the changes' do
subject
expect(json_response).to include(
'commands_changes' => include(
'assignee_ids' => [Integer]
),
'summary' => include("Assigned #{assignee.to_reference}.")
)
end
end
context 'when the merge request discussion is locked' do context 'when the merge request discussion is locked' do
before do before do
merge_request.update_attribute(:discussion_locked, true) merge_request.update_attribute(:discussion_locked, true)
end end
context 'when a user is a team member' do context 'when a user is a team member' do
subject { post api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/notes", user), params: { body: 'Hi!' } }
it 'returns 200 status' do it 'returns 200 status' do
subject subject
... ...
......
...@@ -19,7 +19,7 @@ describe "Internal Project Pages Access" do ...@@ -19,7 +19,7 @@ describe "Internal Project Pages Access" do
before do before do
allow(Gitlab.config.pages).to receive(:access_control).and_return(true) allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
group.add_owner(owner) group.add_owner(owner)
project.add_master(master) project.add_maintainer(master)
project.add_developer(developer) project.add_developer(developer)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
... ...
......
...@@ -19,7 +19,7 @@ describe "Private Project Pages Access" do ...@@ -19,7 +19,7 @@ describe "Private Project Pages Access" do
before do before do
allow(Gitlab.config.pages).to receive(:access_control).and_return(true) allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
group.add_owner(owner) group.add_owner(owner)
project.add_master(master) project.add_maintainer(master)
project.add_developer(developer) project.add_developer(developer)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
... ...
......
...@@ -19,7 +19,7 @@ describe "Public Project Pages Access" do ...@@ -19,7 +19,7 @@ describe "Public Project Pages Access" do
before do before do
allow(Gitlab.config.pages).to receive(:access_control).and_return(true) allow(Gitlab.config.pages).to receive(:access_control).and_return(true)
group.add_owner(owner) group.add_owner(owner)
project.add_master(master) project.add_maintainer(master)
project.add_developer(developer) project.add_developer(developer)
project.add_reporter(reporter) project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
... ...
......
...@@ -362,6 +362,21 @@ describe API::Projects do ...@@ -362,6 +362,21 @@ describe API::Projects do
end end
end end
context 'and using search and search_namespaces is true' do
let(:group) { create(:group) }
let!(:project_in_group) { create(:project, group: group) }
before do
group.add_guest(user)
end
it_behaves_like 'projects response' do
let(:filter) { { search: group.name, search_namespaces: true } }
let(:current_user) { user }
let(:projects) { [project_in_group] }
end
end
context 'and using id_after' do context 'and using id_after' do
it_behaves_like 'projects response' do it_behaves_like 'projects response' do
let(:filter) { { id_after: project2.id } } let(:filter) { { id_after: project2.id } }
... ...
......
# frozen_string_literal: true
require 'spec_helper'
describe PrometheusAlertEntity do
let(:user) { create(:user) }
let(:prometheus_alert) { create(:prometheus_alert) }
let(:request) { double('prometheus_alert', current_user: user) }
let(:entity) { described_class.new(prometheus_alert, request: request) }
subject { entity.as_json }
context 'when user can read prometheus alerts' do
before do
prometheus_alert.project.add_maintainer(user)
stub_licensed_features(prometheus_alerts: true)
end
it 'exposes prometheus_alert attributes' do
expect(subject).to include(:id, :title, :query, :operator, :threshold)
end
it 'exposes alert_path' do
expect(subject).to include(:alert_path)
end
end
end
...@@ -92,6 +92,8 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -92,6 +92,8 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
end end
context 'Files::UpdateService success' do context 'Files::UpdateService success' do
let(:merge_request) { project.merge_requests.last }
before do before do
allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :success })) allow(::Files::UpdateService).to receive(:new).and_return(double(execute: { status: :success }))
end end
...@@ -107,6 +109,31 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -107,6 +109,31 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
expect(service_call[:status]).to be :success expect(service_call[:status]).to be :success
expect(service_call[:http_status]).to be :created expect(service_call[:http_status]).to be :created
expect(service_call[:dashboard]).to match dashboard_details expect(service_call[:dashboard]).to match dashboard_details
expect(service_call[:merge_request]).to eq(Gitlab::UrlBuilder.build(merge_request))
end
context 'when the merge request does not succeed' do
let(:error_message) { 'There was an error' }
let(:merge_request) do
build(:merge_request, target_project: project, source_project: project, author: user)
end
before do
merge_request.errors.add(:base, error_message)
allow_next_instance_of(::MergeRequests::CreateService) do |mr|
allow(mr).to receive(:execute).and_return(merge_request)
end
end
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq(error_message)
end
end end
context 'with escaped characters in file name' do context 'with escaped characters in file name' do
...@@ -125,6 +152,25 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto ...@@ -125,6 +152,25 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
expect(service_call[:dashboard]).to match dashboard_details expect(service_call[:dashboard]).to match dashboard_details
end end
end end
context 'when pushing to the default branch' do
let(:branch) { 'master' }
it 'does not create a merge request', :aggregate_failures do
dashboard_details = {
path: '.gitlab/dashboards/custom_dashboard.yml',
display_name: 'custom_dashboard.yml',
default: false,
system_dashboard: false
}
expect(::MergeRequests::CreateService).not_to receive(:new)
expect(service_call.keys).to contain_exactly(:dashboard, :http_status, :status)
expect(service_call[:status]).to be :success
expect(service_call[:http_status]).to be :created
expect(service_call[:dashboard]).to match dashboard_details
end
end
end end
context 'Files::UpdateService fails' do context 'Files::UpdateService fails' do
... ...
......
...@@ -2790,7 +2790,7 @@ describe NotificationService, :mailer do ...@@ -2790,7 +2790,7 @@ describe NotificationService, :mailer do
let!(:developer) { create(:user) } let!(:developer) { create(:user) }
before do before do
project.add_master(master) project.add_maintainer(master)
end end
it 'sends the email to owners and masters' do it 'sends the email to owners and masters' do
... ...
......