diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 21:15:03 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-17 21:15:03 +0300 |
commit | 7468e26195700e0f13081d49377a4ec03f29f6b7 (patch) | |
tree | da070b2fcb72105207f4d2972f129dcb5183c993 /spec | |
parent | 843b1e9386fbda332839d23246d0ee2382fb7f4c (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
24 files changed, 545 insertions, 547 deletions
diff --git a/spec/factories/deployments.rb b/spec/factories/deployments.rb index 7d80ab7b15d..db56b754623 100644 --- a/spec/factories/deployments.rb +++ b/spec/factories/deployments.rb @@ -69,5 +69,9 @@ FactoryBot.define do deployment.succeed! end end + + trait :with_bridge do + deployable { association :ci_bridge, environment: environment.name, pipeline: association(:ci_pipeline, project: environment.project) } + end end end diff --git a/spec/finders/ci/catalog/resources/versions_finder_spec.rb b/spec/finders/ci/catalog/resources/versions_finder_spec.rb index b2418aa45dd..b541b84f198 100644 --- a/spec/finders/ci/catalog/resources/versions_finder_spec.rb +++ b/spec/finders/ci/catalog/resources/versions_finder_spec.rb @@ -22,13 +22,13 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli end.not_to exceed_query_limit(control_count) end - context 'when the user is not authorized for any catalog resource' do + context 'when the user is not authorized' do it 'returns empty response' do is_expected.to be_empty end end - describe 'versions' do + context 'when the user is authorized' do before_all do resource1.project.add_guest(current_user) end @@ -74,7 +74,7 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli end end - describe 'latest versions' do + context 'when `latest` parameter is true' do before_all do resource1.project.add_guest(current_user) resource2.project.add_guest(current_user) @@ -85,22 +85,5 @@ RSpec.describe Ci::Catalog::Resources::VersionsFinder, feature_category: :pipeli it 'returns the latest version for each authorized catalog resource' do expect(execute).to match_array([v1_1, v2_1]) end - - context 'when one catalog resource does not have versions' do - it 'returns the latest version of only the catalog resource with versions' do - resource1.versions.delete_all(:delete_all) - - is_expected.to match_array([v2_1]) - end - end - - context 'when no catalog resource has versions' do - it 'returns empty response' do - resource1.versions.delete_all(:delete_all) - resource2.versions.delete_all(:delete_all) - - is_expected.to be_empty - end - end end end diff --git a/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js b/spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js index f7d82d2b662..953e6173662 100644 --- a/spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js +++ b/spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js @@ -1,5 +1,5 @@ import { reportIssues, parsedReportIssues } from 'jest/ci/reports/codequality_report/mock_data'; -import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser'; +import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/utils/codequality_parser'; describe('Codequality report store utils', () => { let result; diff --git a/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js new file mode 100644 index 00000000000..5f530f2c3be --- /dev/null +++ b/spec/frontend/import_entities/import_groups/components/import_history_link_spec.js @@ -0,0 +1,34 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLink } from '@gitlab/ui'; + +import ImportHistoryLink from '~/import_entities/import_groups/components/import_history_link.vue'; + +describe('import history link', () => { + let wrapper; + + const mockHistoryPath = '/import/history'; + + const createComponent = ({ props } = {}) => { + wrapper = shallowMount(ImportHistoryLink, { + propsData: { + historyPath: mockHistoryPath, + ...props, + }, + }); + }; + + const findGlLink = () => wrapper.findComponent(GlLink); + + it('renders link with href', () => { + const mockId = 174; + + createComponent({ + props: { + id: mockId, + }, + }); + + expect(findGlLink().text()).toBe('View details'); + expect(findGlLink().attributes('href')).toBe('/import/history?bulk_import_id=174'); + }); +}); diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js index 8cfc9d3ccd7..4141bded502 100644 --- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js @@ -13,6 +13,7 @@ import { STATUSES } from '~/import_entities/constants'; import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants'; import ImportTable from '~/import_entities/import_groups/components/import_table.vue'; import ImportStatus from '~/import_entities/import_groups/components/import_status.vue'; +import ImportHistoryLink from '~/import_entities/import_groups/components//import_history_link.vue'; import importGroupsMutation from '~/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; @@ -179,6 +180,23 @@ describe('import table', () => { expect(findAllImportStatuses().wrappers.map((w) => w.text())).toEqual(expectedStatuses); }); + it('renders import history link for imports with id', async () => { + createComponent({ + bulkImportSourceGroups: () => ({ + nodes: FAKE_GROUPS, + pageInfo: FAKE_PAGE_INFO, + versionValidation: FAKE_VERSION_VALIDATION, + }), + }); + await waitForPromises(); + + const importHistoryLinks = wrapper.findAllComponents(ImportHistoryLink); + + expect(importHistoryLinks).toHaveLength(2); + expect(importHistoryLinks.at(0).props('id')).toBe(FAKE_GROUPS[1].id); + expect(importHistoryLinks.at(1).props('id')).toBe(FAKE_GROUPS[3].id); + }); + it('correctly maintains root namespace as last import target', async () => { createComponent({ bulkImportSourceGroups: () => ({ diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js index e7ed712c309..30380bcf6a5 100644 --- a/spec/frontend/organizations/users/components/app_spec.js +++ b/spec/frontend/organizations/users/components/app_spec.js @@ -4,10 +4,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import { createAlert } from '~/alert'; +import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants'; import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql'; import OrganizationsUsersApp from '~/organizations/users/components/app.vue'; import OrganizationsUsersView from '~/organizations/users/components/users_view.vue'; -import { MOCK_ORGANIZATION_GID, MOCK_USERS, MOCK_USERS_FORMATTED } from '../mock_data'; +import { + MOCK_ORGANIZATION_GID, + MOCK_USERS, + MOCK_USERS_FORMATTED, + MOCK_PAGE_INFO, +} from '../mock_data'; jest.mock('~/alert'); @@ -16,10 +22,11 @@ Vue.use(VueApollo); const mockError = new Error(); const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {})); -const successfulResolver = (nodes) => - jest.fn().mockResolvedValue({ - data: { organization: { id: 1, organizationUsers: { nodes } } }, +const successfulResolver = (nodes, pageInfo = {}) => { + return jest.fn().mockResolvedValue({ + data: { organization: { id: 1, organizationUsers: { nodes, pageInfo } } }, }); +}; const errorResolver = jest.fn().mockRejectedValueOnce(mockError); describe('OrganizationsUsersApp', () => { @@ -44,12 +51,13 @@ describe('OrganizationsUsersApp', () => { const findOrganizationUsersView = () => wrapper.findComponent(OrganizationsUsersView); describe.each` - description | mockResolver | loading | userData | error - ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false} - ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${false} - ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false} - ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true} - `('$description', ({ mockResolver, loading, userData, error }) => { + description | mockResolver | loading | userData | pageInfo | error + ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${{}} | ${false} + ${'when API returns successful with one page of results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${{}} | ${false} + ${'when API returns successful with multiple pages of results'} | ${successfulResolver(MOCK_USERS, MOCK_PAGE_INFO)} | ${false} | ${MOCK_USERS_FORMATTED} | ${MOCK_PAGE_INFO} | ${false} + ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${{}} | ${false} + ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${{}} | ${true} + `('$description', ({ mockResolver, loading, userData, pageInfo, error }) => { beforeEach(async () => { createComponent(mockResolver); await waitForPromises(); @@ -63,6 +71,10 @@ describe('OrganizationsUsersApp', () => { expect(findOrganizationUsersView().props('users')).toStrictEqual(userData); }); + it('renders OrganizationUsersView with correct pageInfo prop', () => { + expect(findOrganizationUsersView().props('pageInfo')).toStrictEqual(pageInfo); + }); + it(`does ${error ? '' : 'not '}render an error message`, () => { return error ? expect(createAlert).toHaveBeenCalledWith({ @@ -74,4 +86,40 @@ describe('OrganizationsUsersApp', () => { : expect(createAlert).not.toHaveBeenCalled(); }); }); + + describe('Pagination', () => { + const mockResolver = successfulResolver(MOCK_USERS, MOCK_PAGE_INFO); + + beforeEach(async () => { + createComponent(mockResolver); + await waitForPromises(); + mockResolver.mockClear(); + }); + + it('handleNextPage calls organizationUsersQuery with correct pagination data', async () => { + findOrganizationUsersView().vm.$emit('next'); + await waitForPromises(); + + expect(mockResolver).toHaveBeenCalledWith({ + id: MOCK_ORGANIZATION_GID, + before: '', + after: MOCK_PAGE_INFO.endCursor, + first: ORGANIZATION_USERS_PER_PAGE, + last: null, + }); + }); + + it('handlePrevPage calls organizationUsersQuery with correct pagination data', async () => { + findOrganizationUsersView().vm.$emit('prev'); + await waitForPromises(); + + expect(mockResolver).toHaveBeenCalledWith({ + id: MOCK_ORGANIZATION_GID, + before: MOCK_PAGE_INFO.startCursor, + after: '', + first: ORGANIZATION_USERS_PER_PAGE, + last: null, + }); + }); + }); }); diff --git a/spec/frontend/organizations/users/components/users_view_spec.js b/spec/frontend/organizations/users/components/users_view_spec.js index 5f47e18edd8..d665c60d425 100644 --- a/spec/frontend/organizations/users/components/users_view_spec.js +++ b/spec/frontend/organizations/users/components/users_view_spec.js @@ -1,8 +1,8 @@ -import { GlLoadingIcon } from '@gitlab/ui'; +import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import UsersView from '~/organizations/users/components/users_view.vue'; import UsersTable from '~/vue_shared/components/users_table/users_table.vue'; -import { MOCK_PATHS, MOCK_USERS_FORMATTED } from '../mock_data'; +import { MOCK_PATHS, MOCK_USERS_FORMATTED, MOCK_PAGE_INFO } from '../mock_data'; describe('UsersView', () => { let wrapper; @@ -12,6 +12,7 @@ describe('UsersView', () => { propsData: { loading: false, users: MOCK_USERS_FORMATTED, + pageInfo: MOCK_PAGE_INFO, ...props, }, provide: { @@ -22,6 +23,7 @@ describe('UsersView', () => { const findGlLoading = () => wrapper.findComponent(GlLoadingIcon); const findUsersTable = () => wrapper.findComponent(UsersTable); + const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination); describe.each` description | loading | usersData @@ -40,5 +42,27 @@ describe('UsersView', () => { it(`does ${!loading ? '' : 'not '}render users table`, () => { expect(findUsersTable().exists()).toBe(!loading); }); + + it(`does ${!loading ? '' : 'not '}render pagination`, () => { + expect(findGlKeysetPagination().exists()).toBe(Boolean(!loading)); + }); + }); + + describe('Pagination', () => { + beforeEach(() => { + createComponent(); + }); + + it('@next event forwards up to the parent component', () => { + findGlKeysetPagination().vm.$emit('next'); + + expect(wrapper.emitted('next')).toHaveLength(1); + }); + + it('@prev event forwards up to the parent component', () => { + findGlKeysetPagination().vm.$emit('prev'); + + expect(wrapper.emitted('prev')).toHaveLength(1); + }); }); }); diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js index b6ca00bed79..16b3ec3bbcb 100644 --- a/spec/frontend/organizations/users/mock_data.js +++ b/spec/frontend/organizations/users/mock_data.js @@ -40,3 +40,11 @@ export const MOCK_USERS = [ export const MOCK_USERS_FORMATTED = MOCK_USERS.map(({ badges, user }) => { return { ...user, badges, email: user.publicEmail }; }); + +export const MOCK_PAGE_INFO = { + startCursor: 'aaaa', + endCursor: 'bbbb', + hasNextPage: true, + hasPreviousPage: true, + __typename: 'PageInfo', +}; diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js index be50858bc88..3db77469d6b 100644 --- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js +++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js @@ -1,16 +1,23 @@ import { GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import axios from '~/lib/utils/axios_utils'; import waitForPromises from 'helpers/wait_for_promises'; import { HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import { getParameterValues } from '~/lib/utils/url_utility'; + +import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue'; import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue'; import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; -import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue'; -import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + getParameterValues: jest.fn().mockReturnValue([]), +})); describe('BulkImportsHistoryApp', () => { - const API_URL = '/api/v4/bulk_imports/entities'; + const BULK_IMPORTS_API_URL = '/api/v4/bulk_imports/entities'; const DEFAULT_HEADERS = { 'x-page': 1, @@ -73,14 +80,14 @@ describe('BulkImportsHistoryApp', () => { } const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync); + const findPaginationBar = () => wrapper.findComponent(PaginationBar); beforeEach(() => { gon.api_version = 'v4'; - }); - beforeEach(() => { + getParameterValues.mockReturnValue([]); mock = new MockAdapter(axios); - mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS); + mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS); }); afterEach(() => { @@ -94,9 +101,9 @@ describe('BulkImportsHistoryApp', () => { }); it('renders empty state when no data is available', async () => { - mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS); + mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS); createComponent(); - await axios.waitForAll(); + await waitForPromises(); expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); expect(wrapper.findComponent(GlEmptyState).exists()).toBe(true); @@ -104,7 +111,7 @@ describe('BulkImportsHistoryApp', () => { it('renders table with data when history is available', async () => { createComponent(); - await axios.waitForAll(); + await waitForPromises(); const table = wrapper.findComponent(GlTableLite); expect(table.exists()).toBe(true); @@ -116,26 +123,46 @@ describe('BulkImportsHistoryApp', () => { const NEW_PAGE = 4; createComponent(); - await axios.waitForAll(); + await waitForPromises(); mock.resetHistory(); - wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE); - await axios.waitForAll(); + findPaginationBar().vm.$emit('set-page', NEW_PAGE); + await waitForPromises(); expect(mock.history.get.length).toBe(1); expect(mock.history.get[0].params).toStrictEqual(expect.objectContaining({ page: NEW_PAGE })); }); }); + describe('when filtering by bulk_import_id param', () => { + const mockId = 2; + + beforeEach(() => { + getParameterValues.mockReturnValue([mockId]); + }); + + it('makes a request to bulk_import_history endpoint', async () => { + createComponent(); + await waitForPromises(); + + expect(mock.history.get.length).toBe(1); + expect(mock.history.get[0].url).toBe(`/api/v4/bulk_imports/${mockId}/entities`); + expect(mock.history.get[0].params).toStrictEqual({ + page: 1, + per_page: 20, + }); + }); + }); + it('changes page size when requested by pagination bar', async () => { const NEW_PAGE_SIZE = 4; createComponent(); - await axios.waitForAll(); + await waitForPromises(); mock.resetHistory(); - wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE); - await axios.waitForAll(); + findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE); + await waitForPromises(); expect(mock.history.get.length).toBe(1); expect(mock.history.get[0].params).toStrictEqual( @@ -146,15 +173,14 @@ describe('BulkImportsHistoryApp', () => { it('resets page to 1 when page size is changed', async () => { const NEW_PAGE_SIZE = 4; - mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS); createComponent(); - await axios.waitForAll(); - wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2); - await axios.waitForAll(); + await waitForPromises(); + findPaginationBar().vm.$emit('set-page', 2); + await waitForPromises(); mock.resetHistory(); - wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE); - await axios.waitForAll(); + findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE); + await waitForPromises(); expect(mock.history.get.length).toBe(1); expect(mock.history.get[0].params).toStrictEqual( @@ -166,18 +192,18 @@ describe('BulkImportsHistoryApp', () => { const NEW_PAGE_SIZE = 4; createComponent(); - await axios.waitForAll(); + await waitForPromises(); mock.resetHistory(); - wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE); - await axios.waitForAll(); + findPaginationBar().vm.$emit('set-page-size', NEW_PAGE_SIZE); + await waitForPromises(); expect(findLocalStorageSync().props('value')).toBe(NEW_PAGE_SIZE); }); it('renders link to destination_full_path for destination group', async () => { createComponent({ shallow: false }); - await axios.waitForAll(); + await waitForPromises(); expect(wrapper.find('tbody tr a').attributes().href).toBe( `/${DUMMY_RESPONSE[0].destination_full_path}`, @@ -187,9 +213,9 @@ describe('BulkImportsHistoryApp', () => { it('renders destination as text when destination_full_path is not defined', async () => { const RESPONSE = [{ ...DUMMY_RESPONSE[0], destination_full_path: null }]; - mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS); + mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS); createComponent({ shallow: false }); - await axios.waitForAll(); + await waitForPromises(); expect(wrapper.find('tbody tr a').exists()).toBe(false); expect(wrapper.find('tbody tr span').text()).toBe( @@ -199,14 +225,14 @@ describe('BulkImportsHistoryApp', () => { it('adds slash to group urls', async () => { createComponent({ shallow: false }); - await axios.waitForAll(); + await waitForPromises(); expect(wrapper.find('tbody tr a').text()).toBe(`${DUMMY_RESPONSE[0].destination_full_path}/`); }); it('does not prefixes project urls with slash', async () => { createComponent({ shallow: false }); - await axios.waitForAll(); + await waitForPromises(); expect(wrapper.findAll('tbody tr a').at(1).text()).toBe( DUMMY_RESPONSE[1].destination_full_path, @@ -215,9 +241,9 @@ describe('BulkImportsHistoryApp', () => { describe('details button', () => { beforeEach(() => { - mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS); + mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS); createComponent({ shallow: false }); - return axios.waitForAll(); + return waitForPromises(); }); it('renders details button if relevant item has failures', () => { @@ -255,7 +281,7 @@ describe('BulkImportsHistoryApp', () => { createComponent({ shallow: false }); await waitForPromises(); - expect(mock.history.get.map((x) => x.url)).toEqual([API_URL]); + expect(mock.history.get.map((x) => x.url)).toEqual([BULK_IMPORTS_API_URL]); }); }); @@ -279,7 +305,7 @@ describe('BulkImportsHistoryApp', () => { const RESPONSE = [mockCreatedImport, ...DUMMY_RESPONSE]; const POLL_HEADERS = { 'poll-interval': pollInterval }; - mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS); + mock.onGet(BULK_IMPORTS_API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS); mock.onGet(mockRealtimeChangesPath).replyOnce(HTTP_STATUS_OK, [], POLL_HEADERS); mock .onGet(mockRealtimeChangesPath) @@ -293,7 +319,10 @@ describe('BulkImportsHistoryApp', () => { it('starts polling for realtime changes', () => { jest.advanceTimersByTime(pollInterval); - expect(mock.history.get.map((x) => x.url)).toEqual([API_URL, mockRealtimeChangesPath]); + expect(mock.history.get.map((x) => x.url)).toEqual([ + BULK_IMPORTS_API_URL, + mockRealtimeChangesPath, + ]); expect(wrapper.findAll('tbody tr').at(0).text()).toContain('Pending'); }); @@ -305,7 +334,7 @@ describe('BulkImportsHistoryApp', () => { await waitForPromises(); expect(mock.history.get.map((x) => x.url)).toEqual([ - API_URL, + BULK_IMPORTS_API_URL, mockRealtimeChangesPath, mockRealtimeChangesPath, ]); diff --git a/spec/frontend_integration/fly_out_nav_browser_spec.js b/spec/frontend_integration/fly_out_nav_browser_spec.js deleted file mode 100644 index 07ddc0220e6..00000000000 --- a/spec/frontend_integration/fly_out_nav_browser_spec.js +++ /dev/null @@ -1,366 +0,0 @@ -import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; -import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar'; -import { - calculateTop, - showSubLevelItems, - canShowSubItems, - canShowActiveSubItems, - mouseEnterTopItems, - mouseLeaveTopItem, - getOpenMenu, - setOpenMenu, - mousePos, - getHideSubItemsInterval, - documentMouseMove, - getHeaderHeight, - setSidebar, - subItemsMouseLeave, -} from '~/fly_out_nav'; - -describe('Fly out sidebar navigation', () => { - let el; - let breakpointSize = 'lg'; - - const OLD_SIDEBAR_WIDTH = 200; - const CONTAINER_INITIAL_BOUNDING_RECT = { - x: 8, - y: 8, - width: 769, - height: 0, - top: 8, - right: 777, - bottom: 8, - left: 8, - }; - const SUB_ITEMS_INITIAL_BOUNDING_RECT = { - x: 148, - y: 8, - width: 0, - height: 150, - top: 8, - right: 148, - bottom: 158, - left: 148, - }; - const mockBoundingClientRect = (elem, rect) => { - jest.spyOn(elem, 'getBoundingClientRect').mockReturnValue(rect); - }; - - const findSubItems = () => document.querySelector('.sidebar-sub-level-items'); - const mockBoundingRects = () => { - const subItems = findSubItems(); - mockBoundingClientRect(el, CONTAINER_INITIAL_BOUNDING_RECT); - mockBoundingClientRect(subItems, SUB_ITEMS_INITIAL_BOUNDING_RECT); - }; - const mockSidebarFragment = (styleProps = '') => - `<div class="sidebar-sub-level-items" style="${styleProps}"></div>`; - - beforeEach(() => { - el = document.createElement('div'); - el.style.position = 'relative'; - document.body.appendChild(el); - - jest.spyOn(GlBreakpointInstance, 'getBreakpointSize').mockImplementation(() => breakpointSize); - }); - - afterEach(() => { - document.body.innerHTML = ''; - breakpointSize = 'lg'; - mousePos.length = 0; - - setSidebar(null); - }); - - describe('calculateTop', () => { - it('returns boundingRect top', () => { - const boundingRect = { - top: 100, - height: 100, - }; - - expect(calculateTop(boundingRect, 100)).toBe(100); - }); - }); - - describe('getHideSubItemsInterval', () => { - beforeEach(() => { - el.innerHTML = mockSidebarFragment('position: fixed; top: 0; left: 100px; height: 150px;'); - mockBoundingRects(); - }); - - it('returns 0 if currentOpenMenu is nil', () => { - setOpenMenu(null); - expect(getHideSubItemsInterval()).toBe(0); - }); - - it('returns 0 if mousePos is empty', () => { - expect(getHideSubItemsInterval()).toBe(0); - }); - - it('returns 0 when mouse above sub-items', () => { - showSubLevelItems(el); - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top, - }); - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top - 50, - }); - - expect(getHideSubItemsInterval()).toBe(0); - }); - - it('returns 0 when mouse is below sub-items', () => { - const subItems = findSubItems(); - - showSubLevelItems(el); - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top, - }); - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top - subItems.getBoundingClientRect().height + 50, - }); - - expect(getHideSubItemsInterval()).toBe(0); - }); - - it('returns 300 when mouse is moved towards sub-items', () => { - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top, - }); - - showSubLevelItems(el); - documentMouseMove({ - clientX: el.getBoundingClientRect().left + 20, - clientY: el.getBoundingClientRect().top + 10, - }); - - expect(getHideSubItemsInterval()).toBe(300); - }); - }); - - describe('mouseLeaveTopItem', () => { - beforeEach(() => { - jest.spyOn(el.classList, 'remove'); - }); - - it('removes is-over class if currentOpenMenu is null', () => { - setOpenMenu(null); - - mouseLeaveTopItem(el); - - expect(el.classList.remove).toHaveBeenCalledWith('is-over'); - }); - - it('removes is-over class if currentOpenMenu is null & there are sub-items', () => { - setOpenMenu(null); - el.innerHTML = mockSidebarFragment('position: absolute'); - - mouseLeaveTopItem(el); - - expect(el.classList.remove).toHaveBeenCalledWith('is-over'); - }); - - it('does not remove is-over class if currentOpenMenu is the passed in sub-items', () => { - setOpenMenu(null); - el.innerHTML = mockSidebarFragment('position: absolute'); - - setOpenMenu(findSubItems()); - mouseLeaveTopItem(el); - - expect(el.classList.remove).not.toHaveBeenCalled(); - }); - }); - - describe('mouseEnterTopItems', () => { - beforeEach(() => { - el.innerHTML = mockSidebarFragment( - `position: absolute; top: 0; left: 100px; height: ${OLD_SIDEBAR_WIDTH}px;`, - ); - mockBoundingRects(); - }); - - it('shows sub-items after 0ms if no menu is open', () => { - const subItems = findSubItems(); - mouseEnterTopItems(el); - - expect(getHideSubItemsInterval()).toBe(0); - - return new Promise((resolve) => { - setTimeout(() => { - expect(subItems.style.display).toBe('block'); - resolve(); - }); - }); - }); - - it('shows sub-items after 300ms if a menu is currently open', () => { - const subItems = findSubItems(); - - documentMouseMove({ - clientX: el.getBoundingClientRect().left, - clientY: el.getBoundingClientRect().top, - }); - - setOpenMenu(subItems); - - documentMouseMove({ - clientX: el.getBoundingClientRect().left + 20, - clientY: el.getBoundingClientRect().top + 10, - }); - - mouseEnterTopItems(el, 0); - - return new Promise((resolve) => { - setTimeout(() => { - expect(subItems.style.display).toBe('block'); - resolve(); - }); - }); - }); - }); - - describe('showSubLevelItems', () => { - beforeEach(() => { - el.innerHTML = mockSidebarFragment('position: absolute'); - }); - - it('adds is-over class to el', () => { - jest.spyOn(el.classList, 'add'); - - showSubLevelItems(el); - - expect(el.classList.add).toHaveBeenCalledWith('is-over'); - }); - - it('does not show sub-items on mobile', () => { - breakpointSize = 'xs'; - - showSubLevelItems(el); - - expect(findSubItems().style.display).not.toBe('block'); - }); - - it('shows sub-items', () => { - showSubLevelItems(el); - - expect(findSubItems().style.display).toBe('block'); - }); - - it('shows collapsed only sub-items if icon only sidebar', () => { - const subItems = findSubItems(); - const sidebar = document.createElement('div'); - sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); - subItems.classList.add('is-fly-out-only'); - - setSidebar(sidebar); - - showSubLevelItems(el); - - expect(findSubItems().style.display).toBe('block'); - }); - - it('does not show collapsed only sub-items if icon only sidebar', () => { - const subItems = findSubItems(); - subItems.classList.add('is-fly-out-only'); - - showSubLevelItems(el); - - expect(subItems.style.display).not.toBe('block'); - }); - - it('sets transform of sub-items', () => { - const sidebar = document.createElement('div'); - const subItems = findSubItems(); - - sidebar.style.width = `${OLD_SIDEBAR_WIDTH}px`; - - document.body.appendChild(sidebar); - - setSidebar(sidebar); - showSubLevelItems(el); - - expect(subItems.style.transform).toBe( - `translate3d(${OLD_SIDEBAR_WIDTH}px, ${ - Math.floor(el.getBoundingClientRect().top) - getHeaderHeight() - }px, 0)`, - ); - }); - - it('sets is-above when element is above', () => { - const subItems = findSubItems(); - mockBoundingRects(); - - subItems.style.height = `${window.innerHeight + el.offsetHeight}px`; - el.style.top = `${window.innerHeight - el.offsetHeight}px`; - - jest.spyOn(subItems.classList, 'add'); - - showSubLevelItems(el); - - expect(subItems.classList.add).toHaveBeenCalledWith('is-above'); - }); - }); - - describe('canShowSubItems', () => { - it('returns true if on desktop size', () => { - expect(canShowSubItems()).toBe(true); - }); - - it('returns false if on mobile size', () => { - breakpointSize = 'xs'; - - expect(canShowSubItems()).toBe(false); - }); - }); - - describe('canShowActiveSubItems', () => { - it('returns true by default', () => { - expect(canShowActiveSubItems(el)).toBe(true); - }); - - it('returns false when active & expanded sidebar', () => { - const sidebar = document.createElement('div'); - el.classList.add('active'); - - setSidebar(sidebar); - - expect(canShowActiveSubItems(el)).toBe(false); - }); - - it('returns true when active & collapsed sidebar', () => { - const sidebar = document.createElement('div'); - sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS); - el.classList.add('active'); - - setSidebar(sidebar); - - expect(canShowActiveSubItems(el)).toBe(true); - }); - }); - - describe('subItemsMouseLeave', () => { - beforeEach(() => { - el.innerHTML = mockSidebarFragment('position: absolute'); - - setOpenMenu(findSubItems()); - }); - - it('hides subMenu if element is not hovered', () => { - subItemsMouseLeave(el); - - expect(getOpenMenu()).toBeNull(); - }); - - it('does not hide subMenu if element is hovered', () => { - el.classList.add('is-over'); - subItemsMouseLeave(el); - - expect(getOpenMenu()).not.toBeNull(); - }); - }); -}); diff --git a/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb new file mode 100644 index 00000000000..1ce0e91765f --- /dev/null +++ b/spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::Catalog::Resources::VersionsResolver, feature_category: :pipeline_composition do + include GraphqlHelpers + + include_context 'when there are catalog resources with versions' + + let(:sort) { nil } + let(:args) { { sort: sort }.compact } + let(:ctx) { { current_user: current_user } } + + subject(:result) { resolve(described_class, ctx: ctx, obj: resource1, args: args) } + + describe '#resolve' do + context 'when the user is authorized to read project releases' do + before_all do + resource1.project.add_guest(current_user) + end + + context 'when sort argument is not provided' do + it 'returns versions ordered by released_at descending' do + expect(result.items).to eq([v1_1, v1_0]) + end + end + + context 'when sort argument is provided' do + context 'when sort is CREATED_ASC' do + let(:sort) { 'CREATED_ASC' } + + it 'returns versions ordered by created_at ascending' do + expect(result.items.to_a).to eq([v1_1, v1_0]) + end + end + + context 'when sort is CREATED_DESC' do + let(:sort) { 'CREATED_DESC' } + + it 'returns versions ordered by created_at descending' do + expect(result.items).to eq([v1_0, v1_1]) + end + end + + context 'when sort is RELEASED_AT_ASC' do + let(:sort) { 'RELEASED_AT_ASC' } + + it 'returns versions ordered by released_at ascending' do + expect(result.items).to eq([v1_0, v1_1]) + end + end + + context 'when sort is RELEASED_AT_DESC' do + let(:sort) { 'RELEASED_AT_DESC' } + + it 'returns versions ordered by released_at descending' do + expect(result.items).to eq([v1_1, v1_0]) + end + end + end + end + + context 'when the user is not authorized to read project releases' do + it 'returns empty response' do + expect(result).to be_empty + end + end + end +end diff --git a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb b/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb deleted file mode 100644 index 02fb3dfaee4..00000000000 --- a/spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb +++ /dev/null @@ -1,66 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -# In this context, a `version` is equivalent to a `release` -RSpec.describe Resolvers::Ci::Catalog::VersionsResolver, feature_category: :pipeline_composition do - include GraphqlHelpers - - let_it_be(:today) { Time.now } - let_it_be(:yesterday) { today - 1.day } - let_it_be(:tomorrow) { today + 1.day } - - let_it_be(:project) { create(:project, :private) } - # rubocop: disable Layout/LineLength - let_it_be(:version1) { create(:release, project: project, tag: 'v1.0.0', released_at: yesterday, created_at: tomorrow) } - let_it_be(:version2) { create(:release, project: project, tag: 'v2.0.0', released_at: today, created_at: yesterday) } - let_it_be(:version3) { create(:release, project: project, tag: 'v3.0.0', released_at: tomorrow, created_at: today) } - # rubocop: enable Layout/LineLength - let_it_be(:developer) { create(:user) } - let_it_be(:public_user) { create(:user) } - - let(:args) { { sort: :released_at_desc } } - let(:all_releases) { [version1, version2, version3] } - - before_all do - project.add_developer(developer) - end - - describe '#resolve' do - it_behaves_like 'releases and group releases resolver' - - describe 'when order_by is created_at' do - let(:current_user) { developer } - - context 'with sort: desc' do - let(:args) { { sort: :created_desc } } - - it 'returns the releases ordered by created_at in descending order' do - expect(resolve_releases.to_a) - .to match_array(all_releases) - .and be_sorted(:created_at, :desc) - end - end - - context 'with sort: asc' do - let(:args) { { sort: :created_asc } } - - it 'returns the releases ordered by created_at in ascending order' do - expect(resolve_releases.to_a) - .to match_array(all_releases) - .and be_sorted(:created_at, :asc) - end - end - end - end - - private - - def resolve_versions - context = { current_user: current_user } - resolve(described_class, obj: project, args: args, ctx: context, arg_style: :internal) - end - - # Required for shared examples - alias_method :resolve_releases, :resolve_versions -end diff --git a/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb b/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb new file mode 100644 index 00000000000..fd0f1a1e553 --- /dev/null +++ b/spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['CiCatalogResourceVersionSort'], feature_category: :pipeline_composition do + it { expect(described_class.graphql_name).to eq('CiCatalogResourceVersionSort') } + + it 'exposes all the existing catalog resource version sort options' do + expect(described_class.values.keys).to include( + *%w[RELEASED_AT_ASC RELEASED_AT_DESC CREATED_ASC CREATED_DESC] + ) + end +end diff --git a/spec/graphql/types/ci/catalog/resources/version_type_spec.rb b/spec/graphql/types/ci/catalog/resources/version_type_spec.rb new file mode 100644 index 00000000000..9faf3f16313 --- /dev/null +++ b/spec/graphql/types/ci/catalog/resources/version_type_spec.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::Catalog::Resources::VersionType, feature_category: :pipeline_composition do + specify { expect(described_class.graphql_name).to eq('CiCatalogResourceVersion') } + + it 'exposes the expected fields' do + expected_fields = %i[ + id + created_at + released_at + tag_name + tag_path + author + commit + ] + + expect(described_class).to have_graphql_fields(*expected_fields) + end +end diff --git a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb index 338475fa9c4..648213dc152 100644 --- a/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb +++ b/spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb @@ -11,7 +11,9 @@ RSpec.describe 'new tables with gitlab_main schema', feature_category: :cell do # Specific tables can be exempted from this requirement, and such tables must be added to the `exempted_tables` list. let!(:exempted_tables) do - [] + [ + "audit_events_instance_amazon_s3_configurations" # https://gitlab.com/gitlab-org/gitlab/-/issues/431327 + ] end let!(:starting_from_milestone) { 16.7 } diff --git a/spec/models/ci/bridge_spec.rb b/spec/models/ci/bridge_spec.rb index 775695b406c..53ad3a3a698 100644 --- a/spec/models/ci/bridge_spec.rb +++ b/spec/models/ci/bridge_spec.rb @@ -36,6 +36,24 @@ RSpec.describe Ci::Bridge, feature_category: :continuous_integration do expect(bridge).to have_one(:downstream_pipeline) end + describe 'no-op methods for compatibility with Ci::Build' do + it 'returns an empty array job_artifacts' do + expect(bridge.job_artifacts).to eq(Ci::JobArtifact.none) + end + + it 'return nil for artifacts_expire_at' do + expect(bridge.artifacts_expire_at).to be_nil + end + + it 'return nil for runner' do + expect(bridge.runner).to be_nil + end + + it 'returns an empty TagList for tag_list' do + expect(bridge.tag_list).to be_a(ActsAsTaggableOn::TagList) + end + end + describe '#retryable?' do let(:bridge) { create(:ci_bridge, :success) } diff --git a/spec/models/ci/catalog/resources/version_spec.rb b/spec/models/ci/catalog/resources/version_spec.rb index ccc729ee715..aafd51699b5 100644 --- a/spec/models/ci/catalog/resources/version_spec.rb +++ b/spec/models/ci/catalog/resources/version_spec.rb @@ -10,9 +10,6 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: it { is_expected.to belong_to(:project) } it { is_expected.to have_many(:components).class_name('Ci::Catalog::Resources::Component') } - it { is_expected.to delegate_method(:name).to(:release) } - it { is_expected.to delegate_method(:description).to(:release) } - it { is_expected.to delegate_method(:tag).to(:release) } it { is_expected.to delegate_method(:sha).to(:release) } it { is_expected.to delegate_method(:released_at).to(:release) } it { is_expected.to delegate_method(:author_id).to(:release) } @@ -127,4 +124,28 @@ RSpec.describe Ci::Catalog::Resources::Version, type: :model, feature_category: end end end + + describe '#name' do + it 'is equivalent to release.tag' do + release_v1_0.update!(name: 'Release v1.0') + + expect(v1_0.name).to eq(release_v1_0.tag) + end + end + + describe '#commit' do + subject(:commit) { v1_0.commit } + + it 'returns a commit' do + is_expected.to be_a(Commit) + end + + context 'when the sha is nil' do + it 'returns nil' do + release_v1_0.update!(sha: nil) + + is_expected.to be_nil + end + end + end end diff --git a/spec/requests/api/deployments_spec.rb b/spec/requests/api/deployments_spec.rb index 41c5847e940..d9248ad6855 100644 --- a/spec/requests/api/deployments_spec.rb +++ b/spec/requests/api/deployments_spec.rb @@ -161,24 +161,56 @@ RSpec.describe API::Deployments, feature_category: :continuous_delivery do end describe 'GET /projects/:id/deployments/:deployment_id' do - let(:project) { deployment.environment.project } - let!(:deployment) { create(:deployment, :success) } + let_it_be(:deployment_with_bridge) { create(:deployment, :with_bridge, :success) } + let_it_be(:deployment_with_build) { create(:deployment, :success) } context 'as a member of the project' do - it 'returns the projects deployment' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + shared_examples "returns project deployments" do + let(:project) { deployment.environment.project } - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['sha']).to match /\A\h{40}\z/ - expect(json_response['id']).to eq(deployment.id) + it 'returns the expected response' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['sha']).to match /\A\h{40}\z/ + expect(json_response['id']).to eq(deployment.id) + end + end + + context 'when the deployable is a build' do + it_behaves_like 'returns project deployments' do + let!(:deployment) { deployment_with_build } + end + end + + context 'when the deployable is a bridge' do + it_behaves_like 'returns project deployments' do + let!(:deployment) { deployment_with_bridge } + end end end context 'as non member' do - it 'returns a 404 status code' do - get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + shared_examples 'deployment will not be found' do + let(:project) { deployment.environment.project } - expect(response).to have_gitlab_http_status(:not_found) + it 'returns a 404 status code' do + get api("/projects/#{project.id}/deployments/#{deployment.id}", non_member) + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context 'when the deployable is a build' do + it_behaves_like 'deployment will not be found' do + let!(:deployment) { deployment_with_build } + end + end + + context 'when the deployable is a bridge' do + it_behaves_like 'deployment will not be found' do + let!(:deployment) { deployment_with_bridge } + end end end end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 498e030da0b..aed97bcfe7c 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -374,32 +374,71 @@ RSpec.describe API::Environments, feature_category: :continuous_delivery do end describe 'GET /projects/:id/environments/:environment_id' do + let_it_be(:bridge_job) { create(:ci_bridge, :running, project: project, user: user) } + let_it_be(:build_job) { create(:ci_build, :running, project: project, user: user) } + context 'as member of the project' do - it 'returns project environments' do - create(:deployment, :success, project: project, environment: environment) + shared_examples "returns project environments" do + it 'returns expected response' do + create( + :deployment, + :success, + project: project, + environment: environment, + deployable: job + ) + + get api("/projects/#{project.id}/environments/#{environment.id}", user) - get api("/projects/#{project.id}/environments/#{environment.id}", user) + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('public_api/v4/environment') + expect(json_response['last_deployment']).to be_present + end + end - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('public_api/v4/environment') - expect(json_response['last_deployment']).to be_present + context "when the deployable is a bridge" do + it_behaves_like "returns project environments" do + let(:job) { bridge_job } + end + + # No test for Ci::Bridge JOB-TOKEN auth because it doesn't implement the `.token` method. end - it 'returns 200 HTTP status when using JOB-TOKEN auth' do - job = create(:ci_build, :running, project: project, user: user) + context "when the deployable is a build" do + it_behaves_like "returns project environments" do + let(:job) { build_job } + end - get api("/projects/#{project.id}/environments/#{environment.id}"), - params: { job_token: job.token } + it 'returns 200 HTTP status when using JOB-TOKEN auth' do + get( + api("/projects/#{project.id}/environments/#{environment.id}"), + params: { job_token: build_job.token } + ) - expect(response).to have_gitlab_http_status(:ok) + expect(response).to have_gitlab_http_status(:ok) + end end end context 'as non member' do - it 'returns a 404 status code' do - get api("/projects/#{project.id}/environments/#{environment.id}", non_member) + shared_examples 'environment will not be found' do + it 'returns a 404 status code' do + get api("/projects/#{project.id}/environments/#{environment.id}", non_member) - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:not_found) + end + end + + context "when the deployable is a bridge" do + it_behaves_like "environment will not be found" do + let(:job) { bridge_job } + end + end + + context "when the deployable is a build" do + it_behaves_like "environment will not be found" do + let(:job) { build_job } + end end end end diff --git a/spec/requests/api/graphql/ci/catalog/resource_spec.rb b/spec/requests/api/graphql/ci/catalog/resource_spec.rb index 56c6c341647..e9610e3c435 100644 --- a/spec/requests/api/graphql/ci/catalog/resource_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resource_spec.rb @@ -81,6 +81,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio nodes { id tagName + tagPath releasedAt author { id @@ -98,11 +99,13 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio let_it_be(:author) { create(:user, name: 'author') } let_it_be(:version1) do - create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author).catalog_resource_version end let_it_be(:version2) do - create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end it 'returns the resource with the versions data' do @@ -115,13 +118,15 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio expect(graphql_data_at(:ciCatalogResource, :versions, :nodes)).to contain_exactly( a_graphql_entity_for( version1, - tagName: version1.tag, + tagName: version1.name, + tagPath: project_tag_path(project, version1.name), releasedAt: version1.released_at, author: a_graphql_entity_for(author, :name) ), a_graphql_entity_for( version2, - tagName: version2.tag, + tagName: version2.name, + tagPath: project_tag_path(project, version2.name), releasedAt: version2.released_at, author: a_graphql_entity_for(author, :name) ) @@ -157,6 +162,7 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio latestVersion { id tagName + tagPath releasedAt author { id @@ -173,12 +179,14 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio let_it_be(:author) { create(:user, name: 'author') } let_it_be(:latest_version) do - create(:release, project: project, released_at: '2023-02-01T00:00:00Z', author: author) + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-02-01T00:00:00Z', + author: author).catalog_resource_version end before_all do - # Previous version of the project - create(:release, project: project, released_at: '2023-01-01T00:00:00Z', author: author) + # Previous version of the catalog resource + create(:release, :with_catalog_resource_version, project: project, released_at: '2023-01-01T00:00:00Z', + author: author) end it 'returns the resource with the latest version data' do @@ -189,7 +197,8 @@ RSpec.describe 'Query.ciCatalogResource', feature_category: :pipeline_compositio resource, latestVersion: a_graphql_entity_for( latest_version, - tagName: latest_version.tag, + tagName: latest_version.name, + tagPath: project_tag_path(project, latest_version.name), releasedAt: latest_version.released_at, author: a_graphql_entity_for(author, :name) ) diff --git a/spec/requests/api/graphql/ci/catalog/resources_spec.rb b/spec/requests/api/graphql/ci/catalog/resources_spec.rb index 35f72f8b10f..dee841898bb 100644 --- a/spec/requests/api/graphql/ci/catalog/resources_spec.rb +++ b/spec/requests/api/graphql/ci/catalog/resources_spec.rb @@ -133,11 +133,13 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi let_it_be(:author2) { create(:user, name: 'author2') } let_it_be(:latest_version1) do - create(:release, project: project1, released_at: '2023-02-01T00:00:00Z', author: author1) + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-02-01T00:00:00Z', + author: author1).catalog_resource_version end let_it_be(:latest_version2) do - create(:release, project: public_project, released_at: '2023-02-01T00:00:00Z', author: author2) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-02-01T00:00:00Z', + author: author2).catalog_resource_version end let(:query) do @@ -165,9 +167,11 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi before_all do namespace.add_developer(user) - # Previous versions of the projects - create(:release, project: project1, released_at: '2023-01-01T00:00:00Z', author: author1) - create(:release, project: public_project, released_at: '2023-01-01T00:00:00Z', author: author2) + # Previous versions of the catalog resources + create(:release, :with_catalog_resource_version, project: project1, released_at: '2023-01-01T00:00:00Z', + author: author1) + create(:release, :with_catalog_resource_version, project: public_project, released_at: '2023-01-01T00:00:00Z', + author: author2) end it 'returns all resources with the latest version data' do @@ -178,7 +182,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi resource1, latestVersion: a_graphql_entity_for( latest_version1, - tagName: latest_version1.tag, + tagName: latest_version1.name, releasedAt: latest_version1.released_at, author: a_graphql_entity_for(author1, :name) ) @@ -187,7 +191,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi public_resource, latestVersion: a_graphql_entity_for( latest_version2, - tagName: latest_version2.tag, + tagName: latest_version2.name, releasedAt: latest_version2.released_at, author: a_graphql_entity_for(author2, :name) ) @@ -195,8 +199,7 @@ RSpec.describe 'Query.ciCatalogResources', feature_category: :pipeline_compositi ) end - # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/430350 - # it_behaves_like 'avoids N+1 queries' + it_behaves_like 'avoids N+1 queries' end describe 'rootNamespace' do diff --git a/spec/services/ml/model_versions/delete_service_spec.rb b/spec/services/ml/model_versions/delete_service_spec.rb new file mode 100644 index 00000000000..1cc5a2f85a5 --- /dev/null +++ b/spec/services/ml/model_versions/delete_service_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ml::ModelVersions::DeleteService, feature_category: :mlops do + let_it_be(:valid_model_version) do + create(:ml_model_versions, :with_package) + end + + let(:project) { valid_model_version.project } + let(:user) { valid_model_version.project.owner } + let(:name) { valid_model_version.name } + let(:version) { valid_model_version.version } + + subject(:execute_service) { described_class.new(project, name, version, user).execute } + + describe '#execute' do + context 'when model version exists' do + it 'deletes the model version', :aggregate_failures do + expect(execute_service).to be_success + expect(Ml::ModelVersion.find_by(id: valid_model_version.id)).to be_nil + end + end + + context 'when model version does not exist' do + let(:version) { 'wrong-version' } + + it { is_expected.to be_error.and have_attributes(message: 'Model not found') } + end + + context 'when model version has no package' do + before do + valid_model_version.update!(package: nil) + end + + it 'does not trigger destroy package service', :aggregate_failures do + expect(Packages::MarkPackageForDestructionService).not_to receive(:new) + expect(execute_service).to be_success + end + end + + context 'when package cannot be marked for destruction' do + before do + allow_next_instance_of(Packages::MarkPackageForDestructionService) do |service| + allow(service).to receive(:execute).and_return(ServiceResponse.error(message: 'error')) + end + end + + it 'does not delete the model version', :aggregate_failures do + is_expected.to be_error.and have_attributes(message: 'error') + expect(Ml::ModelVersion.find_by(id: valid_model_version.id)).to eq(valid_model_version) + end + end + end +end diff --git a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb index ac2db624a38..3eeaa52d221 100644 --- a/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb +++ b/spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb @@ -10,7 +10,7 @@ RSpec.shared_context 'when there are catalog resources with versions' do let_it_be_with_reload(:resource2) { create(:ci_catalog_resource, project: project2) } let_it_be(:resource3) { create(:ci_catalog_resource, project: project3) } - let_it_be(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) } + let_it_be_with_reload(:release_v1_0) { create(:release, project: project1, tag: 'v1.0', released_at: 4.days.ago) } let_it_be(:release_v1_1) { create(:release, project: project1, tag: 'v1.1', released_at: 3.days.ago) } let_it_be(:release_v2_0) { create(:release, project: project2, tag: 'v2.0', released_at: 2.days.ago) } let_it_be(:release_v2_1) { create(:release, project: project2, tag: 'v2.1', released_at: 1.day.ago) } diff --git a/spec/support/shared_examples/services/protected_branches_shared_examples.rb b/spec/support/shared_examples/services/protected_branches_shared_examples.rb index 80e2f09ed44..6d4b82730da 100644 --- a/spec/support/shared_examples/services/protected_branches_shared_examples.rb +++ b/spec/support/shared_examples/services/protected_branches_shared_examples.rb @@ -12,7 +12,7 @@ RSpec.shared_context 'with scan result policy blocking protected branches' do end let(:scan_result_policy) do - build(:scan_result_policy, branches: [branch_name], approval_settings: { block_unprotecting_branches: true }) + build(:scan_result_policy, branches: [branch_name], approval_settings: { block_branch_modification: true }) end before do |