summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
Diffstat (limited to 'spec')
-rw-r--r--spec/frontend/blob/components/blob_embeddable_spec.js35
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap5
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js8
-rw-r--r--spec/frontend/monitoring/components/dashboard_time_url_spec.js51
-rw-r--r--spec/frontend/monitoring/components/dashboard_time_window_spec.js69
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js145
-rw-r--r--spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap15
-rw-r--r--spec/frontend/snippets/components/app_spec.js6
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js54
-rw-r--r--spec/frontend/vue_shared/components/clipboard_button_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js236
-rw-r--r--spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb46
-rw-r--r--spec/lib/gitlab/database/with_lock_retries_spec.rb19
-rw-r--r--spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb79
-rw-r--r--spec/support/matchers/background_migrations_matchers.rb23
-rw-r--r--spec/support/migrations_helpers/namespaces_helper.rb14
17 files changed, 566 insertions, 335 deletions
diff --git a/spec/frontend/blob/components/blob_embeddable_spec.js b/spec/frontend/blob/components/blob_embeddable_spec.js
new file mode 100644
index 00000000000..b2fe71f1401
--- /dev/null
+++ b/spec/frontend/blob/components/blob_embeddable_spec.js
@@ -0,0 +1,35 @@
+import { shallowMount } from '@vue/test-utils';
+import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import { GlFormInputGroup } from '@gitlab/ui';
+
+describe('Blob Embeddable', () => {
+ let wrapper;
+ const url = 'https://foo.bar';
+
+ function createComponent() {
+ wrapper = shallowMount(BlobEmbeddable, {
+ propsData: {
+ url,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders gl-form-input-group component', () => {
+ expect(wrapper.find(GlFormInputGroup).exists()).toBe(true);
+ });
+
+ it('makes up optionValues based on the url prop', () => {
+ expect(wrapper.vm.optionValues).toEqual([
+ { name: 'Embed', value: expect.stringContaining(`${url}.js`) },
+ { name: 'Share', value: url },
+ ]);
+ });
+});
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index 2ed93396376..2b2ff074f83 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -69,9 +69,8 @@ exports[`Dashboard template matches the default snapshot 1`] = `
label-size="sm"
>
<date-time-picker-stub
- end="2020-01-01T18:57:47.000Z"
- start="2020-01-01T18:27:47.000Z"
- timewindows="[object Object]"
+ options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
+ value="[object Object]"
/>
</gl-form-group-stub>
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index d525f4821f4..38523ab82bc 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -5,13 +5,7 @@ import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import { propsData } from '../init_utils';
-jest.mock('~/lib/utils/url_utility', () => ({
- getParameterValues: jest.fn().mockImplementation(param => {
- if (param === 'start') return ['2020-01-01T18:27:47.000Z'];
- if (param === 'end') return ['2020-01-01T18:57:47.000Z'];
- return [];
- }),
-}));
+jest.mock('~/lib/utils/url_utility');
describe('Dashboard template', () => {
let wrapper;
diff --git a/spec/frontend/monitoring/components/dashboard_time_url_spec.js b/spec/frontend/monitoring/components/dashboard_time_url_spec.js
deleted file mode 100644
index 2da377eb79f..00000000000
--- a/spec/frontend/monitoring/components/dashboard_time_url_spec.js
+++ /dev/null
@@ -1,51 +0,0 @@
-import { mount } from '@vue/test-utils';
-import createFlash from '~/flash';
-import MockAdapter from 'axios-mock-adapter';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import { createStore } from '~/monitoring/stores';
-import { propsData } from '../init_utils';
-import axios from '~/lib/utils/axios_utils';
-
-jest.mock('~/flash');
-
-jest.mock('~/lib/utils/url_utility', () => ({
- getParameterValues: jest.fn().mockReturnValue('<script>alert("XSS")</script>'),
-}));
-
-describe('dashboard invalid url parameters', () => {
- let store;
- let wrapper;
- let mock;
-
- const createMountedWrapper = (props = {}, options = {}) => {
- wrapper = mount(Dashboard, {
- propsData: { ...propsData, ...props },
- store,
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- mock.restore();
- });
-
- it('shows an error message if invalid url parameters are passed', done => {
- createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
-
- wrapper.vm
- .$nextTick()
- .then(() => {
- expect(createFlash).toHaveBeenCalled();
- done();
- })
- .catch(done.fail);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_time_window_spec.js b/spec/frontend/monitoring/components/dashboard_time_window_spec.js
deleted file mode 100644
index e9f2a67983a..00000000000
--- a/spec/frontend/monitoring/components/dashboard_time_window_spec.js
+++ /dev/null
@@ -1,69 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import statusCodes from '~/lib/utils/http_status';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import { createStore } from '~/monitoring/stores';
-import { propsData, setupComponentStore } from '../init_utils';
-import { metricsDashboardPayload, mockApiEndpoint } from '../mock_data';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- getParameterValues: jest.fn().mockImplementation(param => {
- if (param === 'start') return ['2019-10-01T18:27:47.000Z'];
- if (param === 'end') return ['2019-10-01T18:57:47.000Z'];
- return [];
- }),
- mergeUrlParams: jest.fn().mockReturnValue('#'),
-}));
-
-describe('dashboard time window', () => {
- let store;
- let wrapper;
- let mock;
-
- const createComponentWrapperMounted = (props = {}, options = {}) => {
- wrapper = mount(Dashboard, {
- propsData: { ...propsData, ...props },
- store,
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
- mock.restore();
- });
-
- it('shows an active quick range option', done => {
- mock.onGet(mockApiEndpoint).reply(statusCodes.OK, metricsDashboardPayload);
-
- createComponentWrapperMounted({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
-
- setupComponentStore(wrapper);
-
- wrapper.vm
- .$nextTick()
- .then(() => {
- const timeWindowDropdownItems = wrapper
- .find({ ref: 'dateTimePicker' })
- .findAll(GlDropdownItem);
-
- const activeItem = timeWindowDropdownItems.wrappers.filter(itemWrapper =>
- itemWrapper.find('.active').exists(),
- );
-
- expect(activeItem.length).toBe(1);
-
- done();
- })
- .catch(done.fail);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
new file mode 100644
index 00000000000..33fbfac486f
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -0,0 +1,145 @@
+import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
+import createFlash from '~/flash';
+import { queryToObject, redirectTo, removeParams, mergeUrlParams } from '~/lib/utils/url_utility';
+import axios from '~/lib/utils/axios_utils';
+import { mockProjectDir } from '../mock_data';
+
+import Dashboard from '~/monitoring/components/dashboard.vue';
+import { createStore } from '~/monitoring/stores';
+import { propsData } from '../init_utils';
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility');
+
+describe('dashboard invalid url parameters', () => {
+ let store;
+ let wrapper;
+ let mock;
+
+ const fetchDataMock = jest.fn();
+
+ const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
+ wrapper = mount(Dashboard, {
+ propsData: { ...propsData, ...props },
+ store,
+ stubs: ['graph-group', 'panel-type'],
+ methods: {
+ fetchData: fetchDataMock,
+ },
+ ...options,
+ });
+ };
+
+ const findDateTimePicker = () => wrapper.find({ ref: 'dateTimePicker' });
+
+ beforeEach(() => {
+ store = createStore();
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ mock.restore();
+ fetchDataMock.mockReset();
+ queryToObject.mockReset();
+ });
+
+ it('passes default url parameters to the time range picker', () => {
+ queryToObject.mockReturnValue({});
+
+ createMountedWrapper();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDateTimePicker().props('value')).toMatchObject({
+ duration: { seconds: 28800 },
+ });
+
+ expect(fetchDataMock).toHaveBeenCalledTimes(1);
+ expect(fetchDataMock).toHaveBeenCalledWith({
+ start: expect.any(String),
+ end: expect.any(String),
+ });
+ });
+ });
+
+ it('passes a fixed time range in the URL to the time range picker', () => {
+ const params = {
+ start: '2019-01-01T00:00:00.000Z',
+ end: '2019-01-10T00:00:00.000Z',
+ };
+
+ queryToObject.mockReturnValue(params);
+
+ createMountedWrapper();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDateTimePicker().props('value')).toEqual(params);
+
+ expect(fetchDataMock).toHaveBeenCalledTimes(1);
+ expect(fetchDataMock).toHaveBeenCalledWith(params);
+ });
+ });
+
+ it('passes a rolling time range in the URL to the time range picker', () => {
+ queryToObject.mockReturnValue({
+ duration_seconds: '120',
+ });
+
+ createMountedWrapper();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findDateTimePicker().props('value')).toMatchObject({
+ duration: { seconds: 60 * 2 },
+ });
+
+ expect(fetchDataMock).toHaveBeenCalledTimes(1);
+ expect(fetchDataMock).toHaveBeenCalledWith({
+ start: expect.any(String),
+ end: expect.any(String),
+ });
+ });
+ });
+
+ it('shows an error message and loads a default time range if invalid url parameters are passed', () => {
+ queryToObject.mockReturnValue({
+ start: '<script>alert("XSS")</script>',
+ end: '<script>alert("XSS")</script>',
+ });
+
+ createMountedWrapper();
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createFlash).toHaveBeenCalled();
+
+ expect(findDateTimePicker().props('value')).toMatchObject({
+ duration: { seconds: 28800 },
+ });
+
+ expect(fetchDataMock).toHaveBeenCalledTimes(1);
+ expect(fetchDataMock).toHaveBeenCalledWith({
+ start: expect.any(String),
+ end: expect.any(String),
+ });
+ });
+ });
+
+ it('redirects to different time range', () => {
+ const toUrl = `${mockProjectDir}/-/environments/1/metrics`;
+ removeParams.mockReturnValueOnce(toUrl);
+
+ createMountedWrapper();
+
+ return wrapper.vm.$nextTick().then(() => {
+ findDateTimePicker().vm.$emit('input', {
+ duration: { seconds: 120 },
+ });
+
+ // redirect to plus + new parameters
+ expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ });
+ });
+});
diff --git a/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap
index c8482bf08ca..426bc5c0e6c 100644
--- a/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap
+++ b/spec/frontend/registry/list/components/__snapshots__/project_empty_state_spec.js.snap
@@ -90,11 +90,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
- aria-hidden="true"
- class="s16 ic-duplicate"
+ class="gl-icon s16"
>
<use
- xlink:href="#duplicate"
+ href="#copy-to-clipboard"
/>
</svg>
</button>
@@ -128,11 +127,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
- aria-hidden="true"
- class="s16 ic-duplicate"
+ class="gl-icon s16"
>
<use
- xlink:href="#duplicate"
+ href="#copy-to-clipboard"
/>
</svg>
</button>
@@ -158,11 +156,10 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
type="button"
>
<svg
- aria-hidden="true"
- class="s16 ic-duplicate"
+ class="gl-icon s16"
>
<use
- xlink:href="#duplicate"
+ href="#copy-to-clipboard"
/>
</svg>
</button>
diff --git a/spec/frontend/snippets/components/app_spec.js b/spec/frontend/snippets/components/app_spec.js
index 6576e5b075f..a683ed9aaba 100644
--- a/spec/frontend/snippets/components/app_spec.js
+++ b/spec/frontend/snippets/components/app_spec.js
@@ -1,5 +1,7 @@
import SnippetApp from '~/snippets/components/app.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
+import SnippetTitle from '~/snippets/components/snippet_title.vue';
+import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
@@ -35,8 +37,10 @@ describe('Snippet view app', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders SnippetHeader component after the query is finished', () => {
+ it('renders all components after the query is finished', () => {
createComponent();
expect(wrapper.find(SnippetHeader).exists()).toBe(true);
+ expect(wrapper.find(SnippetTitle).exists()).toBe(true);
+ expect(wrapper.find(SnippetBlob).exists()).toBe(true);
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
new file mode 100644
index 00000000000..8401c08b1da
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
+import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
+import {
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PUBLIC,
+} from '~/snippets/constants';
+
+describe('Blob Embeddable', () => {
+ let wrapper;
+ const snippet = {
+ id: 'gid://foo.bar/snippet',
+ webUrl: 'https://foo.bar',
+ visibilityLevel: SNIPPET_VISIBILITY_PUBLIC,
+ };
+
+ function createComponent(props = {}) {
+ wrapper = shallowMount(SnippetBlobView, {
+ propsData: {
+ snippet: {
+ ...snippet,
+ ...props,
+ },
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders blob-embeddable component', () => {
+ createComponent();
+ expect(wrapper.find(BlobEmbeddable).exists()).toBe(true);
+ });
+
+ it('does not render blob-embeddable for internal snippet', () => {
+ createComponent({
+ visibilityLevel: SNIPPET_VISIBILITY_INTERNAL,
+ });
+ expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
+
+ createComponent({
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+ });
+ expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
+
+ createComponent({
+ visibilityLevel: 'foo',
+ });
+ expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/clipboard_button_spec.js b/spec/frontend/vue_shared/components/clipboard_button_spec.js
index 37f71867ab9..07ff86828e7 100644
--- a/spec/frontend/vue_shared/components/clipboard_button_spec.js
+++ b/spec/frontend/vue_shared/components/clipboard_button_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import Icon from '~/vue_shared/components/icon.vue';
describe('clipboard button', () => {
let wrapper;
@@ -29,7 +28,7 @@ describe('clipboard button', () => {
it('renders a button for clipboard', () => {
expect(wrapper.find(GlButton).exists()).toBe(true);
expect(wrapper.attributes('data-clipboard-text')).toBe('copy me');
- expect(wrapper.find(Icon).props('name')).toBe('duplicate');
+ expect(wrapper.find(GlIcon).props('name')).toBe('copy-to-clipboard');
});
it('should have a tooltip with default values', () => {
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
index b7b024183e1..3a75ab2d127 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_lib_spec.js
@@ -54,97 +54,6 @@ describe('date time picker lib', () => {
});
});
- describe('getTimeWindow', () => {
- [
- {
- args: [
- {
- start: '2019-10-01T18:27:47.000Z',
- end: '2019-10-01T21:27:47.000Z',
- },
- dateTimePickerLib.defaultTimeWindows,
- ],
- expected: 'threeHours',
- },
- {
- args: [
- {
- start: '2019-10-01T28:27:47.000Z',
- end: '2019-10-01T21:27:47.000Z',
- },
- dateTimePickerLib.defaultTimeWindows,
- ],
- expected: null,
- },
- {
- args: [
- {
- start: '',
- end: '',
- },
- dateTimePickerLib.defaultTimeWindows,
- ],
- expected: null,
- },
- {
- args: [
- {
- start: null,
- end: null,
- },
- dateTimePickerLib.defaultTimeWindows,
- ],
- expected: null,
- },
- {
- args: [{}, dateTimePickerLib.defaultTimeWindows],
- expected: null,
- },
- ].forEach(({ args, expected }) => {
- it(`returns "${expected}" with args=${JSON.stringify(args)}`, () => {
- expect(dateTimePickerLib.getTimeWindowKey(...args)).toEqual(expected);
- });
- });
- });
-
- describe('getTimeRange', () => {
- function secondsBetween({ start, end }) {
- return (new Date(end) - new Date(start)) / 1000;
- }
-
- function minutesBetween(timeRange) {
- return secondsBetween(timeRange) / 60;
- }
-
- function hoursBetween(timeRange) {
- return minutesBetween(timeRange) / 60;
- }
-
- it('defaults to an 8 hour (28800s) difference', () => {
- const params = dateTimePickerLib.getTimeRange();
-
- expect(hoursBetween(params)).toEqual(8);
- });
-
- it('accepts time window as an argument', () => {
- const params = dateTimePickerLib.getTimeRange('thirtyMinutes');
-
- expect(minutesBetween(params)).toEqual(30);
- });
-
- it('returns a value for every defined time window', () => {
- const nonDefaultWindows = Object.entries(dateTimePickerLib.defaultTimeWindows).filter(
- ([, timeWindow]) => !timeWindow.default,
- );
- nonDefaultWindows.forEach(timeWindow => {
- const params = dateTimePickerLib.getTimeRange(timeWindow[0]);
-
- // Ensure we're not returning the default
- expect(hoursBetween(params)).not.toEqual(8);
- });
- });
- });
-
describe('stringToISODate', () => {
['', 'null', undefined, 'abc'].forEach(input => {
it(`throws error for invalid input like ${input}`, done => {
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index 98dfbe9cd14..90130917d8f 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import { defaultTimeWindows } from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
+import {
+ defaultTimeRanges,
+ defaultTimeRange,
+} from '~/vue_shared/components/date_time_picker/date_time_picker_lib';
-const timeWindowsCount = Object.entries(defaultTimeWindows).length;
-const start = '2019-10-10T07:00:00.000Z';
-const end = '2019-10-13T07:00:00.000Z';
-const selectedTimeWindowText = `3 days`;
+const optionsCount = defaultTimeRanges.length;
describe('DateTimePicker', () => {
let dateTimePicker;
@@ -15,19 +15,10 @@ describe('DateTimePicker', () => {
const applyButtonElement = () => dateTimePicker.find('button.btn-success').element;
const findQuickRangeItems = () => dateTimePicker.findAll('.dropdown-item');
const cancelButtonElement = () => dateTimePicker.find('button.btn-secondary').element;
- const fillInputAndBlur = (input, val) => {
- dateTimePicker.find(input).setValue(val);
- return dateTimePicker.vm.$nextTick().then(() => {
- dateTimePicker.find(input).trigger('blur');
- return dateTimePicker.vm.$nextTick();
- });
- };
const createComponent = props => {
dateTimePicker = mount(DateTimePicker, {
propsData: {
- start,
- end,
...props,
},
});
@@ -40,7 +31,7 @@ describe('DateTimePicker', () => {
it('renders dropdown toggle button with selected text', done => {
createComponent();
dateTimePicker.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe(selectedTimeWindowText);
+ expect(dropdownToggle().text()).toBe(defaultTimeRange.label);
done();
});
});
@@ -54,8 +45,10 @@ describe('DateTimePicker', () => {
it('renders inputs with h/m/s truncated if its all 0s', done => {
createComponent({
- start: '2019-10-10T00:00:00.000Z',
- end: '2019-10-14T00:10:00.000Z',
+ value: {
+ start: '2019-10-10T00:00:00.000Z',
+ end: '2019-10-14T00:10:00.000Z',
+ },
});
dateTimePicker.vm.$nextTick(() => {
expect(dateTimePicker.find('#custom-time-from').element.value).toBe('2019-10-10');
@@ -64,22 +57,21 @@ describe('DateTimePicker', () => {
});
});
- it(`renders dropdown with ${timeWindowsCount} (default) items in quick range`, done => {
+ it(`renders dropdown with ${optionsCount} (default) items in quick range`, done => {
createComponent();
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
- expect(findQuickRangeItems().length).toBe(timeWindowsCount);
+ expect(findQuickRangeItems().length).toBe(optionsCount);
done();
});
});
- it(`renders dropdown with correct quick range item selected`, done => {
+ it('renders dropdown with a default quick range item selected', done => {
createComponent();
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
- expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(selectedTimeWindowText);
-
- expect(dateTimePicker.find('.dropdown-item.active svg').isVisible()).toBe(true);
+ expect(dateTimePicker.find('.dropdown-item.active').exists()).toBe(true);
+ expect(dateTimePicker.find('.dropdown-item.active').text()).toBe(defaultTimeRange.label);
done();
});
});
@@ -92,99 +84,142 @@ describe('DateTimePicker', () => {
expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
});
- it('displays inline error message if custom time range inputs are invalid', done => {
- createComponent();
- fillInputAndBlur('#custom-time-from', '2019-10-01abc')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc'))
- .then(() => {
- expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
- done();
- })
- .catch(done);
- });
+ describe('user input', () => {
+ const fillInputAndBlur = (input, val) => {
+ dateTimePicker.find(input).setValue(val);
+ return dateTimePicker.vm.$nextTick().then(() => {
+ dateTimePicker.find(input).trigger('blur');
+ return dateTimePicker.vm.$nextTick();
+ });
+ };
- it('keeps apply button disabled with invalid custom time range inputs', done => {
- createComponent();
- fillInputAndBlur('#custom-time-from', '2019-10-01abc')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19'))
- .then(() => {
- expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
- done();
- })
- .catch(done);
- });
+ beforeEach(done => {
+ createComponent();
+ dateTimePicker.vm.$nextTick(done);
+ });
- it('enables apply button with valid custom time range inputs', done => {
- createComponent();
- fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
- .then(() => {
- expect(applyButtonElement().getAttribute('disabled')).toBeNull();
- done();
- })
- .catch(done.fail);
- });
+ it('displays inline error message if custom time range inputs are invalid', done => {
+ fillInputAndBlur('#custom-time-from', '2019-10-01abc')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-10abc'))
+ .then(() => {
+ expect(dateTimePicker.findAll('.invalid-feedback').length).toBe(2);
+ done();
+ })
+ .catch(done);
+ });
- it('emits dates in an object when apply is clicked', done => {
- createComponent();
- fillInputAndBlur('#custom-time-from', '2019-10-01')
- .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
- .then(() => {
- applyButtonElement().click();
-
- expect(dateTimePicker.emitted().apply).toHaveLength(1);
- expect(dateTimePicker.emitted().apply[0]).toEqual([
- {
- end: '2019-10-19T00:00:00Z',
- start: '2019-10-01T00:00:00Z',
- },
- ]);
- done();
- })
- .catch(done.fail);
- });
+ it('keeps apply button disabled with invalid custom time range inputs', done => {
+ fillInputAndBlur('#custom-time-from', '2019-10-01abc')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-09-19'))
+ .then(() => {
+ expect(applyButtonElement().getAttribute('disabled')).toBe('disabled');
+ done();
+ })
+ .catch(done);
+ });
- it('hides the popover with cancel button', done => {
- createComponent();
- dropdownToggle().trigger('click');
+ it('enables apply button with valid custom time range inputs', done => {
+ fillInputAndBlur('#custom-time-from', '2019-10-01')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
+ .then(() => {
+ expect(applyButtonElement().getAttribute('disabled')).toBeNull();
+ done();
+ })
+ .catch(done.fail);
+ });
- dateTimePicker.vm.$nextTick(() => {
- cancelButtonElement().click();
+ it('emits dates in an object when apply is clicked', done => {
+ fillInputAndBlur('#custom-time-from', '2019-10-01')
+ .then(() => fillInputAndBlur('#custom-time-to', '2019-10-19'))
+ .then(() => {
+ applyButtonElement().click();
+
+ expect(dateTimePicker.emitted().input).toHaveLength(1);
+ expect(dateTimePicker.emitted().input[0]).toEqual([
+ {
+ end: '2019-10-19T00:00:00Z',
+ start: '2019-10-01T00:00:00Z',
+ },
+ ]);
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('unchecks quick range when text is input is clicked', done => {
+ const findActiveItems = () => findQuickRangeItems().filter(w => w.is('.active'));
+
+ expect(findActiveItems().length).toBe(1);
+
+ fillInputAndBlur('#custom-time-from', '2019-10-01')
+ .then(() => {
+ expect(findActiveItems().length).toBe(0);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+
+ it('emits dates in an object when a is clicked', () => {
+ findQuickRangeItems()
+ .at(3) // any item
+ .trigger('click');
+
+ expect(dateTimePicker.emitted().input).toHaveLength(1);
+ expect(dateTimePicker.emitted().input[0][0]).toMatchObject({
+ duration: {
+ seconds: expect.any(Number),
+ },
+ });
+ });
+
+ it('hides the popover with cancel button', done => {
+ dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
- expect(dropdownMenu().classes('show')).toBe(false);
- done();
+ cancelButtonElement().click();
+
+ dateTimePicker.vm.$nextTick(() => {
+ expect(dropdownMenu().classes('show')).toBe(false);
+ done();
+ });
});
});
});
describe('when using non-default time windows', () => {
- const otherTimeWindows = {
- oneMinute: {
+ const MOCK_NOW = Date.UTC(2020, 0, 23, 20);
+
+ const otherTimeRanges = [
+ {
label: '1 minute',
- seconds: 60,
+ duration: { seconds: 60 },
},
- twoMinutes: {
+ {
label: '2 minutes',
- seconds: 60 * 2,
+ duration: { seconds: 60 * 2 },
default: true,
},
- fiveMinutes: {
+ {
label: '5 minutes',
- seconds: 60 * 5,
+ duration: { seconds: 60 * 5 },
},
- };
+ ];
+
+ beforeEach(() => {
+ jest.spyOn(Date, 'now').mockImplementation(() => MOCK_NOW);
+ });
it('renders dropdown with a label in the quick range', done => {
createComponent({
- // 2 minutes range
- start: '2020-01-21T15:00:00.000Z',
- end: '2020-01-21T15:02:00.000Z',
- timeWindows: otherTimeWindows,
+ value: {
+ duration: { seconds: 60 * 5 },
+ },
+ options: otherTimeRanges,
});
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe('2 minutes');
+ expect(dropdownToggle().text()).toBe('5 minutes');
done();
});
@@ -192,16 +227,16 @@ describe('DateTimePicker', () => {
it('renders dropdown with quick range items', done => {
createComponent({
- // 2 minutes range
- start: '2020-01-21T15:00:00.000Z',
- end: '2020-01-21T15:02:00.000Z',
- timeWindows: otherTimeWindows,
+ value: {
+ duration: { seconds: 60 * 2 },
+ },
+ options: otherTimeRanges,
});
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
const items = findQuickRangeItems();
- expect(items.length).toBe(Object.keys(otherTimeWindows).length);
+ expect(items.length).toBe(Object.keys(otherTimeRanges).length);
expect(items.at(0).text()).toBe('1 minute');
expect(items.at(0).is('.active')).toBe(false);
@@ -217,14 +252,13 @@ describe('DateTimePicker', () => {
it('renders dropdown with a label not in the quick range', done => {
createComponent({
- // 10 minutes range
- start: '2020-01-21T15:00:00.000Z',
- end: '2020-01-21T15:10:00.000Z',
- timeWindows: otherTimeWindows,
+ value: {
+ duration: { seconds: 60 * 4 },
+ },
});
dropdownToggle().trigger('click');
dateTimePicker.vm.$nextTick(() => {
- expect(dropdownToggle().text()).toBe('2020-01-21 15:00:00 to 2020-01-21 15:10:00');
+ expect(dropdownToggle().text()).toBe('2020-01-23 19:56:00 to 2020-01-23 20:00:00');
done();
});
diff --git a/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb b/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
new file mode 100644
index 00000000000..37280110b91
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::UpdateExistingSubgroupToMatchVisibilityLevelOfParent, :migration, schema: 2020_01_10_121314 do
+ include MigrationHelpers::NamespacesHelpers
+
+ context 'private visibility level' do
+ it 'updates the project visibility' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
+ child = create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent.id)
+
+ expect { subject.perform([parent.id], Gitlab::VisibilityLevel::PRIVATE) }.to change { child.reload.visibility_level }.to(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'updates sub-sub groups' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
+ middle_group = create_namespace('middle', Gitlab::VisibilityLevel::PRIVATE, parent_id: parent.id)
+ child = create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
+
+ subject.perform([parent.id, middle_group.id], Gitlab::VisibilityLevel::PRIVATE)
+
+ expect(child.reload.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it 'updates all sub groups' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
+ middle_group = create_namespace('middle', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent.id)
+ child = create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
+
+ subject.perform([parent.id], Gitlab::VisibilityLevel::PRIVATE)
+
+ expect(child.reload.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ expect(middle_group.reload.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+
+ context 'internal visibility level' do
+ it 'updates the project visibility' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::INTERNAL)
+ child = create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent.id)
+
+ expect { subject.perform([parent.id], Gitlab::VisibilityLevel::INTERNAL) }.to change { child.reload.visibility_level }.to(Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb
index c3be6510584..b6321f2eab1 100644
--- a/spec/lib/gitlab/database/with_lock_retries_spec.rb
+++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb
@@ -128,4 +128,23 @@ describe Gitlab::Database::WithLockRetries do
end
end
end
+
+ context 'casting durations correctly' do
+ let(:timing_configuration) { [[0.015.seconds, 0.025.seconds], [0.015.seconds, 0.025.seconds]] } # 15ms, 25ms
+
+ it 'executes `SET LOCAL lock_timeout` using the configured timeout value in milliseconds' do
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("SAVEPOINT active_record_1").and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("SET LOCAL lock_timeout TO '15ms'").and_call_original
+ expect(ActiveRecord::Base.connection).to receive(:execute).with("RELEASE SAVEPOINT active_record_1").and_call_original
+
+ subject.run { }
+ end
+
+ it 'calls `sleep` after the first iteration fails, using the configured sleep time' do
+ expect(subject).to receive(:run_block_with_transaction).and_raise(ActiveRecord::LockWaitTimeout).twice
+ expect(subject).to receive(:sleep).with(0.025)
+
+ subject.run { }
+ end
+ end
end
diff --git a/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb b/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
new file mode 100644
index 00000000000..5ff9ff4641f
--- /dev/null
+++ b/spec/migrations/schedule_update_existing_subgroup_to_match_visibility_level_of_parent_spec.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20200110121314_schedule_update_existing_subgroup_to_match_visibility_level_of_parent.rb')
+
+describe ScheduleUpdateExistingSubgroupToMatchVisibilityLevelOfParent, :migration, :sidekiq do
+ include MigrationHelpers::NamespacesHelpers
+ let(:migration_class) { described_class::MIGRATION }
+ let(:migration_name) { migration_class.to_s.demodulize }
+
+ context 'private visibility level' do
+ it 'correctly schedules background migrations' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
+ create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent.id)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args([parent.id], Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+
+ it 'correctly schedules background migrations for groups and subgroups' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::PRIVATE)
+ middle_group = create_namespace('middle_group', Gitlab::VisibilityLevel::PRIVATE, parent_id: parent.id)
+ create_namespace('middle_empty_group', Gitlab::VisibilityLevel::PRIVATE, parent_id: parent.id)
+ create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args([middle_group.id, parent.id], Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+ end
+
+ context 'internal visibility level' do
+ it 'correctly schedules background migrations' do
+ parent = create_namespace('parent', Gitlab::VisibilityLevel::INTERNAL)
+ middle_group = create_namespace('child', Gitlab::VisibilityLevel::INTERNAL, parent_id: parent.id)
+ create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(1)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args([parent.id, middle_group.id], Gitlab::VisibilityLevel::INTERNAL)
+ end
+ end
+ end
+ end
+
+ context 'mixed visibility levels' do
+ it 'correctly schedules background migrations' do
+ parent1 = create_namespace('parent1', Gitlab::VisibilityLevel::INTERNAL)
+ create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: parent1.id)
+ parent2 = create_namespace('parent2', Gitlab::VisibilityLevel::PRIVATE)
+ middle_group = create_namespace('middle_group', Gitlab::VisibilityLevel::INTERNAL, parent_id: parent2.id)
+ create_namespace('child', Gitlab::VisibilityLevel::PUBLIC, parent_id: middle_group.id)
+
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args([parent1.id, middle_group.id], Gitlab::VisibilityLevel::INTERNAL)
+ expect(migration_name).to be_scheduled_migration_with_multiple_args([parent2.id], Gitlab::VisibilityLevel::PRIVATE)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb
index c38aa7ad6a6..8735dac8b2a 100644
--- a/spec/support/matchers/background_migrations_matchers.rb
+++ b/spec/support/matchers/background_migrations_matchers.rb
@@ -26,3 +26,26 @@ RSpec::Matchers.define :be_scheduled_migration do |*expected|
"Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
end
end
+
+RSpec::Matchers.define :be_scheduled_migration_with_multiple_args do |*expected|
+ match do |migration|
+ BackgroundMigrationWorker.jobs.any? do |job|
+ args = job['args'].size == 1 ? [BackgroundMigrationWorker.jobs[0]['args'][0], []] : job['args']
+ args[0] == migration && compare_args(args, expected)
+ end
+ end
+
+ failure_message do |migration|
+ "Migration `#{migration}` with args `#{expected.inspect}` not scheduled!"
+ end
+
+ def compare_args(args, expected)
+ args[1].map.with_index do |arg, i|
+ arg.is_a?(Array) ? same_arrays?(arg, expected[i]) : arg == expected[i]
+ end.all?
+ end
+
+ def same_arrays?(arg, expected)
+ arg.sort == expected.sort
+ end
+end
diff --git a/spec/support/migrations_helpers/namespaces_helper.rb b/spec/support/migrations_helpers/namespaces_helper.rb
new file mode 100644
index 00000000000..4ca01c87568
--- /dev/null
+++ b/spec/support/migrations_helpers/namespaces_helper.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module MigrationHelpers
+ module NamespacesHelpers
+ def create_namespace(name, visibility, options = {})
+ table(:namespaces).create({
+ name: name,
+ path: name,
+ type: 'Group',
+ visibility_level: visibility
+ }.merge(options))
+ end
+ end
+end