diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-27 21:09:41 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-27 21:09:41 +0300 |
commit | f569792df8a25caa1bed9c448c8c4c3f837f5164 (patch) | |
tree | 8c2ed7dae5ba132a97c0321a7649174e5832d637 /spec | |
parent | c2908ec6a0d7b62996cdb8da0350705bdad691bf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
41 files changed, 2008 insertions, 1713 deletions
diff --git a/spec/factories/identities.rb b/spec/factories/identities.rb index fda4bfa589b..a2615ce30c3 100644 --- a/spec/factories/identities.rb +++ b/spec/factories/identities.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :identity do provider { 'ldapmain' } - sequence(:extern_uid) { |n| "my-ldap-id-#{n}" } + extern_uid { 'my-ldap-id' } end end diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index 9753300d035..c18c2ec0d53 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -12,7 +12,10 @@ describe('AlertManagementList', () => { function mountComponent({ stubs = {}, - props = { alertManagementEnabled: false }, + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, data = {}, loading = false, } = {}) { @@ -62,7 +65,7 @@ describe('AlertManagementList', () => { it('loading state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: null }, loading: true, }); @@ -73,7 +76,7 @@ describe('AlertManagementList', () => { it('error state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: null, errored: true }, loading: false, }); @@ -86,7 +89,7 @@ describe('AlertManagementList', () => { it('empty state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: [], errored: false }, loading: false, }); diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index 5c5315fd465..05a44138275 100644 --- a/spec/frontend/boards/boards_store_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -1040,5 +1040,66 @@ describe('boardsStore', () => { }); }); }); + + describe('updateIssue', () => { + let issue; + let patchSpy; + + beforeEach(() => { + issue = new ListIssue({ + title: 'Testing', + id: 1, + iid: 1, + confidential: false, + labels: [ + { + id: 1, + title: 'test', + color: 'red', + description: 'testing', + }, + ], + assignees: [ + { + id: 1, + name: 'name', + username: 'username', + avatar_url: 'http://avatar_url', + }, + ], + real_path: 'path/to/issue', + }); + + patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]); + axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data))); + }); + + it('passes assignee ids when there are assignees', () => { + boardsStore.updateIssue(issue); + return boardsStore.updateIssue(issue).then(() => { + expect(patchSpy).toHaveBeenCalledWith({ + issue: { + milestone_id: null, + assignee_ids: [1], + label_ids: [1], + }, + }); + }); + }); + + it('passes assignee ids of [0] when there are no assignees', () => { + issue.removeAllAssignees(); + + return boardsStore.updateIssue(issue).then(() => { + expect(patchSpy).toHaveBeenCalledWith({ + issue: { + milestone_id: null, + assignee_ids: [0], + label_ids: [1], + }, + }); + }); + }); + }); }); }); diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js index ff72edaa695..412f20684f5 100644 --- a/spec/frontend/boards/issue_spec.js +++ b/spec/frontend/boards/issue_spec.js @@ -1,6 +1,5 @@ /* global ListIssue */ -import axios from '~/lib/utils/axios_utils'; import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/issue'; @@ -173,25 +172,12 @@ describe('Issue model', () => { }); describe('update', () => { - it('passes assignee ids when there are assignees', done => { - jest.spyOn(axios, 'patch').mockImplementation((url, data) => { - expect(data.issue.assignee_ids).toEqual([1]); - done(); - return Promise.resolve(); - }); - - issue.update('url'); - }); + it('passes update to boardsStore', () => { + jest.spyOn(boardsStore, 'updateIssue').mockImplementation(); - it('passes assignee ids of [0] when there are no assignees', done => { - jest.spyOn(axios, 'patch').mockImplementation((url, data) => { - expect(data.issue.assignee_ids).toEqual([0]); - done(); - return Promise.resolve(); - }); + issue.update(); - issue.removeAllAssignees(); - issue.update('url'); + expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue); }); }); }); diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js new file mode 100644 index 00000000000..b8211b02464 --- /dev/null +++ b/spec/frontend/deploy_keys/components/action_btn_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import eventHub from '~/deploy_keys/eventhub'; +import actionBtn from '~/deploy_keys/components/action_btn.vue'; + +describe('Deploy keys action btn', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + const deployKey = data.enabled_keys[0]; + let wrapper; + + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + + beforeEach(() => { + wrapper = shallowMount(actionBtn, { + propsData: { + deployKey, + type: 'enable', + }, + slots: { + default: 'Enable', + }, + }); + }); + + it('renders the default slot', () => { + expect(wrapper.text()).toBe('Enable'); + }); + + it('sends eventHub event with btn type', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything()); + }); + }); + + it('shows loading spinner after click', () => { + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + it('disables button after click', () => { + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.attributes('disabled')).toBe('disabled'); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js new file mode 100644 index 00000000000..291502c9ed7 --- /dev/null +++ b/spec/frontend/deploy_keys/components/app_spec.js @@ -0,0 +1,142 @@ +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import eventHub from '~/deploy_keys/eventhub'; +import deployKeysApp from '~/deploy_keys/components/app.vue'; + +const TEST_ENDPOINT = `${TEST_HOST}/dummy/`; + +describe('Deploy keys app component', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + let wrapper; + let mock; + + const mountComponent = () => { + wrapper = mount(deployKeysApp, { + propsData: { + endpoint: TEST_ENDPOINT, + projectId: '8', + }, + }); + + return waitForPromises(); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(TEST_ENDPOINT).reply(200, data); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + const findLoadingIcon = () => wrapper.find('.gl-spinner'); + const findKeyPanels = () => wrapper.findAll('.deploy-keys .nav-links li'); + + it('renders loading icon while waiting for request', () => { + mock.onGet(TEST_ENDPOINT).reply(() => new Promise()); + + mountComponent(); + + return wrapper.vm.$nextTick().then(() => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + it('renders keys panels', () => { + return mountComponent().then(() => { + expect(findKeyPanels().length).toBe(3); + }); + }); + + it.each` + selector | label | count + ${'.js-deployKeys-tab-enabled_keys'} | ${'Enabled deploy keys'} | ${1} + ${'.js-deployKeys-tab-available_project_keys'} | ${'Privately accessible deploy keys'} | ${0} + ${'.js-deployKeys-tab-public_keys'} | ${'Publicly accessible deploy keys'} | ${1} + `('$selector title is $label with keys count equal to $count', ({ selector, label, count }) => { + return mountComponent().then(() => { + const element = wrapper.find(selector); + expect(element.exists()).toBe(true); + expect(element.text().trim()).toContain(label); + + expect( + element + .find('.badge') + .text() + .trim(), + ).toBe(count.toString()); + }); + }); + + it('does not render key panels when keys object is empty', () => { + mock.onGet(TEST_ENDPOINT).reply(200, []); + + return mountComponent().then(() => { + expect(findKeyPanels().length).toBe(0); + }); + }); + + it('re-fetches deploy keys when enabling a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('enable.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('re-fetches deploy keys when disabling a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('disable.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('calls disableKey when removing a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('remove.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('hasKeys returns true when there are keys', () => { + return mountComponent().then(() => { + expect(wrapper.vm.hasKeys).toEqual(3); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js new file mode 100644 index 00000000000..7d942d969bb --- /dev/null +++ b/spec/frontend/deploy_keys/components/key_spec.js @@ -0,0 +1,161 @@ +import { mount } from '@vue/test-utils'; +import DeployKeysStore from '~/deploy_keys/store'; +import key from '~/deploy_keys/components/key.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; + +describe('Deploy keys key', () => { + let wrapper; + let store; + + const data = getJSONFixture('deploy_keys/keys.json'); + + const findTextAndTrim = selector => + wrapper + .find(selector) + .text() + .trim(); + + const createComponent = propsData => { + wrapper = mount(key, { + propsData: { + store, + endpoint: 'https://test.host/dummy/endpoint', + ...propsData, + }, + }); + }; + + beforeEach(() => { + store = new DeployKeysStore(); + store.keys = data; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('enabled key', () => { + const deployKey = data.enabled_keys[0]; + + it('renders the keys title', () => { + createComponent({ deployKey }); + + expect(findTextAndTrim('.title')).toContain('My title'); + }); + + it('renders human friendly formatted created date', () => { + createComponent({ deployKey }); + + expect(findTextAndTrim('.key-created-at')).toBe( + `${getTimeago().format(deployKey.created_at)}`, + ); + }); + + it('shows pencil button for editing', () => { + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-pencil')).toExist(); + }); + + it('shows disable button when the project is not deletable', () => { + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-cancel')).toExist(); + }); + + it('shows remove button when the project is deletable', () => { + createComponent({ + deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true }, + }); + expect(wrapper.find('.btn .ic-remove')).toExist(); + }); + }); + + describe('deploy key labels', () => { + const deployKey = data.enabled_keys[0]; + const deployKeysProjects = [...deployKey.deploy_keys_projects]; + it('shows write access title when key has write access', () => { + deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: true }; + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } }); + + expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe( + 'Write access allowed', + ); + }); + + it('does not show write access title when key has write access', () => { + deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: false }; + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } }); + + expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe( + 'Read access only', + ); + }); + + it('shows expandable button if more than two projects', () => { + createComponent({ deployKey }); + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(2); + expect(labels.at(1).text()).toContain('others'); + expect(labels.at(1).attributes('data-original-title')).toContain('Expand'); + }); + + it('expands all project labels after click', () => { + createComponent({ deployKey }); + const { length } = deployKey.deploy_keys_projects; + wrapper + .findAll('.deploy-project-label') + .at(1) + .trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(length); + expect(labels.at(1).text()).not.toContain(`+${length} others`); + expect(labels.at(1).attributes('data-original-title')).not.toContain('Expand'); + }); + }); + + it('shows two projects', () => { + createComponent({ + deployKey: { ...deployKey, deploy_keys_projects: [...deployKeysProjects].slice(0, 2) }, + }); + + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(2); + expect(labels.at(1).text()).toContain(deployKey.deploy_keys_projects[1].project.full_name); + }); + }); + + describe('public keys', () => { + const deployKey = data.public_keys[0]; + + it('renders deploy keys without any enabled projects', () => { + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: [] } }); + + expect(findTextAndTrim('.deploy-project-list')).toBe('None'); + }); + + it('shows enable button', () => { + createComponent({ deployKey }); + expect(findTextAndTrim('.btn')).toBe('Enable'); + }); + + it('shows pencil button for editing', () => { + createComponent({ deployKey }); + expect(wrapper.find('.btn .ic-pencil')).toExist(); + }); + + it('shows disable button when key is enabled', () => { + store.keys.enabled_keys.push(deployKey); + + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-cancel')).toExist(); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js new file mode 100644 index 00000000000..53c8ba073bc --- /dev/null +++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js @@ -0,0 +1,63 @@ +import { mount } from '@vue/test-utils'; +import DeployKeysStore from '~/deploy_keys/store'; +import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue'; + +describe('Deploy keys panel', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + let wrapper; + + const findTableRowHeader = () => wrapper.find('.table-row-header'); + + const mountComponent = props => { + const store = new DeployKeysStore(); + store.keys = data; + wrapper = mount(deployKeysPanel, { + propsData: { + title: 'test', + keys: data.enabled_keys, + showHelpBox: true, + store, + endpoint: 'https://test.host/dummy/endpoint', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders list of keys', () => { + mountComponent(); + expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length); + }); + + it('renders table header', () => { + mountComponent(); + const tableHeader = findTableRowHeader(); + + expect(tableHeader).toExist(); + expect(tableHeader.text()).toContain('Deploy key'); + expect(tableHeader.text()).toContain('Project usage'); + expect(tableHeader.text()).toContain('Created'); + }); + + it('renders help box if keys are empty', () => { + mountComponent({ keys: [] }); + + expect(wrapper.find('.settings-message').exists()).toBe(true); + + expect( + wrapper + .find('.settings-message') + .text() + .trim(), + ).toBe('No deploy keys found. Create one with the form above.'); + }); + + it('renders no table header if keys are empty', () => { + mountComponent({ keys: [] }); + expect(findTableRowHeader().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/dirty_submit/dirty_submit_collection_spec.js b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js new file mode 100644 index 00000000000..170d581be23 --- /dev/null +++ b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js @@ -0,0 +1,22 @@ +import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection'; +import { setInputValue, createForm } from './helper'; + +jest.mock('lodash/throttle', () => jest.fn(fn => fn)); + +describe('DirtySubmitCollection', () => { + const testElementsCollection = [createForm(), createForm()]; + const forms = testElementsCollection.map(testElements => testElements.form); + + new DirtySubmitCollection(forms); // eslint-disable-line no-new + + it.each(testElementsCollection)('disables submits until there are changes', testElements => { + const { input, submit } = testElements; + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + setInputValue(input, `${originalValue} changes`); + expect(submit.disabled).toBe(false); + setInputValue(input, originalValue); + expect(submit.disabled).toBe(true); + }); +}); diff --git a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js index 40843a68582..40843a68582 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_factory_spec.js +++ b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js diff --git a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js b/spec/frontend/dirty_submit/dirty_submit_form_spec.js index 42f806fa1bf..d7f690df1f3 100644 --- a/spec/javascripts/dirty_submit/dirty_submit_form_spec.js +++ b/spec/frontend/dirty_submit/dirty_submit_form_spec.js @@ -1,95 +1,78 @@ -import { range as rge } from 'lodash'; +import { range as rge, throttle } from 'lodash'; import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; import { getInputValue, setInputValue, createForm } from './helper'; +jest.mock('lodash/throttle', () => jest.fn(fn => fn)); +const lodash = jest.requireActual('lodash'); + function expectToToggleDisableOnDirtyUpdate(submit, input) { const originalValue = getInputValue(input); expect(submit.disabled).toBe(true); - return setInputValue(input, `${originalValue} changes`) - .then(() => expect(submit.disabled).toBe(false)) - .then(() => setInputValue(input, originalValue)) - .then(() => expect(submit.disabled).toBe(true)); + setInputValue(input, `${originalValue} changes`); + expect(submit.disabled).toBe(false); + setInputValue(input, originalValue); + expect(submit.disabled).toBe(true); } describe('DirtySubmitForm', () => { - const originalThrottleDuration = DirtySubmitForm.THROTTLE_DURATION; - describe('submit button tests', () => { - beforeEach(() => { - DirtySubmitForm.THROTTLE_DURATION = 0; - }); - - afterEach(() => { - DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration; - }); - - it('disables submit until there are changes', done => { + it('disables submit until there are changes', () => { const { form, input, submit } = createForm(); new DirtySubmitForm(form); // eslint-disable-line no-new - return expectToToggleDisableOnDirtyUpdate(submit, input) - .then(done) - .catch(done.fail); + expectToToggleDisableOnDirtyUpdate(submit, input); }); - it('disables submit until there are changes when initializing with a falsy value', done => { + it('disables submit until there are changes when initializing with a falsy value', () => { const { form, input, submit } = createForm(); input.value = ''; new DirtySubmitForm(form); // eslint-disable-line no-new - return expectToToggleDisableOnDirtyUpdate(submit, input) - .then(done) - .catch(done.fail); + expectToToggleDisableOnDirtyUpdate(submit, input); }); - it('disables submit until there are changes for radio inputs', done => { + it('disables submit until there are changes for radio inputs', () => { const { form, input, submit } = createForm('radio'); new DirtySubmitForm(form); // eslint-disable-line no-new - return expectToToggleDisableOnDirtyUpdate(submit, input) - .then(done) - .catch(done.fail); + expectToToggleDisableOnDirtyUpdate(submit, input); }); - it('disables submit until there are changes for checkbox inputs', done => { + it('disables submit until there are changes for checkbox inputs', () => { const { form, input, submit } = createForm('checkbox'); new DirtySubmitForm(form); // eslint-disable-line no-new - return expectToToggleDisableOnDirtyUpdate(submit, input) - .then(done) - .catch(done.fail); + expectToToggleDisableOnDirtyUpdate(submit, input); }); }); describe('throttling tests', () => { beforeEach(() => { - jasmine.clock().install(); - jasmine.clock().mockDate(); - DirtySubmitForm.THROTTLE_DURATION = 100; + throttle.mockImplementation(lodash.throttle); + jest.useFakeTimers(); }); afterEach(() => { - jasmine.clock().uninstall(); - DirtySubmitForm.THROTTLE_DURATION = originalThrottleDuration; + throttle.mockReset(); }); it('throttles updates when rapid changes are made to a single form element', () => { const { form, input } = createForm(); - const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); + const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); rge(10).forEach(i => { setInputValue(input, `change ${i}`, false); }); - jasmine.clock().tick(101); + jest.runOnlyPendingTimers(); - expect(updateDirtyInputSpy).toHaveBeenCalledTimes(2); + expect(updateDirtyInputSpy).toHaveBeenCalledTimes(1); }); it('does not throttle updates when rapid changes are made to different form elements', () => { @@ -99,14 +82,14 @@ describe('DirtySubmitForm', () => { form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`; }); - const updateDirtyInputSpy = spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); + const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); range.forEach(i => { const input = form.querySelector(`.js-input-${i}`); setInputValue(input, `change`, false); }); - jasmine.clock().tick(101); + jest.runOnlyPendingTimers(); expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length); }); diff --git a/spec/javascripts/dirty_submit/helper.js b/spec/frontend/dirty_submit/helper.js index b51783cb915..c02512b7671 100644 --- a/spec/javascripts/dirty_submit/helper.js +++ b/spec/frontend/dirty_submit/helper.js @@ -1,6 +1,3 @@ -import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; -import setTimeoutPromiseHelper from '../helpers/set_timeout_promise_helper'; - function isCheckableType(type) { return /^(radio|checkbox)$/.test(type); } @@ -22,8 +19,6 @@ export function setInputValue(element, value) { bubbles: true, }), ); - - return setTimeoutPromiseHelper(DirtySubmitForm.THROTTLE_DURATION); } export function getInputValue(input) { diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js new file mode 100644 index 00000000000..f876987cd88 --- /dev/null +++ b/spec/frontend/pipelines/mock_data.js @@ -0,0 +1,423 @@ +export const pipelineWithStages = { + id: 20333396, + user: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + active: true, + coverage: '58.24', + source: 'push', + created_at: '2018-04-11T14:04:53.881Z', + updated_at: '2018-04-11T14:05:00.792Z', + path: '/gitlab-org/gitlab/pipelines/20333396', + flags: { + latest: true, + stuck: false, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'build', + title: 'build: skipped', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#build', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#build', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build', + }, + { + name: 'prepare', + title: 'prepare: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#prepare', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare', + }, + { + name: 'test', + title: 'test: running', + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#test', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test', + }, + { + name: 'post-test', + title: 'post-test: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#post-test', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test', + }, + { + name: 'pages', + title: 'pages: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#pages', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#pages', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages', + }, + { + name: 'post-cleanup', + title: 'post-cleanup: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup', + }, + ], + artifacts: [ + { + name: 'gitlab:assets:compile', + expired: false, + expire_at: '2018-05-12T14:22:54.730Z', + path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse', + }, + { + name: 'rspec-mysql 12 28', + expired: false, + expire_at: '2018-05-12T14:22:45.136Z', + path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse', + }, + { + name: 'rspec-mysql 6 28', + expired: false, + expire_at: '2018-05-12T14:22:41.523Z', + path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse', + }, + { + name: 'rspec-pg geo 0 1', + expired: false, + expire_at: '2018-05-12T14:22:13.287Z', + path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse', + }, + { + name: 'rspec-mysql 0 28', + expired: false, + expire_at: '2018-05-12T14:22:06.834Z', + path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse', + }, + { + name: 'spinach-mysql 0 2', + expired: false, + expire_at: '2018-05-12T14:21:51.409Z', + path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse', + }, + { + name: 'karma', + expired: false, + expire_at: '2018-05-12T14:21:20.934Z', + path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse', + }, + { + name: 'spinach-pg 0 2', + expired: false, + expire_at: '2018-05-12T14:20:01.028Z', + path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse', + }, + { + name: 'spinach-pg 1 2', + expired: false, + expire_at: '2018-05-12T14:19:04.336Z', + path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse', + }, + { + name: 'sast', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse', + }, + { + name: 'code_quality', + expired: false, + expire_at: '2018-04-18T14:16:24.484Z', + path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse', + }, + { + name: 'cache gems', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse', + }, + { + name: 'dependency_scanning', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse', + }, + { + name: 'compile-assets', + expired: false, + expire_at: '2018-04-18T14:12:07.638Z', + path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse', + }, + { + name: 'setup-test-env', + expired: false, + expire_at: '2018-04-18T14:10:27.024Z', + path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse', + }, + { + name: 'retrieve-tests-metadata', + expired: false, + expire_at: '2018-05-12T14:06:35.926Z', + path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse', + }, + ], + manual_actions: [ + { + name: 'package-and-qa', + path: '/gitlab-org/gitlab/-/jobs/62411330/play', + playable: true, + }, + { + name: 'review-docs-deploy', + path: '/gitlab-org/gitlab/-/jobs/62411332/play', + playable: true, + }, + ], + }, + ref: { + name: 'master', + path: '/gitlab-org/gitlab/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'e6a2885c503825792cb8a84a8731295e361bd059', + short_id: 'e6a2885c', + title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'", + created_at: '2018-04-11T14:04:39.000Z', + parent_ids: [ + '5d9b5118f6055f72cff1a82b88133609912f2c1d', + '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02', + ], + message: + "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326", + author_name: 'Rémy Coutable', + author_email: 'remy@rymai.me', + authored_date: '2018-04-11T14:04:39.000Z', + committer_name: 'Rémy Coutable', + committer_email: 'remy@rymai.me', + committed_date: '2018-04-11T14:04:39.000Z', + author: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + author_gravatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + commit_url: + 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', + commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', + }, + cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel', + triggered_by: null, + triggered: [], +}; + +export const stageReply = { + name: 'deploy', + title: 'deploy: running', + latest_statuses: [ + { + id: 928, + name: 'stop staging', + started: false, + build_path: '/twitter/flight/-/jobs/928', + cancel_path: '/twitter/flight/-/jobs/928/cancel', + playable: false, + created_at: '2018-04-04T20:02:02.728Z', + updated_at: '2018-04-04T20:02:02.766Z', + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/twitter/flight/-/jobs/928', + favicon: + '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico', + action: { + icon: 'cancel', + title: 'Cancel', + path: '/twitter/flight/-/jobs/928/cancel', + method: 'post', + }, + }, + }, + { + id: 926, + name: 'production', + started: false, + build_path: '/twitter/flight/-/jobs/926', + retry_path: '/twitter/flight/-/jobs/926/retry', + play_path: '/twitter/flight/-/jobs/926/play', + playable: true, + created_at: '2018-04-04T20:00:57.202Z', + updated_at: '2018-04-04T20:11:13.110Z', + status: { + icon: 'status_canceled', + text: 'canceled', + label: 'manual play action', + group: 'canceled', + tooltip: 'canceled', + has_details: true, + details_path: '/twitter/flight/-/jobs/926', + favicon: + '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico', + action: { + icon: 'play', + title: 'Play', + path: '/twitter/flight/-/jobs/926/play', + method: 'post', + }, + }, + }, + { + id: 217, + name: 'staging', + started: '2018-03-07T08:41:46.234Z', + build_path: '/twitter/flight/-/jobs/217', + retry_path: '/twitter/flight/-/jobs/217/retry', + playable: false, + created_at: '2018-03-07T14:41:58.093Z', + updated_at: '2018-03-07T14:41:58.093Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/twitter/flight/-/jobs/217', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/twitter/flight/-/jobs/217/retry', + method: 'post', + }, + }, + }, + ], + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + tooltip: 'running', + has_details: true, + details_path: '/twitter/flight/pipelines/13#deploy', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + path: '/twitter/flight/pipelines/13#deploy', + dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy', +}; diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js new file mode 100644 index 00000000000..40cd0ad9047 --- /dev/null +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -0,0 +1,659 @@ +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import PipelinesComponent from '~/pipelines/components/pipelines.vue'; +import Store from '~/pipelines/stores/pipelines_store'; +import { pipelineWithStages, stageReply } from './mock_data'; + +describe('Pipelines', () => { + const jsonFixtureName = 'pipelines/pipelines.json'; + + preloadFixtures(jsonFixtureName); + + let pipelines; + let wrapper; + let mock; + + const paths = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + ciLintPath: '/ci/lint', + resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', + newPipelinePath: '/twitter/flight/pipelines/new', + }; + + const noPermissions = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + }; + + const defaultProps = { + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }; + + const createComponent = (props = defaultProps, methods) => { + wrapper = mount(PipelinesComponent, { + propsData: { + store: new Store(), + ...props, + }, + methods: { + ...methods, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + pipelines = getJSONFixture(jsonFixtureName); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('With permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + createComponent(); + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches'); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent(); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders empty state', () => { + expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence'); + + expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual( + paths.helpPagePath, + ); + }); + + it('does not render tabs nor buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('Without permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders empty state without button to set CI', () => { + expect(wrapper.find('.js-empty-state').text()).toEqual( + 'This project is not currently set up to run pipelines.', + ); + + expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy(); + }); + + it('does not render tabs or buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('successful request', () => { + describe('with pipelines', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent(); + return waitForPromises(); + }); + + it('should render table', () => { + expect(wrapper.find('.table-holder').exists()).toBe(true); + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + + it('should render navigation tabs', () => { + expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending'); + + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + + expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running'); + + expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished'); + + expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches'); + + expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags'); + }); + + it('should make an API request when using tabs', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises().then(() => { + wrapper.find('.js-pipelines-tab-finished').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + }); + }); + + describe('with pagination', () => { + it('should make an API request when using pagination', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises() + .then(() => { + // Mock pagination + wrapper.vm.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper.find('.next-page-item').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + }); + }); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => null); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangeTab('running'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangePage(4); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' }); + }); + }); + }); + + describe('computed properties', () => { + beforeEach(() => { + createComponent(); + }); + + describe('tabs', () => { + it('returns default tabs', () => { + expect(wrapper.vm.tabs).toEqual([ + { name: 'All', scope: 'all', count: undefined, isActive: true }, + { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, + { name: 'Running', scope: 'running', count: undefined, isActive: false }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); + }); + + describe('emptyTabMessage', () => { + it('returns message with scope', () => { + wrapper.vm.scope = 'pending'; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + }); + }); + + it('returns message without scope when scope is `all`', () => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); + }); + + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(wrapper.vm.stateToRender).toEqual('loading'); + }); + + it('returns error state when app has error', () => { + wrapper.vm.hasError = true; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('error'); + }); + }); + + it('returns table list when app has pipelines', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('tableList'); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', () => { + wrapper.vm.state.count.all = 10; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty tab when project has CI', () => { + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty state when project does not have pipelines nor CI', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyState'); + }); + }); + }); + + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', () => { + wrapper.vm.isLoading = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is tableList & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is error & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is empty tab & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.count.all = 10; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns false when has not made first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + + it('returns false when state is empty state', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + }); + + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', () => { + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(true); + }); + }); + + it('returns false when it has not made the first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(false); + }); + }); + }); + }); + + describe('updates results when a staged is clicked', () => { + beforeEach(() => { + const copyPipeline = Object.assign({}, pipelineWithStages); + copyPipeline.id += 1; + mock + .onGet('twitter/flight/pipelines.json') + .reply( + 200, + { + pipelines: [pipelineWithStages], + count: { + all: 1, + finished: 1, + pending: 0, + running: 0, + }, + }, + { + 'POLL-INTERVAL': 100, + }, + ) + .onGet(pipelineWithStages.details.stages[0].dropdown_path) + .reply(200, stageReply); + + createComponent(); + }); + + describe('when a request is being made', () => { + it('stops polling, cancels the request, & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.vm.isMakingRequest = true; + wrapper.find('.js-builds-dropdown-button').trigger('click'); + }) + .then(() => { + expect(cancelMock).toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + + describe('when no request is being made', () => { + it('stops polling & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.find('.js-builds-dropdown-button').trigger('click'); + expect(stopMock).toHaveBeenCalled(); + }) + .then(() => { + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js deleted file mode 100644 index a09935d8a04..00000000000 --- a/spec/frontend/users_select/utils_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'jquery'; -import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils'; - -const options = { - fooBar: 'baz', - activeUserId: 1, -}; - -describe('getAjaxUsersSelectOptions', () => { - it('returns options built from select data attributes', () => { - const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 }); - - expect( - getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }), - ).toEqual(options); - }); -}); - -describe('getAjaxUsersSelectParams', () => { - it('returns query parameters built from provided options', () => { - expect( - getAjaxUsersSelectParams(options, { - foo_bar: 'fooBar', - active_user_id: 'activeUserId', - non_existent_key: 'nonExistentKey', - }), - ).toEqual({ - foo_bar: 'baz', - active_user_id: 1, - non_existent_key: null, - }); - }); -}); diff --git a/spec/graphql/types/jira_import_type_spec.rb b/spec/graphql/types/jira_import_type_spec.rb index 8448a120682..e73568c9710 100644 --- a/spec/graphql/types/jira_import_type_spec.rb +++ b/spec/graphql/types/jira_import_type_spec.rb @@ -6,6 +6,6 @@ describe GitlabSchema.types['JiraImport'] do it { expect(described_class.graphql_name).to eq('JiraImport') } it 'has the expected fields' do - expect(described_class).to have_graphql_fields(:jira_project_key, :scheduled_at, :scheduled_by) + expect(described_class).to have_graphql_fields(:jira_project_key, :createdAt, :scheduled_at, :scheduled_by) end end diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb index f8240dd3a4c..1a6af3be055 100644 --- a/spec/helpers/explore_helper_spec.rb +++ b/spec/helpers/explore_helper_spec.rb @@ -19,23 +19,10 @@ describe ExploreHelper do end describe '#public_visibility_restricted?' do - using RSpec::Parameterized::TableSyntax + it 'delegates to Gitlab::VisibilityLevel' do + expect(Gitlab::VisibilityLevel).to receive(:public_visibility_restricted?).and_call_original - where(:visibility_levels, :expected_status) do - nil | nil - [Gitlab::VisibilityLevel::PRIVATE] | false - [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] | false - [Gitlab::VisibilityLevel::PUBLIC] | true - end - - with_them do - before do - stub_application_setting(restricted_visibility_levels: visibility_levels) - end - - it 'returns the expected status' do - expect(helper.public_visibility_restricted?).to eq(expected_status) - end + helper.public_visibility_restricted? end end end diff --git a/spec/helpers/projects/alert_management_helper_spec.rb b/spec/helpers/projects/alert_management_helper_spec.rb index ee180cef692..9246d1deff6 100644 --- a/spec/helpers/projects/alert_management_helper_spec.rb +++ b/spec/helpers/projects/alert_management_helper_spec.rb @@ -5,21 +5,32 @@ require 'spec_helper' describe Projects::AlertManagementHelper do include Gitlab::Routing.url_helpers - let(:project) { create(:project) } + let_it_be(:project, reload: true) { create(:project) } + let_it_be(:current_user) { create(:user) } describe '#alert_management_data' do + let(:user_can_enable_alert_management) { false } let(:setting_path) { project_settings_operations_path(project) } let(:index_path) do project_alert_management_index_path(project, format: :json) end + before do + allow(helper) + .to receive(:can?) + .with(current_user, :admin_operations, project) + .and_return(user_can_enable_alert_management) + end + context 'without alert_managements_setting' do it 'returns frontend configuration' do - expect(alert_management_data(project)).to eq( + expect(alert_management_data(current_user, project)).to eq( 'index-path' => index_path, 'enable-alert-management-path' => setting_path, - "empty-alert-svg-path" => "/images/illustrations/alert-management-empty-state.svg" + "empty-alert-svg-path" => "/images/illustrations/alert-management-empty-state.svg", + 'user-can-enable-alert-management' => 'false', + 'alert-management-enabled' => 'true' ) end end diff --git a/spec/javascripts/deploy_keys/components/action_btn_spec.js b/spec/javascripts/deploy_keys/components/action_btn_spec.js deleted file mode 100644 index 5bf72cc0018..00000000000 --- a/spec/javascripts/deploy_keys/components/action_btn_spec.js +++ /dev/null @@ -1,72 +0,0 @@ -import Vue from 'vue'; -import eventHub from '~/deploy_keys/eventhub'; -import actionBtn from '~/deploy_keys/components/action_btn.vue'; - -describe('Deploy keys action btn', () => { - const data = getJSONFixture('deploy_keys/keys.json'); - const deployKey = data.enabled_keys[0]; - let vm; - - beforeEach(done => { - const ActionBtnComponent = Vue.extend({ - components: { - actionBtn, - }, - data() { - return { - deployKey, - }; - }, - template: ` - <action-btn - :deploy-key="deployKey" - type="enable"> - Enable - </action-btn>`, - }); - - vm = new ActionBtnComponent().$mount(); - - Vue.nextTick() - .then(done) - .catch(done.fail); - }); - - it('renders the default slot', () => { - expect(vm.$el.textContent.trim()).toBe('Enable'); - }); - - it('sends eventHub event with btn type', done => { - spyOn(eventHub, '$emit'); - - vm.$el.click(); - - Vue.nextTick(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, jasmine.anything()); - - done(); - }); - }); - - it('shows loading spinner after click', done => { - vm.$el.click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.fa')).toBeDefined(); - - done(); - }); - }); - - it('disables button after click', done => { - vm.$el.click(); - - Vue.nextTick(() => { - expect(vm.$el.classList.contains('disabled')).toBeTruthy(); - - expect(vm.$el.getAttribute('disabled')).toBe('disabled'); - - done(); - }); - }); -}); diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js deleted file mode 100644 index c9a9814d122..00000000000 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ /dev/null @@ -1,155 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import { TEST_HOST } from 'spec/test_constants'; -import axios from '~/lib/utils/axios_utils'; -import eventHub from '~/deploy_keys/eventhub'; -import deployKeysApp from '~/deploy_keys/components/app.vue'; - -describe('Deploy keys app component', () => { - const data = getJSONFixture('deploy_keys/keys.json'); - let vm; - let mock; - - beforeEach(done => { - // set up axios mock before component - mock = new MockAdapter(axios); - mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data); - - const Component = Vue.extend(deployKeysApp); - - vm = new Component({ - propsData: { - endpoint: `${TEST_HOST}/dummy`, - projectId: '8', - }, - }).$mount(); - - setTimeout(done); - }); - - afterEach(() => { - mock.restore(); - }); - - it('renders loading icon', done => { - vm.store.keys = {}; - vm.isLoading = false; - - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0); - - expect(vm.$el.querySelector('.fa-spinner')).toBeDefined(); - - done(); - }); - }); - - it('renders keys panels', () => { - expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(3); - }); - - it('renders the titles with keys count', () => { - const textContent = selector => { - const element = vm.$el.querySelector(`${selector}`); - - expect(element).not.toBeNull(); - return element.textContent.trim(); - }; - - expect(textContent('.js-deployKeys-tab-enabled_keys')).toContain('Enabled deploy keys'); - expect(textContent('.js-deployKeys-tab-available_project_keys')).toContain( - 'Privately accessible deploy keys', - ); - - expect(textContent('.js-deployKeys-tab-public_keys')).toContain( - 'Publicly accessible deploy keys', - ); - - expect(textContent('.js-deployKeys-tab-enabled_keys .badge')).toBe( - `${vm.store.keys.enabled_keys.length}`, - ); - - expect(textContent('.js-deployKeys-tab-available_project_keys .badge')).toBe( - `${vm.store.keys.available_project_keys.length}`, - ); - - expect(textContent('.js-deployKeys-tab-public_keys .badge')).toBe( - `${vm.store.keys.public_keys.length}`, - ); - }); - - it('does not render key panels when keys object is empty', done => { - vm.store.keys = {}; - - Vue.nextTick(() => { - expect(vm.$el.querySelectorAll('.deploy-keys .nav-links li').length).toBe(0); - - done(); - }); - }); - - it('re-fetches deploy keys when enabling a key', done => { - const key = data.public_keys[0]; - - spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'enableKey').and.callFake(() => Promise.resolve()); - - eventHub.$emit('enable.key', key); - - Vue.nextTick(() => { - expect(vm.service.enableKey).toHaveBeenCalledWith(key.id); - expect(vm.service.getKeys).toHaveBeenCalled(); - done(); - }); - }); - - it('re-fetches deploy keys when disabling a key', done => { - const key = data.public_keys[0]; - - spyOn(window, 'confirm').and.returnValue(true); - spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve()); - - eventHub.$emit('disable.key', key); - - Vue.nextTick(() => { - expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); - expect(vm.service.getKeys).toHaveBeenCalled(); - done(); - }); - }); - - it('calls disableKey when removing a key', done => { - const key = data.public_keys[0]; - - spyOn(window, 'confirm').and.returnValue(true); - spyOn(vm.service, 'getKeys'); - spyOn(vm.service, 'disableKey').and.callFake(() => Promise.resolve()); - - eventHub.$emit('remove.key', key); - - Vue.nextTick(() => { - expect(vm.service.disableKey).toHaveBeenCalledWith(key.id); - expect(vm.service.getKeys).toHaveBeenCalled(); - done(); - }); - }); - - it('hasKeys returns true when there are keys', () => { - expect(vm.hasKeys).toEqual(3); - }); - - it('resets disable button loading state', done => { - spyOn(window, 'confirm').and.returnValue(false); - - const btn = vm.$el.querySelector('.btn-warning'); - - btn.click(); - - Vue.nextTick(() => { - expect(btn.querySelector('.btn-warning')).not.toExist(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/deploy_keys/components/key_spec.js b/spec/javascripts/deploy_keys/components/key_spec.js deleted file mode 100644 index 7117dc4a9ee..00000000000 --- a/spec/javascripts/deploy_keys/components/key_spec.js +++ /dev/null @@ -1,157 +0,0 @@ -import Vue from 'vue'; -import DeployKeysStore from '~/deploy_keys/store'; -import key from '~/deploy_keys/components/key.vue'; -import { getTimeago } from '~/lib/utils/datetime_utility'; - -describe('Deploy keys key', () => { - let vm; - const KeyComponent = Vue.extend(key); - const data = getJSONFixture('deploy_keys/keys.json'); - const createComponent = deployKey => { - const store = new DeployKeysStore(); - store.keys = data; - - vm = new KeyComponent({ - propsData: { - deployKey, - store, - endpoint: 'https://test.host/dummy/endpoint', - }, - }).$mount(); - }; - - describe('enabled key', () => { - const deployKey = data.enabled_keys[0]; - - beforeEach(done => { - createComponent(deployKey); - - setTimeout(done); - }); - - it('renders the keys title', () => { - expect(vm.$el.querySelector('.title').textContent.trim()).toContain('My title'); - }); - - it('renders human friendly formatted created date', () => { - expect(vm.$el.querySelector('.key-created-at').textContent.trim()).toBe( - `${getTimeago().format(deployKey.created_at)}`, - ); - }); - - it('shows pencil button for editing', () => { - expect(vm.$el.querySelector('.btn .ic-pencil')).toExist(); - }); - - it('shows disable button when the project is not deletable', () => { - expect(vm.$el.querySelector('.btn .ic-cancel')).toExist(); - }); - - it('shows remove button when the project is deletable', done => { - vm.deployKey.destroyed_when_orphaned = true; - vm.deployKey.almost_orphaned = true; - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .ic-remove')).toExist(); - done(); - }); - }); - }); - - describe('deploy key labels', () => { - it('shows write access title when key has write access', done => { - vm.deployKey.deploy_keys_projects[0].can_push = true; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), - ).toBe('Write access allowed'); - done(); - }); - }); - - it('does not show write access title when key has write access', done => { - vm.deployKey.deploy_keys_projects[0].can_push = false; - - Vue.nextTick(() => { - expect( - vm.$el.querySelector('.deploy-project-label').getAttribute('data-original-title'), - ).toBe('Read access only'); - done(); - }); - }); - - it('shows expandable button if more than two projects', () => { - const labels = vm.$el.querySelectorAll('.deploy-project-label'); - - expect(labels.length).toBe(2); - expect(labels[1].textContent).toContain('others'); - expect(labels[1].getAttribute('data-original-title')).toContain('Expand'); - }); - - it('expands all project labels after click', done => { - const { length } = vm.deployKey.deploy_keys_projects; - vm.$el.querySelectorAll('.deploy-project-label')[1].click(); - - Vue.nextTick(() => { - const labels = vm.$el.querySelectorAll('.deploy-project-label'); - - expect(labels.length).toBe(length); - expect(labels[1].textContent).not.toContain(`+${length} others`); - expect(labels[1].getAttribute('data-original-title')).not.toContain('Expand'); - done(); - }); - }); - - it('shows two projects', done => { - vm.deployKey.deploy_keys_projects = [...vm.deployKey.deploy_keys_projects].slice(0, 2); - - Vue.nextTick(() => { - const labels = vm.$el.querySelectorAll('.deploy-project-label'); - - expect(labels.length).toBe(2); - expect(labels[1].textContent).toContain( - vm.deployKey.deploy_keys_projects[1].project.full_name, - ); - done(); - }); - }); - }); - - describe('public keys', () => { - const deployKey = data.public_keys[0]; - - beforeEach(done => { - createComponent(deployKey); - - setTimeout(done); - }); - - it('renders deploy keys without any enabled projects', done => { - vm.deployKey.deploy_keys_projects = []; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.deploy-project-list').textContent.trim()).toBe('None'); - - done(); - }); - }); - - it('shows enable button', () => { - expect(vm.$el.querySelectorAll('.btn')[0].textContent.trim()).toBe('Enable'); - }); - - it('shows pencil button for editing', () => { - expect(vm.$el.querySelector('.btn .ic-pencil')).toExist(); - }); - - it('shows disable button when key is enabled', done => { - vm.store.keys.enabled_keys.push(deployKey); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.btn .ic-cancel')).toExist(); - - done(); - }); - }); - }); -}); diff --git a/spec/javascripts/deploy_keys/components/keys_panel_spec.js b/spec/javascripts/deploy_keys/components/keys_panel_spec.js deleted file mode 100644 index f71f5ccf082..00000000000 --- a/spec/javascripts/deploy_keys/components/keys_panel_spec.js +++ /dev/null @@ -1,63 +0,0 @@ -import Vue from 'vue'; -import DeployKeysStore from '~/deploy_keys/store'; -import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue'; - -describe('Deploy keys panel', () => { - const data = getJSONFixture('deploy_keys/keys.json'); - let vm; - - beforeEach(done => { - const DeployKeysPanelComponent = Vue.extend(deployKeysPanel); - const store = new DeployKeysStore(); - store.keys = data; - - vm = new DeployKeysPanelComponent({ - propsData: { - title: 'test', - keys: data.enabled_keys, - showHelpBox: true, - store, - endpoint: 'https://test.host/dummy/endpoint', - }, - }).$mount(); - - setTimeout(done); - }); - - it('renders list of keys', () => { - expect(vm.$el.querySelectorAll('.deploy-key').length).toBe(vm.keys.length); - }); - - it('renders table header', () => { - const tableHeader = vm.$el.querySelector('.table-row-header'); - - expect(tableHeader).toExist(); - expect(tableHeader.textContent).toContain('Deploy key'); - expect(tableHeader.textContent).toContain('Project usage'); - expect(tableHeader.textContent).toContain('Created'); - }); - - it('renders help box if keys are empty', done => { - vm.keys = []; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.settings-message')).toBeDefined(); - - expect(vm.$el.querySelector('.settings-message').textContent.trim()).toBe( - 'No deploy keys found. Create one with the form above.', - ); - - done(); - }); - }); - - it('renders no table header if keys are empty', done => { - vm.keys = []; - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.table-row-header')).not.toExist(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js b/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js deleted file mode 100644 index 47be0b3ce9d..00000000000 --- a/spec/javascripts/dirty_submit/dirty_submit_collection_spec.js +++ /dev/null @@ -1,29 +0,0 @@ -import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection'; -import { setInputValue, createForm } from './helper'; - -describe('DirtySubmitCollection', () => { - it('disables submits until there are changes', done => { - const testElementsCollection = [createForm(), createForm()]; - const forms = testElementsCollection.map(testElements => testElements.form); - - new DirtySubmitCollection(forms); // eslint-disable-line no-new - - testElementsCollection.forEach(testElements => { - const { input, submit } = testElements; - const originalValue = input.value; - - expect(submit.disabled).toBe(true); - - return setInputValue(input, `${originalValue} changes`) - .then(() => { - expect(submit.disabled).toBe(false); - }) - .then(() => setInputValue(input, originalValue)) - .then(() => { - expect(submit.disabled).toBe(true); - }) - .then(done) - .catch(done.fail); - }); - }); -}); diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js deleted file mode 100644 index 5cd91413c5f..00000000000 --- a/spec/javascripts/pipelines/pipelines_spec.js +++ /dev/null @@ -1,783 +0,0 @@ -import Vue from 'vue'; -import MockAdapter from 'axios-mock-adapter'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; -import axios from '~/lib/utils/axios_utils'; -import pipelinesComp from '~/pipelines/components/pipelines.vue'; -import Store from '~/pipelines/stores/pipelines_store'; -import { pipelineWithStages, stageReply } from './mock_data'; - -describe('Pipelines', () => { - const jsonFixtureName = 'pipelines/pipelines.json'; - - preloadFixtures(jsonFixtureName); - - let PipelinesComponent; - let pipelines; - let vm; - let mock; - - const paths = { - endpoint: 'twitter/flight/pipelines.json', - autoDevopsPath: '/help/topics/autodevops/index.md', - helpPagePath: '/help/ci/quick_start/README', - emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', - errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', - noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', - ciLintPath: '/ci/lint', - resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', - newPipelinePath: '/twitter/flight/pipelines/new', - }; - - const noPermissions = { - endpoint: 'twitter/flight/pipelines.json', - autoDevopsPath: '/help/topics/autodevops/index.md', - helpPagePath: '/help/ci/quick_start/README', - emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', - errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', - noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', - }; - - beforeEach(() => { - mock = new MockAdapter(axios); - - pipelines = getJSONFixture(jsonFixtureName); - - PipelinesComponent = Vue.extend(pipelinesComp); - }); - - afterEach(() => { - vm.$destroy(); - mock.restore(); - }); - - describe('With permission', () => { - describe('With pipelines in main tab', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('renders Run Pipeline link', () => { - expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( - paths.newPipelinePath, - ); - }); - - it('renders CI Lint link', () => { - expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); - }); - - it('renders Clear Runner Cache button', () => { - expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual( - 'Clear Runner Caches', - ); - }); - - it('renders pipelines table', () => { - expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual( - pipelines.pipelines.length + 1, - ); - }); - }); - - describe('Without pipelines on main tab with CI', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, { - pipelines: [], - count: { - all: 0, - pending: 0, - running: 0, - finished: 0, - }, - }); - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('renders Run Pipeline link', () => { - expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( - paths.newPipelinePath, - ); - }); - - it('renders CI Lint link', () => { - expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); - }); - - it('renders Clear Runner Cache button', () => { - expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual( - 'Clear Runner Caches', - ); - }); - - it('renders tab empty state', () => { - expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual( - 'There are currently no pipelines.', - ); - }); - }); - - describe('Without pipelines nor CI', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, { - pipelines: [], - count: { - all: 0, - pending: 0, - running: 0, - finished: 0, - }, - }); - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: false, - canCreatePipeline: true, - ...paths, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders empty state', () => { - expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual( - 'Build with confidence', - ); - - expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual( - paths.helpPagePath, - ); - }); - - it('does not render tabs nor buttons', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); - expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); - expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); - expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); - }); - }); - - describe('When API returns error', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(500, {}); - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: false, - canCreatePipeline: true, - ...paths, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('renders buttons', () => { - expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual( - paths.newPipelinePath, - ); - - expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath); - expect(vm.$el.querySelector('.js-clear-cache').textContent.trim()).toEqual( - 'Clear Runner Caches', - ); - }); - - it('renders error state', () => { - expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain( - 'There was an error fetching the pipelines.', - ); - }); - }); - }); - - describe('Without permission', () => { - describe('With pipelines in main tab', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: false, - canCreatePipeline: false, - ...noPermissions, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('does not render buttons', () => { - expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); - expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); - expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); - }); - - it('renders pipelines table', () => { - expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual( - pipelines.pipelines.length + 1, - ); - }); - }); - - describe('Without pipelines on main tab with CI', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, { - pipelines: [], - count: { - all: 0, - pending: 0, - running: 0, - finished: 0, - }, - }); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: false, - ...noPermissions, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('does not render buttons', () => { - expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); - expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); - expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); - }); - - it('renders tab empty state', () => { - expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual( - 'There are currently no pipelines.', - ); - }); - }); - - describe('Without pipelines nor CI', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(200, { - pipelines: [], - count: { - all: 0, - pending: 0, - running: 0, - finished: 0, - }, - }); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: false, - canCreatePipeline: false, - ...noPermissions, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders empty state without button to set CI', () => { - expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual( - 'This project is not currently set up to run pipelines.', - ); - - expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull(); - }); - - it('does not render tabs or buttons', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull(); - expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); - expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); - expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); - }); - }); - - describe('When API returns error', () => { - beforeEach(done => { - mock.onGet('twitter/flight/pipelines.json').reply(500, {}); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: false, - canCreatePipeline: true, - ...noPermissions, - }); - - setTimeout(() => { - done(); - }); - }); - - it('renders tabs', () => { - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - }); - - it('does not renders buttons', () => { - expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull(); - expect(vm.$el.querySelector('.js-ci-lint')).toBeNull(); - expect(vm.$el.querySelector('.js-clear-cache')).toBeNull(); - }); - - it('renders error state', () => { - expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain( - 'There was an error fetching the pipelines.', - ); - }); - }); - }); - - describe('successful request', () => { - describe('with pipelines', () => { - beforeEach(() => { - mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - }); - - it('should render table', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.table-holder')).toBeDefined(); - expect(vm.$el.querySelectorAll('.gl-responsive-table-row').length).toEqual( - pipelines.pipelines.length + 1, - ); - done(); - }); - }); - - it('should render navigation tabs', done => { - setTimeout(() => { - expect(vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim()).toContain( - 'Pending', - ); - - expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All'); - - expect(vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim()).toContain( - 'Running', - ); - - expect(vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim()).toContain( - 'Finished', - ); - - expect(vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim()).toContain( - 'Branches', - ); - - expect(vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim()).toContain( - 'Tags', - ); - done(); - }); - }); - - it('should make an API request when using tabs', done => { - setTimeout(() => { - spyOn(vm, 'updateContent'); - vm.$el.querySelector('.js-pipelines-tab-finished').click(); - - expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); - done(); - }); - }); - - describe('with pagination', () => { - it('should make an API request when using pagination', done => { - setTimeout(() => { - spyOn(vm, 'updateContent'); - // Mock pagination - vm.store.state.pageInfo = { - page: 1, - total: 10, - perPage: 2, - nextPage: 2, - totalPages: 5, - }; - - vm.$nextTick(() => { - vm.$el.querySelector('.next-page-item').click(); - - expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); - - done(); - }); - }); - }); - }); - }); - }); - - describe('methods', () => { - beforeEach(() => { - spyOn(window.history, 'pushState').and.stub(); - }); - - describe('updateContent', () => { - it('should set given parameters', () => { - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - vm.updateContent({ scope: 'finished', page: '4' }); - - expect(vm.page).toEqual('4'); - expect(vm.scope).toEqual('finished'); - expect(vm.requestData.scope).toEqual('finished'); - expect(vm.requestData.page).toEqual('4'); - }); - }); - - describe('onChangeTab', () => { - it('should set page to 1', () => { - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - spyOn(vm, 'updateContent'); - - vm.onChangeTab('running'); - - expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); - }); - }); - - describe('onChangePage', () => { - it('should update page and keep scope', () => { - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - spyOn(vm, 'updateContent'); - - vm.onChangePage(4); - - expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' }); - }); - }); - }); - - describe('computed properties', () => { - beforeEach(() => { - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - }); - - describe('tabs', () => { - it('returns default tabs', () => { - expect(vm.tabs).toEqual([ - { name: 'All', scope: 'all', count: undefined, isActive: true }, - { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, - { name: 'Running', scope: 'running', count: undefined, isActive: false }, - { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, - { name: 'Branches', scope: 'branches', isActive: false }, - { name: 'Tags', scope: 'tags', isActive: false }, - ]); - }); - }); - - describe('emptyTabMessage', () => { - it('returns message with scope', done => { - vm.scope = 'pending'; - - vm.$nextTick(() => { - expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); - done(); - }); - }); - - it('returns message without scope when scope is `all`', () => { - expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.'); - }); - }); - - describe('stateToRender', () => { - it('returns loading state when the app is loading', () => { - expect(vm.stateToRender).toEqual('loading'); - }); - - it('returns error state when app has error', done => { - vm.hasError = true; - vm.isLoading = false; - - vm.$nextTick(() => { - expect(vm.stateToRender).toEqual('error'); - done(); - }); - }); - - it('returns table list when app has pipelines', done => { - vm.isLoading = false; - vm.hasError = false; - vm.state.pipelines = pipelines.pipelines; - - vm.$nextTick(() => { - expect(vm.stateToRender).toEqual('tableList'); - - done(); - }); - }); - - it('returns empty tab when app does not have pipelines but project has pipelines', done => { - vm.state.count.all = 10; - vm.isLoading = false; - - vm.$nextTick(() => { - expect(vm.stateToRender).toEqual('emptyTab'); - - done(); - }); - }); - - it('returns empty tab when project has CI', done => { - vm.isLoading = false; - vm.$nextTick(() => { - expect(vm.stateToRender).toEqual('emptyTab'); - - done(); - }); - }); - - it('returns empty state when project does not have pipelines nor CI', done => { - vm.isLoading = false; - vm.hasGitlabCi = false; - vm.$nextTick(() => { - expect(vm.stateToRender).toEqual('emptyState'); - - done(); - }); - }); - }); - - describe('shouldRenderTabs', () => { - it('returns true when state is loading & has already made the first request', done => { - vm.isLoading = true; - vm.hasMadeRequest = true; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(true); - - done(); - }); - }); - - it('returns true when state is tableList & has already made the first request', done => { - vm.isLoading = false; - vm.state.pipelines = pipelines.pipelines; - vm.hasMadeRequest = true; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(true); - - done(); - }); - }); - - it('returns true when state is error & has already made the first request', done => { - vm.isLoading = false; - vm.hasError = true; - vm.hasMadeRequest = true; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(true); - - done(); - }); - }); - - it('returns true when state is empty tab & has already made the first request', done => { - vm.isLoading = false; - vm.state.count.all = 10; - vm.hasMadeRequest = true; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(true); - - done(); - }); - }); - - it('returns false when has not made first request', done => { - vm.hasMadeRequest = false; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(false); - - done(); - }); - }); - - it('returns false when state is empty state', done => { - vm.isLoading = false; - vm.hasMadeRequest = true; - vm.hasGitlabCi = false; - - vm.$nextTick(() => { - expect(vm.shouldRenderTabs).toEqual(false); - - done(); - }); - }); - }); - - describe('shouldRenderButtons', () => { - it('returns true when it has paths & has made the first request', done => { - vm.hasMadeRequest = true; - - vm.$nextTick(() => { - expect(vm.shouldRenderButtons).toEqual(true); - - done(); - }); - }); - - it('returns false when it has not made the first request', done => { - vm.hasMadeRequest = false; - - vm.$nextTick(() => { - expect(vm.shouldRenderButtons).toEqual(false); - - done(); - }); - }); - }); - }); - - describe('updates results when a staged is clicked', () => { - beforeEach(() => { - const copyPipeline = Object.assign({}, pipelineWithStages); - copyPipeline.id += 1; - mock - .onGet('twitter/flight/pipelines.json') - .reply( - 200, - { - pipelines: [pipelineWithStages], - count: { - all: 1, - finished: 1, - pending: 0, - running: 0, - }, - }, - { - 'POLL-INTERVAL': 100, - }, - ) - .onGet(pipelineWithStages.details.stages[0].dropdown_path) - .reply(200, stageReply); - - vm = mountComponent(PipelinesComponent, { - store: new Store(), - hasGitlabCi: true, - canCreatePipeline: true, - ...paths, - }); - }); - - describe('when a request is being made', () => { - it('stops polling, cancels the request, & restarts polling', done => { - spyOn(vm.poll, 'stop'); - spyOn(vm.poll, 'restart'); - spyOn(vm.service.cancelationSource, 'cancel').and.callThrough(); - - setTimeout(() => { - vm.isMakingRequest = true; - return vm - .$nextTick() - .then(() => { - vm.$el.querySelector('.js-builds-dropdown-button').click(); - }) - .then(() => { - expect(vm.service.cancelationSource.cancel).toHaveBeenCalled(); - expect(vm.poll.stop).toHaveBeenCalled(); - - setTimeout(() => { - expect(vm.poll.restart).toHaveBeenCalled(); - done(); - }, 0); - }) - .catch(done.fail); - }, 0); - }); - }); - - describe('when no request is being made', () => { - it('stops polling & restarts polling', done => { - spyOn(vm.poll, 'stop'); - spyOn(vm.poll, 'restart'); - - setTimeout(() => { - vm.$el.querySelector('.js-builds-dropdown-button').click(); - - expect(vm.poll.stop).toHaveBeenCalled(); - - setTimeout(() => { - expect(vm.poll.restart).toHaveBeenCalled(); - done(); - }, 0); - }, 0); - }); - }); - }); -}); diff --git a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb index 9033b71b19f..f82e49f9323 100644 --- a/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/chain/sequence_spec.rb @@ -5,11 +5,13 @@ require 'spec_helper' describe Gitlab::Ci::Pipeline::Chain::Sequence do let_it_be(:project) { create(:project) } let_it_be(:user) { create(:user) } + let(:pipeline) { build_stubbed(:ci_pipeline) } let(:command) { Gitlab::Ci::Pipeline::Chain::Command.new } let(:first_step) { spy('first step') } let(:second_step) { spy('second step') } let(:sequence) { [first_step, second_step] } + let(:histogram) { spy('prometheus metric') } subject do described_class.new(pipeline, command, sequence) @@ -52,5 +54,13 @@ describe Gitlab::Ci::Pipeline::Chain::Sequence do it 'returns a pipeline object' do expect(subject.build!).to eq pipeline end + + it 'adds sequence duration to duration histogram' do + allow(command).to receive(:duration_histogram).and_return(histogram) + + subject.build! + + expect(histogram).to have_received(:observe) + end end end diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb index a11a9d08503..2a4a911cf38 100644 --- a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb @@ -3,6 +3,10 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::BaseCommand do + subject(:base_command) do + test_class.new(rbac) + end + let(:application) { create(:clusters_applications_helm) } let(:rbac) { false } @@ -30,87 +34,17 @@ describe Gitlab::Kubernetes::Helm::BaseCommand do end end - let(:base_command) do - test_class.new(rbac) - end - - subject { base_command } - - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) { '' } end - describe '#pod_resource' do - subject { base_command.pod_resource } - - it 'returns a kubeclient resoure with pod content for application' do - is_expected.to be_an_instance_of ::Kubeclient::Resource - end - - context 'when rbac is true' do - let(:rbac) { true } - - it 'also returns a kubeclient resource' do - is_expected.to be_an_instance_of ::Kubeclient::Resource - end - end - end - describe '#pod_name' do subject { base_command.pod_name } it { is_expected.to eq('install-test-class-name') } end - describe '#service_account_resource' do - let(:resource) do - Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) - end - - subject { base_command.service_account_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a Kubeclient resource for the tiller ServiceAccount' do - is_expected.to eq(resource) - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates nothing' do - is_expected.to be_nil - end - end - end - - describe '#cluster_role_binding_resource' do - let(:resource) do - Kubeclient::Resource.new( - metadata: { name: 'tiller-admin' }, - roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, - subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] - ) - end - - subject { base_command.cluster_role_binding_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do - is_expected.to eq(resource) - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates nothing' do - is_expected.to be_nil - end - end + it_behaves_like 'helm command' do + let(:command) { base_command } end end diff --git a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb index 82e15864687..e1ca56b0ba6 100644 --- a/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/delete_command_spec.rb @@ -3,14 +3,13 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::DeleteCommand do + subject(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) } + let(:app_name) { 'app-name' } let(:rbac) { true } let(:files) { {} } - let(:delete_command) { described_class.new(name: app_name, rbac: rbac, files: files) } - - subject { delete_command } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -26,7 +25,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do stub_feature_flags(managed_apps_local_tiller: false) end - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm init --upgrade @@ -48,7 +47,7 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do EOS end - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm init --upgrade @@ -67,29 +66,13 @@ describe Gitlab::Kubernetes::Helm::DeleteCommand do end end - describe '#pod_resource' do - subject { delete_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end - end - describe '#pod_name' do subject { delete_command.pod_name } it { is_expected.to eq('uninstall-app-name') } end + + it_behaves_like 'helm command' do + let(:command) { delete_command } + end end diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb index 13021a08f9f..05d9b63d12b 100644 --- a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb @@ -3,25 +3,24 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::InitCommand do + subject(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) } + let(:application) { create(:clusters_applications_helm) } let(:rbac) { false } let(:files) { {} } - let(:init_command) { described_class.new(name: application.name, files: files, rbac: rbac) } - let(:commands) do - <<~EOS - helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem - EOS + it_behaves_like 'helm command generator' do + let(:commands) do + <<~EOS + helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem + EOS + end end - subject { init_command } - - it_behaves_like 'helm commands' - context 'on a rbac-enabled cluster' do let(:rbac) { true } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm init --tiller-tls --tiller-tls-verify --tls-ca-cert /data/helm/helm/config/ca.pem --tiller-tls-cert /data/helm/helm/config/cert.pem --tiller-tls-key /data/helm/helm/config/key.pem --service-account tiller @@ -30,57 +29,7 @@ describe Gitlab::Kubernetes::Helm::InitCommand do end end - describe '#rbac?' do - subject { init_command.rbac? } - - context 'rbac is enabled' do - let(:rbac) { true } - - it { is_expected.to be_truthy } - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it { is_expected.to be_falsey } - end - end - - describe '#config_map_resource' do - let(:metadata) do - { - name: 'values-content-configuration-helm', - namespace: 'gitlab-managed-apps', - labels: { name: 'values-content-configuration-helm' } - } - end - - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } - - subject { init_command.config_map_resource } - - it 'returns a KubeClient resource with config map content for the application' do - is_expected.to eq(resource) - end - end - - describe '#pod_resource' do - subject { init_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end + it_behaves_like 'helm command' do + let(:command) { init_command } end end diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb index a5ed8f57bf3..abd29e97505 100644 --- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb @@ -3,14 +3,7 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::InstallCommand do - let(:files) { { 'ca.pem': 'some file content' } } - let(:repository) { 'https://repository.example.com' } - let(:rbac) { false } - let(:version) { '1.2.3' } - let(:preinstall) { nil } - let(:postinstall) { nil } - - let(:install_command) do + subject(:install_command) do described_class.new( name: 'app-name', chart: 'chart-name', @@ -23,9 +16,14 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do ) end - subject { install_command } + let(:files) { { 'ca.pem': 'some file content' } } + let(:repository) { 'https://repository.example.com' } + let(:rbac) { false } + let(:version) { '1.2.3' } + let(:preinstall) { nil } + let(:postinstall) { nil } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -66,7 +64,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do EOS end - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm init --upgrade @@ -97,7 +95,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do context 'when rbac is true' do let(:rbac) { true } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -128,7 +126,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do context 'when there is a pre-install script' do let(:preinstall) { ['/bin/date', '/bin/true'] } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -161,7 +159,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do context 'when there is a post-install script' do let(:postinstall) { ['/bin/date', "/bin/false\n"] } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -194,7 +192,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do context 'when there is no ca.pem file' do let(:files) { { 'file.txt': 'some content' } } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -225,7 +223,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do context 'when there is no version' do let(:version) { nil } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -252,57 +250,7 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do end end - describe '#rbac?' do - subject { install_command.rbac? } - - context 'rbac is enabled' do - let(:rbac) { true } - - it { is_expected.to be_truthy } - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it { is_expected.to be_falsey } - end - end - - describe '#pod_resource' do - subject { install_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end - end - - describe '#config_map_resource' do - let(:metadata) do - { - name: "values-content-configuration-app-name", - namespace: 'gitlab-managed-apps', - labels: { name: "values-content-configuration-app-name" } - } - end - - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } - - subject { install_command.config_map_resource } - - it 'returns a KubeClient resource with config map content for the application' do - is_expected.to eq(resource) - end + it_behaves_like 'helm command' do + let(:command) { install_command } end end diff --git a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb index e69570f5371..eee842fa7d6 100644 --- a/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/patch_command_spec.rb @@ -33,7 +33,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do EOS end - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm init --upgrade @@ -57,7 +57,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do end end - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -83,7 +83,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do context 'when rbac is true' do let(:rbac) { true } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -110,7 +110,7 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do context 'when there is no ca.pem file' do let(:files) { { 'file.txt': 'some content' } } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS export HELM_HOST="localhost:44134" @@ -134,69 +134,19 @@ describe Gitlab::Kubernetes::Helm::PatchCommand do end end - describe '#pod_name' do - subject { patch_command.pod_name } - - it { is_expected.to eq 'install-app-name' } - end - context 'when there is no version' do let(:version) { nil } it { expect { patch_command }.to raise_error(ArgumentError, 'version is required') } end - describe '#rbac?' do - subject { patch_command.rbac? } - - context 'rbac is enabled' do - let(:rbac) { true } - - it { is_expected.to be_truthy } - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it { is_expected.to be_falsey } - end - end - - describe '#pod_resource' do - subject { patch_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } + describe '#pod_name' do + subject { patch_command.pod_name } - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end + it { is_expected.to eq 'install-app-name' } end - describe '#config_map_resource' do - let(:metadata) do - { - name: "values-content-configuration-app-name", - namespace: 'gitlab-managed-apps', - labels: { name: "values-content-configuration-app-name" } - } - end - - let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: files) } - - subject { patch_command.config_map_resource } - - it 'returns a KubeClient resource with config map content for the application' do - is_expected.to eq(resource) - end + it_behaves_like 'helm command' do + let(:command) { patch_command } end end diff --git a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb index 2a89b04723d..981bb4e4abf 100644 --- a/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb +++ b/spec/lib/gitlab/kubernetes/helm/reset_command_spec.rb @@ -3,14 +3,13 @@ require 'spec_helper' describe Gitlab::Kubernetes::Helm::ResetCommand do + subject(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) } + let(:rbac) { true } let(:name) { 'helm' } let(:files) { {} } - let(:reset_command) { described_class.new(name: name, rbac: rbac, files: files) } - - subject { reset_command } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS helm reset @@ -23,7 +22,7 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do context 'when there is a ca.pem file' do let(:files) { { 'ca.pem': 'some file content' } } - it_behaves_like 'helm commands' do + it_behaves_like 'helm command generator' do let(:commands) do <<~EOS1.squish + "\n" + <<~EOS2 helm reset @@ -39,29 +38,13 @@ describe Gitlab::Kubernetes::Helm::ResetCommand do end end - describe '#pod_resource' do - subject { reset_command.pod_resource } - - context 'rbac is enabled' do - let(:rbac) { true } - - it 'generates a pod that uses the tiller serviceAccountName' do - expect(subject.spec.serviceAccountName).to eq('tiller') - end - end - - context 'rbac is not enabled' do - let(:rbac) { false } - - it 'generates a pod that uses the default serviceAccountName' do - expect(subject.spec.serviceAcccountName).to be_nil - end - end - end - describe '#pod_name' do subject { reset_command.pod_name } it { is_expected.to eq('uninstall-helm') } end + + it_behaves_like 'helm command' do + let(:command) { reset_command } + end end diff --git a/spec/lib/gitlab/visibility_level_spec.rb b/spec/lib/gitlab/visibility_level_spec.rb index 16a05af2216..a249b3a235e 100644 --- a/spec/lib/gitlab/visibility_level_spec.rb +++ b/spec/lib/gitlab/visibility_level_spec.rb @@ -96,6 +96,30 @@ describe Gitlab::VisibilityLevel do end end + describe '.restricted_level?, .non_restricted_level?, and .public_level_restricted?' do + using RSpec::Parameterized::TableSyntax + + where(:visibility_levels, :expected_status) do + nil | false + [Gitlab::VisibilityLevel::PRIVATE] | false + [Gitlab::VisibilityLevel::PRIVATE, Gitlab::VisibilityLevel::INTERNAL] | false + [Gitlab::VisibilityLevel::PUBLIC] | true + [Gitlab::VisibilityLevel::PUBLIC, Gitlab::VisibilityLevel::INTERNAL] | true + end + + with_them do + before do + stub_application_setting(restricted_visibility_levels: visibility_levels) + end + + it 'returns the expected status' do + expect(described_class.restricted_level?(Gitlab::VisibilityLevel::PUBLIC)).to eq(expected_status) + expect(described_class.non_restricted_level?(Gitlab::VisibilityLevel::PUBLIC)).to eq(!expected_status) + expect(described_class.public_visibility_restricted?).to eq(expected_status) + end + end + end + describe '#visibility_level_decreased?' do let(:project) { create(:project, :internal) } diff --git a/spec/models/jira_import_state_spec.rb b/spec/models/jira_import_state_spec.rb index 4d91bf25b5e..99f9e035205 100644 --- a/spec/models/jira_import_state_spec.rb +++ b/spec/models/jira_import_state_spec.rb @@ -124,6 +124,7 @@ describe JiraImportState do jira_import.schedule expect(jira_import.jid).to eq('some-job-id') + expect(jira_import.scheduled_at).to be_within(1.second).of(Time.now) end end diff --git a/spec/requests/api/graphql/project/jira_import_spec.rb b/spec/requests/api/graphql/project/jira_import_spec.rb index 2e631fb56ba..e063068eb1a 100644 --- a/spec/requests/api/graphql/project/jira_import_spec.rb +++ b/spec/requests/api/graphql/project/jira_import_spec.rb @@ -18,6 +18,7 @@ describe 'query Jira import data' do jiraImports { nodes { jiraProjectKey + createdAt scheduledAt scheduledBy { username diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb index 07e7a48d8c4..95d64ee8124 100644 --- a/spec/requests/api/settings_spec.rb +++ b/spec/requests/api/settings_spec.rb @@ -60,14 +60,14 @@ describe API::Settings, 'Settings' do default_projects_limit: 3, default_project_creation: 2, password_authentication_enabled_for_web: false, - repository_storages: ['custom'], + repository_storages: 'custom', plantuml_enabled: true, plantuml_url: 'http://plantuml.example.com', sourcegraph_enabled: true, sourcegraph_url: 'https://sourcegraph.com', sourcegraph_public_only: false, default_snippet_visibility: 'internal', - restricted_visibility_levels: ['public'], + restricted_visibility_levels: 'public', default_artifacts_expire_in: '2 days', help_page_text: 'custom help text', help_page_hide_commercial_content: true, @@ -89,7 +89,9 @@ describe API::Settings, 'Settings' do push_event_hooks_limit: 2, push_event_activities_limit: 2, snippet_size_limit: 5, - issues_create_limit: 300 + issues_create_limit: 300, + disabled_oauth_sign_in_sources: 'unknown', + import_sources: 'github,bitbucket' } expect(response).to have_gitlab_http_status(:ok) @@ -127,6 +129,8 @@ describe API::Settings, 'Settings' do expect(json_response['push_event_activities_limit']).to eq(2) expect(json_response['snippet_size_limit']).to eq(5) expect(json_response['issues_create_limit']).to eq(300) + expect(json_response['disabled_oauth_sign_in_sources']).to eq([]) + expect(json_response['import_sources']).to match_array(%w(github bitbucket)) end end diff --git a/spec/rubocop/cop/api/grape_api_instance_spec.rb b/spec/rubocop/cop/api/grape_api_instance_spec.rb new file mode 100644 index 00000000000..0199377f104 --- /dev/null +++ b/spec/rubocop/cop/api/grape_api_instance_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rubocop' +require_relative '../../../support/helpers/expect_offense' +require_relative '../../../../rubocop/cop/api/grape_api_instance' + +describe RuboCop::Cop::API::GrapeAPIInstance do + include CopHelper + include ExpectOffense + + subject(:cop) { described_class.new } + + it 'adds an offense when inheriting from Grape::API' do + inspect_source(<<~CODE.strip_indent) + class SomeAPI < Grape::API + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'does not add an offense when inheriting from Grape::API::Instance' do + inspect_source(<<~CODE.strip_indent) + class SomeAPI < Grape::API::Instance + end + CODE + + expect(cop.offenses.size).to be_zero + end +end diff --git a/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb new file mode 100644 index 00000000000..8252e07837d --- /dev/null +++ b/spec/rubocop/cop/api/grape_array_missing_coerce_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' +require 'rubocop' +require_relative '../../../support/helpers/expect_offense' +require_relative '../../../../rubocop/cop/api/grape_array_missing_coerce' + +describe RuboCop::Cop::API::GrapeArrayMissingCoerce do + include CopHelper + include ExpectOffense + + subject(:cop) { described_class.new } + + it 'adds an offense with a required parameter' do + inspect_source(<<~CODE.strip_indent) + class SomeAPI < Grape::API::Instance + params do + requires :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'adds an offense with an optional parameter' do + inspect_source(<<~CODE.strip_indent) + class SomeAPI < Grape::API::Instance + params do + optional :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to eq(1) + end + + it 'does not add an offense' do + inspect_source(<<~CODE.strip_indent) + class SomeAPI < Grape::API::Instance + params do + requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) } + requires :milestone, type: String, desc: 'Milestone title' + optional :assignee_id, types: [Integer, String], integer_none_any: true, + desc: 'Return issues which are assigned to the user with the given ID' + end + end + CODE + + expect(cop.offenses.size).to be_zero + end + + it 'does not add an offense for unrelated classes' do + inspect_source(<<~CODE.strip_indent) + class SomeClass + params do + requires :values, type: Array[String] + end + end + CODE + + expect(cop.offenses.size).to be_zero + end +end diff --git a/spec/rubocop/cop/code_reuse/worker_spec.rb b/spec/rubocop/cop/code_reuse/worker_spec.rb index 97acaeb7643..9005b5a0611 100644 --- a/spec/rubocop/cop/code_reuse/worker_spec.rb +++ b/spec/rubocop/cop/code_reuse/worker_spec.rb @@ -31,7 +31,7 @@ describe RuboCop::Cop::CodeReuse::Worker do .and_return(true) expect_offense(<<~SOURCE) - class Foo < Grape::API + class Foo < Grape::API::Instance resource :projects do get '/' do FooWorker.perform_async diff --git a/spec/rubocop/cop/gitlab/json_spec.rb b/spec/rubocop/cop/gitlab/json_spec.rb new file mode 100644 index 00000000000..d64f60c8583 --- /dev/null +++ b/spec/rubocop/cop/gitlab/json_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/json' + +describe RuboCop::Cop::Gitlab::Json do + include CopHelper + + subject(:cop) { described_class.new } + + shared_examples('registering call offense') do |options| + let(:offending_lines) { options[:offending_lines] } + + it 'registers an offense when the class calls JSON' do + inspect_source(source) + + aggregate_failures do + expect(cop.offenses.size).to eq(offending_lines.size) + expect(cop.offenses.map(&:line)).to eq(offending_lines) + end + end + end + + context 'when JSON is called' do + it_behaves_like 'registering call offense', offending_lines: [3] do + let(:source) do + <<~RUBY + class Foo + def bar + JSON.parse('{ "foo": "bar" }') + end + end + RUBY + end + end + end +end diff --git a/spec/support/shared_examples/helm_commands_shared_examples.rb b/spec/support/shared_examples/helm_commands_shared_examples.rb new file mode 100644 index 00000000000..f0624fbf29f --- /dev/null +++ b/spec/support/shared_examples/helm_commands_shared_examples.rb @@ -0,0 +1,131 @@ +# frozen_string_literal: true + +shared_examples 'helm command generator' do + describe '#generate_script' do + let(:helm_setup) do + <<~EOS + set -xeo pipefail + EOS + end + + it 'returns appropriate command' do + expect(subject.generate_script.strip).to eq((helm_setup + commands).strip) + end + end +end + +shared_examples 'helm command' do + describe '#rbac?' do + subject { command.rbac? } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_truthy } + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_falsey } + end + end + + describe '#pod_resource' do + subject { command.pod_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it { is_expected.to be_an_instance_of ::Kubeclient::Resource } + + it 'generates a pod that uses the tiller serviceAccountName' do + expect(subject.spec.serviceAccountName).to eq('tiller') + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it { is_expected.to be_an_instance_of ::Kubeclient::Resource } + + it 'generates a pod that uses the default serviceAccountName' do + expect(subject.spec.serviceAcccountName).to be_nil + end + end + end + + describe '#config_map_resource' do + subject { command.config_map_resource } + + let(:metadata) do + { + name: "values-content-configuration-#{command.name}", + namespace: 'gitlab-managed-apps', + labels: { name: "values-content-configuration-#{command.name}" } + } + end + + let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: command.files) } + + it 'returns a KubeClient resource with config map content for the application' do + is_expected.to eq(resource) + end + end + + describe '#service_account_resource' do + let(:resource) do + Kubeclient::Resource.new(metadata: { name: 'tiller', namespace: 'gitlab-managed-apps' }) + end + + subject { command.service_account_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the tiller ServiceAccount' do + is_expected.to eq(resource) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end + + describe '#cluster_role_binding_resource' do + let(:resource) do + Kubeclient::Resource.new( + metadata: { name: 'tiller-admin' }, + roleRef: { apiGroup: 'rbac.authorization.k8s.io', kind: 'ClusterRole', name: 'cluster-admin' }, + subjects: [{ kind: 'ServiceAccount', name: 'tiller', namespace: 'gitlab-managed-apps' }] + ) + end + + subject(:cluster_role_binding_resource) { command.cluster_role_binding_resource } + + context 'rbac is enabled' do + let(:rbac) { true } + + it 'generates a Kubeclient resource for the ClusterRoleBinding for tiller' do + is_expected.to eq(resource) + end + + it 'binds the account in #service_account_resource' do + expect(cluster_role_binding_resource.subjects.first.name).to eq(command.service_account_resource.metadata.name) + end + end + + context 'rbac is not enabled' do + let(:rbac) { false } + + it 'generates nothing' do + is_expected.to be_nil + end + end + end +end diff --git a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb deleted file mode 100644 index bbf8a946f8b..00000000000 --- a/spec/support/shared_examples/lib/gitlab/helm_generated_script_shared_examples.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'helm commands' do - describe '#generate_script' do - let(:helm_setup) do - <<~EOS - set -xeo pipefail - EOS - end - - it 'returns appropriate command' do - expect(subject.generate_script.strip).to eq((helm_setup + commands).strip) - end - end -end |