Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb762
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb3
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js56
-rw-r--r--spec/frontend/invite_members/mock_data/api_response_data.js2
-rw-r--r--spec/graphql/types/notes/deleted_note_type_spec.rb15
-rw-r--r--spec/lib/gitlab/ci/artifacts/logger_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/parsers/security/validators/schema_validator_spec.rb22
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/group_spec.rb47
-rw-r--r--spec/models/note_spec.rb1
-rw-r--r--spec/models/project_spec.rb48
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/created_spec.rb118
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/deleted_spec.rb72
-rw-r--r--spec/requests/api/graphql/subscriptions/notes/updated_spec.rb67
-rw-r--r--spec/serializers/import/github_realtime_repo_entity_spec.rb39
-rw-r--r--spec/serializers/import/github_realtime_repo_serializer_spec.rb48
-rw-r--r--spec/serializers/project_import_entity_spec.rb19
-rw-r--r--spec/support/graphql/subscriptions/action_cable/mock_action_cable.rb100
-rw-r--r--spec/support/graphql/subscriptions/action_cable/mock_gitlab_schema.rb41
-rw-r--r--spec/support/graphql/subscriptions/notes/helper.rb94
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_examples/graphql/subscriptions/notes/notes_subscription_shared_examples.rb58
-rw-r--r--spec/tasks/gitlab/db/lock_writes_rake_spec.rb59
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