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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-11-17 21:15:03 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-17 21:15:03 +0300
commit7468e26195700e0f13081d49377a4ec03f29f6b7 (patch)
treeda070b2fcb72105207f4d2972f129dcb5183c993 /spec
parent843b1e9386fbda332839d23246d0ee2382fb7f4c (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/factories/deployments.rb4
-rw-r--r--spec/finders/ci/catalog/resources/versions_finder_spec.rb23
-rw-r--r--spec/frontend/ci/reports/codequality_report/utils/codequality_parser_spec.js (renamed from spec/frontend/ci/reports/codequality_report/store/utils/codequality_parser_spec.js)2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_history_link_spec.js34
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js18
-rw-r--r--spec/frontend/organizations/users/components/app_spec.js68
-rw-r--r--spec/frontend/organizations/users/components/users_view_spec.js28
-rw-r--r--spec/frontend/organizations/users/mock_data.js8
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js99
-rw-r--r--spec/frontend_integration/fly_out_nav_browser_spec.js366
-rw-r--r--spec/graphql/resolvers/ci/catalog/resources/versions_resolver_spec.rb69
-rw-r--r--spec/graphql/resolvers/ci/catalog/versions_resolver_spec.rb66
-rw-r--r--spec/graphql/types/ci/catalog/resources/version_sort_enum_spec.rb13
-rw-r--r--spec/graphql/types/ci/catalog/resources/version_type_spec.rb21
-rw-r--r--spec/lib/gitlab/database/no_new_tables_with_gitlab_main_schema_spec.rb4
-rw-r--r--spec/models/ci/bridge_spec.rb18
-rw-r--r--spec/models/ci/catalog/resources/version_spec.rb27
-rw-r--r--spec/requests/api/deployments_spec.rb52
-rw-r--r--spec/requests/api/environments_spec.rb67
-rw-r--r--spec/requests/api/graphql/ci/catalog/resource_spec.rb25
-rw-r--r--spec/requests/api/graphql/ci/catalog/resources_spec.rb21
-rw-r--r--spec/services/ml/model_versions/delete_service_spec.rb55
-rw-r--r--spec/support/shared_contexts/ci/catalog/resources/version_shared_context.rb2
-rw-r--r--spec/support/shared_examples/services/protected_branches_shared_examples.rb2
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