diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 21:09:52 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2024-01-17 21:09:52 +0300 |
commit | 003efb27fc4d7d0571979553c602fccfbf5ad0c2 (patch) | |
tree | 721ec9af57108c73fc5c4c7a06e996800ead367e /spec | |
parent | 78a5f872de316860ccd7a983c10805bf6c6b771c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
10 files changed, 135 insertions, 62 deletions
diff --git a/spec/frontend/organizations/shared/components/projects_view_spec.js b/spec/frontend/organizations/shared/components/projects_view_spec.js index 3cc71927bfa..c406ba2cd47 100644 --- a/spec/frontend/organizations/shared/components/projects_view_spec.js +++ b/spec/frontend/organizations/shared/components/projects_view_spec.js @@ -2,19 +2,18 @@ import VueApollo from 'vue-apollo'; import Vue from 'vue'; import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; import ProjectsView from '~/organizations/shared/components/projects_view.vue'; +import projectsQuery from '~/organizations/shared/graphql/queries/projects.query.graphql'; import { formatProjects } from '~/organizations/shared/utils'; -import resolvers from '~/organizations/shared/graphql/resolvers'; import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue'; import { createAlert } from '~/alert'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { organizationProjects } from '~/organizations/mock_data'; +import { organizationProjects as nodes, pageInfo, pageInfoEmpty } from '~/organizations/mock_data'; jest.mock('~/alert'); Vue.use(VueApollo); -jest.useFakeTimers(); describe('ProjectsView', () => { let wrapper; @@ -23,14 +22,29 @@ describe('ProjectsView', () => { const defaultProvide = { projectsEmptyStateSvgPath: 'illustrations/empty-state/empty-projects-md.svg', newProjectPath: '/projects/new', + organizationGid: 'gid://gitlab/Organizations::Organization/1', }; const defaultPropsData = { listItemClass: 'gl-px-5', }; - const createComponent = ({ mockResolvers = resolvers, propsData = {} } = {}) => { - mockApollo = createMockApollo([], mockResolvers); + const projects = { + nodes, + pageInfo, + }; + + const successHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + projects, + }, + }, + }); + + const createComponent = ({ handler = successHandler, propsData = {} } = {}) => { + mockApollo = createMockApollo([[projectsQuery, handler]]); wrapper = shallowMountExtended(ProjectsView, { apolloProvider: mockApollo, @@ -42,45 +56,45 @@ describe('ProjectsView', () => { }); }; + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findProjectsList = () => wrapper.findComponent(ProjectsList); + afterEach(() => { mockApollo = null; }); describe('when API call is loading', () => { - beforeEach(() => { - const mockResolvers = { - Query: { - organization: jest.fn().mockReturnValueOnce(new Promise(() => {})), - }, - }; - - createComponent({ mockResolvers }); - }); - it('renders loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + createComponent(); + + expect(findLoadingIcon().exists()).toBe(true); }); }); describe('when API call is successful', () => { describe('when there are no projects', () => { - it('renders empty state without buttons by default', async () => { - const mockResolvers = { - Query: { - organization: jest.fn().mockResolvedValueOnce({ - projects: { nodes: [] }, - }), + const emptyHandler = jest.fn().mockResolvedValue({ + data: { + organization: { + id: defaultProvide.organizationGid, + projects: { + nodes: [], + pageInfo: pageInfoEmpty, + }, }, - }; - createComponent({ mockResolvers }); + }, + }); + + it('renders empty state without buttons by default', async () => { + createComponent({ handler: emptyHandler }); - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + expect(findEmptyState().props()).toMatchObject({ title: "You don't have any projects yet.", description: - 'Projects are where you can store your code, access issues, wiki, and other features of Gitlab.', + 'Projects are where you can store your code, access issues, wiki, and other features of GitLab.', svgHeight: 144, svgPath: defaultProvide.projectsEmptyStateSvgPath, primaryButtonLink: null, @@ -90,19 +104,14 @@ describe('ProjectsView', () => { describe('when `shouldShowEmptyStateButtons` is `true` and `projectsEmptyStateSvgPath` is set', () => { it('renders empty state with buttons', async () => { - const mockResolvers = { - Query: { - organization: jest.fn().mockResolvedValueOnce({ - projects: { nodes: [] }, - }), - }, - }; - createComponent({ mockResolvers, propsData: { shouldShowEmptyStateButtons: true } }); + createComponent({ + handler: emptyHandler, + propsData: { shouldShowEmptyStateButtons: true }, + }); - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(GlEmptyState).props()).toMatchObject({ + expect(findEmptyState().props()).toMatchObject({ primaryButtonLink: defaultProvide.newProjectPath, primaryButtonText: 'New project', }); @@ -116,11 +125,10 @@ describe('ProjectsView', () => { }); it('renders `ProjectsList` component and passes correct props', async () => { - jest.runAllTimers(); await waitForPromises(); - expect(wrapper.findComponent(ProjectsList).props()).toEqual({ - projects: formatProjects(organizationProjects.nodes), + expect(findProjectsList().props()).toMatchObject({ + projects: formatProjects(nodes), showProjectIcon: true, listItemClass: defaultPropsData.listItemClass, }); @@ -132,13 +140,7 @@ describe('ProjectsView', () => { const error = new Error(); beforeEach(() => { - const mockResolvers = { - Query: { - organization: jest.fn().mockRejectedValueOnce(error), - }, - }; - - createComponent({ mockResolvers }); + createComponent({ handler: jest.fn().mockRejectedValue(error) }); }); it('displays error alert', async () => { diff --git a/spec/frontend/organizations/shared/utils_spec.js b/spec/frontend/organizations/shared/utils_spec.js index 778a18ab2bc..d8d5279b670 100644 --- a/spec/frontend/organizations/shared/utils_spec.js +++ b/spec/frontend/organizations/shared/utils_spec.js @@ -5,21 +5,19 @@ import { organizationProjects, organizationGroups } from '~/organizations/mock_d describe('formatProjects', () => { it('correctly formats the projects', () => { - const [firstMockProject] = organizationProjects.nodes; - const formattedProjects = formatProjects(organizationProjects.nodes); + const [firstMockProject] = organizationProjects; + const formattedProjects = formatProjects(organizationProjects); const [firstFormattedProject] = formattedProjects; expect(firstFormattedProject).toMatchObject({ id: getIdFromGraphQLId(firstMockProject.id), name: firstMockProject.nameWithNamespace, - permissions: { - projectAccess: { - accessLevel: firstMockProject.accessLevel.integerValue, - }, - }, + mergeRequestsAccessLevel: firstMockProject.mergeRequestsAccessLevel.stringValue, + issuesAccessLevel: firstMockProject.issuesAccessLevel.stringValue, + forkingAccessLevel: firstMockProject.forkingAccessLevel.stringValue, availableActions: [ACTION_EDIT, ACTION_DELETE], }); - expect(formattedProjects.length).toBe(organizationProjects.nodes.length); + expect(formattedProjects.length).toBe(organizationProjects.length); }); }); diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap index 6af9e38192e..009d9c959cb 100644 --- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap +++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap @@ -18,7 +18,6 @@ exports[`packages_list_app renders 1`] = ` alt="" class="gl-dark-invert-keep-hue gl-max-w-full" height="144" - role="img" src="helpSvg" /> </div> diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap index d6c3d98efa3..89b55b69893 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap +++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap @@ -56,7 +56,7 @@ exports[`PypiInstallation renders all the messages 1`] = ` class="gl-new-dropdown-contents gl-new-dropdown-contents-with-scrim-overlay" id="reference-2" role="listbox" - tabindex="-1" + tabindex="0" > <li aria-hidden="true" diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js index a5a5a43effe..9900baa6bdb 100644 --- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js +++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js @@ -36,6 +36,8 @@ describe('ProjectsListItem', () => { }; const findAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); + const findMergeRequestsLink = () => + wrapper.findByRole('link', { name: ProjectsListItem.i18n.mergeRequests }); const findIssuesLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.issues }); const findForksLink = () => wrapper.findByRole('link', { name: ProjectsListItem.i18n.forks }); const findProjectTopics = () => wrapper.findByTestId('project-topics'); @@ -148,6 +150,42 @@ describe('ProjectsListItem', () => { }); }); + describe('when merge requests are enabled', () => { + it('renders merge requests count', () => { + createComponent({ + propsData: { + project: { + ...project, + openMergeRequestsCount: 5, + }, + }, + }); + + const mergeRequestsLink = findMergeRequestsLink(); + const tooltip = getBinding(mergeRequestsLink.element, 'gl-tooltip'); + + expect(tooltip.value).toBe(ProjectsListItem.i18n.mergeRequests); + expect(mergeRequestsLink.attributes('href')).toBe(`${project.webUrl}/-/merge_requests`); + expect(mergeRequestsLink.text()).toBe('5'); + expect(mergeRequestsLink.findComponent(GlIcon).props('name')).toBe('git-merge'); + }); + }); + + describe('when merge requests are not enabled', () => { + it('does not render merge requests count', () => { + createComponent({ + propsData: { + project: { + ...project, + mergeRequestsAccessLevel: FEATURABLE_DISABLED, + }, + }, + }); + + expect(findMergeRequestsLink().exists()).toBe(false); + }); + }); + describe('when issues are enabled', () => { it('renders issues count', () => { createComponent(); diff --git a/spec/helpers/organizations/organization_helper_spec.rb b/spec/helpers/organizations/organization_helper_spec.rb index 0f2f4ed1b54..0535a860b94 100644 --- a/spec/helpers/organizations/organization_helper_spec.rb +++ b/spec/helpers/organizations/organization_helper_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:organization_detail) { build_stubbed(:organization_detail, description_html: '<em>description</em>') } let_it_be(:organization) { organization_detail.organization } + let_it_be(:organization_gid) { 'gid://gitlab/Organizations::Organization/1' } let_it_be(:new_group_path) { '/groups/new' } let_it_be(:new_project_path) { '/projects/new' } let_it_be(:organizations_empty_state_svg_path) { 'illustrations/empty-state/empty-organizations-md.svg' } @@ -15,6 +16,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do let_it_be(:preview_markdown_organizations_path) { '/-/organizations/preview_markdown' } before do + allow(organization).to receive(:to_global_id).and_return(organization_gid) allow(helper).to receive(:new_group_path).and_return(new_group_path) allow(helper).to receive(:new_project_path).and_return(new_project_path) allow(helper).to receive(:image_path).with(organizations_empty_state_svg_path) @@ -41,6 +43,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do ) ).to eq( { + 'organization_gid' => organization_gid, 'organization' => { 'id' => organization.id, 'name' => organization.name, @@ -66,10 +69,11 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do it 'returns expected json' do expect( Gitlab::Json.parse( - helper.organization_groups_and_projects_app_data + helper.organization_groups_and_projects_app_data(organization) ) ).to eq( { + 'organization_gid' => organization_gid, 'new_group_path' => new_group_path, 'new_project_path' => new_project_path, 'groups_empty_state_svg_path' => groups_empty_state_svg_path, @@ -139,7 +143,7 @@ RSpec.describe Organizations::OrganizationHelper, feature_category: :cell do it 'returns expected json' do expect(Gitlab::Json.parse(helper.organization_user_app_data(organization))).to eq( { - 'organization_gid' => organization.to_global_id.to_s, + 'organization_gid' => organization_gid, 'paths' => { 'admin_user' => admin_user_path(:id) } diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb index 7fc58140fb6..01a8ca055a5 100644 --- a/spec/requests/api/members_spec.rb +++ b/spec/requests/api/members_spec.rb @@ -346,6 +346,24 @@ RSpec.describe API::Members, feature_category: :groups_and_projects do expect(json_response['access_level']).to eq(Member::DEVELOPER) end + it 'returns the error message if there was an error adding the member to the group' do + error_message = 'Test CreateService Error Message' + allow_next_instance_of(::Members::CreateService) do |service| + expect(service).to receive(:execute).and_return(status: :error, message: error_message) + allow(service).to receive(:single_member).and_return( + instance_double(Member, invalid?: false) + ) + end + + expect do + post api("/#{source_type.pluralize}/#{source.id}/members", maintainer), + params: { user_id: stranger.id, access_level: Member::DEVELOPER } + end.not_to change { source.members.count } + expect(response).to have_gitlab_http_status(:bad_request) + expect(json_response['status']).to eq('error') + expect(json_response['message']).to eq(error_message) + end + context 'with invite_source considerations', :snowplow do let(:params) { { user_id: stranger.id, access_level: Member::DEVELOPER } } diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index c08b40e9528..f019e7f4046 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -31,8 +31,10 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ context 'when the current user does not have permission to create members' do let(:current_user) { create(:user) } - it 'raises a Gitlab::Access::AccessDeniedError' do - expect { execute_service }.to raise_error(Gitlab::Access::AccessDeniedError) + it 'returns an unauthorized http_status' do + expect(execute_service[:status]).to eq(:error) + # this is expected by API::Helpers::MembersHelpers#add_single_member_by_user_id + expect(execute_service[:http_status]).to eq(:unauthorized) end context 'when a project maintainer attempts to add owners' do @@ -56,6 +58,15 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ end end + context 'when trying to create a Membership with invalid params' do + let(:additional_params) { Hash[invite_source: '_invite_source_', expires_at: 3.days.ago] } + + it 'returns an error response' do + expect(execute_service[:status]).to eq(:error) + expect(execute_service[:http_status]).to be_nil + end + end + context 'when passing valid parameters' do it 'adds a user to members' do expect(execute_service[:status]).to eq(:success) @@ -251,6 +262,7 @@ RSpec.describe Members::CreateService, :aggregate_failures, :clean_gitlab_redis_ it 'does not update the member' do expect(execute_service[:status]).to eq(:error) + expect(execute_service[:http_status]).to eq(:unauthorized) expect(execute_service[:message]).to eq("#{project_bot.username}: not authorized to update member") expect(Onboarding::Progress.completed?(source.namespace, :user_added)).to be(false) end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 01d6642e814..43b60fad67d 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -139,6 +139,7 @@ RSpec.shared_examples_for "member creation" do expect(source.reload).to have_user(project_bot) expect(member).to be_persisted expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(member.errors.added?(:base, :unauthorized)).to eq(true) expect(member.errors.full_messages).to include(/not authorized to update member/) end end @@ -169,6 +170,7 @@ RSpec.shared_examples_for "member creation" do expect(member).not_to be_persisted expect(source).not_to have_user(user) + expect(member.errors.added?(:base, :unauthorized)).to eq(true) expect(member.errors.full_messages).to include(/not authorized to create member/) end end diff --git a/spec/support/shared_examples/requests/api/members_shared_examples.rb b/spec/support/shared_examples/requests/api/members_shared_examples.rb index 9136f60eb93..135a984f8f9 100644 --- a/spec/support/shared_examples/requests/api/members_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/members_shared_examples.rb @@ -14,7 +14,7 @@ end RSpec.shared_examples 'a 403 response when user does not have rights to manage members of a specific access level' do it 'returns 403' do - route + expect { route }.not_to change { Member.count } expect(response).to have_gitlab_http_status(:forbidden) end |