summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-09-28 12:07:50 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-09-28 12:07:50 +0000
commiteb3a23aaaa99ef8ae08c7b440fad676e3c71a1af (patch)
treebb74f64f73f4a20d4b4e3443c3e7defd4733a78d /spec
parentf3cfb235c76426ce5a18003bb80ba625097bf1d0 (diff)
downloadgitlab-ce-eb3a23aaaa99ef8ae08c7b440fad676e3c71a1af.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb32
-rw-r--r--spec/features/merge_request/batch_comments_spec.rb18
-rw-r--r--spec/features/protected_branches_spec.rb2
-rw-r--r--spec/frontend/api/projects_api_spec.js23
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js16
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js6
-rw-r--r--spec/frontend/fixtures/namespaces.rb47
-rw-r--r--spec/frontend/projects/settings/components/transfer_project_form_spec.js271
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js14
-rw-r--r--spec/lib/gitlab/config_checker/external_database_checker_spec.rb99
-rw-r--r--spec/models/project_statistics_spec.rb24
-rw-r--r--spec/requests/api/pages_domains_spec.rb40
-rw-r--r--spec/services/pages_domains/update_service_spec.rb61
-rw-r--r--spec/support/helpers/exclusive_lease_helpers.rb19
-rw-r--r--spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb87
-rw-r--r--spec/views/layouts/_flash.html.haml_spec.rb21
-rw-r--r--spec/views/layouts/fullscreen.html.haml_spec.rb7
30 files changed, 659 insertions, 222 deletions
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 691508d1e14..899aa259f8c 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -106,6 +106,17 @@ RSpec.describe Projects::PagesDomainsController do
end.to change { pages_domain.reload.certificate }.to(pages_domain_params[:user_provided_certificate])
end
+ it 'publishes PagesDomainUpdatedEvent event' do
+ expect { patch(:update, params: params) }
+ .to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace.id,
+ root_namespace_id: project.root_namespace.id,
+ domain: pages_domain.domain
+ )
+ end
+
it 'redirects to the project page' do
patch(:update, params: params)
@@ -134,6 +145,11 @@ RSpec.describe Projects::PagesDomainsController do
expect(response).to render_template('show')
end
+
+ it 'does not publish PagesDomainUpdatedEvent event' do
+ expect { patch(:update, params: params) }
+ .to not_publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ end
end
context 'when parameters include the domain' do
@@ -216,6 +232,17 @@ RSpec.describe Projects::PagesDomainsController do
expect(response).to redirect_to(project_pages_domain_path(project, pages_domain))
end
+ it 'publishes PagesDomainUpdatedEvent event' do
+ expect { subject }
+ .to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace.id,
+ root_namespace_id: project.root_namespace.id,
+ domain: pages_domain.domain
+ )
+ end
+
it 'removes certificate' do
expect do
subject
@@ -245,6 +272,11 @@ RSpec.describe Projects::PagesDomainsController do
expect(pages_domain.key).to be_present
end
+ it 'does not publish PagesDomainUpdatedEvent event' do
+ expect { subject }
+ .to not_publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ end
+
it 'redirects to show page with a flash message' do
subject
diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb
index bccdc3c4c62..f01217df8c5 100644
--- a/spec/features/merge_request/batch_comments_spec.rb
+++ b/spec/features/merge_request/batch_comments_spec.rb
@@ -74,6 +74,24 @@ RSpec.describe 'Merge request > Batch comments', :js do
expect(page).to have_selector('.draft-note-component', text: 'Testing update')
end
+ context 'multiple times on the same diff line' do
+ it 'shows both drafts at once' do
+ write_diff_comment
+
+ # All of the Diff helpers like click_diff_line (or write_diff_comment)
+ # fail very badly when run a second time.
+ # This recreates the relevant logic.
+ line = find_by_scrolling("[id='#{sample_compare.changes[0][:line_code]}']")
+ line.hover
+ line.find('.js-add-diff-note-button').click
+
+ write_comment(text: 'A second draft!', button_text: 'Add to review')
+
+ expect(page).to have_text('Line is wrong')
+ expect(page).to have_text('A second draft!')
+ end
+ end
+
context 'with image and file draft note' do
let(:merge_request) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project) }
let!(:draft_on_text) { create(:draft_note_on_text_diff, merge_request: merge_request, author: user, path: 'README.md', note: 'Lorem ipsum on text...') }
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index 389a51a10e0..174716d646d 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -183,7 +183,7 @@ RSpec.describe 'Protected Branches', :js do
end
include_examples 'Deploy keys with protected branches' do
- let(:all_dropdown_sections) { %w(Roles Deploy\ Keys) }
+ let(:all_dropdown_sections) { ['Roles', 'Deploy Keys'] }
end
end
end
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 8f40b557e1f..8459021421f 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -1,5 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
+import getTransferLocationsResponse from 'test_fixtures/api/projects/transfer_locations_page_1.json';
import * as projectsApi from '~/api/projects_api';
+import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
describe('~/api/projects_api.js', () => {
@@ -59,4 +61,25 @@ describe('~/api/projects_api.js', () => {
});
});
});
+
+ describe('getTransferLocations', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'get');
+ });
+
+ it('retrieves transfer locations from the correct URL and returns them in the response data', async () => {
+ const params = { page: 1 };
+ const expectedUrl = '/api/v7/projects/1/transfer_locations';
+
+ mock.onGet(expectedUrl).replyOnce(200, { data: getTransferLocationsResponse });
+
+ await expect(projectsApi.getTransferLocations(projectId, params)).resolves.toMatchObject({
+ data: { data: getTransferLocationsResponse },
+ });
+
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, {
+ params: { ...params, per_page: DEFAULT_PER_PAGE },
+ });
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 9f593ee0d49..0bce6451ce4 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -53,7 +53,7 @@ describe('DiffContent', () => {
namespaced: true,
getters: {
draftsForFile: () => () => true,
- draftForLine: () => () => true,
+ draftsForLine: () => () => true,
shouldRenderDraftRow: () => () => true,
hasParallelDraftLeft: () => () => true,
hasParallelDraftRight: () => () => true,
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index a74013dc2d4..a7a95ed2f35 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -219,7 +219,7 @@ describe('DiffRow', () => {
shouldRenderDraftRow: jest.fn(),
hasParallelDraftLeft: jest.fn(),
hasParallelDraftRight: jest.fn(),
- draftForLine: jest.fn(),
+ draftsForLine: jest.fn().mockReturnValue([]),
};
const applyMap = mapParallel(mockDiffContent);
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index 930b8bcdb08..8b25691ce34 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -216,7 +216,7 @@ describe('mapParallel', () => {
diffFile: {},
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
- draftForLine: () => ({}),
+ draftsForLine: () => [],
};
const line = { left: side, right: side };
const expectation = {
@@ -234,13 +234,13 @@ describe('mapParallel', () => {
const leftExpectation = {
renderDiscussion: true,
hasDraft: false,
- lineDraft: {},
+ lineDrafts: [],
hasCommentForm: true,
};
const rightExpectation = {
renderDiscussion: false,
hasDraft: false,
- lineDraft: {},
+ lineDrafts: [],
hasCommentForm: false,
};
const mapped = utils.mapParallel(content)(line);
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 1dd4a2f6c23..9bff6bd14f1 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -21,7 +21,7 @@ describe('DiffView', () => {
getters: {
shouldRenderDraftRow: () => false,
shouldRenderParallelDraftRow: () => () => true,
- draftForLine: () => false,
+ draftsForLine: () => false,
draftsForFile: () => false,
hasParallelDraftLeft: () => false,
hasParallelDraftRight: () => false,
@@ -75,12 +75,12 @@ describe('DiffView', () => {
});
it.each`
- type | side | container | sides | total
- ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
- ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ type | side | container | sides | total
+ ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
+ ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDrafts: [], renderDiscussion: true }, right: { lineDrafts: [], renderDiscussion: true } }} | ${2}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDrafts: [], renderDiscussion: true } }} | ${1}
`(
'renders a $type comment row with comment cell on $side',
({ type, container, sides, total }) => {
@@ -95,7 +95,7 @@ describe('DiffView', () => {
it('renders a draft row', () => {
const wrapper = createWrapper({
- diffLines: [{ renderCommentRow: true, left: { lineDraft: { isDraft: true } } }],
+ diffLines: [{ renderCommentRow: true, left: { lineDrafts: [{ isDraft: true }] } }],
});
expect(wrapper.findComponent(DraftNote).exists()).toBe(true);
});
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 2ca421a20b4..befab3b676b 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -36,7 +36,7 @@ export const diffCodeQuality = {
old_line: 1,
new_line: null,
codequality: [],
- lineDraft: {},
+ lineDrafts: [],
},
},
{
@@ -45,7 +45,7 @@ export const diffCodeQuality = {
old_line: 2,
new_line: 1,
codequality: [],
- lineDraft: {},
+ lineDrafts: [],
},
},
{
@@ -55,7 +55,7 @@ export const diffCodeQuality = {
new_line: 2,
codequality: [multipleFindingsArr[0]],
- lineDraft: {},
+ lineDrafts: [],
},
},
],
diff --git a/spec/frontend/fixtures/namespaces.rb b/spec/frontend/fixtures/namespaces.rb
index b11f661fe09..a3f295f4e66 100644
--- a/spec/frontend/fixtures/namespaces.rb
+++ b/spec/frontend/fixtures/namespaces.rb
@@ -7,38 +7,43 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
include JavaScriptFixturesHelpers
include GraphqlHelpers
- describe GraphQL::Query, type: :request do
+ describe API::Projects, type: :request do
let_it_be(:user) { create(:user) }
- let_it_be(:groups) { create_list(:group, 4) }
- before_all do
- groups.each { |group| group.add_owner(user) }
- end
+ describe 'transfer_locations' do
+ let_it_be(:groups) { create_list(:group, 4) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
- query_name = 'search_namespaces_where_user_can_transfer_projects'
- query_extension = '.query.graphql'
+ before_all do
+ groups.each { |group| group.add_owner(user) }
+ end
- full_input_path = "projects/settings/graphql/queries/#{query_name}#{query_extension}"
- base_output_path = "graphql/projects/settings/#{query_name}"
+ it 'api/projects/transfer_locations_page_1.json' do
+ get api("/projects/#{project.id}/transfer_locations?per_page=2", user)
- it "#{base_output_path}_page_1#{query_extension}.json" do
- query = get_graphql_query_as_string(full_input_path)
+ expect(response).to be_successful
+ end
- post_graphql(query, current_user: user, variables: { first: 2 })
+ it 'api/projects/transfer_locations_page_2.json' do
+ get api("/projects/#{project.id}/transfer_locations?per_page=2&page=2", user)
- expect_graphql_errors_to_be_empty
+ expect(response).to be_successful
+ end
end
+ end
+
+ describe GraphQL::Query, type: :request do
+ let_it_be(:user) { create(:user) }
+
+ query_name = 'current_user_namespace.query.graphql'
- it "#{base_output_path}_page_2#{query_extension}.json" do
- query = get_graphql_query_as_string(full_input_path)
+ input_path = "projects/settings/graphql/queries/#{query_name}"
+ output_path = "graphql/projects/settings/#{query_name}.json"
- post_graphql(query, current_user: user, variables: { first: 2 })
+ it output_path do
+ query = get_graphql_query_as_string(input_path)
- post_graphql(
- query,
- current_user: user,
- variables: { first: 2, after: graphql_data_at('currentUser', 'groups', 'pageInfo', 'endCursor') }
- )
+ post_graphql(query, current_user: user)
expect_graphql_errors_to_be_empty
end
diff --git a/spec/frontend/projects/settings/components/transfer_project_form_spec.js b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
index bde7148078d..e455bf529eb 100644
--- a/spec/frontend/projects/settings/components/transfer_project_form_spec.js
+++ b/spec/frontend/projects/settings/components/transfer_project_form_spec.js
@@ -1,41 +1,65 @@
import Vue, { nextTick } from 'vue';
+import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
-import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_1.query.graphql.json';
-import searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2 from 'test_fixtures/graphql/projects/settings/search_namespaces_where_user_can_transfer_projects_page_2.query.graphql.json';
-import {
- groupNamespaces,
- userNamespaces,
-} from 'jest/vue_shared/components/namespace_select/mock_data';
+import currentUserNamespaceQueryResponse from 'test_fixtures/graphql/projects/settings/current_user_namespace.query.graphql.json';
+import transferLocationsResponsePage1 from 'test_fixtures/api/projects/transfer_locations_page_1.json';
+import transferLocationsResponsePage2 from 'test_fixtures/api/projects/transfer_locations_page_2.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TransferProjectForm from '~/projects/settings/components/transfer_project_form.vue';
import NamespaceSelect from '~/vue_shared/components/namespace_select/namespace_select.vue';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
-import searchNamespacesWhereUserCanTransferProjectsQuery from '~/projects/settings/graphql/queries/search_namespaces_where_user_can_transfer_projects.query.graphql';
+import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
+import { getTransferLocations } from '~/api/projects_api';
import waitForPromises from 'helpers/wait_for_promises';
+jest.mock('~/api/projects_api', () => ({
+ getTransferLocations: jest.fn(),
+}));
+
describe('Transfer project form', () => {
let wrapper;
+ const projectId = '1';
const confirmButtonText = 'Confirm';
const confirmationPhrase = 'You must construct additional pylons!';
- const runDebounce = () => jest.runAllTimers();
-
Vue.use(VueApollo);
- const defaultQueryHandler = jest
- .fn()
- .mockResolvedValue(searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1);
+ const defaultQueryHandler = jest.fn().mockResolvedValue(currentUserNamespaceQueryResponse);
+ const mockResolvedGetTransferLocations = ({
+ data = transferLocationsResponsePage1,
+ page = '1',
+ nextPage = '2',
+ prevPage = null,
+ } = {}) => {
+ getTransferLocations.mockResolvedValueOnce({
+ data,
+ headers: {
+ 'x-per-page': '2',
+ 'x-page': page,
+ 'x-total': '4',
+ 'x-total-pages': '2',
+ 'x-next-page': nextPage,
+ 'x-prev-page': prevPage,
+ },
+ });
+ };
+ const mockRejectedGetTransferLocations = () => {
+ const error = new Error();
+
+ getTransferLocations.mockRejectedValueOnce(error);
+ };
const createComponent = ({
- requestHandlers = [[searchNamespacesWhereUserCanTransferProjectsQuery, defaultQueryHandler]],
+ requestHandlers = [[currentUserNamespaceQuery, defaultQueryHandler]],
} = {}) => {
wrapper = shallowMountExtended(TransferProjectForm, {
+ provide: {
+ projectId,
+ },
propsData: {
- userNamespaces,
- groupNamespaces,
confirmButtonText,
confirmationPhrase,
},
@@ -44,7 +68,12 @@ describe('Transfer project form', () => {
};
const findNamespaceSelect = () => wrapper.findComponent(NamespaceSelect);
+ const showNamespaceSelect = async () => {
+ findNamespaceSelect().vm.$emit('show');
+ await waitForPromises();
+ };
const findConfirmDanger = () => wrapper.findComponent(ConfirmDanger);
+ const findAlert = () => wrapper.findComponent(GlAlert);
afterEach(() => {
wrapper.destroy();
@@ -69,66 +98,113 @@ describe('Transfer project form', () => {
});
describe('with a selected namespace', () => {
- const [selectedItem] = groupNamespaces;
+ const [selectedItem] = transferLocationsResponsePage1;
- beforeEach(() => {
+ const arrange = async () => {
+ mockResolvedGetTransferLocations();
createComponent();
-
+ await showNamespaceSelect();
findNamespaceSelect().vm.$emit('select', selectedItem);
- });
+ };
+
+ it('emits the `selectNamespace` event when a namespace is selected', async () => {
+ await arrange();
- it('emits the `selectNamespace` event when a namespace is selected', () => {
const args = [selectedItem.id];
expect(wrapper.emitted('selectNamespace')).toEqual([args]);
});
- it('enables the confirm button', () => {
+ it('enables the confirm button', async () => {
+ await arrange();
+
expect(findConfirmDanger().attributes('disabled')).toBeUndefined();
});
- it('clicking the confirm button emits the `confirm` event', () => {
+ it('clicking the confirm button emits the `confirm` event', async () => {
+ await arrange();
+
findConfirmDanger().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).toBeDefined();
});
});
- it('passes correct props to `NamespaceSelect` component', async () => {
- createComponent();
+ describe('when `NamespaceSelect` is opened', () => {
+ it('fetches user and group namespaces and passes correct props to `NamespaceSelect` component', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+
+ const { namespace } = currentUserNamespaceQueryResponse.data.currentUser;
+
+ expect(findNamespaceSelect().props()).toMatchObject({
+ userNamespaces: [
+ {
+ id: getIdFromGraphQLId(namespace.id),
+ humanName: namespace.fullName,
+ },
+ ],
+ groupNamespaces: transferLocationsResponsePage1.map(({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ })),
+ hasNextPageOfGroups: true,
+ isLoading: false,
+ isSearchLoading: false,
+ shouldFilterNamespaces: false,
+ });
+ });
- runDebounce();
- await waitForPromises();
+ describe('when namespaces have already been fetched', () => {
+ beforeEach(async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ });
+
+ it('does not fetch namespaces', async () => {
+ getTransferLocations.mockClear();
+ defaultQueryHandler.mockClear();
+
+ await showNamespaceSelect();
- const {
- namespace,
- groups,
- } = searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser;
-
- expect(findNamespaceSelect().props()).toMatchObject({
- userNamespaces: [
- {
- id: getIdFromGraphQLId(namespace.id),
- humanName: namespace.fullName,
- },
- ],
- groupNamespaces: groups.nodes.map((node) => ({
- id: getIdFromGraphQLId(node.id),
- humanName: node.fullName,
- })),
- hasNextPageOfGroups: true,
- isLoadingMoreGroups: false,
- isSearchLoading: false,
- shouldFilterNamespaces: false,
+ expect(getTransferLocations).not.toHaveBeenCalled();
+ expect(defaultQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays error alert', async () => {
+ mockRejectedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('when `currentUser` GraphQL query fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ const error = new Error();
+ createComponent({
+ requestHandlers: [[currentUserNamespaceQuery, jest.fn().mockRejectedValueOnce(error)]],
+ });
+ await showNamespaceSelect();
+
+ expect(findAlert().exists()).toBe(true);
+ });
});
});
describe('when `search` event is fired', () => {
const arrange = async () => {
+ mockResolvedGetTransferLocations();
createComponent();
-
+ await showNamespaceSelect();
+ mockResolvedGetTransferLocations();
findNamespaceSelect().vm.$emit('search', 'foo');
-
await nextTick();
};
@@ -138,87 +214,106 @@ describe('Transfer project form', () => {
expect(findNamespaceSelect().props('isSearchLoading')).toBe(true);
});
- it('passes `search` variable to query', async () => {
+ it('passes `search` param to API call', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(defaultQueryHandler).toHaveBeenCalledWith(expect.objectContaining({ search: 'foo' }));
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ projectId,
+ expect.objectContaining({ search: 'foo' }),
+ );
+ });
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays dismissible error alert', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ mockRejectedGetTransferLocations();
+ findNamespaceSelect().vm.$emit('search', 'foo');
+ await waitForPromises();
+
+ const alert = findAlert();
+
+ expect(alert.exists()).toBe(true);
+
+ alert.vm.$emit('dismiss');
+ await nextTick();
+
+ expect(alert.exists()).toBe(false);
+ });
});
});
describe('when `load-more-groups` event is fired', () => {
- let queryHandler;
-
const arrange = async () => {
- queryHandler = jest.fn();
- queryHandler.mockResolvedValueOnce(
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1,
- );
- queryHandler.mockResolvedValueOnce(
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2,
- );
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
- createComponent({
- requestHandlers: [[searchNamespacesWhereUserCanTransferProjectsQuery, queryHandler]],
+ mockResolvedGetTransferLocations({
+ data: transferLocationsResponsePage2,
+ page: '2',
+ nextPage: null,
+ prevPage: '1',
});
- runDebounce();
- await waitForPromises();
-
findNamespaceSelect().vm.$emit('load-more-groups');
await nextTick();
};
- it('sets `isLoadingMoreGroups` prop to `true`', async () => {
+ it('sets `isLoading` prop to `true`', async () => {
await arrange();
- expect(findNamespaceSelect().props('isLoadingMoreGroups')).toBe(true);
+ expect(findNamespaceSelect().props('isLoading')).toBe(true);
});
- it('passes `after` and `first` variables to query', async () => {
+ it('passes `page` param to API call', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(queryHandler).toHaveBeenCalledWith(
- expect.objectContaining({
- first: 25,
- after:
- searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
- .pageInfo.endCursor,
- }),
+ expect(getTransferLocations).toHaveBeenCalledWith(
+ projectId,
+ expect.objectContaining({ page: 2 }),
);
});
it('updates `groupNamespaces` prop with new groups', async () => {
await arrange();
- runDebounce();
await waitForPromises();
- expect(findNamespaceSelect().props('groupNamespaces')).toEqual(
- [
- ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage1.data.currentUser.groups
- .nodes,
- ...searchNamespacesWhereUserCanTransferProjectsQueryResponsePage2.data.currentUser.groups
- .nodes,
- ].map((node) => ({
- id: getIdFromGraphQLId(node.id),
- humanName: node.fullName,
- })),
+ expect(findNamespaceSelect().props('groupNamespaces')).toMatchObject(
+ [...transferLocationsResponsePage1, ...transferLocationsResponsePage2].map(
+ ({ id, full_name: humanName }) => ({
+ id,
+ humanName,
+ }),
+ ),
);
});
it('updates `hasNextPageOfGroups` prop', async () => {
await arrange();
- runDebounce();
await waitForPromises();
expect(findNamespaceSelect().props('hasNextPageOfGroups')).toBe(false);
});
+
+ describe('when `getTransferLocations` API call fails', () => {
+ it('displays error alert', async () => {
+ mockResolvedGetTransferLocations();
+ createComponent();
+ await showNamespaceSelect();
+ mockRejectedGetTransferLocations();
+ findNamespaceSelect().vm.$emit('load-more-groups');
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
index 441e21ee905..5b0772f6e34 100644
--- a/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
@@ -146,7 +146,7 @@ describe('LabelsSelectRoot', () => {
});
it('creates flash with error message', () => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
message: 'Error fetching epic color.',
});
@@ -186,7 +186,7 @@ describe('LabelsSelectRoot', () => {
findDropdownContents().vm.$emit('setColor', color);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
captureError: true,
error: expect.anything(),
message: 'An error occurred while updating color.',
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 4140ec09b4e..66ef473f368 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
@@ -159,7 +159,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -233,7 +233,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -252,7 +252,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -298,7 +298,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
@@ -376,7 +376,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/groups/fake_group_endpoint/members');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
@@ -395,7 +395,7 @@ describe('Filters actions', () => {
[],
).then(() => {
expect(mock.history.get[0].url).toBe('/api/v1/projects/fake_project_endpoint/users');
- expect(createFlash).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalled();
});
});
});
@@ -441,7 +441,7 @@ describe('Filters actions', () => {
},
],
[],
- ).then(() => expect(createFlash).toHaveBeenCalled());
+ ).then(() => expect(createAlert).toHaveBeenCalled());
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 302dfabffb2..5371b9af475 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -140,13 +140,13 @@ describe('AuthorToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchAuthors').mockRejectedValue({});
getBaseToken().vm.$emit('fetch-suggestions', 'root');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching users.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
index 1de35daa3a5..05b42011fe1 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/branch_token_spec.js
@@ -9,7 +9,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BranchToken from '~/vue_shared/components/filtered_search_bar/tokens/branch_token.vue';
@@ -87,13 +87,13 @@ describe('BranchToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchBranches').mockRejectedValue({});
wrapper.vm.fetchBranches('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching branches.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index c9879987931..5b744521979 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -94,7 +94,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -108,7 +108,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -134,7 +134,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -148,7 +148,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmContactsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -159,7 +159,7 @@ describe('CrmContactToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -167,7 +167,7 @@ describe('CrmContactToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM contacts.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 16333b052e6..3a3e96032e8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -8,7 +8,7 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -93,7 +93,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -107,7 +107,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchGroupCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'group',
isProject: false,
@@ -133,7 +133,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', 'foo');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -147,7 +147,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions', '5');
await waitForPromises();
- expect(createFlash).not.toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
expect(searchProjectCrmOrganizationsQueryHandler).toHaveBeenCalledWith({
fullPath: 'project',
isProject: true,
@@ -158,7 +158,7 @@ describe('CrmOrganizationToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', async () => {
+ it('calls `createAlert` with flash error message when request fails', async () => {
mountComponent();
jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
@@ -166,7 +166,7 @@ describe('CrmOrganizationToken', () => {
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching CRM organizations.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
index bf4a6eb7635..e8436d2db17 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/emoji_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -93,13 +93,13 @@ describe('EmojiToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchEmojis').mockRejectedValue({});
wrapper.vm.fetchEmojis('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching emojis.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index 01e281884ed..8ca12afacec 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -11,7 +11,7 @@ import {
mockRegularLabel,
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -116,13 +116,13 @@ describe('LabelToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
wrapper.vm.fetchLabels('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching labels.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
index f71ba51fc5b..589697fe542 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -8,7 +8,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { sortMilestonesByDueDate } from '~/milestones/utils';
@@ -112,13 +112,13 @@ describe('MilestoneToken', () => {
});
});
- it('calls `createFlash` with flash error message when request fails', () => {
+ it('calls `createAlert` with flash error message when request fails', () => {
jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
wrapper.vm.fetchMilestones('foo');
return waitForPromises().then(() => {
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching milestones.',
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
index 4bbbaab9b7a..0e5fa0f66d4 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/release_token_spec.js
@@ -2,7 +2,7 @@ import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui'
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import ReleaseToken from '~/vue_shared/components/filtered_search_bar/tokens/release_token.vue';
import { mockReleaseToken } from '../mock_data';
@@ -73,7 +73,7 @@ describe('ReleaseToken', () => {
});
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
+ expect(createAlert).toHaveBeenCalledWith({
message: 'There was a problem fetching releases.',
});
});
diff --git a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
index 2c14d65186b..6003f2b4c48 100644
--- a/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
+++ b/spec/frontend/vue_shared/components/namespace_select/namespace_select_spec.js
@@ -207,9 +207,9 @@ describe('Namespace Select', () => {
expect(wrapper.emitted('load-more-groups')).toEqual([[]]);
});
- describe('when `isLoadingMoreGroups` prop is `true`', () => {
+ describe('when `isLoading` prop is `true`', () => {
it('renders a loading icon', () => {
- wrapper = createComponent({ hasNextPageOfGroups: true, isLoadingMoreGroups: true });
+ wrapper = createComponent({ hasNextPageOfGroups: true, isLoading: true });
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
@@ -223,4 +223,14 @@ describe('Namespace Select', () => {
expect(wrapper.findComponent(GlSearchBoxByType).props('isLoading')).toBe(true);
});
});
+
+ describe('when dropdown is opened', () => {
+ it('emits `show` event', () => {
+ wrapper = createComponent();
+
+ findDropdown().vm.$emit('show');
+
+ expect(wrapper.emitted('show')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
index 933b6d6be9e..bf9c738a3e1 100644
--- a/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
+++ b/spec/lib/gitlab/config_checker/external_database_checker_spec.rb
@@ -6,36 +6,97 @@ RSpec.describe Gitlab::ConfigChecker::ExternalDatabaseChecker do
describe '#check' do
subject { described_class.check }
- context 'when database meets minimum supported version' do
+ let(:old_database_version) { 8 }
+ let(:old_database) { instance_double(Gitlab::Database::Reflection) }
+ let(:new_database) { instance_double(Gitlab::Database::Reflection) }
+
+ before do
+ allow(Gitlab::Database::Reflection).to receive(:new).and_call_original
+ allow(old_database).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ allow(old_database).to receive(:version).and_return(old_database_version)
+ allow(new_database).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ end
+
+ context 'with a single database' do
before do
- allow(ApplicationRecord.database).to receive(:postgresql_minimum_supported_version?).and_return(true)
+ skip_if_multiple_databases_are_setup
+ end
+
+ context 'when database meets minimum supported version' do
+ before do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(new_database)
+ end
+
+ it { is_expected.to be_empty }
end
- it { is_expected.to be_empty }
+ context 'when database does not meet minimum supported version' do
+ before do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(old_database)
+ end
+
+ it 'reports deprecated database notice' do
+ is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ end
+ end
end
- context 'when database does not meet minimum supported version' do
+ context 'with a multiple database' do
before do
- allow(ApplicationRecord.database).to receive(:postgresql_minimum_supported_version?).and_return(false)
+ skip_if_multiple_databases_not_setup
end
- let(:notice_deprecated_database) do
- {
- type: 'warning',
- message: _('You are using PostgreSQL %{pg_version_current}, but PostgreSQL ' \
- '%{pg_version_minimum} is required for this version of GitLab. ' \
- 'Please upgrade your environment to a supported PostgreSQL version, ' \
- 'see %{pg_requirements_url} for details.') % {
- pg_version_current: ApplicationRecord.database.version,
- pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
- pg_requirements_url: '<a href="https://docs.gitlab.com/ee/install/requirements.html#database">database requirements</a>'
- }
- }
+ context 'when both databases meets minimum supported version' do
+ before do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(new_database)
+ allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(new_database)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when the one of the databases does not meet minimum supported version' do
+ it 'reports deprecated database notice if the main database is using an old version' do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(old_database)
+ allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(new_database)
+ is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ end
+
+ it 'reports deprecated database notice if the ci database is using an old version' do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(new_database)
+ allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(old_database)
+ is_expected.to contain_exactly(notice_deprecated_database(old_database_version))
+ end
end
- it 'reports deprecated database notice' do
- is_expected.to contain_exactly(notice_deprecated_database)
+ context 'when both databases do not meet minimum supported version' do
+ before do
+ allow(Gitlab::Database::Reflection).to receive(:new).with(ActiveRecord::Base).and_return(old_database)
+ allow(Gitlab::Database::Reflection).to receive(:new).with(Ci::ApplicationRecord).and_return(old_database)
+ end
+
+ it 'reports deprecated database notice' do
+ is_expected.to match_array [
+ notice_deprecated_database(old_database_version),
+ notice_deprecated_database(old_database_version)
+ ]
+ end
end
end
end
+
+ def notice_deprecated_database(database_version)
+ {
+ type: 'warning',
+ message: _('You are using PostgreSQL %{pg_version_current}, but PostgreSQL ' \
+ '%{pg_version_minimum} is required for this version of GitLab. ' \
+ 'Please upgrade your environment to a supported PostgreSQL version, ' \
+ 'see %{pg_requirements_url} for details.') % \
+ {
+ pg_version_current: database_version,
+ pg_version_minimum: Gitlab::Database::MINIMUM_POSTGRES_VERSION,
+ pg_requirements_url: Gitlab::ConfigChecker::ExternalDatabaseChecker::PG_REQUIREMENTS_LINK
+ }
+ }
+ end
end
diff --git a/spec/models/project_statistics_spec.rb b/spec/models/project_statistics_spec.rb
index b2158baa670..f3f1e43f6c5 100644
--- a/spec/models/project_statistics_spec.rb
+++ b/spec/models/project_statistics_spec.rb
@@ -98,6 +98,8 @@ RSpec.describe ProjectStatistics do
end
describe '#refresh!' do
+ subject { statistics.refresh! }
+
before do
allow(statistics).to receive(:update_commit_count)
allow(statistics).to receive(:update_repository_size)
@@ -111,7 +113,7 @@ RSpec.describe ProjectStatistics do
context "without arguments" do
before do
- statistics.refresh!
+ subject
end
it "sums all counters" do
@@ -146,7 +148,7 @@ RSpec.describe ProjectStatistics do
expect(project.repository.exists?).to be_falsey
expect(project.wiki.repository.exists?).to be_falsey
- statistics.refresh!
+ subject
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
@@ -174,7 +176,7 @@ RSpec.describe ProjectStatistics do
end
it 'does not crash' do
- statistics.refresh!
+ subject
expect(statistics).to have_received(:update_commit_count)
expect(statistics).to have_received(:update_repository_size)
@@ -209,7 +211,7 @@ RSpec.describe ProjectStatistics do
expect(Namespaces::ScheduleAggregationWorker)
.to receive(:perform_async)
- statistics.refresh!
+ subject
end
end
end
@@ -238,9 +240,13 @@ RSpec.describe ProjectStatistics do
expect(Namespaces::ScheduleAggregationWorker)
.not_to receive(:perform_async)
- statistics.refresh!
+ subject
end
end
+
+ it_behaves_like 'obtaining lease to update database' do
+ let(:model) { statistics }
+ end
end
describe '#update_commit_count' do
@@ -408,6 +414,8 @@ RSpec.describe ProjectStatistics do
end
describe '#refresh_storage_size!' do
+ subject { statistics.refresh_storage_size! }
+
it 'recalculates storage size from its components and save it' do
statistics.update_columns(
repository_size: 2,
@@ -422,7 +430,11 @@ RSpec.describe ProjectStatistics do
storage_size: 0
)
- expect { statistics.refresh_storage_size! }.to change { statistics.storage_size }.from(0).to(28)
+ expect { subject }.to change { statistics.storage_size }.from(0).to(28)
+ end
+
+ it_behaves_like 'obtaining lease to update database' do
+ let(:model) { statistics }
end
end
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index cd4e8b30d8f..8b045a32e47 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -378,6 +378,17 @@ RSpec.describe API::PagesDomains do
expect(pages_domain_secure.auto_ssl_enabled).to be false
end
+ it 'publishes PagesDomainUpdatedEvent event' do
+ expect { put api(route_secure_domain, user), params: { certificate: nil, key: nil } }
+ .to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: project.id,
+ namespace_id: project.namespace.id,
+ root_namespace_id: project.root_namespace.id,
+ domain: pages_domain_secure.domain
+ )
+ end
+
it 'updates pages domain adding certificate' do
put api(route_domain, user), params: params_secure
pages_domain.reload
@@ -446,22 +457,29 @@ RSpec.describe API::PagesDomains do
end.to change { pages_domain.reload.certificate_source }.from('gitlab_provided').to('user_provided')
end
- it 'fails to update pages domain adding certificate without key' do
- put api(route_domain, user), params: params_secure_nokey
+ context 'with invalid params' do
+ it 'fails to update pages domain adding certificate without key' do
+ put api(route_domain, user), params: params_secure_nokey
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'does not publish PagesDomainUpdatedEvent event' do
+ expect { put api(route_domain, user), params: params_secure_nokey }
+ .not_to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ end
- it 'fails to update pages domain adding certificate with missing chain' do
- put api(route_domain, user), params: pages_domain_secure_missing_chain_params.slice(:certificate)
+ it 'fails to update pages domain adding certificate with missing chain' do
+ put api(route_domain, user), params: pages_domain_secure_missing_chain_params.slice(:certificate)
- expect(response).to have_gitlab_http_status(:bad_request)
- end
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
- it 'fails to update pages domain with key missmatch' do
- put api(route_secure_domain, user), params: pages_domain_secure_key_missmatch_params.slice(:certificate, :key)
+ it 'fails to update pages domain with key missmatch' do
+ put api(route_secure_domain, user), params: pages_domain_secure_key_missmatch_params.slice(:certificate, :key)
- expect(response).to have_gitlab_http_status(:bad_request)
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
end
end
diff --git a/spec/services/pages_domains/update_service_spec.rb b/spec/services/pages_domains/update_service_spec.rb
new file mode 100644
index 00000000000..f6558f56422
--- /dev/null
+++ b/spec/services/pages_domains/update_service_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe PagesDomains::UpdateService do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:pages_domain) { create(:pages_domain, :with_project) }
+
+ let(:params) do
+ attributes_for(:pages_domain, :with_trusted_chain).slice(:key, :certificate).tap do |params|
+ params[:user_provided_key] = params.delete(:key)
+ params[:user_provided_certificate] = params.delete(:certificate)
+ end
+ end
+
+ subject(:service) { described_class.new(pages_domain.project, user, params) }
+
+ context 'when the user does not have the required permissions' do
+ it 'does not update the pages domain and does not publish a PagesDomainUpdatedEvent' do
+ expect do
+ expect(service.execute(pages_domain)).to be_nil
+ end.to not_publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ end
+ end
+
+ context 'when the user has the required permissions' do
+ before do
+ pages_domain.project.add_maintainer(user)
+ end
+
+ context 'when it updates the domain successfully' do
+ it 'updates the domain' do
+ expect(service.execute(pages_domain)).to eq(true)
+ end
+
+ it 'publishes a PagesDomainUpdatedEvent' do
+ expect { service.execute(pages_domain) }
+ .to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ .with(
+ project_id: pages_domain.project.id,
+ namespace_id: pages_domain.project.namespace.id,
+ root_namespace_id: pages_domain.project.root_namespace.id,
+ domain: pages_domain.domain
+ )
+ end
+ end
+
+ context 'when it fails to update the domain' do
+ let(:params) { { user_provided_certificate: 'blabla' } }
+
+ it 'does not update a pages domain' do
+ expect(service.execute(pages_domain)).to be(false)
+ end
+
+ it 'does not publish a PagesDomainUpdatedEvent' do
+ expect { service.execute(pages_domain) }
+ .not_to publish_event(PagesDomains::PagesDomainUpdatedEvent)
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/exclusive_lease_helpers.rb b/spec/support/helpers/exclusive_lease_helpers.rb
index 95cfc56c273..06e5ae5427c 100644
--- a/spec/support/helpers/exclusive_lease_helpers.rb
+++ b/spec/support/helpers/exclusive_lease_helpers.rb
@@ -2,6 +2,8 @@
module ExclusiveLeaseHelpers
def stub_exclusive_lease(key = nil, uuid = 'uuid', renew: false, timeout: nil)
+ prepare_exclusive_lease_stub
+
key ||= instance_of(String)
timeout ||= instance_of(Integer)
@@ -37,4 +39,21 @@ module ExclusiveLeaseHelpers
.to receive(:cancel)
.with(key, uuid)
end
+
+ private
+
+ # This prepares the stub to be able to stub specific lease keys
+ # while allowing unstubbed lease keys to behave as original.
+ #
+ # allow(Gitlab::ExclusiveLease).to receive(:new).and_call_original
+ # can only be called once to prevent resetting stubs when
+ # `stub_exclusive_lease` is called multiple times.
+ def prepare_exclusive_lease_stub
+ return if @exclusive_lease_allowed_to_call_original
+
+ allow(Gitlab::ExclusiveLease)
+ .to receive(:new).and_call_original
+
+ @exclusive_lease_allowed_to_call_original = true
+ end
end
diff --git a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
index 91e517270ff..ea4ba536403 100644
--- a/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
+++ b/spec/support/shared_examples/models/concerns/counter_attribute_shared_examples.rb
@@ -92,8 +92,8 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
it 'obtains an exclusive lease during processing' do
expect(model)
.to receive(:in_lock)
- .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
- .and_call_original
+ .with(model.counter_lock_key(incremented_attribute), ttl: described_class::WORKER_LOCK_TTL)
+ .and_call_original
subject
end
@@ -186,31 +186,88 @@ RSpec.shared_examples_for CounterAttribute do |counter_attributes|
end
end
- describe '#clear_counter!' do
+ describe '#reset_counter!' do
let(:attribute) { counter_attributes.first }
before do
+ model.update!(attribute => 123)
model.increment_counter(attribute, 10)
end
- it 'deletes the counter key for the given attribute and logs it' do
- expect(Gitlab::AppLogger).to receive(:info).with(
- hash_including(
- message: 'Clear counter attribute',
- attribute: attribute,
- project_id: model.project_id,
- 'correlation_id' => an_instance_of(String),
- 'meta.feature_category' => 'test',
- 'meta.caller_id' => 'caller'
- )
- )
+ subject { model.reset_counter!(attribute) }
- model.clear_counter!(attribute)
+ it 'resets the attribute value to 0 and clears existing counter', :aggregate_failures do
+ expect { subject }.to change { model.reload.send(attribute) }.from(123).to(0)
Gitlab::Redis::SharedState.with do |redis|
key_exists = redis.exists?(model.counter_key(attribute))
expect(key_exists).to be_falsey
end
end
+
+ it_behaves_like 'obtaining lease to update database' do
+ context 'when the execution raises error' do
+ before do
+ allow(model).to receive(:update!).and_raise(StandardError, 'Something went wrong')
+ end
+
+ it 'reraises error' do
+ expect { subject }.to raise_error(StandardError, 'Something went wrong')
+ end
+ end
+ end
+ end
+
+ describe '#update_counters_with_lease' do
+ let(:increments) { { build_artifacts_size: 1, packages_size: 2 } }
+
+ subject { model.update_counters_with_lease(increments) }
+
+ it 'updates counters of the record' do
+ expect { subject }
+ .to change { model.reload.build_artifacts_size }.by(1)
+ .and change { model.reload.packages_size }.by(2)
+ end
+
+ it_behaves_like 'obtaining lease to update database' do
+ context 'when the execution raises error' do
+ before do
+ allow(model.class).to receive(:update_counters).and_raise(StandardError, 'Something went wrong')
+ end
+
+ it 'reraises error' do
+ expect { subject }.to raise_error(StandardError, 'Something went wrong')
+ end
+ end
+ end
+ end
+end
+
+RSpec.shared_examples 'obtaining lease to update database' do
+ context 'when it is unable to obtain lock' do
+ before do
+ allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+ end
+
+ it 'logs a warning' do
+ allow(model).to receive(:in_lock).and_raise(Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError)
+
+ expect(Gitlab::AppLogger).to receive(:warn).once
+
+ expect { subject }.not_to raise_error
+ end
+ end
+
+ context 'when feature flag counter_attribute_db_lease_for_update is disabled' do
+ before do
+ stub_feature_flags(counter_attribute_db_lease_for_update: false)
+ allow(model).to receive(:in_lock).and_call_original
+ end
+
+ it 'does not attempt to get a lock' do
+ expect(model).not_to receive(:in_lock)
+
+ subject
+ end
end
end
diff --git a/spec/views/layouts/_flash.html.haml_spec.rb b/spec/views/layouts/_flash.html.haml_spec.rb
index a4bed09368f..437cb940c9e 100644
--- a/spec/views/layouts/_flash.html.haml_spec.rb
+++ b/spec/views/layouts/_flash.html.haml_spec.rb
@@ -3,9 +3,20 @@
require 'spec_helper'
RSpec.describe 'layouts/_flash' do
+ let_it_be(:template) { 'layouts/_flash' }
+ let_it_be(:flash_container_no_margin_class) { 'flash-container-no-margin' }
+
+ let(:locals) { {} }
+
before do
allow(view).to receive(:flash).and_return(flash)
- render
+ render(template: template, locals: locals)
+ end
+
+ describe 'default' do
+ it 'does not render flash container no margin class' do
+ expect(rendered).not_to have_selector(".#{flash_container_no_margin_class}")
+ end
end
describe 'closable flash messages' do
@@ -35,4 +46,12 @@ RSpec.describe 'layouts/_flash' do
end
end
end
+
+ describe 'with flash_class in locals' do
+ let(:locals) { { flash_container_no_margin: true } }
+
+ it 'adds class to flash-container' do
+ expect(rendered).to have_selector(".flash-container.#{flash_container_no_margin_class}")
+ end
+ end
end
diff --git a/spec/views/layouts/fullscreen.html.haml_spec.rb b/spec/views/layouts/fullscreen.html.haml_spec.rb
index 14b382bc238..c29099df1bd 100644
--- a/spec/views/layouts/fullscreen.html.haml_spec.rb
+++ b/spec/views/layouts/fullscreen.html.haml_spec.rb
@@ -16,6 +16,13 @@ RSpec.describe 'layouts/fullscreen' do
expect(rendered).to have_selector(".gl--flex-full.gl-w-full")
end
+ it 'renders flash container' do
+ render
+
+ expect(view).to render_template("layouts/_flash")
+ expect(rendered).to have_selector(".flash-container.flash-container-no-margin")
+ end
+
it_behaves_like 'a layout which reflects the application theme setting'
describe 'sidebar' do