diff options
Diffstat (limited to 'spec')
44 files changed, 973 insertions, 175 deletions
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb index 2c6ad00515e..847ac6f2be0 100644 --- a/spec/controllers/projects/services_controller_spec.rb +++ b/spec/controllers/projects/services_controller_spec.rb @@ -114,5 +114,41 @@ describe Projects::ServicesController do expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.' end end + + context 'with a deprecated service' do + let(:service) { create(:kubernetes_service, project: project) } + + before do + put :update, + namespace_id: project.namespace, project_id: project, id: service.to_param, service: { namespace: 'updated_namespace' } + end + + it 'should not update the service' do + service.reload + expect(service.namespace).not_to eq('updated_namespace') + end + end + end + + describe "GET #edit" do + before do + get :edit, namespace_id: project.namespace, project_id: project, id: service_id + end + + context 'with approved services' do + let(:service_id) { 'jira' } + + it 'should render edit page' do + expect(response).to be_success + end + end + + context 'with a deprecated service' do + let(:service_id) { 'kubernetes' } + + it 'should render edit page' do + expect(response).to be_success + end + end end end diff --git a/spec/factories/keys.rb b/spec/factories/keys.rb index e6eb76f71d3..552b4b7e06e 100644 --- a/spec/factories/keys.rb +++ b/spec/factories/keys.rb @@ -21,12 +21,14 @@ FactoryBot.define do factory :rsa_key_2048 do key do - 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9' \ - '6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5' \ - '/jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7' \ - 'M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC' \ - 'rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0' \ - '5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 + 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 + /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 + M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC + rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 + 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com + KEY end factory :rsa_deploy_key_2048, class: 'DeployKey' @@ -34,37 +36,44 @@ FactoryBot.define do factory :dsa_key_2048 do key do - 'ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G' \ - 'Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp' \ - 'YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ' \ - '/pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz' \ - 'OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv' \ - '5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB' \ - 'AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t' \ - 'poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1' \ - 'M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH' \ - 'MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H' \ - 'nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A' \ - '1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb' \ - 'aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI' \ - 'zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex' \ - 'PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z' \ - 'wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS' \ - 'Taja+Cf9kMo== dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-dss AAAAB3NzaC1kc3MAAAEBAO/3/NPLA/zSFkMOCaTtGo+uos1flfQ5f038Uk+G + Y9AeLGzX+Srhw59GdVXmOQLYBrOt5HdGwqYcmLnE2VurUGmhtfeO5H+3p5pGJbkS0Gxp + YH1HRO9lWsncF3Hh1w4lYsDjkclDiSTdfTuN8F4Kb3DXNnVSCieeonp+B25F/CXagyTQ + /pvNmHFeYgGCVdnBtFdi+xfxaZ8NKdPrGggzokbKHElDZQ4Xo5EpdcyLajgM7nB2r2Rz + OrmeaevKi5lV68ehRa9Yyrb7vxvwiwBwOgqR/mnN7Gnaq1jUdmJY+ct04Qwx37f5jvhv + 5gA4U40SGMoiHM8RFIN7Ksz0jsyX73MAAAAVALRWOfjfzHpK7KLz4iqDvvTUAevJAAAB + AEa9NZ+6y9iQ5erGsdfLTXFrhSefTG0NhghoO/5IFkSGfd8V7kzTvCHaFrcfpEA5kP8t + poeOG0TASB6tgGOxm1Bq4Wncry5RORBPJlAVpDGRcvZ931ddH7IgltEInS6za2uH6F/1 + M1QfKePSLr6xJ1ZLYfP0Og5KTp1x6yMQvfwV0a+XdA+EPgaJWLWp/pWwKWa0oLUgjsIH + MYzuOGh5c708uZrmkzqvgtW2NgXhcIroRgynT3IfI2lP2rqqb3uuuE/qH5UCUFO+Dc3H + nAFNeQDT/M25AERdPYBAY5a+iPjIgO+jT7BfmfByT+AZTqZySrCyc7nNZL3YgGLK0l6A + 1GgAAAEBAN9FpFOdIXE+YEZhKl1vPmbcn+b1y5zOl6N4x1B7Q8pD/pLMziWROIS8uLzb + aZ0sMIWezHIkxuo1iROMeT+jtCubn7ragaN6AX7nMpxYUH9+mYZZs/fyElt6wCviVhTI + zM+u7VdQsnZttOOlQfogHdL+SpeAft0DsfJjlcgQnsLlHQKv6aPqCPYUST2nE7RyW/Ex + PrMxLtOWt0/j8RYHbwwqvyeZqBz3ESBgrS9c5tBdBfauwYUV/E7gPLOU3OZFw9ue7o+z + wzoTZqW6Xouy5wtWvSLQSLT5XwOslmQz8QMBxD0AQyDfEFGsBCWzmbTgKv9uqrBjubsS + Taja+Cf9kMo== dummy@gitlab.com + KEY end end factory :ecdsa_key_256 do key do - 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA' \ - 'AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO' \ - 'Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com' + <<~KEY.delete("\n") + ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYA + AABBBJZmkzTgY0fiCQ+DVReyH/fFwTFz0XoR3RUO0u+199H19KFw7mNPxRSMOVS7tEtO + Nj3Q7FcZXfqthHvgAzDiHsc= dummy@gitlab.com + KEY end end factory :ed25519_key_256 do key do - 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJOhDRvf59ohL6 dummy@gitlab.com' + <<~KEY.delete("\n") + ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIETnVTgzqC1gatgSlC4zH6aYt2CAQzgJ + OhDRvf59ohL6 dummy@gitlab.com + KEY end end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index 4b0377967c7..110ef33c6f7 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -18,6 +18,7 @@ FactoryBot.define do factory :kubernetes_service do project + type 'KubernetesService' active true properties({ api_url: 'https://kubernetes.example.com', diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index d92c002b4e7..a71020002dc 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -94,22 +94,14 @@ feature 'Dashboard Groups page', :js do end it 'can toggle parent group' do - # Collapsed by default - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).to have_selector("#group-#{group.id} .fa-caret-right") - # expand click_group_caret(group) - expect(page).to have_selector("#group-#{group.id} .fa-caret-down") - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-right", count: 1) expect(page).to have_selector("#group-#{group.id} #group-#{subgroup.id}") # collapse click_group_caret(group) - expect(page).not_to have_selector("#group-#{group.id} .fa-caret-down", count: 1) - expect(page).to have_selector("#group-#{group.id} .fa-caret-right") expect(page).not_to have_selector("#group-#{group.id} #group-#{subgroup.id}") end end diff --git a/spec/features/projects/clusters/interchangeability_spec.rb b/spec/features/projects/clusters/interchangeability_spec.rb index 01f9526608f..3ddb35c755c 100644 --- a/spec/features/projects/clusters/interchangeability_spec.rb +++ b/spec/features/projects/clusters/interchangeability_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature 'Interchangeability between KubernetesService and Platform::Kubernetes' do - EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url=].freeze + EXCEPT_METHODS = %i[test title description help fields initialize_properties namespace namespace= api_url api_url= deprecated? deprecation_message].freeze EXCEPT_METHODS_GREP_V = %w[_touched? _changed? _was].freeze it 'Clusters::Platform::Kubernetes covers core interfaces in KubernetesService' do diff --git a/spec/fixtures/api/schemas/entities/merge_request_metrics.json b/spec/fixtures/api/schemas/entities/merge_request_metrics.json new file mode 100644 index 00000000000..3fa767f85df --- /dev/null +++ b/spec/fixtures/api/schemas/entities/merge_request_metrics.json @@ -0,0 +1,21 @@ +{ + "type": "object", + "required": ["closed_at", "merged_at", "closed_by", "merged_by"], + "properties" : { + "closed_at": { "type": ["datetime", "null"] }, + "merged_at": { "type": ["datetime", "null"] }, + "closed_by": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + }, + "merged_by": { + "oneOf": [ + { "type": "null" }, + { "$ref": "user.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json index 342890c3dee..9de27bee751 100644 --- a/spec/fixtures/api/schemas/entities/merge_request_widget.json +++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json @@ -31,8 +31,12 @@ "source_project_id": { "type": "integer" }, "target_branch": { "type": "string" }, "target_project_id": { "type": "integer" }, - "merge_event": { "type": ["object", "null"] }, - "closed_event": { "type": ["object", "null"] }, + "metrics": { + "oneOf": [ + { "type": "null" }, + { "$ref": "merge_request_metrics.json" } + ] + }, "author": { "type": ["object", "null"] }, "merge_user": { "type": ["object", "null"] }, "diff_head_sha": { "type": ["string", "null"] }, diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json new file mode 100644 index 00000000000..6482e0eedd2 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/user.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "required": [ + "id", + "state", + "avatar_url", + "web_url", + "path" + ], + "properties": { + "id": { "type": "integer" }, + "state": { "type": "string" }, + "avatar_url": { "type": "string" }, + "web_url": { "type": "string" }, + "path": { "type": "string" } + } +} diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js index 4310a07e6e6..8faad455825 100644 --- a/spec/javascripts/groups/components/item_caret_spec.js +++ b/spec/javascripts/groups/components/item_caret_spec.js @@ -16,24 +16,20 @@ describe('ItemCaretComponent', () => { describe('template', () => { it('should render component template correctly', () => { const vm = createComponent(); - vm.$mount(); expect(vm.$el.classList.contains('folder-caret')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length).toBe(1); vm.$destroy(); }); it('should render caret down icon if `isGroupOpen` prop is `true`', () => { const vm = createComponent(true); - vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(1); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(0); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-down'); vm.$destroy(); }); it('should render caret right icon if `isGroupOpen` prop is `false`', () => { const vm = createComponent(); - vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-down').length).toBe(0); - expect(vm.$el.querySelectorAll('i.fa.fa-caret-right').length).toBe(1); + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('angle-right'); vm.$destroy(); }); }); diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js index e200f9f08bd..55a7a713ca6 100644 --- a/spec/javascripts/groups/components/item_stats_spec.js +++ b/spec/javascripts/groups/components/item_stats_spec.js @@ -26,7 +26,6 @@ describe('ItemStatsComponent', () => { Object.keys(VISIBILITY_TYPE_ICON).forEach((visibility) => { const item = Object.assign({}, mockParentGroupItem, { visibility }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityIcon).toBe(VISIBILITY_TYPE_ICON[visibility]); vm.$destroy(); }); @@ -41,7 +40,6 @@ describe('ItemStatsComponent', () => { type: ITEM_TYPE.GROUP, }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityTooltip).toBe(GROUP_VISIBILITY_TYPE[visibility]); vm.$destroy(); }); @@ -54,7 +52,6 @@ describe('ItemStatsComponent', () => { type: ITEM_TYPE.PROJECT, }); const vm = createComponent(item); - vm.$mount(); expect(vm.visibilityTooltip).toBe(PROJECT_VISIBILITY_TYPE[visibility]); vm.$destroy(); }); @@ -68,13 +65,11 @@ describe('ItemStatsComponent', () => { item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); vm = createComponent(item); - vm.$mount(); expect(vm.isProject).toBeTruthy(); vm.$destroy(); item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); vm = createComponent(item); - vm.$mount(); expect(vm.isProject).toBeFalsy(); vm.$destroy(); }); @@ -87,13 +82,11 @@ describe('ItemStatsComponent', () => { item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); vm = createComponent(item); - vm.$mount(); expect(vm.isGroup).toBeTruthy(); vm.$destroy(); item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT }); vm = createComponent(item); - vm.$mount(); expect(vm.isGroup).toBeFalsy(); vm.$destroy(); }); @@ -101,57 +94,37 @@ describe('ItemStatsComponent', () => { }); describe('template', () => { - it('should render component template correctly', () => { + it('renders component container element correctly', () => { const vm = createComponent(); - vm.$mount(); - const visibilityIconEl = vm.$el.querySelector('.item-visibility'); - expect(vm.$el.classList.contains('.stats')).toBeDefined(); - expect(visibilityIconEl).toBeDefined(); - expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); - expect(visibilityIconEl.querySelector('i.fa')).toBeDefined(); + expect(vm.$el.classList.contains('stats')).toBeTruthy(); vm.$destroy(); }); - it('should render stat icons if `item.type` is Group', () => { - const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.GROUP }); - const vm = createComponent(item); - vm.$mount(); - - const subgroupIconEl = vm.$el.querySelector('span.number-subgroups'); - expect(subgroupIconEl).toBeDefined(); - expect(subgroupIconEl.dataset.originalTitle).toBe('Subgroups'); - expect(subgroupIconEl.querySelector('i.fa.fa-folder')).toBeDefined(); - expect(subgroupIconEl.innerText.trim()).toBe(`${vm.item.subgroupCount}`); - - const projectsIconEl = vm.$el.querySelector('span.number-projects'); - expect(projectsIconEl).toBeDefined(); - expect(projectsIconEl.dataset.originalTitle).toBe('Projects'); - expect(projectsIconEl.querySelector('i.fa.fa-bookmark')).toBeDefined(); - expect(projectsIconEl.innerText.trim()).toBe(`${vm.item.projectCount}`); - - const membersIconEl = vm.$el.querySelector('span.number-users'); - expect(membersIconEl).toBeDefined(); - expect(membersIconEl.dataset.originalTitle).toBe('Members'); - expect(membersIconEl.querySelector('i.fa.fa-users')).toBeDefined(); - expect(membersIconEl.innerText.trim()).toBe(`${vm.item.memberCount}`); + it('renders item visibility icon and tooltip correctly', () => { + const vm = createComponent(); + + const visibilityIconEl = vm.$el.querySelector('.item-visibility'); + expect(visibilityIconEl).not.toBe(null); + expect(visibilityIconEl.dataset.originalTitle).toBe(vm.visibilityTooltip); + expect(visibilityIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); vm.$destroy(); }); - it('should render stat icons if `item.type` is Project', () => { + it('renders start count and last updated information for project item correctly', () => { const item = Object.assign({}, mockParentGroupItem, { type: ITEM_TYPE.PROJECT, starCount: 4, }); const vm = createComponent(item); - vm.$mount(); const projectStarIconEl = vm.$el.querySelector('.project-stars'); - expect(projectStarIconEl).toBeDefined(); - expect(projectStarIconEl.querySelector('i.fa.fa-star')).toBeDefined(); - expect(projectStarIconEl.innerText.trim()).toBe(`${vm.item.starCount}`); + expect(projectStarIconEl).not.toBe(null); + expect(projectStarIconEl.querySelectorAll('svg').length > 0).toBeTruthy(); + expect(projectStarIconEl.querySelectorAll('.stat-value').length > 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('.last-updated').length > 0).toBeTruthy(); vm.$destroy(); }); diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js new file mode 100644 index 00000000000..e990870aaa6 --- /dev/null +++ b/spec/javascripts/groups/components/item_stats_value_spec.js @@ -0,0 +1,81 @@ +import Vue from 'vue'; + +import itemStatsValueComponent from '~/groups/components/item_stats_value.vue'; + +import mountComponent from '../../helpers/vue_mount_component_helper'; + +const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => { + const Component = Vue.extend(itemStatsValueComponent); + + return mountComponent(Component, { + title, + cssClass, + iconName, + tooltipPlacement, + value, + }); +}; + +describe('ItemStatsValueComponent', () => { + describe('computed', () => { + let vm; + const itemConfig = { + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + }; + + describe('isValuePresent', () => { + it('returns true if non-empty `value` is present', () => { + vm = createComponent(Object.assign({}, itemConfig, { value: 10 })); + expect(vm.isValuePresent).toBeTruthy(); + }); + + it('returns false if empty `value` is present', () => { + vm = createComponent(itemConfig); + expect(vm.isValuePresent).toBeFalsy(); + }); + + afterEach(() => { + vm.$destroy(); + }); + }); + }); + + describe('template', () => { + let vm; + beforeEach(() => { + vm = createComponent({ + title: 'Subgroups', + cssClass: 'number-subgroups', + iconName: 'folder', + tooltipPlacement: 'left', + value: 10, + }); + }); + + it('renders component element correctly', () => { + expect(vm.$el.classList.contains('number-subgroups')).toBeTruthy(); + expect(vm.$el.querySelectorAll('svg').length > 0).toBeTruthy(); + expect(vm.$el.querySelectorAll('.stat-value').length > 0).toBeTruthy(); + }); + + it('renders element tooltip correctly', () => { + expect(vm.$el.dataset.originalTitle).toBe('Subgroups'); + expect(vm.$el.dataset.placement).toBe('left'); + }); + + it('renders element icon correctly', () => { + expect(vm.$el.querySelector('svg use').getAttribute('xlink:href')).toContain('folder'); + }); + + it('renders value count correctly', () => { + expect(vm.$el.querySelector('.stat-value').innerText.trim()).toContain('10'); + }); + + afterEach(() => { + vm.$destroy(); + }); + }); +}); diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js index 528e6ed1b4c..495cc97b475 100644 --- a/spec/javascripts/groups/components/item_type_icon_spec.js +++ b/spec/javascripts/groups/components/item_type_icon_spec.js @@ -28,12 +28,12 @@ describe('ItemTypeIconComponent', () => { vm = createComponent(ITEM_TYPE.GROUP, true); vm.$mount(); - expect(vm.$el.querySelector('i.fa.fa-folder-open')).toBeDefined(); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder-open'); vm.$destroy(); vm = createComponent(ITEM_TYPE.GROUP); vm.$mount(); - expect(vm.$el.querySelector('i.fa.fa-folder')).toBeDefined(); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('folder'); vm.$destroy(); }); @@ -42,12 +42,12 @@ describe('ItemTypeIconComponent', () => { vm = createComponent(ITEM_TYPE.PROJECT); vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(1); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).toContain('bookmark'); vm.$destroy(); vm = createComponent(ITEM_TYPE.GROUP); vm.$mount(); - expect(vm.$el.querySelectorAll('i.fa.fa-bookmark').length).toBe(0); + expect(vm.$el.querySelector('use').getAttribute('xlink:href')).not.toContain('bookmark'); vm.$destroy(); }); }); diff --git a/spec/javascripts/groups/mock_data.js b/spec/javascripts/groups/mock_data.js index 6184d671790..8bf6417487d 100644 --- a/spec/javascripts/groups/mock_data.js +++ b/spec/javascripts/groups/mock_data.js @@ -18,9 +18,9 @@ export const PROJECT_VISIBILITY_TYPE = { }; export const VISIBILITY_TYPE_ICON = { - public: 'fa-globe', - internal: 'fa-shield', - private: 'fa-lock', + public: 'earth', + internal: 'shield', + private: 'lock', }; export const mockParentGroupItem = { @@ -46,6 +46,7 @@ export const mockParentGroupItem = { isOpen: true, isChildrenLoading: false, isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', }; export const mockRawChildren = [ @@ -69,6 +70,7 @@ export const mockRawChildren = [ subgroup_count: 2, can_leave: false, children: [], + updated_at: '2017-04-09T18:40:39.101Z', }, ]; @@ -96,6 +98,7 @@ export const mockChildren = [ isOpen: true, isChildrenLoading: false, isBeingRemoved: false, + updatedAt: '2017-04-09T18:40:39.101Z', }, ]; @@ -119,6 +122,7 @@ export const mockGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 67, @@ -139,6 +143,7 @@ export const mockGroups = [ project_count: 0, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 54, @@ -159,6 +164,7 @@ export const mockGroups = [ project_count: 0, subgroup_count: 1, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 5, @@ -179,6 +185,7 @@ export const mockGroups = [ project_count: 1, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 4, @@ -199,6 +206,7 @@ export const mockGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 3, @@ -219,6 +227,7 @@ export const mockGroups = [ project_count: 1, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, { id: 2, @@ -239,6 +248,7 @@ export const mockGroups = [ project_count: 4, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', }, ]; @@ -262,6 +272,7 @@ export const mockSearchedGroups = [ project_count: 1, subgroup_count: 2, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 57, @@ -282,6 +293,7 @@ export const mockSearchedGroups = [ project_count: 4, subgroup_count: 2, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 60, @@ -302,6 +314,7 @@ export const mockSearchedGroups = [ project_count: 0, subgroup_count: 1, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 61, @@ -322,6 +335,7 @@ export const mockSearchedGroups = [ project_count: 2, subgroup_count: 0, can_leave: false, + updated_at: '2017-04-09T18:40:39.101Z', children: [ { id: 17, @@ -336,6 +350,7 @@ export const mockSearchedGroups = [ permission: null, edit_path: '/platform/hardware/bsp/kernel/common/v4.4/edit', star_count: 0, + updated_at: '2017-09-12T06:37:04.925Z', }, { id: 16, @@ -350,6 +365,7 @@ export const mockSearchedGroups = [ permission: null, edit_path: '/platform/hardware/bsp/kernel/common/v4.1/edit', star_count: 0, + updated_at: '2017-04-09T18:41:03.112Z', }, ], }, diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js index d23b558f4ea..1bf97bbf093 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js @@ -4,13 +4,16 @@ import closedComponent from '~/vue_merge_request_widget/components/states/mr_wid const mr = { targetBranch: 'good-branch', targetBranchPath: '/good-branch', - closedEvent: { - author: { + metrics: { + mergedBy: {}, + mergedAt: 'mergedUpdatedAt', + closedBy: { name: 'Fatih Acet', username: 'fatihacet', }, - updatedAt: 'closedEventUpdatedAt', - formattedUpdatedAt: '', + closedAt: 'closedEventUpdatedAt', + readableMergedAt: '', + readableClosedAt: '', }, updatedAt: 'mrUpdatedAt', closedAt: '1 day ago', @@ -56,7 +59,7 @@ describe('MRWidgetClosed', () => { it('should have correct elements', () => { expect(el.querySelector('h4').textContent).toContain('Closed by'); - expect(el.querySelector('h4').textContent).toContain(mr.closedEvent.author.name); + expect(el.querySelector('h4').textContent).toContain(mr.metrics.closedBy.name); expect(el.textContent).toContain('The changes were not merged into'); expect(el.querySelector('.label-branch').getAttribute('href')).toEqual(mr.targetBranchPath); expect(el.querySelector('.label-branch').textContent).toContain(mr.targetBranch); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 9d3ae4b448f..2dc3b72ea40 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -14,10 +14,13 @@ const createComponent = () => { canRevertInCurrentMR: true, canRemoveSourceBranch: true, sourceBranchRemoved: true, - mergedEvent: { - author: {}, - updatedAt: 'mergedUpdatedAt', - formattedUpdatedAt: '', + metrics: { + mergedBy: {}, + mergedAt: 'mergedUpdatedAt', + readableMergedAt: '', + closedBy: {}, + closedAt: 'mergedUpdatedAt', + readableClosedAt: '', }, updatedAt: 'mrUpdatedAt', targetBranch, diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index 1ad7c2d8efa..ca29c9fee32 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -33,8 +33,8 @@ export default { "source_project_id": 19, "target_branch": "master", "target_project_id": 19, - "merge_event": { - "author": { + "metrics": { + "merged_by": { "name": "Administrator", "username": "root", "id": 1, @@ -42,9 +42,10 @@ export default { "avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon", "web_url": "http://localhost:3000/root" }, - "updated_at": "2017-04-07T15:39:25.696Z" + "merged_at": "2017-04-07T15:39:25.696Z", + "closed_by": null, + "closed_at": null }, - "closed_event": null, "author": { "name": "Administrator", "username": "root", diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js new file mode 100644 index 00000000000..70ce3dffaba --- /dev/null +++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js @@ -0,0 +1,59 @@ +import Vue from 'vue'; +import panelResizer from '~/vue_shared/components/panel_resizer.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Panel Resizer component', () => { + let vm; + let PanelResizer; + + const triggerEvent = (eventName, el = vm.$el, clientX = 0) => { + const event = document.createEvent('MouseEvents'); + event.initMouseEvent(eventName, true, true, window, 1, clientX, 0, clientX, 0, false, false, + false, false, 0, null); + + el.dispatchEvent(event); + }; + + beforeEach(() => { + PanelResizer = Vue.extend(panelResizer); + }); + + afterEach(() => { + vm.$destroy(); + }); + + it('should render a div element with the correct classes and styles', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'left', + }); + + expect(vm.$el.tagName).toEqual('DIV'); + expect(vm.$el.getAttribute('class')).toBe('dragHandle dragleft'); + expect(vm.$el.getAttribute('style')).toBe('cursor: ew-resize;'); + }); + + it('should render a div element with the correct classes for a right side panel', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'right', + }); + + expect(vm.$el.tagName).toEqual('DIV'); + expect(vm.$el.getAttribute('class')).toBe('dragHandle dragright'); + }); + + it('drag the resizer', () => { + vm = mountComponent(PanelResizer, { + startSize: 100, + side: 'left', + }); + + spyOn(vm, '$emit'); + triggerEvent('mousedown', vm.$el); + triggerEvent('mousemove', document); + triggerEvent('mouseup', document); + expect(vm.$emit.calls.allArgs()).toEqual([['resize-start', 100], ['update:size', 100], ['resize-end', 100]]); + expect(vm.size).toBe(100); + }); +}); diff --git a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb index 5c471cbdeda..9bae7e53b71 100644 --- a/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb +++ b/spec/lib/gitlab/background_migration/delete_conflicting_redirect_routes_range_spec.rb @@ -24,17 +24,12 @@ describe Gitlab::BackgroundMigration::DeleteConflictingRedirectRoutesRange, :mig redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') end - it 'deletes the conflicting redirect_routes in the range' do + # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252 + it 'NO-OP: does not delete any redirect_routes' do expect(redirect_routes.count).to eq(8) - expect do - described_class.new.perform(1, 3) - end.to change { redirect_routes.where("path like 'foo%'").count }.from(5).to(2) + described_class.new.perform(1, 5) - expect do - described_class.new.perform(4, 5) - end.to change { redirect_routes.where("path like 'foo%'").count }.from(2).to(0) - - expect(redirect_routes.count).to eq(3) + expect(redirect_routes.count).to eq(8) end end diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb index 7351d45336a..5432d270555 100644 --- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb +++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb @@ -281,6 +281,17 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati migration.process_event(event) end + + it 'handles an error gracefully' do + event1 = create_push_event(project, author, { commits: [] }) + + expect(migration).to receive(:replicate_event).and_call_original + expect(migration).to receive(:create_push_event_payload).and_raise(ActiveRecord::InvalidForeignKey, 'invalid foreign key') + + migration.process_event(event1) + + expect(described_class::EventForMigration.all.count).to eq(0) + end end describe '#replicate_event' do @@ -335,9 +346,8 @@ describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migrati it 'does not create push event payloads for removed events' do allow(event).to receive(:id).and_return(-1) - payload = migration.create_push_event_payload(event) + expect { migration.create_push_event_payload(event) }.to raise_error(ActiveRecord::InvalidForeignKey) - expect(payload).to be_nil expect(PushEventPayload.count).to eq(0) end diff --git a/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb new file mode 100644 index 00000000000..dfe3b31f1c0 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_merge_request_metrics_with_events_data_spec.rb @@ -0,0 +1,124 @@ +require 'rails_helper' + +describe Gitlab::BackgroundMigration::PopulateMergeRequestMetricsWithEventsData, :migration, schema: 20171128214150 do + describe '#perform' do + let(:mr_with_event) { create(:merge_request) } + let!(:merged_event) { create(:event, :merged, target: mr_with_event) } + let!(:closed_event) { create(:event, :closed, target: mr_with_event) } + + before do + # Make sure no metrics are created and kept through after_* callbacks. + mr_with_event.metrics.destroy! + end + + it 'inserts metrics and updates closed and merged events' do + subject.perform(mr_with_event.id, mr_with_event.id) + + mr_with_event.reload + + expect(mr_with_event.metrics).to have_attributes(latest_closed_by_id: closed_event.author_id, + merged_by_id: merged_event.author_id) + expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(closed_event.updated_at.to_s) + end + end + + describe '#insert_metrics_for_range' do + let!(:mrs_without_metrics) { create_list(:merge_request, 3) } + let!(:mrs_with_metrics) { create_list(:merge_request, 2) } + + before do + # Make sure no metrics are created and kept through after_* callbacks. + mrs_without_metrics.each { |m| m.metrics.destroy! } + end + + it 'inserts merge_request_metrics for merge_requests without one' do + expect { subject.insert_metrics_for_range(MergeRequest.first.id, MergeRequest.last.id) } + .to change(MergeRequest::Metrics, :count).from(2).to(5) + + mrs_without_metrics.each do |mr_without_metrics| + expect(mr_without_metrics.reload.metrics).to be_present + end + end + + it 'does not inserts merge_request_metrics for MRs out of given range' do + expect { subject.insert_metrics_for_range(mrs_with_metrics.first.id, mrs_with_metrics.last.id) } + .not_to change(MergeRequest::Metrics, :count).from(2) + end + end + + describe '#update_metrics_with_events_data' do + context 'closed events data update' do + let(:users) { create_list(:user, 3) } + let(:mrs_with_event) { create_list(:merge_request, 3) } + + before do + create_list(:event, 2, :closed, author: users.first, target: mrs_with_event.first) + create_list(:event, 3, :closed, author: users.second, target: mrs_with_event.second) + create(:event, :closed, author: users.third, target: mrs_with_event.third) + end + + it 'migrates multiple MR metrics with closed event data' do + mr_without_event = create(:merge_request) + create(:event, :merged) + + subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id) + + mrs_with_event.each do |mr_with_event| + latest_event = Event.where(action: 3, target: mr_with_event).last + + mr_with_event.metrics.reload + + expect(mr_with_event.metrics.latest_closed_by).to eq(latest_event.author) + expect(mr_with_event.metrics.latest_closed_at.to_s).to eq(latest_event.updated_at.to_s) + end + + expect(mr_without_event.metrics.reload).to have_attributes(latest_closed_by_id: nil, + latest_closed_at: nil) + end + + it 'does not updates metrics out of given range' do + out_of_range_mr = create(:merge_request) + create(:event, :closed, author: users.last, target: out_of_range_mr) + + expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) } + .not_to change { out_of_range_mr.metrics.reload.merged_by } + .from(nil) + end + end + + context 'merged events data update' do + let(:users) { create_list(:user, 3) } + let(:mrs_with_event) { create_list(:merge_request, 3) } + + before do + create_list(:event, 2, :merged, author: users.first, target: mrs_with_event.first) + create_list(:event, 3, :merged, author: users.second, target: mrs_with_event.second) + create(:event, :merged, author: users.third, target: mrs_with_event.third) + end + + it 'migrates multiple MR metrics with merged event data' do + mr_without_event = create(:merge_request) + create(:event, :merged) + + subject.update_metrics_with_events_data(mrs_with_event.first.id, mrs_with_event.last.id) + + mrs_with_event.each do |mr_with_event| + latest_event = Event.where(action: Event::MERGED, target: mr_with_event).last + + expect(mr_with_event.metrics.reload.merged_by).to eq(latest_event.author) + end + + expect(mr_without_event.metrics.reload).to have_attributes(merged_by_id: nil) + end + + it 'does not updates metrics out of given range' do + out_of_range_mr = create(:merge_request) + create(:event, :merged, author: users.last, target: out_of_range_mr) + + expect { subject.perform(mrs_with_event.first.id, mrs_with_event.second.id) } + .not_to change { out_of_range_mr.metrics.reload.merged_by } + .from(nil) + end + end + end +end diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 664ba0f7234..7727a1d81b1 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -902,7 +902,7 @@ describe Gitlab::Database::MigrationHelpers do describe '#check_trigger_permissions!' do it 'does nothing when the user has the correct permissions' do expect { model.check_trigger_permissions!('users') } - .not_to raise_error(RuntimeError) + .not_to raise_error end it 'raises RuntimeError when the user does not have the correct permissions' do @@ -1036,4 +1036,93 @@ describe Gitlab::Database::MigrationHelpers do end end end + + describe '#change_column_type_using_background_migration' do + let!(:issue) { create(:issue) } + + let(:issue_model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'issues' + include EachBatch + end + end + + it 'changes the type of a column using a background migration' do + expect(model) + .to receive(:add_column) + .with('issues', 'closed_at_for_type_change', :datetime_with_timezone) + + expect(model) + .to receive(:install_rename_triggers) + .with('issues', :closed_at, 'closed_at_for_type_change') + + expect(BackgroundMigrationWorker) + .to receive(:perform_in) + .ordered + .with( + 10.minutes, + 'CopyColumn', + ['issues', :closed_at, 'closed_at_for_type_change', issue.id, issue.id] + ) + + expect(BackgroundMigrationWorker) + .to receive(:perform_in) + .ordered + .with( + 1.hour + 10.minutes, + 'CleanupConcurrentTypeChange', + ['issues', :closed_at, 'closed_at_for_type_change'] + ) + + expect(Gitlab::BackgroundMigration) + .to receive(:steal) + .ordered + .with('CopyColumn') + + expect(Gitlab::BackgroundMigration) + .to receive(:steal) + .ordered + .with('CleanupConcurrentTypeChange') + + model.change_column_type_using_background_migration( + issue_model.all, + :closed_at, + :datetime_with_timezone + ) + end + end + + describe '#perform_background_migration_inline?' do + it 'returns true in a test environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(true) + + expect(model.perform_background_migration_inline?).to eq(true) + end + + it 'returns true in a development environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(false) + + allow(Rails.env) + .to receive(:development?) + .and_return(true) + + expect(model.perform_background_migration_inline?).to eq(true) + end + + it 'returns false in a production environment' do + allow(Rails.env) + .to receive(:test?) + .and_return(false) + + allow(Rails.env) + .to receive(:development?) + .and_return(false) + + expect(model.perform_background_migration_inline?).to eq(false) + end + end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 7c2c3584788..faccc2c8e00 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1016,7 +1016,7 @@ describe Gitlab::Git::Repository, seed_helper: true do shared_examples 'extended commit counting' do context 'with after timestamp' do it 'returns the number of commits after timestamp' do - options = { ref: 'master', limit: nil, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + options = { ref: 'master', after: Time.iso8601('2013-03-03T20:15:01+00:00') } expect(repository.count_commits(options)).to eq(25) end @@ -1024,7 +1024,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with before timestamp' do it 'returns the number of commits before timestamp' do - options = { ref: 'feature', limit: nil, before: Time.iso8601('2015-03-03T20:15:01+00:00') } + options = { ref: 'feature', before: Time.iso8601('2015-03-03T20:15:01+00:00') } expect(repository.count_commits(options)).to eq(9) end @@ -1032,11 +1032,19 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'with path' do it 'returns the number of commits with path ' do - options = { ref: 'master', limit: nil, path: "encoding" } + options = { ref: 'master', path: "encoding" } expect(repository.count_commits(options)).to eq(2) end end + + context 'with max_count' do + it 'returns the number of commits up to the passed limit' do + options = { ref: 'master', max_count: 10, after: Time.iso8601('2013-03-03T20:15:01+00:00') } + + expect(repository.count_commits(options)).to eq(10) + end + end end context 'when Gitaly count_commits feature is enabled' do @@ -1720,6 +1728,20 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(result.repo_created).to eq(false) expect(result.branch_created).to eq(false) end + + it 'returns nil if there was a concurrent branch update' do + concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + result = repository.merge(user, source_sha, target_branch, 'Test merge') do + # This ref update should make the merge fail + repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) + end + + # This 'nil' signals that the merge was not applied + expect(result).to be_nil + + # Our concurrent ref update should not have been undone + expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id) + end end context 'with gitaly' do diff --git a/spec/migrations/delete_conflicting_redirect_routes_spec.rb b/spec/migrations/delete_conflicting_redirect_routes_spec.rb index 1df2477da51..8a191bd7139 100644 --- a/spec/migrations/delete_conflicting_redirect_routes_spec.rb +++ b/spec/migrations/delete_conflicting_redirect_routes_spec.rb @@ -10,9 +10,6 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do end before do - stub_const("DeleteConflictingRedirectRoutes::BATCH_SIZE", 2) - stub_const("Gitlab::Database::MigrationHelpers::BACKGROUND_MIGRATION_JOB_BUFFER_SIZE", 2) - routes.create!(id: 1, source_id: 1, source_type: 'Namespace', path: 'foo1') routes.create!(id: 2, source_id: 2, source_type: 'Namespace', path: 'foo2') routes.create!(id: 3, source_id: 3, source_type: 'Namespace', path: 'foo3') @@ -32,27 +29,14 @@ describe DeleteConflictingRedirectRoutes, :migration, :sidekiq do redirect_routes.create!(source_id: 1, source_type: 'Namespace', path: 'foo5') end - it 'correctly schedules background migrations' do + # No-op. See https://gitlab.com/gitlab-com/infrastructure/issues/3460#note_53223252 + it 'NO-OP: does not schedule any background migrations' do Sidekiq::Testing.fake! do Timecop.freeze do migrate! - expect(BackgroundMigrationWorker.jobs[0]['args']).to eq([described_class::MIGRATION, [1, 2]]) - expect(BackgroundMigrationWorker.jobs[0]['at']).to eq(12.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs[1]['args']).to eq([described_class::MIGRATION, [3, 4]]) - expect(BackgroundMigrationWorker.jobs[1]['at']).to eq(24.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs[2]['args']).to eq([described_class::MIGRATION, [5, 5]]) - expect(BackgroundMigrationWorker.jobs[2]['at']).to eq(36.seconds.from_now.to_f) - expect(BackgroundMigrationWorker.jobs.size).to eq 3 + expect(BackgroundMigrationWorker.jobs.size).to eq 0 end end end - - it 'schedules background migrations' do - Sidekiq::Testing.inline! do - expect do - migrate! - end.to change { redirect_routes.count }.from(8).to(3) - end - end end diff --git a/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb new file mode 100644 index 00000000000..97e089c5cb8 --- /dev/null +++ b/spec/migrations/schedule_populate_merge_request_metrics_with_events_data_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20171128214150_schedule_populate_merge_request_metrics_with_events_data.rb') + +describe SchedulePopulateMergeRequestMetricsWithEventsData, :migration, :sidekiq do + let!(:mrs) { create_list(:merge_request, 3) } + + it 'correctly schedules background migrations' do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(described_class::MIGRATION) + .to be_scheduled_migration(10.minutes, mrs.first.id, mrs.second.id) + + expect(described_class::MIGRATION) + .to be_scheduled_migration(20.minutes, mrs.third.id, mrs.third.id) + + expect(BackgroundMigrationWorker.jobs.size).to eq(2) + end + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c9deeb45c1a..5ced000cdb6 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -23,6 +23,32 @@ describe Issue do it { is_expected.to have_db_index(:deleted_at) } end + describe 'callbacks' do + describe '#ensure_metrics' do + it 'creates metrics after saving' do + issue = create(:issue) + + expect(issue.metrics).to be_persisted + expect(Issue::Metrics.count).to eq(1) + end + + it 'does not create duplicate metrics for an issue' do + issue = create(:issue) + + issue.close! + + expect(issue.metrics).to be_persisted + expect(Issue::Metrics.count).to eq(1) + end + + it 'records current metrics' do + expect_any_instance_of(Issue::Metrics).to receive(:record!) + + create(:issue) + end + end + end + describe '#order_by_position_and_priority' do let(:project) { create :project } let(:p1) { create(:label, title: 'P1', project: project, priority: 1) } diff --git a/spec/models/merge_request/metrics_spec.rb b/spec/models/merge_request/metrics_spec.rb index 9353d5c3c8a..02ff7839739 100644 --- a/spec/models/merge_request/metrics_spec.rb +++ b/spec/models/merge_request/metrics_spec.rb @@ -1,16 +1,11 @@ require 'spec_helper' describe MergeRequest::Metrics do - subject { create(:merge_request) } + subject { described_class.new } - describe "when recording the default set of metrics on merge request save" do - it "records the merge time" do - time = Time.now - Timecop.freeze(time) { subject.mark_as_merged } - metrics = subject.metrics - - expect(metrics).to be_present - expect(metrics.merged_at).to be_like_time(time) - end + describe 'associations' do + it { is_expected.to belong_to(:merge_request) } + it { is_expected.to belong_to(:latest_closed_by).class_name('User') } + it { is_expected.to belong_to(:merged_by).class_name('User') } end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index df94617a19e..d8ebd46faab 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -65,6 +65,25 @@ describe MergeRequest do end end + describe 'callbacks' do + describe '#ensure_merge_request_metrics' do + it 'creates metrics after saving' do + merge_request = create(:merge_request) + + expect(merge_request.metrics).to be_persisted + expect(MergeRequest::Metrics.count).to eq(1) + end + + it 'does not duplicate metrics for a merge request' do + merge_request = create(:merge_request) + + merge_request.mark_as_merged! + + expect(MergeRequest::Metrics.count).to eq(1) + end + end + end + describe 'respond to' do it { is_expected.to respond_to(:unchecked?) } it { is_expected.to respond_to(:can_be_merged?) } diff --git a/spec/models/project_services/kubernetes_service_spec.rb b/spec/models/project_services/kubernetes_service_spec.rb index f037ee77a94..6980ba335b8 100644 --- a/spec/models/project_services/kubernetes_service_spec.rb +++ b/spec/models/project_services/kubernetes_service_spec.rb @@ -52,12 +52,75 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do context 'when service is inactive' do before do + subject.project = project subject.active = false end it { is_expected.not_to validate_presence_of(:api_url) } it { is_expected.not_to validate_presence_of(:token) } end + + context 'with a deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + before do + kubernetes_service.update_attribute(:active, false) + kubernetes_service.properties[:namespace] = "foo" + end + + it 'should not update attributes' do + expect(kubernetes_service.save).to be_falsy + end + + it 'should include an error with a deprecation message' do + kubernetes_service.valid? + expect(kubernetes_service.errors[:base].first).to match(/Kubernetes service integration has been deprecated/) + end + end + + context 'with a non-deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + it 'should update attributes' do + kubernetes_service.properties[:namespace] = 'foo' + expect(kubernetes_service.save).to be_truthy + end + end + + context 'with an active and deprecated service' do + let(:kubernetes_service) { create(:kubernetes_service) } + + before do + kubernetes_service.active = false + kubernetes_service.properties[:namespace] = 'foo' + kubernetes_service.save + end + + it 'should deactive the service' do + expect(kubernetes_service.active?).to be_falsy + end + + it 'should not include a deprecation message as error' do + expect(kubernetes_service.errors.messages.count).to eq(0) + end + + it 'should update attributes' do + expect(kubernetes_service.properties[:namespace]).to eq("foo") + end + end + + context 'with a template service' do + let(:kubernetes_service) { create(:kubernetes_service, template: true, active: false) } + + before do + kubernetes_service.properties[:namespace] = 'foo' + end + + it 'should update attributes' do + expect(kubernetes_service.save).to be_truthy + expect(kubernetes_service.properties[:namespace]).to eq('foo') + end + end end describe '#initialize_properties' do @@ -318,4 +381,42 @@ describe KubernetesService, :use_clean_rails_memory_store_caching do it { is_expected.to eq(pods: []) } end end + + describe "#deprecated?" do + let(:kubernetes_service) { create(:kubernetes_service) } + + context 'with an active kubernetes service' do + it 'should return false' do + expect(kubernetes_service.deprecated?).to be_falsy + end + end + + context 'with a inactive kubernetes service' do + it 'should return true' do + kubernetes_service.update_attribute(:active, false) + expect(kubernetes_service.deprecated?).to be_truthy + end + end + end + + describe "#deprecation_message" do + let(:kubernetes_service) { create(:kubernetes_service) } + + it 'should indicate the service is deprecated' do + expect(kubernetes_service.deprecation_message).to match(/Kubernetes service integration has been deprecated/) + end + + context 'if the services is active' do + it 'should return a message' do + expect(kubernetes_service.deprecation_message).to match(/Your cluster information on this page is still editable/) + end + end + + context 'if the service is not active' do + it 'should return a message' do + kubernetes_service.update_attribute(:active, false) + expect(kubernetes_service.deprecation_message).to match(/Fields on this page are now uneditable/) + end + end + end end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0f2f906c667..540615de117 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -254,4 +254,22 @@ describe Service do end end end + + describe "#deprecated?" do + let(:project) { create(:project, :repository) } + + it 'should return false by default' do + service = create(:service, project: project) + expect(service.deprecated?).to be_falsy + end + end + + describe "#deprecation_message" do + let(:project) { create(:project, :repository) } + + it 'should be empty by default' do + service = create(:service, project: project) + expect(service.deprecation_message).to be_nil + end + end end diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb index be8d9c19125..981c9c27325 100644 --- a/spec/requests/api/notes_spec.rb +++ b/spec/requests/api/notes_spec.rb @@ -464,7 +464,7 @@ describe API::Notes do describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) post api("/projects/#{project.id}/issues/#{issue.iid}/notes", user), body: 'hi!' end diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb index 6fe8ab5a3f6..08ea7314bb3 100644 --- a/spec/requests/api/project_milestones_spec.rb +++ b/spec/requests/api/project_milestones_spec.rb @@ -16,7 +16,7 @@ describe API::ProjectMilestones do describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) put api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb index ceafa0e2058..26d56c04862 100644 --- a/spec/requests/api/services_spec.rb +++ b/spec/requests/api/services_spec.rb @@ -53,6 +53,10 @@ describe API::Services do describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service + before do + initialize_service(service) + end + it "deletes #{service}" do delete api("/projects/#{project.id}/services/#{dashed_service}", user) @@ -67,9 +71,7 @@ describe API::Services do # inject some properties into the service before do - service_object = project.find_or_initialize_service(service) - service_object.properties = service_attrs - service_object.save + initialize_service(service) end it 'returns authentication error when unauthenticated' do diff --git a/spec/requests/api/v3/milestones_spec.rb b/spec/requests/api/v3/milestones_spec.rb index 9ee71ea9c5d..6021600e09c 100644 --- a/spec/requests/api/v3/milestones_spec.rb +++ b/spec/requests/api/v3/milestones_spec.rb @@ -161,7 +161,7 @@ describe API::V3::Milestones do describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do it 'creates an activity event when an milestone is closed' do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) put v3_api("/projects/#{project.id}/milestones/#{milestone.id}", user), state_event: 'close' diff --git a/spec/requests/api/v3/notes_spec.rb b/spec/requests/api/v3/notes_spec.rb index 6428d9daaba..5532795ab02 100644 --- a/spec/requests/api/v3/notes_spec.rb +++ b/spec/requests/api/v3/notes_spec.rb @@ -302,7 +302,7 @@ describe API::V3::Notes do describe "POST /projects/:id/noteable/:noteable_id/notes to test observer on create" do it "creates an activity event when an issue note is created" do - expect(Event).to receive(:create) + expect(Event).to receive(:create!) post v3_api("/projects/#{project.id}/issues/#{issue.id}/notes", user), body: 'hi!' end diff --git a/spec/requests/api/v3/services_spec.rb b/spec/requests/api/v3/services_spec.rb index 8f212ab6be6..c69a7d58ca6 100644 --- a/spec/requests/api/v3/services_spec.rb +++ b/spec/requests/api/v3/services_spec.rb @@ -10,6 +10,10 @@ describe API::V3::Services do describe "DELETE /projects/:id/services/#{service.dasherize}" do include_context service + before do + initialize_service(service) + end + it "deletes #{service}" do delete v3_api("/projects/#{project.id}/services/#{dashed_service}", user) diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 65bd001e491..fb0806ff9f1 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -12,6 +12,8 @@ require 'spec_helper' describe API::Wikis do let(:user) { create(:user) } + let(:group) { create(:group).tap { |g| g.add_owner(user) } } + let(:project_wiki) { create(:project_wiki, project: project, user: user) } let(:payload) { { content: 'content', format: 'rdoc', title: 'title' } } let(:expected_keys_with_content) { %w(content format slug title) } let(:expected_keys_without_content) { %w(format slug title) } @@ -19,8 +21,8 @@ describe API::Wikis do shared_examples_for 'returns list of wiki pages' do context 'when wiki has pages' do let!(:pages) do - [create(:wiki_page, wiki: project.wiki, attrs: { title: 'page1', content: 'content of page1' }), - create(:wiki_page, wiki: project.wiki, attrs: { title: 'page2', content: 'content of page2' })] + [create(:wiki_page, wiki: project_wiki, attrs: { title: 'page1', content: 'content of page1' }), + create(:wiki_page, wiki: project_wiki, attrs: { title: 'page2', content: 'content of page2' })] end it 'returns the list of wiki pages without content' do @@ -445,7 +447,7 @@ describe API::Wikis do end describe 'PUT /projects/:id/wikis/:slug' do - let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:page) { create(:wiki_page, wiki: project_wiki) } let(:payload) { { title: 'new title', content: 'new content' } } let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } @@ -568,10 +570,20 @@ describe API::Wikis do end end end + + context 'when wiki belongs to a group project' do + let(:project) { create(:project, namespace: group) } + + before do + put(api(url, user), payload) + end + + include_examples 'updates wiki page' + end end describe 'DELETE /projects/:id/wikis/:slug' do - let(:page) { create(:wiki_page, wiki: project.wiki) } + let(:page) { create(:wiki_page, wiki: project_wiki) } let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" } context 'when wiki is disabled' do @@ -675,5 +687,15 @@ describe API::Wikis do end end end + + context 'when wiki belongs to a group project' do + let(:project) { create(:project, namespace: group) } + + before do + delete(api(url, user)) + end + + include_examples '204 No Content' + end end end diff --git a/spec/serializers/event_entity_spec.rb b/spec/serializers/event_entity_spec.rb deleted file mode 100644 index bb54597c967..00000000000 --- a/spec/serializers/event_entity_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe EventEntity do - subject { described_class.represent(create(:event)).as_json } - - it 'exposes author' do - expect(subject).to include(:author) - end - - it 'exposes core elements of event' do - expect(subject).to include(:updated_at) - end -end diff --git a/spec/serializers/merge_request_widget_entity_spec.rb b/spec/serializers/merge_request_widget_entity_spec.rb index a5924a8589c..e25552eb0d8 100644 --- a/spec/serializers/merge_request_widget_entity_spec.rb +++ b/spec/serializers/merge_request_widget_entity_spec.rb @@ -35,6 +35,81 @@ describe MergeRequestWidgetEntity do end end + describe 'metrics' do + context 'when metrics record exists with merged data' do + before do + resource.mark_as_merged! + resource.metrics.update!(merged_by: user) + end + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from metrics record' do + expect(subject.dig(:metrics, :merged_by, :id)) + .to eq(resource.metrics.merged_by_id) + end + end + + context 'when metrics record exists with closed data' do + before do + resource.close! + resource.metrics.update!(latest_closed_by: user) + end + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from metrics record' do + expect(subject.dig(:metrics, :closed_by, :id)) + .to eq(resource.metrics.latest_closed_by_id) + end + end + + context 'when metrics does not exists' do + before do + resource.mark_as_merged! + resource.metrics.destroy! + resource.reload + end + + context 'when events exists' do + let!(:closed_event) { create(:event, :closed, project: project, target: resource) } + let!(:merge_event) { create(:event, :merged, project: project, target: resource) } + + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + + it 'returns values from events record' do + expect(subject.dig(:metrics, :merged_by, :id)) + .to eq(merge_event.author_id) + + expect(subject.dig(:metrics, :closed_by, :id)) + .to eq(closed_event.author_id) + + expect(subject.dig(:metrics, :merged_at).to_s) + .to eq(merge_event.updated_at.to_s) + + expect(subject.dig(:metrics, :closed_at).to_s) + .to eq(closed_event.updated_at.to_s) + end + end + + context 'when events does not exists' do + it 'matches merge request metrics schema' do + expect(subject[:metrics].with_indifferent_access) + .to match_schema('entities/merge_request_metrics') + end + end + end + end + it 'has email_patches_path' do expect(subject[:email_patches_path]) .to eq("/#{resource.project.full_path}/merge_requests/#{resource.iid}.patch") diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index 08267d6e6a0..b9bfbb11511 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -266,7 +266,7 @@ describe CreateDeploymentService do context "while updating the 'first_deployed_to_production_at' time" do before do - merge_request.mark_as_merged + merge_request.metrics.update!(merged_at: Time.now) end context "for merge requests merged before the current deploy" do diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index 2a59bc4594a..4d12de3ecce 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -52,6 +52,19 @@ describe MergeRequests::CloseService do end end + it 'updates metrics' do + metrics = merge_request.metrics + metrics_service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(metrics_service) + + expect(metrics_service).to receive(:close) + + described_class.new(project, user, {}).execute(merge_request) + end + it 'refreshes the number of open merge requests for a valid MR', :use_clean_rails_memory_store_caching do service = described_class.new(project, user, {}) diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 8f2c5df5907..70957431942 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -22,5 +22,18 @@ describe MergeRequests::PostMergeService do expect { service.execute(merge_request) } .to change { project.open_merge_requests_count }.from(1).to(0) end + + it 'updates metrics' do + metrics = merge_request.metrics + metrics_service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(metrics_service) + + expect(metrics_service).to receive(:merge) + + described_class.new(project, user, {}).execute(merge_request) + end end end diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index 94f31ff139c..a44d63e5f9f 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -47,6 +47,19 @@ describe MergeRequests::ReopenService do end end + it 'updates metrics' do + metrics = merge_request.metrics + service = double(MergeRequestMetricsService) + allow(MergeRequestMetricsService) + .to receive(:new) + .with(metrics) + .and_return(service) + + expect(service).to receive(:reopen) + + described_class.new(project, user, {}).execute(merge_request) + end + it 'refreshes the number of open merge requests for a valid MR' do service = described_class.new(project, user, {}) diff --git a/spec/services/update_merge_request_metrics_service_spec.rb b/spec/services/update_merge_request_metrics_service_spec.rb new file mode 100644 index 00000000000..b5fb999381d --- /dev/null +++ b/spec/services/update_merge_request_metrics_service_spec.rb @@ -0,0 +1,42 @@ +require 'rails_helper' + +describe MergeRequestMetricsService do + let(:metrics) { create(:merge_request).metrics } + + describe '#merge' do + it 'updates metrics' do + user = create(:user) + service = described_class.new(metrics) + event = double(Event, author_id: user.id, created_at: Time.now) + + service.merge(event) + + expect(metrics.merged_by).to eq(user) + expect(metrics.merged_at).to eq(event.created_at) + end + end + + describe '#close' do + it 'updates metrics' do + user = create(:user) + service = described_class.new(metrics) + event = double(Event, author_id: user.id, created_at: Time.now) + + service.close(event) + + expect(metrics.latest_closed_by).to eq(user) + expect(metrics.latest_closed_at).to eq(event.created_at) + end + end + + describe '#reopen' do + it 'updates metrics' do + service = described_class.new(metrics) + + service.reopen + + expect(metrics.latest_closed_by).to be_nil + expect(metrics.latest_closed_at).to be_nil + end + end +end diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb index 7457484a932..3f1fd169b72 100644 --- a/spec/support/services_shared_context.rb +++ b/spec/support/services_shared_context.rb @@ -29,5 +29,13 @@ Service.available_services_names.each do |service| end end end + + def initialize_service(service) + service_item = project.find_or_initialize_service(service) + service_item.properties = service_attrs + service_item.active = true if service == "kubernetes" + service_item.save + service_item + end end end |