summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb28
-rw-r--r--spec/controllers/import/github_controller_spec.rb13
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js18
-rw-r--r--spec/frontend/__helpers__/matchers/to_have_sprite_icon.js2
-rw-r--r--spec/frontend/admin/users/index_spec.js8
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js4
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js4
-rw-r--r--spec/frontend/blob/viewer/index_spec.js6
-rw-r--r--spec/frontend/cascading_settings/components/lock_popovers_spec.js10
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js12
-rw-r--r--spec/frontend/confirm_modal_spec.js6
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js7
-rw-r--r--spec/frontend/issues/create_merge_request_dropdown_spec.js4
-rw-r--r--spec/frontend/labels/delete_label_modal_spec.js6
-rw-r--r--spec/frontend/lazy_loader_spec.js4
-rw-r--r--spec/frontend/members/index_spec.js2
-rw-r--r--spec/frontend/members/utils_spec.js2
-rw-r--r--spec/frontend/notebook/cells/markdown_spec.js4
-rw-r--r--spec/frontend/notes/stores/actions_spec.js10
-rw-r--r--spec/frontend/performance_bar/index_spec.js10
-rw-r--r--spec/frontend/search_autocomplete_spec.js2
-rw-r--r--spec/frontend/user_popovers_spec.js2
-rw-r--r--spec/frontend/users_select/test_helper.js8
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js192
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js30
-rw-r--r--spec/frontend_integration/ide/helpers/ide_helper.js4
-rw-r--r--spec/lib/container_registry/migration_spec.rb27
-rw-r--r--spec/models/group_spec.rb47
-rw-r--r--spec/models/members/last_group_owner_assigner_spec.rb13
-rw-r--r--spec/requests/api/terraform/modules/v1/packages_spec.rb47
-rw-r--r--spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb13
-rw-r--r--spec/workers/delete_container_repository_worker_spec.rb120
39 files changed, 820 insertions, 128 deletions
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 25d32436d58..c6fd184ede0 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -305,11 +305,37 @@ RSpec.describe Groups::GroupMembersController do
group.add_owner(user)
end
- it 'cannot removes himself from the group' do
+ it 'cannot remove user from the group' do
delete :leave, params: { group_id: group }
expect(response).to have_gitlab_http_status(:forbidden)
end
+
+ context 'and there is a group project bot owner' do
+ before do
+ create(:group_member, :owner, source: group, user: create(:user, :project_bot))
+ end
+
+ it 'cannot remove user from the group' do
+ delete :leave, params: { group_id: group }
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'and there is another owner' do
+ before do
+ create(:group_member, :owner, source: group)
+ end
+
+ it 'removes user from members', :aggregate_failures do
+ delete :leave, params: { group_id: group }
+
+ expect(controller).to set_flash.to "You left the \"#{group.name}\" group."
+ expect(response).to redirect_to(dashboard_groups_path)
+ expect(group.users).not_to include user
+ end
+ end
end
context 'and is a requester' do
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index ef66124bff1..56e55c45e66 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -96,19 +96,6 @@ RSpec.describe Import::GithubController do
describe "POST personal_access_token" do
it_behaves_like 'a GitHub-ish import controller: POST personal_access_token'
-
- it 'passes namespace_id param as query param if it was present' do
- namespace_id = 5
- status_import_url = public_send("status_import_#{provider}_url", { namespace_id: namespace_id })
-
- allow_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
- allow(client).to receive(:user).and_return(true)
- end
-
- post :personal_access_token, params: { personal_access_token: 'some-token', namespace_id: 5 }
-
- expect(controller).to redirect_to(status_import_url)
- end
end
describe "GET status" do
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index ee01e9e6268..6b719a32480 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -13,16 +13,16 @@ export default function initVueMRPage() {
const diffsAppProjectPath = 'testproject';
const mrEl = document.createElement('div');
mrEl.className = 'merge-request fixture-mr';
- mrEl.setAttribute('data-mr-action', 'diffs');
+ mrEl.dataset.mrAction = 'diffs';
mrTestEl.appendChild(mrEl);
const mrDiscussionsEl = document.createElement('div');
mrDiscussionsEl.id = 'js-vue-mr-discussions';
- mrDiscussionsEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
- mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
- mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
- mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
- mrDiscussionsEl.setAttribute('data-is-locked', 'false');
+ mrDiscussionsEl.dataset.currentUserData = JSON.stringify(userDataMock);
+ mrDiscussionsEl.dataset.noteableData = JSON.stringify(noteableDataMock);
+ mrDiscussionsEl.dataset.notesData = JSON.stringify(notesDataMock);
+ mrDiscussionsEl.dataset.noteableType = 'merge-request';
+ mrDiscussionsEl.dataset.isLocked = 'false';
mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
@@ -31,9 +31,9 @@ export default function initVueMRPage() {
const diffsAppEl = document.createElement('div');
diffsAppEl.id = 'js-diffs-app';
- diffsAppEl.setAttribute('data-endpoint', diffsAppEndpoint);
- diffsAppEl.setAttribute('data-project-path', diffsAppProjectPath);
- diffsAppEl.setAttribute('data-current-user-data', JSON.stringify(userDataMock));
+ diffsAppEl.dataset.endpoint = diffsAppEndpoint;
+ diffsAppEl.dataset.projectPath = diffsAppProjectPath;
+ diffsAppEl.dataset.currentUserData = JSON.stringify(userDataMock);
mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
diff --git a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
index bce9d93bea8..45b9c31c4db 100644
--- a/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
+++ b/spec/frontend/__helpers__/matchers/to_have_sprite_icon.js
@@ -9,7 +9,7 @@ export const toHaveSpriteIcon = (element, iconName) => {
const iconReferences = [].slice.apply(element.querySelectorAll('svg use'));
const matchingIcon = iconReferences.find(
- (reference) => reference.parentNode.getAttribute('data-testid') === `${iconName}-icon`,
+ (reference) => reference.parentNode.dataset.testid === `${iconName}-icon`,
);
const pass = Boolean(matchingIcon);
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 06dbadd6d3d..961fa96acdd 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -12,8 +12,8 @@ describe('initAdminUsersApp', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-users', JSON.stringify(users));
- el.setAttribute('data-paths', JSON.stringify(paths));
+ el.dataset.users = JSON.stringify(users);
+ el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUsersApp(el));
});
@@ -40,8 +40,8 @@ describe('initAdminUserActions', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-user', JSON.stringify(user));
- el.setAttribute('data-paths', JSON.stringify(paths));
+ el.dataset.user = JSON.stringify(user);
+ el.dataset.paths = JSON.stringify(paths);
wrapper = createWrapper(initAdminUserActions(el));
});
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index 0ff9d60f409..f9a6b2df662 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -15,8 +15,8 @@ describe('initRecoveryCodes', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('class', 'js-2fa-recovery-codes');
- el.setAttribute('data-codes', codesJsonString);
- el.setAttribute('data-profile-account-path', profileAccountPath);
+ el.dataset.codes = codesJsonString;
+ el.dataset.profileAccountPath = profileAccountPath;
document.body.appendChild(el);
wrapper = createWrapper(initRecoveryCodes());
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 358ac31819c..2cbac809a0d 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -11,7 +11,7 @@ function createComponent() {
}
async function setLoaded(loaded) {
- document.querySelector('.blob-viewer').setAttribute('data-loaded', loaded);
+ document.querySelector('.blob-viewer').dataset.loaded = loaded;
await nextTick();
}
@@ -53,7 +53,7 @@ describe('Markdown table of contents component', () => {
it('does not show dropdown when viewing non-rich content', async () => {
createComponent();
- document.querySelector('.blob-viewer').setAttribute('data-type', 'simple');
+ document.querySelector('.blob-viewer').dataset.type = 'simple';
await setLoaded(true);
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 5f6baf3f63d..b2559af182b 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -80,9 +80,9 @@ describe('Blob viewer', () => {
return asyncClick()
.then(() => asyncClick())
.then(() => {
- expect(
- document.querySelector('.blob-viewer[data-type="simple"]').getAttribute('data-loaded'),
- ).toBe('true');
+ expect(document.querySelector('.blob-viewer[data-type="simple"]').dataset.loaded).toBe(
+ 'true',
+ );
});
});
diff --git a/spec/frontend/cascading_settings/components/lock_popovers_spec.js b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
index 585e6ac505b..182e3c1c8ff 100644
--- a/spec/frontend/cascading_settings/components/lock_popovers_spec.js
+++ b/spec/frontend/cascading_settings/components/lock_popovers_spec.js
@@ -21,12 +21,12 @@ describe('LockPopovers', () => {
};
if (lockedByApplicationSetting) {
- popoverMountEl.setAttribute('data-popover-data', JSON.stringify(popoverData));
+ popoverMountEl.dataset.popoverData = JSON.stringify(popoverData);
} else if (lockedByAncestor) {
- popoverMountEl.setAttribute(
- 'data-popover-data',
- JSON.stringify({ ...popoverData, ancestor_namespace: mockNamespace }),
- );
+ popoverMountEl.dataset.popoverData = JSON.stringify({
+ ...popoverData,
+ ancestor_namespace: mockNamespace,
+ });
}
document.body.appendChild(popoverMountEl);
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index c47a9e697b6..8eee61d1342 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -195,8 +195,8 @@ describe('Code navigation actions', () => {
it('commits SET_CURRENT_DEFINITION with LSIF data', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@@ -218,8 +218,8 @@ describe('Code navigation actions', () => {
it('adds hll class to target element', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
@@ -243,8 +243,8 @@ describe('Code navigation actions', () => {
it('caches current target element', () => {
target.classList.add('js-code-navigation');
- target.setAttribute('data-line-index', '0');
- target.setAttribute('data-char-index', '0');
+ target.dataset.lineIndex = '0';
+ target.dataset.charIndex = '0';
return testAction(
actions.showDefinition,
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index 53991349ee5..4224fb6be2a 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -31,9 +31,9 @@ describe('ConfirmModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-confirm-modal-button');
- button.setAttribute('data-path', x.path);
- button.setAttribute('data-method', x.method);
- button.setAttribute('data-modal-attributes', JSON.stringify(x.modalAttributes));
+ button.dataset.path = x.path;
+ button.dataset.method = x.method;
+ button.dataset.modalAttributes = JSON.stringify(x.modalAttributes);
button.innerHTML = 'Action';
buttonContainer.appendChild(button);
});
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
index 2236b5aa261..05161437c22 100644
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ b/spec/frontend/helpers/startup_css_helper_spec.js
@@ -59,9 +59,10 @@ describe('waitForCSSLoaded', () => {
<link href="two.css" data-startupcss="loading">
`);
const events = waitForCSSLoaded(mockedCallback);
- document
- .querySelectorAll('[data-startupcss="loading"]')
- .forEach((elem) => elem.setAttribute('data-startupcss', 'loaded'));
+ document.querySelectorAll('[data-startupcss="loading"]').forEach((elem) => {
+ // eslint-disable-next-line no-param-reassign
+ elem.dataset.startupcss = 'loaded';
+ });
document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
await events;
diff --git a/spec/frontend/issues/create_merge_request_dropdown_spec.js b/spec/frontend/issues/create_merge_request_dropdown_spec.js
index 20b26f5abba..cb7173c56a8 100644
--- a/spec/frontend/issues/create_merge_request_dropdown_spec.js
+++ b/spec/frontend/issues/create_merge_request_dropdown_spec.js
@@ -84,7 +84,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('enables when can create confidential issue', () => {
- document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+ document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
confidentialState.selectedProject = { name: 'test' };
dropdown.enable();
@@ -93,7 +93,7 @@ describe('CreateMergeRequestDropdown', () => {
});
it('does not enable when can not create confidential issue', () => {
- document.querySelector('.js-create-mr').setAttribute('data-is-confidential', 'true');
+ document.querySelector('.js-create-mr').dataset.isConfidential = 'true';
dropdown.enable();
diff --git a/spec/frontend/labels/delete_label_modal_spec.js b/spec/frontend/labels/delete_label_modal_spec.js
index 98049538948..67220821fe0 100644
--- a/spec/frontend/labels/delete_label_modal_spec.js
+++ b/spec/frontend/labels/delete_label_modal_spec.js
@@ -25,11 +25,11 @@ describe('DeleteLabelModal', () => {
buttons.forEach((x) => {
const button = document.createElement('button');
button.setAttribute('class', 'js-delete-label-modal-button');
- button.setAttribute('data-label-name', x.labelName);
- button.setAttribute('data-destroy-path', x.destroyPath);
+ button.dataset.labelName = x.labelName;
+ button.dataset.destroyPath = x.destroyPath;
if (x.subjectName) {
- button.setAttribute('data-subject-name', x.subjectName);
+ button.dataset.subjectName = x.subjectName;
}
button.innerHTML = 'Action';
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index 3d8b0d9c307..e0b6c7119f9 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -27,7 +27,7 @@ describe('LazyLoader', () => {
const createLazyLoadImage = () => {
const newImg = document.createElement('img');
newImg.className = 'lazy';
- newImg.setAttribute('data-src', TEST_PATH);
+ newImg.dataset.src = TEST_PATH;
document.body.appendChild(newImg);
triggerChildMutation();
@@ -108,7 +108,7 @@ describe('LazyLoader', () => {
expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
- expect(img.getAttribute('data-src')).toBe(null);
+ expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
});
diff --git a/spec/frontend/members/index_spec.js b/spec/frontend/members/index_spec.js
index efabe54f238..251a8b0b774 100644
--- a/spec/frontend/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -24,7 +24,7 @@ describe('initMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-members-data', dataAttribute);
+ el.dataset.membersData = dataAttribute;
window.gon = { current_user_id: 123 };
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index a157cfa1c1d..b0c9459ff4f 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -256,7 +256,7 @@ describe('Members Utils', () => {
beforeEach(() => {
el = document.createElement('div');
- el.setAttribute('data-members-data', dataAttribute);
+ el.dataset.membersData = dataAttribute;
});
afterEach(() => {
diff --git a/spec/frontend/notebook/cells/markdown_spec.js b/spec/frontend/notebook/cells/markdown_spec.js
index 7dc6f90d202..de415b5bfe0 100644
--- a/spec/frontend/notebook/cells/markdown_spec.js
+++ b/spec/frontend/notebook/cells/markdown_spec.js
@@ -78,8 +78,8 @@ describe('Markdown component', () => {
});
await nextTick();
- expect(findLink().getAttribute('data-remote')).toBe(null);
- expect(findLink().getAttribute('data-type')).toBe(null);
+ expect(findLink().dataset.remote).toBeUndefined();
+ expect(findLink().dataset.type).toBeUndefined();
});
describe('When parsing images', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 4ecfbc5de1f..38f29ac2559 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -404,13 +404,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', () => {
@@ -440,7 +440,7 @@ describe('Actions Notes Store', () => {
it('dispatches removeDiscussionsFromDiff on merge request page', () => {
const note = { path: endpoint, id: 1 };
- document.body.setAttribute('data-page', 'projects:merge_requests:show');
+ document.body.dataset.page = 'projects:merge_requests:show';
return testAction(
actions.removeNote,
@@ -473,13 +473,13 @@ describe('Actions Notes Store', () => {
beforeEach(() => {
axiosMock.onDelete(endpoint).replyOnce(200, {});
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
afterEach(() => {
axiosMock.restore();
- document.body.setAttribute('data-page', '');
+ document.body.dataset.page = '';
});
it('dispatches removeNote', () => {
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 008961bf709..2da176dbfe4 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -17,11 +17,11 @@ describe('performance bar wrapper', () => {
performance.getEntriesByType = jest.fn().mockReturnValue([]);
peekWrapper.setAttribute('id', 'js-peek');
- peekWrapper.setAttribute('data-env', 'development');
- peekWrapper.setAttribute('data-request-id', '123');
- peekWrapper.setAttribute('data-peek-url', '/-/peek/results');
- peekWrapper.setAttribute('data-stats-url', 'https://log.gprd.gitlab.net/app/dashboards#/view/');
- peekWrapper.setAttribute('data-profile-url', '?lineprofiler=true');
+ peekWrapper.dataset.env = 'development';
+ peekWrapper.dataset.requestId = '123';
+ peekWrapper.dataset.peekUrl = '/-/peek/results';
+ peekWrapper.dataset.statsUrl = 'https://log.gprd.gitlab.net/app/dashboards#/view/';
+ peekWrapper.dataset.profileUrl = '?lineprofiler=true';
mock = new MockAdapter(axios);
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 4639552b4d3..266f047e9dc 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -53,7 +53,7 @@ describe('Search autocomplete dropdown', () => {
};
const disableProjectIssues = () => {
- document.querySelector('.js-search-project-options').setAttribute('data-issues-disabled', true);
+ document.querySelector('.js-search-project-options').dataset.issuesDisabled = true;
};
// Mock `gl` object in window for dashboard specific page. App code will need it.
diff --git a/spec/frontend/user_popovers_spec.js b/spec/frontend/user_popovers_spec.js
index 2c3db36d7e6..1544fed5240 100644
--- a/spec/frontend/user_popovers_spec.js
+++ b/spec/frontend/user_popovers_spec.js
@@ -22,7 +22,7 @@ describe('User Popovers', () => {
const link = document.createElement('a');
link.classList.add('js-user-link');
- link.setAttribute('data-user', '1');
+ link.dataset.user = '1';
return link;
};
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 59edde48eab..9231e38ea90 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -95,10 +95,10 @@ export const setAssignees = (...users) => {
const input = document.createElement('input');
input.name = 'merge_request[assignee_ids][]';
input.value = user.id.toString();
- input.setAttribute('data-avatar-url', user.avatar_url);
- input.setAttribute('data-name', user.name);
- input.setAttribute('data-username', user.username);
- input.setAttribute('data-can-merge', user.can_merge);
+ input.dataset.avatarUrl = user.avatar_url;
+ input.dataset.name = user.name;
+ input.dataset.username = user.username;
+ input.dataset.canMerge = user.can_merge;
return input;
}),
);
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 8efc4d84624..29ee7e0010f 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -193,9 +193,7 @@ describe('MRWidgetMerged', () => {
it('shows button to copy commit SHA to clipboard', () => {
expect(selectors.copyMergeShaButton).not.toBe(null);
- expect(selectors.copyMergeShaButton.getAttribute('data-clipboard-text')).toBe(
- vm.mr.mergeCommitSha,
- );
+ expect(selectors.copyMergeShaButton.dataset.clipboardText).toBe(vm.mr.mergeCommitSha);
});
it('hides button to copy commit SHA if SHA does not exist', async () => {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index a8c55c2c735..d134877e584 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -424,7 +424,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
const favicon = document.createElement('link');
favicon.setAttribute('id', 'favicon');
- favicon.setAttribute('data-original-href', faviconDataUrl);
+ favicon.dataset.originalHref = faviconDataUrl;
document.body.appendChild(favicon);
faviconElement = document.getElementById('favicon');
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
new file mode 100644
index 00000000000..fe614f03119
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_item_spec.js
@@ -0,0 +1,35 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { hexToRgb } from '~/lib/utils/color_utils';
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import { color } from './mock_data';
+
+describe('ColorItem', () => {
+ let wrapper;
+
+ const propsData = color;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ColorItem, {
+ propsData,
+ });
+ };
+
+ const findColorItem = () => wrapper.findByTestId('color-item');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the correct title', () => {
+ expect(wrapper.text()).toBe(propsData.title);
+ });
+
+ it('renders the correct background color for the color item', () => {
+ const convertedColor = hexToRgb(propsData.color).join(', ');
+ expect(findColorItem().attributes('style')).toBe(`background-color: rgb(${convertedColor});`);
+ });
+});
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
new file mode 100644
index 00000000000..93b59800c27
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/color_select_root_spec.js
@@ -0,0 +1,192 @@
+import { shallowMount } from '@vue/test-utils';
+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 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';
+import epicColorQuery from '~/vue_shared/components/color_select_dropdown/graphql/epic_color.query.graphql';
+import updateEpicColorMutation from '~/vue_shared/components/color_select_dropdown/graphql/epic_update_color.mutation.graphql';
+import ColorSelectRoot from '~/vue_shared/components/color_select_dropdown/color_select_root.vue';
+import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import { colorQueryResponse, updateColorMutationResponse, color } from './mock_data';
+
+jest.mock('~/flash');
+
+Vue.use(VueApollo);
+
+const successfulQueryHandler = jest.fn().mockResolvedValue(colorQueryResponse);
+const successfulMutationHandler = jest.fn().mockResolvedValue(updateColorMutationResponse);
+const errorQueryHandler = jest.fn().mockRejectedValue('Error fetching epic color.');
+const errorMutationHandler = jest.fn().mockRejectedValue('An error occurred while updating color.');
+
+const defaultProps = {
+ allowEdit: true,
+ iid: '1',
+ fullPath: 'workspace-1',
+};
+
+describe('LabelsSelectRoot', () => {
+ let wrapper;
+
+ const findSidebarEditableItem = () => wrapper.findComponent(SidebarEditableItem);
+ const findDropdownValue = () => wrapper.findComponent(DropdownValue);
+ const findDropdownContents = () => wrapper.findComponent(DropdownContents);
+
+ const createComponent = ({
+ queryHandler = successfulQueryHandler,
+ mutationHandler = successfulMutationHandler,
+ propsData,
+ } = {}) => {
+ const mockApollo = createMockApollo([
+ [epicColorQuery, queryHandler],
+ [updateEpicColorMutation, mutationHandler],
+ ]);
+
+ wrapper = shallowMount(ColorSelectRoot, {
+ apolloProvider: mockApollo,
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ provide: {
+ canUpdate: true,
+ },
+ stubs: {
+ SidebarEditableItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ const defaultClasses = ['labels-select-wrapper', 'gl-relative'];
+
+ it.each`
+ variant | cssClass
+ ${'sidebar'} | ${defaultClasses}
+ ${'embedded'} | ${[...defaultClasses, 'is-embedded']}
+ `(
+ 'renders component root element with CSS class `$cssClass` when variant is "$variant"',
+ async ({ variant, cssClass }) => {
+ createComponent({
+ propsData: { variant },
+ });
+
+ expect(wrapper.classes()).toEqual(cssClass);
+ },
+ );
+ });
+
+ describe('if the variant is `sidebar`', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders SidebarEditableItem component', () => {
+ expect(findSidebarEditableItem().exists()).toBe(true);
+ });
+
+ it('renders correct props for the SidebarEditableItem component', () => {
+ expect(findSidebarEditableItem().props()).toMatchObject({
+ title: wrapper.vm.$options.i18n.widgetTitle,
+ canEdit: defaultProps.allowEdit,
+ loading: true,
+ });
+ });
+
+ describe('when colors are loaded', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('passes false `loading` prop to sidebar editable item', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(false);
+ });
+
+ it('renders dropdown value component when query colors is resolved', () => {
+ expect(findDropdownValue().props('selectedColor')).toMatchObject(color);
+ });
+ });
+ });
+
+ describe('if the variant is `embedded`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { iid: undefined, variant: DROPDOWN_VARIANT.Embedded } });
+ });
+
+ it('renders DropdownContents component', () => {
+ expect(findDropdownContents().exists()).toBe(true);
+ });
+
+ it('renders correct props for the DropdownContents component', () => {
+ expect(findDropdownContents().props()).toMatchObject({
+ variant: DROPDOWN_VARIANT.Embedded,
+ dropdownTitle: wrapper.vm.$options.i18n.assignColor,
+ dropdownButtonText: wrapper.vm.$options.i18n.dropdownButtonText,
+ });
+ });
+
+ it('handles DropdownContents setColor', () => {
+ findDropdownContents().vm.$emit('setColor', color);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ });
+ });
+
+ describe('when epicColorQuery errored', () => {
+ beforeEach(async () => {
+ createComponent({ queryHandler: errorQueryHandler });
+ await waitForPromises();
+ });
+
+ it('creates flash with error message', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ captureError: true,
+ message: 'Error fetching epic color.',
+ });
+ });
+ });
+
+ it('emits `updateSelectedColor` event on dropdown contents `setColor` event if iid is not set', () => {
+ createComponent({ propsData: { iid: undefined } });
+
+ findDropdownContents().vm.$emit('setColor', color);
+ expect(wrapper.emitted('updateSelectedColor')).toEqual([[color]]);
+ });
+
+ describe('when updating color for epic', () => {
+ beforeEach(() => {
+ createComponent();
+ findDropdownContents().vm.$emit('setColor', color);
+ });
+
+ it('sets the loading state', () => {
+ expect(findSidebarEditableItem().props('loading')).toBe(true);
+ });
+
+ it('updates color correctly after successful mutation', async () => {
+ await waitForPromises();
+ expect(findDropdownValue().props('selectedColor').color).toEqual(
+ updateColorMutationResponse.data.updateIssuableColor.issuable.color,
+ );
+ });
+
+ it('displays an error if mutation was rejected', async () => {
+ createComponent({ mutationHandler: errorMutationHandler });
+ findDropdownContents().vm.$emit('setColor', color);
+ await waitForPromises();
+
+ expect(createFlash).toHaveBeenCalledWith({
+ captureError: true,
+ error: expect.anything(),
+ message: 'An error occurred while updating color.',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
new file mode 100644
index 00000000000..303824c77b3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_color_view_spec.js
@@ -0,0 +1,43 @@
+import { GlDropdownForm } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import { ISSUABLE_COLORS } from '~/vue_shared/components/color_select_dropdown/constants';
+import { color as defaultColor } from './mock_data';
+
+const propsData = {
+ selectedColor: defaultColor,
+};
+
+describe('DropdownContentsColorView', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(DropdownContentsColorView, {
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findColors = () => wrapper.findAllComponents(ColorItem);
+ const findColorList = () => wrapper.findComponent(GlDropdownForm);
+
+ it('renders color list', async () => {
+ expect(findColorList().exists()).toBe(true);
+ expect(findColors()).toHaveLength(ISSUABLE_COLORS.length);
+ });
+
+ it.each(ISSUABLE_COLORS)('emits an `input` event with %o on click on the option %#', (color) => {
+ const colorIndex = ISSUABLE_COLORS.indexOf(color);
+ findColors().at(colorIndex).trigger('click');
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject(color);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
new file mode 100644
index 00000000000..74f50b878e2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_contents_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { DROPDOWN_VARIANT } from '~/vue_shared/components/color_select_dropdown/constants';
+import DropdownContents from '~/vue_shared/components/color_select_dropdown/dropdown_contents.vue';
+import DropdownContentsColorView from '~/vue_shared/components/color_select_dropdown/dropdown_contents_color_view.vue';
+
+import { color } from './mock_data';
+
+const showDropdown = jest.fn();
+const focusInput = jest.fn();
+
+const defaultProps = {
+ dropdownTitle: '',
+ selectedColor: color,
+ dropdownButtonText: '',
+ variant: '',
+ isVisible: false,
+};
+
+const GlDropdownStub = {
+ template: `
+ <div>
+ <slot name="header"></slot>
+ <slot></slot>
+ </div>
+ `,
+ methods: {
+ show: showDropdown,
+ hide: jest.fn(),
+ },
+};
+
+const DropdownHeaderStub = {
+ template: `
+ <div>Hello, I am a header</div>
+ `,
+ methods: {
+ focusInput,
+ },
+};
+
+describe('DropdownContent', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMount(DropdownContents, {
+ propsData: {
+ ...defaultProps,
+ ...propsData,
+ },
+ stubs: {
+ GlDropdown: GlDropdownStub,
+ DropdownHeader: DropdownHeaderStub,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findColorView = () => wrapper.findComponent(DropdownContentsColorView);
+ const findDropdownHeader = () => wrapper.findComponent(DropdownHeaderStub);
+ const findDropdown = () => wrapper.findComponent(GlDropdownStub);
+
+ it('calls dropdown `show` method on `isVisible` prop change', async () => {
+ createComponent();
+ await wrapper.setProps({
+ isVisible: true,
+ });
+
+ expect(showDropdown).toHaveBeenCalledTimes(1);
+ });
+
+ it('does not emit `setColor` event on dropdown hide if color did not change', () => {
+ createComponent();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setColor')).toBeUndefined();
+ });
+
+ it('emits `setColor` event on dropdown hide if color changed on non-sidebar widget', async () => {
+ createComponent({ propsData: { variant: DROPDOWN_VARIANT.Embedded } });
+ const updatedColor = {
+ title: 'Blue-gray',
+ color: '#6699cc',
+ };
+ findColorView().vm.$emit('input', updatedColor);
+ await nextTick();
+ findDropdown().vm.$emit('hide');
+
+ expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
+ });
+
+ it('emits `setColor` event on visibility change if color changed on sidebar widget', async () => {
+ createComponent({ propsData: { variant: DROPDOWN_VARIANT.Sidebar, isVisible: true } });
+ const updatedColor = {
+ title: 'Blue-gray',
+ color: '#6699cc',
+ };
+ findColorView().vm.$emit('input', updatedColor);
+ wrapper.setProps({ isVisible: false });
+ await nextTick();
+
+ expect(wrapper.emitted('setColor')).toEqual([[updatedColor]]);
+ });
+
+ it('renders header', () => {
+ createComponent();
+
+ expect(findDropdownHeader().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
new file mode 100644
index 00000000000..d203d78477f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_header_spec.js
@@ -0,0 +1,40 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import DropdownHeader from '~/vue_shared/components/color_select_dropdown/dropdown_header.vue';
+
+const propsData = {
+ dropdownTitle: 'Epic color',
+};
+
+describe('DropdownHeader', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(DropdownHeader, { propsData });
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the correct title', () => {
+ expect(wrapper.text()).toBe(propsData.dropdownTitle);
+ });
+
+ it('renders a close button', () => {
+ expect(findButton().attributes('aria-label')).toBe('Close');
+ });
+
+ it('emits `closeDropdown` event on button click', () => {
+ expect(wrapper.emitted('closeDropdown')).toBeUndefined();
+ findButton().vm.$emit('click');
+
+ expect(wrapper.emitted('closeDropdown')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
new file mode 100644
index 00000000000..f22592dd604
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/dropdown_value_spec.js
@@ -0,0 +1,46 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ColorItem from '~/vue_shared/components/color_select_dropdown/color_item.vue';
+import DropdownValue from '~/vue_shared/components/color_select_dropdown/dropdown_value.vue';
+
+import { color } from './mock_data';
+
+const propsData = {
+ selectedColor: color,
+};
+
+describe('DropdownValue', () => {
+ let wrapper;
+
+ const findColorItems = () => wrapper.findAllComponents(ColorItem);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(DropdownValue, { propsData });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when there is a color set', () => {
+ it('renders the color', () => {
+ expect(findColorItems()).toHaveLength(2);
+ });
+
+ it.each`
+ index | cssClass
+ ${0} | ${['gl-font-base', 'gl-line-height-24']}
+ ${1} | ${['hide-collapsed']}
+ `(
+ 'passes correct props to the ColorItem with CSS class `$cssClass`',
+ async ({ index, cssClass }) => {
+ expect(findColorItems().at(index).props()).toMatchObject(propsData.selectedColor);
+ expect(findColorItems().at(index).classes()).toEqual(cssClass);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
new file mode 100644
index 00000000000..097f47cc731
--- /dev/null
+++ b/spec/frontend/vue_shared/components/color_select_dropdown/mock_data.js
@@ -0,0 +1,30 @@
+export const color = {
+ color: '#217645',
+ title: 'Green',
+};
+
+export const colorQueryResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Workspace/1',
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/1',
+ color: '#217645',
+ },
+ },
+ },
+};
+
+export const updateColorMutationResponse = {
+ data: {
+ updateIssuableColor: {
+ issuable: {
+ __typename: 'Epic',
+ id: 'gid://gitlab/Epic/1',
+ color: '#217645',
+ },
+ errors: [],
+ },
+ },
+};
diff --git a/spec/frontend_integration/ide/helpers/ide_helper.js b/spec/frontend_integration/ide/helpers/ide_helper.js
index 8c5ff816c74..0c3786929d8 100644
--- a/spec/frontend_integration/ide/helpers/ide_helper.js
+++ b/spec/frontend_integration/ide/helpers/ide_helper.js
@@ -46,14 +46,14 @@ export const findMonacoDiffEditor = () =>
export const findAndSetEditorValue = async (value) => {
const editor = await findMonacoEditor();
- const uri = editor.getAttribute('data-uri');
+ const { uri } = editor.dataset;
monacoEditor.getModel(uri).setValue(value);
};
export const getEditorValue = async () => {
const editor = await findMonacoEditor();
- const uri = editor.getAttribute('data-uri');
+ const { uri } = editor.dataset;
return monacoEditor.getModel(uri).getValue();
};
diff --git a/spec/lib/container_registry/migration_spec.rb b/spec/lib/container_registry/migration_spec.rb
index 8236ab0a70c..fea66d3c8f4 100644
--- a/spec/lib/container_registry/migration_spec.rb
+++ b/spec/lib/container_registry/migration_spec.rb
@@ -229,4 +229,31 @@ RSpec.describe ContainerRegistry::Migration do
it { is_expected.to eq(false) }
end
end
+
+ describe '.delete_container_repository_worker_support?' do
+ subject { described_class.delete_container_repository_worker_support? }
+
+ it { is_expected.to eq(true) }
+
+ context 'feature flag disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_delete_container_repository_worker_support: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '.dynamic_pre_import_timeout_for' do
+ let(:container_repository) { build(:container_repository) }
+
+ subject { described_class.dynamic_pre_import_timeout_for(container_repository) }
+
+ it 'returns the expected seconds' do
+ stub_application_setting(container_registry_pre_import_tags_rate: 0.6)
+ expect(container_repository).to receive(:tags_count).and_return(50)
+
+ expect(subject).to eq((0.6 * 50).seconds)
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ab92606e6fc..d47f43a630d 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -991,6 +991,14 @@ RSpec.describe Group do
it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+ context 'there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it { expect(group.last_owner?(@members[:owner])).to be_truthy }
+ end
+
context 'with two owners' do
before do
create(:group_member, :owner, group: group)
@@ -1116,35 +1124,58 @@ RSpec.describe Group do
end
end
- describe '#single_owner?' do
+ describe '#all_owners_excluding_project_bots' do
let_it_be(:user) { create(:user) }
context 'when there is only one owner' do
- before do
+ let!(:owner) do
group.add_user(user, GroupMember::OWNER)
end
- it 'returns true' do
- expect(group.single_owner?).to eq(true)
+ it 'returns the owner' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
+ end
+
+ context 'and there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it 'returns only the human owner' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner)
+ end
end
end
context 'when there are multiple owners' do
let_it_be(:user_2) { create(:user) }
- before do
+ let!(:owner) do
group.add_user(user, GroupMember::OWNER)
+ end
+
+ let!(:owner2) do
group.add_user(user_2, GroupMember::OWNER)
end
- it 'returns true' do
- expect(group.single_owner?).to eq(false)
+ it 'returns both owners' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
+ end
+
+ context 'and there is also a project_bot owner' do
+ before do
+ group.add_user(create(:user, :project_bot), GroupMember::OWNER)
+ end
+
+ it 'returns only the human owners' do
+ expect(group.all_owners_excluding_project_bots).to contain_exactly(owner, owner2)
+ end
end
end
context 'when there are no owners' do
it 'returns false' do
- expect(group.single_owner?).to eq(false)
+ expect(group.all_owners_excluding_project_bots).to be_empty
end
end
end
diff --git a/spec/models/members/last_group_owner_assigner_spec.rb b/spec/models/members/last_group_owner_assigner_spec.rb
index bb0f751e7d5..429cf4190cf 100644
--- a/spec/models/members/last_group_owner_assigner_spec.rb
+++ b/spec/models/members/last_group_owner_assigner_spec.rb
@@ -94,5 +94,18 @@ RSpec.describe LastGroupOwnerAssigner do
end
end
end
+
+ context 'when there are bot members' do
+ context 'with a bot owner' do
+ specify do
+ create(:group_member, :owner, source: group, user: create(:user, :project_bot))
+
+ expect { assigner.execute }.to change(group_member, :last_owner)
+ .from(nil).to(true)
+ .and change(group_member, :last_blocked_owner)
+ .from(nil).to(false)
+ end
+ end
+ end
end
end
diff --git a/spec/requests/api/terraform/modules/v1/packages_spec.rb b/spec/requests/api/terraform/modules/v1/packages_spec.rb
index 7d86244cb1b..f7736130245 100644
--- a/spec/requests/api/terraform/modules/v1/packages_spec.rb
+++ b/spec/requests/api/terraform/modules/v1/packages_spec.rb
@@ -106,14 +106,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
- :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :personal_access_token | true | 'grants terraform module download' | :success
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | true | 'grants terraform module download' | :success
+ :public | :guest | false | :personal_access_token | true | 'grants terraform module download' | :success
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :anonymous | false | :anonymous | true | 'grants terraform module download' | :success
:private | :developer | true | :personal_access_token | true | 'grants terraform module download' | :success
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
@@ -122,12 +122,12 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :unauthorized
+ :private | :anonymous | false | :anonymous | true | 'rejects terraform module packages access' | :unauthorized
:public | :developer | true | :job_token | true | 'grants terraform module download' | :success
- :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | true | 'grants terraform module download' | :success
:public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :job_token | true | 'grants terraform module download' | :success
+ :public | :guest | false | :job_token | true | 'grants terraform module download' | :success
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'grants terraform module download' | :success
@@ -146,6 +146,7 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
before do
group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
@@ -158,7 +159,8 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
let(:tokens) do
{
personal_access_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = personal_access_token.id }.encoded,
- job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded
+ job_token: ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = job.token }.encoded,
+ anonymous: ""
}
end
@@ -167,14 +169,14 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
context 'with valid namespace' do
where(:visibility, :user_role, :member, :token_type, :valid_token, :shared_examples_name, :expected_status) do
:public | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
- :public | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :personal_access_token | true | 'grants terraform module package file access' | :success
:public | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :developer | false | :personal_access_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | false | :personal_access_token | true | 'grants terraform module package file access' | :success
:public | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :anonymous | false | :anonymous | true | 'grants terraform module package file access' | :success
:private | :developer | true | :personal_access_token | true | 'grants terraform module package file access' | :success
:private | :guest | true | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | true | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
@@ -183,12 +185,12 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
:private | :guest | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
:private | :developer | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :guest | false | :personal_access_token | false | 'rejects terraform module packages access' | :unauthorized
- :private | :anonymous | false | :personal_access_token | true | 'rejects terraform module packages access' | :forbidden
+ :private | :anonymous | false | :anonymous | true | 'rejects terraform module packages access' | :unauthorized
:public | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
- :public | :guest | true | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
- :public | :developer | false | :job_token | true | 'rejects terraform module packages access' | :not_found
- :public | :guest | false | :job_token | true | 'rejects terraform module packages access' | :not_found
+ :public | :guest | true | :job_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | true | :job_token | false | 'rejects terraform module packages access' | :unauthorized
+ :public | :developer | false | :job_token | true | 'grants terraform module package file access' | :success
+ :public | :guest | false | :job_token | true | 'grants terraform module package file access' | :success
:public | :developer | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:public | :guest | false | :job_token | false | 'rejects terraform module packages access' | :unauthorized
:private | :developer | true | :job_token | true | 'grants terraform module package file access' | :success
@@ -203,10 +205,17 @@ RSpec.describe API::Terraform::Modules::V1::Packages do
with_them do
let(:token) { valid_token ? tokens[token_type] : 'invalid-token123' }
- let(:snowplow_gitlab_standard_context) { { project: project, user: user, namespace: project.namespace } }
+ let(:snowplow_gitlab_standard_context) do
+ {
+ project: project,
+ user: user_role == :anonymous ? nil : user,
+ namespace: project.namespace
+ }
+ end
before do
group.update!(visibility: visibility.to_s)
+ project.update!(visibility: visibility.to_s)
end
it_behaves_like params[:shared_examples_name], params[:user_role], params[:expected_status], params[:member]
diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
index 2ea98002de1..5faf462c23c 100644
--- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb
@@ -36,6 +36,19 @@ RSpec.shared_examples 'a GitHub-ish import controller: POST personal_access_toke
expect(session[:"#{provider}_access_token"]).to eq(token)
expect(controller).to redirect_to(status_import_url)
end
+
+ it 'passes namespace_id param as query param if it was present' do
+ namespace_id = 5
+ status_import_url = public_send("status_import_#{provider}_url", { namespace_id: namespace_id })
+
+ allow_next_instance_of(Gitlab::LegacyGithubImport::Client) do |client|
+ allow(client).to receive(:user).and_return(true)
+ end
+
+ post :personal_access_token, params: { personal_access_token: 'some-token', namespace_id: 5 }
+
+ expect(controller).to redirect_to(status_import_url)
+ end
end
RSpec.shared_examples 'a GitHub-ish import controller: GET new' do
diff --git a/spec/workers/delete_container_repository_worker_spec.rb b/spec/workers/delete_container_repository_worker_spec.rb
index ec040eab2d4..a011457444a 100644
--- a/spec/workers/delete_container_repository_worker_spec.rb
+++ b/spec/workers/delete_container_repository_worker_spec.rb
@@ -3,31 +3,119 @@
require 'spec_helper'
RSpec.describe DeleteContainerRepositoryWorker do
- let(:registry) { create(:container_repository) }
- let(:project) { registry.project }
- let(:user) { project.first_owner }
+ let_it_be(:repository) { create(:container_repository) }
- subject { described_class.new }
+ let(:project) { repository.project }
+ let(:user) { project.first_owner }
+ let(:worker) { described_class.new }
describe '#perform' do
+ let(:user_id) { user.id }
+ let(:repository_id) { repository.id }
+
+ subject(:perform) { worker.perform(user_id, repository_id) }
+
it 'executes the destroy service' do
- service = instance_double(Projects::ContainerRepository::DestroyService)
- expect(service).to receive(:execute)
- expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
+ expect_destroy_service_execution
+
+ perform
+ end
+
+ context 'with an invalid user id' do
+ let(:user_id) { -1 }
+
+ it { expect { perform }.not_to raise_error }
+ end
- subject.perform(user.id, registry.id)
+ context 'with an invalid repository id' do
+ let(:repository_id) { -1 }
+
+ it { expect { perform }.not_to raise_error }
end
- it 'does not raise error when user could not be found' do
- expect do
- subject.perform(-1, registry.id)
- end.not_to raise_error
+ context 'with a repository being migrated', :freeze_time do
+ before do
+ stub_application_setting(
+ container_registry_pre_import_tags_rate: 0.5,
+ container_registry_import_timeout: 10.minutes.to_i
+ )
+ end
+
+ shared_examples 'destroying the repository' do
+ it 'does destroy the repository' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).not_to receive(:tags_count)
+ end
+ expect(described_class).not_to receive(:perform_in)
+ expect_destroy_service_execution
+
+ perform
+ end
+ end
+
+ shared_examples 'not re enqueuing job if feature flag is disabled' do
+ before do
+ stub_feature_flags(container_registry_migration_phase2_delete_container_repository_worker_support: false)
+ end
+
+ it_behaves_like 'destroying the repository'
+ end
+
+ context 'with migration state set to pre importing' do
+ let_it_be(:repository) { create(:container_repository, :pre_importing) }
+
+ let(:tags_count) { 60 }
+ let(:delay) { (tags_count * 0.5).seconds + 10.minutes + described_class::FIXED_DELAY }
+
+ it 'does not destroy the repository and re enqueue the job' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).to receive(:tags_count).and_return(tags_count)
+ end
+ expect(described_class).to receive(:perform_in).with(delay.from_now)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
+ expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ perform
+ end
+
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+
+ %i[pre_import_done importing import_aborted].each do |migration_state|
+ context "with migration state set to #{migration_state}" do
+ let_it_be(:repository) { create(:container_repository, migration_state) }
+
+ let(:delay) { 10.minutes + described_class::FIXED_DELAY }
+
+ it 'does not destroy the repository and re enqueue the job' do
+ expect_next_found_instance_of(ContainerRepository) do |container_repository|
+ expect(container_repository).not_to receive(:tags_count)
+ end
+ expect(described_class).to receive(:perform_in).with(delay.from_now)
+ expect(worker).to receive(:log_extra_metadata_on_done).with(:delete_postponed, delay)
+ expect(::Projects::ContainerRepository::DestroyService).not_to receive(:new)
+
+ perform
+ end
+
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+ end
+
+ %i[default import_done import_skipped].each do |migration_state|
+ context "with migration state set to #{migration_state}" do
+ let_it_be(:repository) { create(:container_repository, migration_state) }
+
+ it_behaves_like 'destroying the repository'
+ it_behaves_like 'not re enqueuing job if feature flag is disabled'
+ end
+ end
end
- it 'does not raise error when registry could not be found' do
- expect do
- subject.perform(user.id, -1)
- end.not_to raise_error
+ def expect_destroy_service_execution
+ service = instance_double(Projects::ContainerRepository::DestroyService)
+ expect(service).to receive(:execute)
+ expect(Projects::ContainerRepository::DestroyService).to receive(:new).with(project, user).and_return(service)
end
end
end