diff options
Diffstat (limited to 'spec')
23 files changed, 1269 insertions, 408 deletions
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb index fb27fe58cd9..ab33195eb83 100644 --- a/spec/controllers/projects/project_members_controller_spec.rb +++ b/spec/controllers/projects/project_members_controller_spec.rb @@ -8,183 +8,168 @@ RSpec.describe Projects::ProjectMembersController do let_it_be(:sub_group) { create(:group, parent: group) } let_it_be(:project, reload: true) { create(:project, :public) } - before do - travel_to DateTime.new(2019, 4, 1) - end + shared_examples_for 'controller actions' do + before do + travel_to DateTime.new(2019, 4, 1) + end - after do - travel_back - end + after do + travel_back + end - describe 'GET index' do - it 'has the project_members address with a 200 status code' do - get :index, params: { namespace_id: project.namespace, project_id: project } + describe 'GET index' do + it 'has the project_members address with a 200 status code' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(response).to have_gitlab_http_status(:ok) - end + expect(response).to have_gitlab_http_status(:ok) + end - context 'project members' do - context 'when project belongs to group' do - let_it_be(:user_in_group) { create(:user) } - let_it_be(:project_in_group) { create(:project, :public, group: group) } + context 'project members' do + context 'when project belongs to group' do + let_it_be(:user_in_group) { create(:user) } + let_it_be(:project_in_group) { create(:project, :public, group: group) } - before do - group.add_owner(user_in_group) - project_in_group.add_maintainer(user) - sign_in(user) - end + before do + group.add_owner(user_in_group) + project_in_group.add_maintainer(user) + sign_in(user) + end - it 'lists inherited project members by default' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group } + it 'lists inherited project members by default' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id) - end + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id) + end - it 'lists direct project members only' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' } + it 'lists direct project members only' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id) - end + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id) + end - it 'lists inherited project members only' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' } + it 'lists inherited project members only' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id) + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id) + end end - end - context 'when project belongs to a sub-group' do - let_it_be(:user_in_group) { create(:user) } - let_it_be(:project_in_group) { create(:project, :public, group: sub_group) } + context 'when project belongs to a sub-group' do + let_it_be(:user_in_group) { create(:user) } + let_it_be(:project_in_group) { create(:project, :public, group: sub_group) } - before do - group.add_owner(user_in_group) - project_in_group.add_maintainer(user) - sign_in(user) - end + before do + group.add_owner(user_in_group) + project_in_group.add_maintainer(user) + sign_in(user) + end - it 'lists inherited project members by default' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group } + it 'lists inherited project members by default' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id) - end + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id, user_in_group.id) + end - it 'lists direct project members only' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' } + it 'lists direct project members only' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'exclude' } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id) - end + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user.id) + end - it 'lists inherited project members only' do - get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' } + it 'lists inherited project members only' do + get :index, params: { namespace_id: project_in_group.namespace, project_id: project_in_group, with_inherited_permissions: 'only' } - expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id) + expect(assigns(:project_members).map(&:user_id)).to contain_exactly(user_in_group.id) + end end - end - context 'when invited project members are present' do - let!(:invited_member) { create(:project_member, :invited, project: project) } + context 'when invited project members are present' do + let!(:invited_member) { create(:project_member, :invited, project: project) } - before do - project.add_maintainer(user) - sign_in(user) - end + before do + project.add_maintainer(user) + sign_in(user) + end - it 'excludes the invited members from project members list' do - get :index, params: { namespace_id: project.namespace, project_id: project } + it 'excludes the invited members from project members list' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email) + expect(assigns(:project_members).map(&:invite_email)).not_to contain_exactly(invited_member.invite_email) + end end end - end - - context 'invited members' do - let_it_be(:invited_member) { create(:project_member, :invited, project: project) } - before do - sign_in(user) - end + context 'invited members' do + let_it_be(:invited_member) { create(:project_member, :invited, project: project) } - context 'when user has `admin_project_member` permissions' do before do - project.add_maintainer(user) + sign_in(user) end - it 'lists invited members' do - get :index, params: { namespace_id: project.namespace, project_id: project } + context 'when user has `admin_project_member` permissions' do + before do + project.add_maintainer(user) + end + + it 'lists invited members' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email) + expect(assigns(:invited_members).map(&:invite_email)).to contain_exactly(invited_member.invite_email) + end end - end - context 'when user does not have `admin_project_member` permissions' do - it 'does not list invited members' do - get :index, params: { namespace_id: project.namespace, project_id: project } + context 'when user does not have `admin_project_member` permissions' do + it 'does not list invited members' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(assigns(:invited_members)).to be_nil + expect(assigns(:invited_members)).to be_nil + end end end - end - context 'access requests' do - let_it_be(:access_requester_user) { create(:user) } - - before do - project.request_access(access_requester_user) - sign_in(user) - end + context 'access requests' do + let_it_be(:access_requester_user) { create(:user) } - context 'when user has `admin_project_member` permissions' do before do - project.add_maintainer(user) + project.request_access(access_requester_user) + sign_in(user) end - it 'lists access requests' do - get :index, params: { namespace_id: project.namespace, project_id: project } + context 'when user has `admin_project_member` permissions' do + before do + project.add_maintainer(user) + end + + it 'lists access requests' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id) + expect(assigns(:requesters).map(&:user_id)).to contain_exactly(access_requester_user.id) + end end - end - context 'when user does not have `admin_project_member` permissions' do - it 'does not list access requests' do - get :index, params: { namespace_id: project.namespace, project_id: project } + context 'when user does not have `admin_project_member` permissions' do + it 'does not list access requests' do + get :index, params: { namespace_id: project.namespace, project_id: project } - expect(assigns(:requesters)).to be_nil + expect(assigns(:requesters)).to be_nil + end end end end - end - - describe 'PUT update' do - let_it_be(:requester) { create(:project_member, :access_request, project: project) } - - before do - project.add_maintainer(user) - sign_in(user) - end - context 'access level' do - Gitlab::Access.options.each do |label, value| - it "can change the access level to #{label}" do - params = { - project_member: { access_level: value }, - namespace_id: project.namespace, - project_id: project, - id: requester - } + describe 'PUT update' do + let_it_be(:requester) { create(:project_member, :access_request, project: project) } - put :update, params: params, xhr: true - - expect(requester.reload.human_access).to eq(label) - end + before do + project.add_maintainer(user) + sign_in(user) end - describe 'managing project direct owners' do - context 'when a Maintainer tries to elevate another user to OWNER' do - it 'does not allow the operation' do + context 'access level' do + Gitlab::Access.options.each do |label, value| + it "can change the access level to #{label}" do params = { - project_member: { access_level: Gitlab::Access::OWNER }, + project_member: { access_level: value }, namespace_id: project.namespace, project_id: project, id: requester @@ -192,368 +177,395 @@ RSpec.describe Projects::ProjectMembersController do put :update, params: params, xhr: true - expect(response).to have_gitlab_http_status(:forbidden) + expect(requester.reload.human_access).to eq(label) end end - context 'when a user with OWNER access tries to elevate another user to OWNER' do - # inherited owner role via personal project association - let(:user) { project.first_owner } + describe 'managing project direct owners' do + context 'when a Maintainer tries to elevate another user to OWNER' do + it 'does not allow the operation' do + params = { + project_member: { access_level: Gitlab::Access::OWNER }, + namespace_id: project.namespace, + project_id: project, + id: requester + } - before do - sign_in(user) + put :update, params: params, xhr: true + + expect(response).to have_gitlab_http_status(:forbidden) + end end - it 'returns success' do - params = { - project_member: { access_level: Gitlab::Access::OWNER }, - namespace_id: project.namespace, - project_id: project, - id: requester - } + context 'when a user with OWNER access tries to elevate another user to OWNER' do + # inherited owner role via personal project association + let(:user) { project.first_owner } - put :update, params: params, xhr: true + before do + sign_in(user) + end + + it 'returns success' do + params = { + project_member: { access_level: Gitlab::Access::OWNER }, + namespace_id: project.namespace, + project_id: project, + id: requester + } + + put :update, params: params, xhr: true - expect(response).to have_gitlab_http_status(:ok) - expect(requester.reload.access_level).to eq(Gitlab::Access::OWNER) + expect(response).to have_gitlab_http_status(:ok) + expect(requester.reload.access_level).to eq(Gitlab::Access::OWNER) + end end end end - end - context 'access expiry date' do - subject do - put :update, xhr: true, params: { - project_member: { - expires_at: expires_at - }, - namespace_id: project.namespace, - project_id: project, - id: requester - } - end + context 'access expiry date' do + subject do + put :update, xhr: true, params: { + project_member: { + expires_at: expires_at + }, + namespace_id: project.namespace, + project_id: project, + id: requester + } + end - context 'when set to a date in the past' do - let(:expires_at) { 2.days.ago } + context 'when set to a date in the past' do + let(:expires_at) { 2.days.ago } - it 'does not update the member' do - subject + it 'does not update the member' do + subject - expect(requester.reload.expires_at).not_to eq(expires_at.to_date) - end + expect(requester.reload.expires_at).not_to eq(expires_at.to_date) + end - it 'returns error status' do - subject + it 'returns error status' do + subject - expect(response).to have_gitlab_http_status(:unprocessable_entity) - end + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end - it 'returns error message' do - subject + it 'returns error message' do + subject - expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' }) + expect(json_response).to eq({ 'message' => 'Expires at cannot be a date in the past' }) + end end - end - context 'when set to a date in the future' do - let(:expires_at) { 5.days.from_now } + context 'when set to a date in the future' do + let(:expires_at) { 5.days.from_now } - it 'updates the member' do - subject + it 'updates the member' do + subject - expect(requester.reload.expires_at).to eq(expires_at.to_date) + expect(requester.reload.expires_at).to eq(expires_at.to_date) + end end end - end - context 'expiration date' do - let(:expiry_date) { 1.month.from_now.to_date } + context 'expiration date' do + let(:expiry_date) { 1.month.from_now.to_date } - before do - travel_to Time.now.utc.beginning_of_day - - put( - :update, - params: { - project_member: { expires_at: expiry_date }, - namespace_id: project.namespace, - project_id: project, - id: requester - }, - format: :json - ) - end + before do + travel_to Time.now.utc.beginning_of_day + + put( + :update, + params: { + project_member: { expires_at: expiry_date }, + namespace_id: project.namespace, + project_id: project, + id: requester + }, + format: :json + ) + end - context 'when `expires_at` is set' do - it 'returns correct json response' do - expect(json_response).to eq({ - "expires_soon" => false, - "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) - }) + context 'when `expires_at` is set' do + it 'returns correct json response' do + expect(json_response).to eq({ + "expires_soon" => false, + "expires_at_formatted" => expiry_date.to_time.in_time_zone.to_s(:medium) + }) + end end - end - context 'when `expires_at` is not set' do - let(:expiry_date) { nil } + context 'when `expires_at` is not set' do + let(:expiry_date) { nil } - it 'returns empty json response' do - expect(json_response).to be_empty + it 'returns empty json response' do + expect(json_response).to be_empty + end end end end - end - describe 'DELETE destroy' do - let_it_be(:member) { create(:project_member, :developer, project: project) } + describe 'DELETE destroy' do + let_it_be(:member) { create(:project_member, :developer, project: project) } - before do - sign_in(user) - end + before do + sign_in(user) + end - context 'when member is not found' do - it 'returns 404' do - delete :destroy, params: { - namespace_id: project.namespace, - project_id: project, - id: 42 - } + context 'when member is not found' do + it 'returns 404' do + delete :destroy, params: { + namespace_id: project.namespace, + project_id: project, + id: 42 + } - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:not_found) + end end - end - context 'when member is found' do - context 'when user does not have enough rights' do - context 'when user does not have rights to manage other members' do - before do - project.add_developer(user) + context 'when member is found' do + context 'when user does not have enough rights' do + context 'when user does not have rights to manage other members' do + before do + project.add_developer(user) + end + + it 'returns 404', :aggregate_failures do + delete :destroy, params: { + namespace_id: project.namespace, + project_id: project, + id: member + } + + expect(response).to have_gitlab_http_status(:not_found) + expect(project.members).to include member + end end - it 'returns 404', :aggregate_failures do - delete :destroy, params: { - namespace_id: project.namespace, - project_id: project, - id: member - } + context 'when user does not have rights to manage Owner members' do + let_it_be(:member) { create(:project_member, project: project, access_level: Gitlab::Access::OWNER) } - expect(response).to have_gitlab_http_status(:not_found) - expect(project.members).to include member + before do + project.add_maintainer(user) + end + + it 'returns 403', :aggregate_failures do + delete :destroy, params: { + namespace_id: project.namespace, + project_id: project, + id: member + } + + expect(response).to have_gitlab_http_status(:forbidden) + expect(project.members).to include member + end end end - context 'when user does not have rights to manage Owner members' do - let_it_be(:member) { create(:project_member, project: project, access_level: Gitlab::Access::OWNER) } - + context 'when user has enough rights' do before do project.add_maintainer(user) end - it 'returns 403', :aggregate_failures do + it '[HTML] removes user from members', :aggregate_failures do + delete :destroy, params: { + namespace_id: project.namespace, + project_id: project, + id: member + } + + expect(response).to redirect_to( + project_project_members_path(project) + ) + expect(project.members).not_to include member + end + + it '[JS] removes user from members', :aggregate_failures do delete :destroy, params: { namespace_id: project.namespace, project_id: project, id: member - } + }, xhr: true - expect(response).to have_gitlab_http_status(:forbidden) - expect(project.members).to include member + expect(response).to be_successful + expect(project.members).not_to include member end end end - - context 'when user has enough rights' do - before do - project.add_maintainer(user) - end - - it '[HTML] removes user from members', :aggregate_failures do - delete :destroy, params: { - namespace_id: project.namespace, - project_id: project, - id: member - } - - expect(response).to redirect_to( - project_project_members_path(project) - ) - expect(project.members).not_to include member - end - - it '[JS] removes user from members', :aggregate_failures do - delete :destroy, params: { - namespace_id: project.namespace, - project_id: project, - id: member - }, xhr: true - - expect(response).to be_successful - expect(project.members).not_to include member - end - end - end - end - - describe 'DELETE leave' do - before do - sign_in(user) end - context 'when member is not found' do - it 'returns 404' do - delete :leave, params: { - namespace_id: project.namespace, - project_id: project - } - - expect(response).to have_gitlab_http_status(:not_found) + describe 'DELETE leave' do + before do + sign_in(user) end - end - - context 'when member is found' do - context 'and is not an owner' do - before do - project.add_developer(user) - end - it 'removes user from members', :aggregate_failures do + context 'when member is not found' do + it 'returns 404' do delete :leave, params: { namespace_id: project.namespace, project_id: project } - expect(controller).to set_flash.to "You left the \"#{project.human_name}\" project." - expect(response).to redirect_to(dashboard_projects_path) - expect(project.users).not_to include user + expect(response).to have_gitlab_http_status(:not_found) end end - context 'and is an owner' do - let(:project) { create(:project, namespace: user.namespace) } + context 'when member is found' do + context 'and is not an owner' do + before do + project.add_developer(user) + end + + it 'removes user from members', :aggregate_failures do + delete :leave, params: { + namespace_id: project.namespace, + project_id: project + } - before do - project.add_maintainer(user) + expect(controller).to set_flash.to "You left the \"#{project.human_name}\" project." + expect(response).to redirect_to(dashboard_projects_path) + expect(project.users).not_to include user + end end - it 'cannot remove themselves from the project' do - delete :leave, params: { - namespace_id: project.namespace, - project_id: project - } + context 'and is an owner' do + let(:project) { create(:project, namespace: user.namespace) } - expect(response).to have_gitlab_http_status(:forbidden) - end - end + before do + project.add_maintainer(user) + end - context 'and is a requester' do - before do - project.request_access(user) + it 'cannot remove themselves from the project' do + delete :leave, params: { + namespace_id: project.namespace, + project_id: project + } + + expect(response).to have_gitlab_http_status(:forbidden) + end end - it 'removes user from members', :aggregate_failures do - delete :leave, params: { - namespace_id: project.namespace, - project_id: project - } + context 'and is a requester' do + before do + project.request_access(user) + end + + it 'removes user from members', :aggregate_failures do + delete :leave, params: { + namespace_id: project.namespace, + project_id: project + } - expect(controller).to set_flash.to 'Your access request to the project has been withdrawn.' - expect(response).to redirect_to(project_path(project)) - expect(project.requesters).to be_empty - expect(project.users).not_to include user + expect(controller).to set_flash.to 'Your access request to the project has been withdrawn.' + expect(response).to redirect_to(project_path(project)) + expect(project.requesters).to be_empty + expect(project.users).not_to include user + end end end end - end - - describe 'POST request_access' do - before do - sign_in(user) - end - it 'creates a new ProjectMember that is not a team member', :aggregate_failures do - post :request_access, params: { - namespace_id: project.namespace, - project_id: project - } - - expect(controller).to set_flash.to 'Your request for access has been queued for review.' - expect(response).to redirect_to( - project_path(project) - ) - expect(project.requesters.exists?(user_id: user)).to be_truthy - expect(project.users).not_to include user - end - end + describe 'POST request_access' do + before do + sign_in(user) + end - describe 'POST approve' do - let_it_be(:member) { create(:project_member, :access_request, project: project) } + it 'creates a new ProjectMember that is not a team member', :aggregate_failures do + post :request_access, params: { + namespace_id: project.namespace, + project_id: project + } - before do - sign_in(user) + expect(controller).to set_flash.to 'Your request for access has been queued for review.' + expect(response).to redirect_to( + project_path(project) + ) + expect(project.requesters.exists?(user_id: user)).to be_truthy + expect(project.users).not_to include user + end end - context 'when member is not found' do - it 'returns 404' do - post :approve_access_request, params: { - namespace_id: project.namespace, - project_id: project, - id: 42 - } + describe 'POST approve' do + let_it_be(:member) { create(:project_member, :access_request, project: project) } - expect(response).to have_gitlab_http_status(:not_found) + before do + sign_in(user) end - end - - context 'when member is found' do - context 'when user does not have rights to manage other members' do - before do - project.add_developer(user) - end - it 'returns 404', :aggregate_failures do + context 'when member is not found' do + it 'returns 404' do post :approve_access_request, params: { namespace_id: project.namespace, project_id: project, - id: member + id: 42 } expect(response).to have_gitlab_http_status(:not_found) - expect(project.members).not_to include member end end - context 'when user has enough rights' do - before do - project.add_maintainer(user) + context 'when member is found' do + context 'when user does not have rights to manage other members' do + before do + project.add_developer(user) + end + + it 'returns 404', :aggregate_failures do + post :approve_access_request, params: { + namespace_id: project.namespace, + project_id: project, + id: member + } + + expect(response).to have_gitlab_http_status(:not_found) + expect(project.members).not_to include member + end end - it 'adds user to members', :aggregate_failures do - post :approve_access_request, params: { - namespace_id: project.namespace, - project_id: project, - id: member - } + context 'when user has enough rights' do + before do + project.add_maintainer(user) + end - expect(response).to redirect_to( - project_project_members_path(project) - ) - expect(project.members).to include member + it 'adds user to members', :aggregate_failures do + post :approve_access_request, params: { + namespace_id: project.namespace, + project_id: project, + id: member + } + + expect(response).to redirect_to( + project_project_members_path(project) + ) + expect(project.members).to include member + end end end end - end - describe 'POST resend_invite' do - let_it_be(:member) { create(:project_member, project: project) } + describe 'POST resend_invite' do + let_it_be(:member) { create(:project_member, project: project) } - before do - project.add_maintainer(user) - sign_in(user) + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'is successful' do + post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member } + + expect(response).to have_gitlab_http_status(:found) + end end + end - it 'is successful' do - post :resend_invite, params: { namespace_id: project.namespace, project_id: project, id: member } + it_behaves_like 'controller actions' - expect(response).to have_gitlab_http_status(:found) + context 'when project_members_index_by_project_namespace feature flag is disabled' do + before do + stub_feature_flags(project_members_index_by_project_namespace: false) end + + it_behaves_like 'controller actions' end end diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb index fac4d5a99a5..159a83a261d 100644 --- a/spec/features/projects/settings/user_manages_project_members_spec.rb +++ b/spec/features/projects/settings/user_manages_project_members_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe 'Projects > Settings > User manages project members', feature_category: :projects do include Spec::Support::Helpers::Features::MembersHelpers include Spec::Support::Helpers::ModalHelpers + include ListboxHelpers let(:group) { create(:group, name: 'OpenSource') } let(:project) { create(:project, :with_namespace_settings) } @@ -46,7 +47,7 @@ RSpec.describe 'Projects > Settings > User manages project members', feature_cat click_on 'Select a project' wait_for_requests - click_button project2.name + select_listbox_item(project2.name_with_namespace) click_button 'Import project members' wait_for_requests diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js index acc062b5fff..6fbf95362fa 100644 --- a/spec/frontend/invite_members/components/project_select_spec.js +++ b/spec/frontend/invite_members/components/project_select_spec.js @@ -1,4 +1,4 @@ -import { GlSearchBoxByType, GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui'; +import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; import * as projectsApi from '~/api/projects_api'; @@ -9,7 +9,12 @@ describe('ProjectSelect', () => { let wrapper; const createComponent = () => { - wrapper = shallowMountExtended(ProjectSelect, {}); + wrapper = shallowMountExtended(ProjectSelect, { + stubs: { + GlCollapsibleListbox, + GlAvatarLabeled, + }, + }); }; beforeEach(() => { @@ -22,16 +27,24 @@ describe('ProjectSelect', () => { wrapper.destroy(); }); - const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType); - const findDropdownItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index); - const findAvatarLabeled = (index) => findDropdownItem(index).findComponent(GlAvatarLabeled); - const findEmptyResultMessage = () => wrapper.findByTestId('empty-result-message'); - const findErrorMessage = () => wrapper.findByTestId('error-message'); - - it('renders GlSearchBoxByType with default attributes', () => { - expect(findSearchBoxByType().exists()).toBe(true); - expect(findSearchBoxByType().vm.$attrs).toMatchObject({ - placeholder: 'Search projects', + const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox); + const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index); + + it('renders GlCollapsibleListbox with default props', () => { + expect(findGlCollapsibleListbox().exists()).toBe(true); + expect(findGlCollapsibleListbox().props()).toMatchObject({ + items: [], + loading: false, + multiple: false, + noResultsText: 'No matching results', + placement: 'left', + searchPlaceholder: 'Search projects', + searchable: true, + searching: false, + size: 'medium', + toggleText: 'Select a project', + totalItems: null, + variant: 'default', }); }); @@ -48,7 +61,7 @@ describe('ProjectSelect', () => { }), ); - findSearchBoxByType().vm.$emit('input', project1.name); + findGlCollapsibleListbox().vm.$emit('search', project1.name); }); it('calls the API', () => { @@ -61,14 +74,12 @@ describe('ProjectSelect', () => { }); it('displays loading icon while waiting for API call to resolve and then sets loading false', async () => { - expect(findSearchBoxByType().props('isLoading')).toBe(true); + expect(findGlCollapsibleListbox().props('searching')).toBe(true); resolveApiRequest({ data: allProjects }); await waitForPromises(); - expect(findSearchBoxByType().props('isLoading')).toBe(false); - expect(findEmptyResultMessage().exists()).toBe(false); - expect(findErrorMessage().exists()).toBe(false); + expect(findGlCollapsibleListbox().props('searching')).toBe(false); }); it('displays a dropdown item and avatar for each project fetched', async () => { @@ -76,11 +87,11 @@ describe('ProjectSelect', () => { await waitForPromises(); allProjects.forEach((project, index) => { - expect(findDropdownItem(index).attributes('name')).toBe(project.name_with_namespace); expect(findAvatarLabeled(index).attributes()).toMatchObject({ src: project.avatar_url, 'entity-id': String(project.id), 'entity-name': project.name_with_namespace, + size: '32', }); expect(findAvatarLabeled(index).props('label')).toBe(project.name_with_namespace); }); @@ -90,16 +101,17 @@ describe('ProjectSelect', () => { resolveApiRequest({ data: [] }); await waitForPromises(); - expect(findEmptyResultMessage().text()).toBe('No matching results'); + expect(findGlCollapsibleListbox().text()).toBe('No matching results'); }); it('displays the error message when the fetch fails', async () => { rejectApiRequest(); await waitForPromises(); - expect(findErrorMessage().text()).toBe( - 'There was an error fetching the projects. Please try again.', - ); + // To be displayed in GlCollapsibleListbox once we implement + // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2132 + // https://gitlab.com/gitlab-org/gitlab/-/issues/389974 + expect(findGlCollapsibleListbox().text()).toBe('No matching results'); }); }); }); diff --git a/spec/frontend/invite_members/mock_data/api_response_data.js b/spec/frontend/invite_members/mock_data/api_response_data.js index 9509422b603..4ab9026c531 100644 --- a/spec/frontend/invite_members/mock_data/api_response_data.js +++ b/spec/frontend/invite_members/mock_data/api_response_data.js @@ -6,7 +6,7 @@ export const project1 = { }; export const project2 = { id: 2, - name: 'Project One', + name: 'Project Two', name_with_namespace: 'Project Two', avatar_url: 'test2', }; diff --git a/spec/graphql/types/notes/deleted_note_type_spec.rb b/spec/graphql/types/notes/deleted_note_type_spec.rb new file mode 100644 index 00000000000..70985484e75 --- /dev/null +++ b/spec/graphql/types/notes/deleted_note_type_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['DeletedNote'], feature_category: :team_planning do + it 'exposes the expected fields' do + expected_fields = %i[ + id + discussion_id + last_discussion_note + ] + + expect(described_class).to have_graphql_fields(*expected_fields).only + end +end diff --git a/spec/lib/gitlab/ci/artifacts/logger_spec.rb b/spec/lib/gitlab/ci/artifacts/logger_spec.rb index 09d8a549640..7a2f8b6ea37 100644 --- a/spec/lib/gitlab/ci/artifacts/logger_spec.rb +++ b/spec/lib/gitlab/ci/artifacts/logger_spec.rb @@ -19,7 +19,7 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do message: 'Artifact created', job_artifact_id: artifact.id, size: artifact.size, - type: artifact.file_type, + file_type: artifact.file_type, build_id: artifact.job_id, project_id: artifact.project_id, 'correlation_id' => an_instance_of(String), @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Ci::Artifacts::Logger do job_artifact_id: artifact.id, expire_at: artifact.expire_at, size: artifact.size, - type: artifact.file_type, + file_type: artifact.file_type, build_id: artifact.job_id, project_id: artifact.project_id, method: method, diff --git a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb index 12886c79d7d..5fbaae58a73 100644 --- a/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb +++ b/spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb @@ -567,6 +567,28 @@ RSpec.describe Gitlab::Ci::Parsers::Security::Validators::SchemaValidator, featu end it { is_expected.to match_array([message]) } + + context 'without license', unless: Gitlab.ee? do + let(:schema_path) { Rails.root.join(*%w[lib gitlab ci parsers security validators schemas]) } + + it 'tries to validate against the latest patch version available' do + expect(File).to receive(:file?).with("#{schema_path}/#{report_version}/#{report_type}-report-format.json") + expect(File).to receive(:file?).with("#{schema_path}/#{latest_patch_version}/#{report_type}-report-format.json") + + subject + end + end + + context 'with license', if: Gitlab.ee? do + let(:schema_path) { Rails.root.join(*%w[ee lib ee gitlab ci parsers security validators schemas]) } + + it 'tries to validate against the latest patch version available' do + expect(File).to receive(:file?).with("#{schema_path}/#{report_version}/#{report_type}-report-format.json") + expect(File).to receive(:file?).with("#{schema_path}/#{latest_patch_version}/#{report_type}-report-format.json") + + subject + end + end end context 'and the report is invalid' do diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml index 32746356803..0c2c3ffc664 100644 --- a/spec/lib/gitlab/import_export/all_models.yml +++ b/spec/lib/gitlab/import_export/all_models.yml @@ -487,6 +487,7 @@ project: - requesters - namespace_members - namespace_requesters +- namespace_members_and_requesters - deploy_keys_projects - deploy_keys - users_star_projects diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index 384d3d5c82e..c51e098556c 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -17,6 +17,7 @@ RSpec.describe Group, feature_category: :subgroups do it { is_expected.to have_many(:requesters).dependent(:destroy) } it { is_expected.to have_many(:namespace_requesters) } it { is_expected.to have_many(:members_and_requesters) } + it { is_expected.to have_many(:namespace_members_and_requesters) } it { is_expected.to have_many(:project_group_links).dependent(:destroy) } it { is_expected.to have_many(:shared_projects).through(:project_group_links) } it { is_expected.to have_many(:notification_settings).dependent(:destroy) } @@ -93,6 +94,34 @@ RSpec.describe Group, feature_category: :subgroups do end end + describe '#namespace_members_and_requesters' do + let_it_be(:group) { create(:group) } + let_it_be(:requester) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:invited_member) { create(:group_member, :invited, :owner, group: group) } + + before do + group.request_access(requester) + group.add_developer(developer) + end + + it 'includes the correct users' do + expect(group.namespace_members_and_requesters).to include( + Member.find_by(user: requester), + Member.find_by(user: developer), + Member.find(invited_member.id) + ) + end + + it 'is equivalent to #members_and_requesters' do + expect(group.namespace_members_and_requesters).to match_array group.members_and_requesters + end + + it_behaves_like 'query without source filters' do + subject { group.namespace_members_and_requesters } + end + end + shared_examples 'polymorphic membership relationship' do it do expect(membership.attributes).to include( @@ -139,6 +168,24 @@ RSpec.describe Group, feature_category: :subgroups do it_behaves_like 'member_namespace membership relationship' end + describe '#namespace_members_and_requesters setters' do + let(:requested_at) { Time.current } + let(:user) { create(:user) } + let(:membership) do + group.namespace_members_and_requesters.create!( + user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER + ) + end + + it { expect(membership).to be_instance_of(GroupMember) } + it { expect(membership.user).to eq user } + it { expect(membership.group).to eq group } + it { expect(membership.requested_at).to eq requested_at } + + it_behaves_like 'polymorphic membership relationship' + it_behaves_like 'member_namespace membership relationship' + end + describe '#members & #requesters' do let_it_be(:requester) { create(:user) } let_it_be(:developer) { create(:user) } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index aa284f34c2f..013070f7be5 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -1482,6 +1482,7 @@ RSpec.describe Note do end it "expires cache for note's issue when note is destroyed" do + note.save! expect_expiration(note.noteable) note.destroy! diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 1c71c3d209a..4988ba3b70b 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -112,6 +112,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do it { is_expected.to have_many(:uploads) } it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:members_and_requesters) } + it { is_expected.to have_many(:namespace_members_and_requesters) } it { is_expected.to have_many(:clusters) } it { is_expected.to have_many(:management_clusters).class_name('Clusters::Cluster') } it { is_expected.to have_many(:kubernetes_namespaces) } @@ -402,6 +403,34 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do end end + describe '#namespace_members_and_requesters' do + let_it_be(:project) { create(:project, :public) } + let_it_be(:requester) { create(:user) } + let_it_be(:developer) { create(:user) } + let_it_be(:invited_member) { create(:project_member, :invited, :owner, project: project) } + + before_all do + project.request_access(requester) + project.add_developer(developer) + end + + it 'includes the correct users' do + expect(project.namespace_members_and_requesters).to include( + Member.find_by(user: requester), + Member.find_by(user: developer), + Member.find(invited_member.id) + ) + end + + it 'is equivalent to #project_members' do + expect(project.namespace_members_and_requesters).to match_array(project.members_and_requesters) + end + + it_behaves_like 'query without source filters' do + subject { project.namespace_members_and_requesters } + end + end + shared_examples 'polymorphic membership relationship' do it do expect(membership.attributes).to include( @@ -450,6 +479,25 @@ RSpec.describe Project, factory_default: :keep, feature_category: :projects do it_behaves_like 'member_namespace membership relationship' end + describe '#namespace_members_and_requesters setters' do + let_it_be(:requested_at) { Time.current } + let_it_be(:project) { create(:project) } + let_it_be(:user) { create(:user) } + let_it_be(:membership) do + project.namespace_members_and_requesters.create!( + user: user, requested_at: requested_at, access_level: Gitlab::Access::DEVELOPER + ) + end + + it { expect(membership).to be_instance_of(ProjectMember) } + it { expect(membership.user).to eq user } + it { expect(membership.project).to eq project } + it { expect(membership.requested_at).to eq requested_at } + + it_behaves_like 'polymorphic membership relationship' + it_behaves_like 'member_namespace membership relationship' + end + describe '#members & #requesters' do let_it_be(:project) { create(:project, :public) } let_it_be(:requester) { create(:user) } diff --git a/spec/requests/api/graphql/subscriptions/notes/created_spec.rb b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb new file mode 100644 index 00000000000..7161b17d0a8 --- /dev/null +++ b/spec/requests/api/graphql/subscriptions/notes/created_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe 'Subscriptions::Notes::Created', feature_category: :team_planning do + include GraphqlHelpers + include Graphql::Subscriptions::Notes::Helper + + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + + let(:current_user) { nil } + let(:subscribe) { notes_subscription('workItemNoteCreated', task, current_user) } + let(:response_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteCreated) } + let(:discussion) { graphql_dig_at(response_note, :discussion) } + let(:discussion_notes) { graphql_dig_at(discussion, :notes, :nodes) } + + before do + stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema) + Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks + project.add_guest(guest) + project.add_reporter(reporter) + end + + subject(:response) do + subscription_response do + # this creates note defined with let lazily and triggers the subscription event + new_note + end + end + + context 'when user is unauthorized' do + let(:new_note) { create(:note, noteable: task, project: project, type: 'DiscussionNote') } + + it 'does not receive any data' do + expect(response).to be_nil + end + end + + context 'when user is authorized' do + let(:current_user) { guest } + let(:new_note) { create(:note, noteable: task, project: project, type: 'DiscussionNote') } + + it 'receives created note' do + response + note = Note.find(new_note.id) + + expect(response_note['id']).to eq(note.to_gid.to_s) + expect(discussion['id']).to eq(note.discussion.to_gid.to_s) + expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s]) + end + + context 'when a new note is created as a reply' do + let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') } + + let(:new_note) do + create(:note, noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id) + end + + it 'receives created note' do + response + reply = Note.find(new_note.id) + + expect(response_note['id']).to eq(reply.to_gid.to_s) + expect(discussion['id']).to eq(reply.discussion.to_gid.to_s) + expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s, reply.to_gid.to_s]) + end + end + + context 'when note is confidential' do + let(:current_user) { reporter } + let(:new_note) { create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') } + + context 'and user has permission to read confidential notes' do + it 'receives created note' do + response + confidential_note = Note.find(new_note.id) + + expect(response_note['id']).to eq(confidential_note.to_gid.to_s) + expect(discussion['id']).to eq(confidential_note.discussion.to_gid.to_s) + expect(discussion_notes.pluck('id')).to eq([confidential_note.to_gid.to_s]) + end + + context 'and replying' do + let_it_be(:note, refind: true) do + create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') + end + + let(:new_note) do + create(:note, :confidential, + noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id) + end + + it 'receives created note' do + response + reply = Note.find(new_note.id) + + expect(response_note['id']).to eq(reply.to_gid.to_s) + expect(discussion['id']).to eq(reply.discussion.to_gid.to_s) + expect(discussion_notes.pluck('id')).to eq([note.to_gid.to_s, reply.to_gid.to_s]) + end + end + end + + context 'and user does not have permission to read confidential notes' do + let(:current_user) { guest } + let(:new_note) { create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') } + + it 'does not receive note data' do + response + expect(response_note).to be_nil + end + end + end + end +end diff --git a/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb b/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb new file mode 100644 index 00000000000..d98f1cfe77e --- /dev/null +++ b/spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe 'Subscriptions::Notes::Deleted', feature_category: :team_planning do + include GraphqlHelpers + include Graphql::Subscriptions::Notes::Helper + + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') } + let_it_be(:reply_note, refind: true) do + create(:note, noteable: task, project: project, in_reply_to: note, discussion_id: note.discussion_id) + end + + let(:current_user) { nil } + let(:subscribe) { notes_subscription('workItemNoteDeleted', task, current_user) } + let(:deleted_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteDeleted) } + + before do + stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema) + Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks + project.add_guest(guest) + project.add_reporter(reporter) + end + + subject(:response) do + subscription_response do + note.destroy! + end + end + + context 'when user is unauthorized' do + it 'does not receive any data' do + expect(response).to be_nil + end + end + + context 'when user is authorized' do + let(:current_user) { guest } + + it 'receives note id that is removed' do + expect(deleted_note['id']).to eq(note.to_gid.to_s) + expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s) + expect(deleted_note['lastDiscussionNote']).to be false + end + + context 'when last discussion note is deleted' do + let_it_be(:note, refind: true) { create(:note, noteable: task, project: project, type: 'DiscussionNote') } + + it 'receives note id that is removed' do + expect(deleted_note['id']).to eq(note.to_gid.to_s) + expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s) + expect(deleted_note['lastDiscussionNote']).to be true + end + end + + context 'when note is confidential' do + let_it_be(:note, refind: true) do + create(:note, :confidential, noteable: task, project: project, type: 'DiscussionNote') + end + + it 'receives note id that is removed' do + expect(deleted_note['id']).to eq(note.to_gid.to_s) + expect(deleted_note['discussionId']).to eq(note.discussion.to_gid.to_s) + expect(deleted_note['lastDiscussionNote']).to be true + end + end + end +end diff --git a/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb b/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb new file mode 100644 index 00000000000..25c0a79e7aa --- /dev/null +++ b/spec/requests/api/graphql/subscriptions/notes/updated_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe 'Subscriptions::Notes::Updated', feature_category: :team_planning do + include GraphqlHelpers + include Graphql::Subscriptions::Notes::Helper + + let_it_be(:guest) { create(:user) } + let_it_be(:reporter) { create(:user) } + let_it_be(:project) { create(:project) } + let_it_be(:task) { create(:work_item, :task, project: project) } + let_it_be(:note, refind: true) { create(:note, noteable: task, project: task.project, type: 'DiscussionNote') } + + let(:current_user) { nil } + let(:subscribe) { note_subscription('workItemNoteUpdated', task, current_user) } + let(:updated_note) { graphql_dig_at(graphql_data(response[:result]), :workItemNoteUpdated) } + + before do + stub_const('GitlabSchema', Graphql::Subscriptions::ActionCable::MockGitlabSchema) + Graphql::Subscriptions::ActionCable::MockActionCable.clear_mocks + project.add_guest(guest) + project.add_reporter(reporter) + end + + subject(:response) do + subscription_response do + note.update!(note: 'changing the note body') + end + end + + context 'when user is unauthorized' do + it 'does not receive any data' do + expect(response).to be_nil + end + end + + context 'when user is authorized' do + let(:current_user) { reporter } + + it 'receives updated note data' do + expect(updated_note['id']).to eq(note.to_gid.to_s) + expect(updated_note['body']).to eq('changing the note body') + end + + context 'when note is confidential' do + let_it_be(:note, refind: true) do + create(:note, :confidential, noteable: task, project: task.project, type: 'DiscussionNote') + end + + context 'and user has permission to read confidential notes' do + it 'receives updated note data' do + expect(updated_note['id']).to eq(note.to_gid.to_s) + expect(updated_note['body']).to eq('changing the note body') + end + end + + context 'and user does not have permission to read confidential notes' do + let(:current_user) { guest } + + it 'does not receive updated note data' do + expect(updated_note).to be_nil + end + end + end + end +end diff --git a/spec/serializers/import/github_realtime_repo_entity_spec.rb b/spec/serializers/import/github_realtime_repo_entity_spec.rb new file mode 100644 index 00000000000..7f137366be2 --- /dev/null +++ b/spec/serializers/import/github_realtime_repo_entity_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::GithubRealtimeRepoEntity, feature_category: :importers do + subject(:entity) { described_class.new(project) } + + let(:import_state) { instance_double(ProjectImportState, failed?: false, in_progress?: true) } + let(:import_failures) { [instance_double(ImportFailure, exception_message: 'test error')] } + let(:project) do + instance_double( + Project, + id: 100500, + import_status: 'importing', + import_state: import_state, + import_failures: import_failures, + import_checksums: {} + ) + end + + it 'exposes correct attributes' do + data = entity.as_json + + expect(data.keys).to contain_exactly(:id, :import_status, :stats) + expect(data[:id]).to eq project.id + expect(data[:import_status]).to eq project.import_status + end + + context 'when import stats is failed' do + let(:import_state) { instance_double(ProjectImportState, failed?: true, in_progress?: false) } + + it 'includes import_error' do + data = entity.as_json + + expect(data.keys).to contain_exactly(:id, :import_status, :stats, :import_error) + expect(data[:import_error]).to eq 'test error' + end + end +end diff --git a/spec/serializers/import/github_realtime_repo_serializer_spec.rb b/spec/serializers/import/github_realtime_repo_serializer_spec.rb new file mode 100644 index 00000000000..b656132e332 --- /dev/null +++ b/spec/serializers/import/github_realtime_repo_serializer_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Import::GithubRealtimeRepoSerializer, feature_category: :importers do + subject(:serializer) { described_class.new } + + it '.entity_class' do + expect(described_class.entity_class).to eq(Import::GithubRealtimeRepoEntity) + end + + describe '#represent' do + let(:import_state) { instance_double(ProjectImportState, failed?: false, in_progress?: true) } + let(:project) do + instance_double( + Project, + id: 100500, + import_status: 'importing', + import_state: import_state + ) + end + + let(:expected_data) do + { + id: project.id, + import_status: 'importing', + stats: { fetched: {}, imported: {} } + }.deep_stringify_keys + end + + context 'when a single object is being serialized' do + let(:resource) { project } + + it 'serializes organization object' do + expect(serializer.represent(resource).as_json).to eq expected_data + end + end + + context 'when multiple objects are being serialized' do + let(:count) { 3 } + let(:resource) { Array.new(count, project) } + + it 'serializes array of organizations' do + expect(serializer.represent(resource).as_json).to all(eq(expected_data)) + end + end + end +end diff --git a/spec/serializers/project_import_entity_spec.rb b/spec/serializers/project_import_entity_spec.rb index 94af9f1cbd8..6d292d18ae7 100644 --- a/spec/serializers/project_import_entity_spec.rb +++ b/spec/serializers/project_import_entity_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe ProjectImportEntity do +RSpec.describe ProjectImportEntity, feature_category: :importers do include ImportHelper let_it_be(:project) { create(:project, import_status: :started, import_source: 'namespace/project') } @@ -10,6 +10,10 @@ RSpec.describe ProjectImportEntity do let(:provider_url) { 'https://provider.com' } let(:entity) { described_class.represent(project, provider_url: provider_url) } + before do + create(:import_failure, project: project) + end + describe '#as_json' do subject { entity.as_json } @@ -18,6 +22,19 @@ RSpec.describe ProjectImportEntity do expect(subject[:import_status]).to eq(project.import_status) expect(subject[:human_import_status_name]).to eq(project.human_import_status_name) expect(subject[:provider_link]).to eq(provider_project_link_url(provider_url, project[:import_source])) + expect(subject[:import_error]).to eq(nil) + end + + context 'when import is failed' do + let!(:last_import_failure) { create(:import_failure, project: project, exception_message: 'LAST ERROR') } + + before do + project.import_state.fail_op! + end + + it 'includes only the last import failure' do + expect(subject[:import_error]).to eq(last_import_failure.exception_message) + end end end end diff --git a/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb b/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb new file mode 100644 index 00000000000..5467564a79e --- /dev/null +++ b/spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +# A stub implementation of ActionCable. +# Any methods to support the mock backend have `mock` in the name. +module Graphql + module Subscriptions + module ActionCable + class MockActionCable + class MockChannel + def initialize + @mock_broadcasted_messages = [] + end + + attr_reader :mock_broadcasted_messages + + def stream_from(stream_name, coder: nil, &block) + # Rails uses `coder`, we don't + block ||= ->(msg) { @mock_broadcasted_messages << msg } + MockActionCable.mock_stream_for(stream_name).add_mock_channel(self, block) + end + end + + class MockStream + def initialize + @mock_channels = {} + end + + def add_mock_channel(channel, handler) + @mock_channels[channel] = handler + end + + def mock_broadcast(message) + @mock_channels.each do |channel, handler| + handler && handler.call(message) + end + end + end + + class << self + def clear_mocks + @mock_streams = {} + end + + def server + self + end + + def broadcast(stream_name, message) + stream = @mock_streams[stream_name] + stream && stream.mock_broadcast(message) + end + + def mock_stream_for(stream_name) + @mock_streams[stream_name] ||= MockStream.new + end + + def get_mock_channel + MockChannel.new + end + + def mock_stream_names + @mock_streams.keys + end + end + end + + class MockSchema < GraphQL::Schema + class << self + def find_by_gid(gid) + return unless gid + + if gid.model_class < ApplicationRecord + Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find + elsif gid.model_class.respond_to?(:lazy_find) + gid.model_class.lazy_find(gid.model_id) + else + gid.find + end + end + + def id_from_object(object, _type = nil, _ctx = nil) + unless object.respond_to?(:to_global_id) + # This is an error in our schema and needs to be solved. So raise a + # more meaningful error message + raise "#{object} does not implement `to_global_id`. " \ + "Include `GlobalID::Identification` into `#{object.class}" + end + + object.to_global_id + end + end + + query(::Types::QueryType) + subscription(::Types::SubscriptionType) + + use GraphQL::Subscriptions::ActionCableSubscriptions, action_cable: MockActionCable, action_cable_coder: JSON + end + end + end +end diff --git a/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb b/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb new file mode 100644 index 00000000000..cd5d78cc78b --- /dev/null +++ b/spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +# A stub implementation of ActionCable. +# Any methods to support the mock backend have `mock` in the name. +module Graphql + module Subscriptions + module ActionCable + class MockGitlabSchema < GraphQL::Schema + class << self + def find_by_gid(gid) + return unless gid + + if gid.model_class < ApplicationRecord + Gitlab::Graphql::Loaders::BatchModelLoader.new(gid.model_class, gid.model_id).find + elsif gid.model_class.respond_to?(:lazy_find) + gid.model_class.lazy_find(gid.model_id) + else + gid.find + end + end + + def id_from_object(object, _type = nil, _ctx = nil) + unless object.respond_to?(:to_global_id) + # This is an error in our schema and needs to be solved. So raise a + # more meaningful error message + raise "#{object} does not implement `to_global_id`. " \ + "Include `GlobalID::Identification` into `#{object.class}" + end + + object.to_global_id + end + end + + query(::Types::QueryType) + subscription(::Types::SubscriptionType) + + use GraphQL::Subscriptions::ActionCableSubscriptions, action_cable: MockActionCable, action_cable_coder: JSON + end + end + end +end diff --git a/spec/support/graphql/subscriptions/notes/helper.rb b/spec/support/graphql/subscriptions/notes/helper.rb new file mode 100644 index 00000000000..9a552f9879e --- /dev/null +++ b/spec/support/graphql/subscriptions/notes/helper.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +module Graphql + module Subscriptions + module Notes + module Helper + def subscription_response + subscription_channel = subscribe + yield + subscription_channel.mock_broadcasted_messages.first + end + + def notes_subscription(name, noteable, current_user) + mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel + + query = case name + when 'workItemNoteDeleted' + note_deleted_subscription_query(name, noteable) + when 'workItemNoteUpdated' + note_updated_subscription_query(name, noteable) + when 'workItemNoteCreated' + note_created_subscription_query(name, noteable) + else + raise "Subscription query unknown: #{name}" + end + + GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel }) + + mock_channel + end + + def note_subscription(name, noteable, current_user) + mock_channel = Graphql::Subscriptions::ActionCable::MockActionCable.get_mock_channel + + query = <<~SUBSCRIPTION + subscription { + #{name}(noteableId: \"#{noteable.to_gid}\") { + id + body + } + } + SUBSCRIPTION + + GitlabSchema.execute(query, context: { current_user: current_user, channel: mock_channel }) + + mock_channel + end + + private + + def note_deleted_subscription_query(name, noteable) + <<~SUBSCRIPTION + subscription { + #{name}(noteableId: \"#{noteable.to_gid}\") { + id + discussionId + lastDiscussionNote + } + } + SUBSCRIPTION + end + + def note_created_subscription_query(name, noteable) + <<~SUBSCRIPTION + subscription { + #{name}(noteableId: \"#{noteable.to_gid}\") { + id + discussion { + id + notes { + nodes { + id + } + } + } + } + } + SUBSCRIPTION + end + + def note_updated_subscription_query(name, noteable) + <<~SUBSCRIPTION + subscription { + #{name}(noteableId: \"#{noteable.to_gid}\") { + id + body + } + } + SUBSCRIPTION + end + end + end + end +end diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml index 240f9cac103..c43dd88e3fe 100644 --- a/spec/support/rspec_order_todo.yml +++ b/spec/support/rspec_order_todo.yml @@ -165,7 +165,6 @@ - './ee/spec/controllers/projects/vulnerability_feedback_controller_spec.rb' - './ee/spec/controllers/registrations/company_controller_spec.rb' - './ee/spec/controllers/registrations/groups_projects_controller_spec.rb' -- './ee/spec/controllers/registrations/verification_controller_spec.rb' - './ee/spec/controllers/repositories/git_http_controller_spec.rb' - './ee/spec/controllers/security/dashboard_controller_spec.rb' - './ee/spec/controllers/security/projects_controller_spec.rb' diff --git a/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb b/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb new file mode 100644 index 00000000000..949eb4fb643 --- /dev/null +++ b/spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'graphql notes subscriptions' do + describe '#resolve' do + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:work_item) { create(:work_item, :task) } + let_it_be(:note) { create(:note, noteable: work_item, project: work_item.project) } + let_it_be(:current_user) { work_item.author } + let_it_be(:noteable_id) { work_item.to_gid } + + subject { resolver.resolve_with_support(noteable_id: noteable_id) } + + context 'on initial subscription' do + let(:resolver) do + resolver_instance(described_class, ctx: { current_user: current_user }, subscription_update: false) + end + + it 'returns nil' do + expect(subject).to eq(nil) + end + + context 'when user is unauthorized' do + let(:current_user) { unauthorized_user } + + it 'raises an exception' do + expect { subject }.to raise_error(GraphQL::ExecutionError) + end + end + + context 'when work_item does not exist' do + let(:noteable_id) { GlobalID.parse("gid://gitlab/WorkItem/#{non_existing_record_id}") } + + it 'raises an exception' do + expect { subject }.to raise_error(GraphQL::ExecutionError) + end + end + end + + context 'on subscription updates' do + let(:resolver) do + resolver_instance(described_class, obj: note, ctx: { current_user: current_user }, subscription_update: true) + end + + it 'returns the resolved object' do + expect(subject).to eq(note) + end + + context 'when user is unauthorized' do + let(:current_user) { unauthorized_user } + + it 'unsubscribes the user' do + # GraphQL::Execution::Execute::Skip is returned when unsubscribed + expect(subject).to be_an(GraphQL::Execution::Execute::Skip) + end + end + end + end +end diff --git a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb index 24a2c0a48f7..9d54241aa7f 100644 --- a/spec/tasks/gitlab/db/lock_writes_rake_spec.rb +++ b/spec/tasks/gitlab/db/lock_writes_rake_spec.rb @@ -14,7 +14,9 @@ RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, featu end let(:table_locker) { instance_double(Gitlab::Database::TablesLocker) } - let(:logger) { instance_double(Logger) } + let(:logger) { instance_double(Logger, level: nil) } + let(:dry_run) { false } + let(:verbose) { false } before do allow(Logger).to receive(:new).with($stdout).and_return(logger) @@ -24,7 +26,10 @@ RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, featu end shared_examples "call table locker" do |method| + let(:log_level) { verbose ? Logger::INFO : Logger::WARN } + it "creates TablesLocker with dry run set and calls #{method}" do + expect(logger).to receive(:level=).with(log_level) expect(table_locker).to receive(method) run_rake_task("gitlab:db:#{method}") @@ -57,6 +62,30 @@ RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, featu include_examples "call table locker", :lock_writes end + + context 'when environment sets VERBOSE to true' do + let(:verbose) { true } + + before do + stub_env('VERBOSE', 'true') + end + + include_examples "call table locker", :lock_writes + end + + context 'when environment sets VERBOSE to false' do + let(:verbose) { false } + + before do + stub_env('VERBOSE', 'false') + end + + include_examples "call table locker", :lock_writes + end + + context 'when environment does not define VERBOSE' do + include_examples "call table locker", :lock_writes + end end describe 'unlock_writes' do @@ -71,8 +100,6 @@ RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, featu end context 'when environment sets DRY_RUN to false' do - let(:dry_run) { false } - before do stub_env('DRY_RUN', 'false') end @@ -81,9 +108,31 @@ RSpec.describe 'gitlab:db:lock_writes', :reestablished_active_record_base, featu end context 'when environment does not define DRY_RUN' do - let(:dry_run) { false } - include_examples "call table locker", :unlock_writes end + + context 'when environment sets VERBOSE to true' do + let(:verbose) { true } + + before do + stub_env('VERBOSE', 'true') + end + + include_examples "call table locker", :lock_writes + end + + context 'when environment sets VERBOSE to false' do + let(:verbose) { false } + + before do + stub_env('VERBOSE', 'false') + end + + include_examples "call table locker", :lock_writes + end + + context 'when environment does not define VERBOSE' do + include_examples "call table locker", :lock_writes + end end end |