diff options
19 files changed, 530 insertions, 546 deletions
diff --git a/app/models/project.rb b/app/models/project.rb index 16317b5827a..245d29554a3 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1966,27 +1966,6 @@ class Project < ApplicationRecord (auto_devops || build_auto_devops)&.predefined_variables end - def append_or_update_attribute(name, value) - if Project.reflect_on_association(name).try(:macro) == :has_many - # if this is 1-to-N relation, update the parent object - value.each do |item| - item.update!( - Project.reflect_on_association(name).foreign_key => id) - end - - # force to drop relation cache - public_send(name).reset # rubocop:disable GitlabSecurity/PublicSend - - # succeeded - true - else - # if this is another relation or attribute, update just object - update_attribute(name, value) - end - rescue ActiveRecord::RecordInvalid => e - raise e, "Failed to set #{name}: #{e.message}" - end - # Tries to set repository as read_only, checking for existing Git transfers in progress beforehand # # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb index 9433a231b4a..c401f96b5c1 100644 --- a/lib/gitlab/import_export/project_tree_restorer.rb +++ b/lib/gitlab/import_export/project_tree_restorer.rb @@ -15,7 +15,6 @@ module Gitlab @user = user @shared = shared @project = project - @saved = true end def restore @@ -33,7 +32,8 @@ module Gitlab ActiveRecord::Base.uncached do ActiveRecord::Base.no_touching do update_project_params! - create_relations + create_project_relations! + post_import! end end @@ -69,77 +69,75 @@ module Gitlab # in the DB. The structure and relationships between models are guessed from # the configuration yaml file too. # Finally, it updates each attribute in the newly imported project. - def create_relations - project_relations.each do |relation_key, relation_definition| - relation_key_s = relation_key.to_s - - if relation_definition.present? - create_sub_relations(relation_key_s, relation_definition, @tree_hash) - elsif @tree_hash[relation_key_s].present? - save_relation_hash(relation_key_s, @tree_hash[relation_key_s]) - end - end + def create_project_relations! + project_relations.each(&method( + :process_project_relation!)) + end + def post_import! @project.merge_requests.set_latest_merge_request_diff_ids! - - @saved end - def save_relation_hash(relation_key, relation_hash_batch) - relation_hash = create_relation(relation_key, relation_hash_batch) + def process_project_relation!(relation_key, relation_definition) + data_hashes = @tree_hash.delete(relation_key) + return unless data_hashes - remove_group_models(relation_hash) if relation_hash.is_a?(Array) + # we do not care if we process array or hash + data_hashes = [data_hashes] unless data_hashes.is_a?(Array) + + # consume and remove objects from memory + while data_hash = data_hashes.shift + process_project_relation_item!(relation_key, relation_definition, data_hash) + end + end - @saved = false unless @project.append_or_update_attribute(relation_key, relation_hash) + def process_project_relation_item!(relation_key, relation_definition, data_hash) + relation_object = build_relation(relation_key, relation_definition, data_hash) + return unless relation_object + return if group_model?(relation_object) - save_id_mappings(relation_key, relation_hash_batch, relation_hash) + relation_object.project = @project + relation_object.save! - @project.reset + save_id_mapping(relation_key, data_hash, relation_object) end # Older, serialized CI pipeline exports may only have a # merge_request_id and not the full hash of the merge request. To # import these pipelines, we need to preserve the mapping between # the old and new the merge request ID. - def save_id_mappings(relation_key, relation_hash_batch, relation_hash) + def save_id_mapping(relation_key, data_hash, relation_object) return unless relation_key == 'merge_requests' - relation_hash = Array(relation_hash) - - Array(relation_hash_batch).each_with_index do |raw_data, index| - merge_requests_mapping[raw_data['id']] = relation_hash[index]['id'] - end - end - - # Remove project models that became group models as we found them at group level. - # This no longer required saving them at the root project level. - # For example, in the case of an existing group label that matched the title. - def remove_group_models(relation_hash) - relation_hash.reject! do |value| - GROUP_MODELS.include?(value.class) && value.group_id - end + merge_requests_mapping[data_hash['id']] = relation_object.id end def project_relations - @project_relations ||= reader.attributes_finder.find_relations_tree(:project) + @project_relations ||= + reader + .attributes_finder + .find_relations_tree(:project) + .deep_stringify_keys end def update_project_params! - Gitlab::Timeless.timeless(@project) do - project_params = @tree_hash.reject do |key, value| - project_relations.include?(key.to_sym) - end + project_params = @tree_hash.reject do |key, value| + project_relations.include?(key) + end + + project_params = project_params.merge( + present_project_override_params) - project_params = project_params.merge(present_project_override_params) + # Cleaning all imported and overridden params + project_params = Gitlab::ImportExport::AttributeCleaner.clean( + relation_hash: project_params, + relation_class: Project, + excluded_keys: excluded_keys_for_relation(:project)) - # Cleaning all imported and overridden params - project_params = Gitlab::ImportExport::AttributeCleaner.clean( - relation_hash: project_params, - relation_class: Project, - excluded_keys: excluded_keys_for_relation(:project)) + @project.assign_attributes(project_params) + @project.drop_visibility_level! - @project.assign_attributes(project_params) - @project.drop_visibility_level! + Gitlab::Timeless.timeless(@project) do @project.save! end end @@ -156,73 +154,61 @@ module Gitlab @project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {} end - # Given a relation hash containing one or more models and its relationships, - # loops through each model and each object from a model type and - # and assigns its correspondent attributes hash from +tree_hash+ - # Example: - # +relation_key+ issues, loops through the list of *issues* and for each individual - # issue, finds any subrelations such as notes, creates them and assign them back to the hash - # - # Recursively calls this method if the sub-relation is a hash containing more sub-relations - def create_sub_relations(relation_key, relation_definition, tree_hash, save: true) - return if tree_hash[relation_key].blank? - - tree_array = [tree_hash[relation_key]].flatten - - # Avoid keeping a possible heavy object in memory once we are done with it - while relation_item = tree_array.shift - # The transaction at this level is less speedy than one single transaction - # But we can't have it in the upper level or GC won't get rid of the AR objects - # after we save the batch. - Project.transaction do - process_sub_relation(relation_key, relation_definition, relation_item) - - # For every subrelation that hangs from Project, save the associated records altogether - # This effectively batches all records per subrelation item, only keeping those in memory - # We have to keep in mind that more batch granularity << Memory, but >> Slowness - if save - save_relation_hash(relation_key, [relation_item]) - tree_hash[relation_key].delete(relation_item) - end - end - end - - tree_hash.delete(relation_key) if save + def build_relations(relation_key, relation_definition, data_hashes) + data_hashes.map do |data_hash| + build_relation(relation_key, relation_definition, data_hash) + end.compact end - def process_sub_relation(relation_key, relation_definition, relation_item) - relation_definition.each do |sub_relation_key, sub_relation_definition| - # We just use author to get the user ID, do not attempt to create an instance. - next if sub_relation_key == :author + def build_relation(relation_key, relation_definition, data_hash) + # TODO: This is hack to not create relation for the author + # Rather make `RelationFactory#set_note_author` to take care of that + return data_hash if relation_key == 'author' - sub_relation_key_s = sub_relation_key.to_s + # create relation objects recursively for all sub-objects + relation_definition.each do |sub_relation_key, sub_relation_definition| + transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) + end - # create dependent relations if present - if sub_relation_definition.present? - create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false) + Gitlab::ImportExport::RelationFactory.create( + relation_sym: relation_key.to_sym, + relation_hash: data_hash, + members_mapper: members_mapper, + merge_requests_mapping: merge_requests_mapping, + user: @user, + project: @project, + excluded_keys: excluded_keys_for_relation(relation_key)) + end + + def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) + sub_data_hash = data_hash[sub_relation_key] + return unless sub_data_hash + + # if object is a hash we can create simple object + # as it means that this is 1-to-1 vs 1-to-many + sub_data_hash = + if sub_data_hash.is_a?(Array) + build_relations( + sub_relation_key, + sub_relation_definition, + sub_data_hash).presence + else + build_relation( + sub_relation_key, + sub_relation_definition, + sub_data_hash) end - # transform relation hash to actual object - sub_relation_hash = relation_item[sub_relation_key_s] - if sub_relation_hash.present? - relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash) - end + # persist object(s) or delete from relation + if sub_data_hash + data_hash[sub_relation_key] = sub_data_hash + else + data_hash.delete(sub_relation_key) end end - def create_relation(relation_key, relation_hash_list) - relation_array = [relation_hash_list].flatten.map do |relation_hash| - Gitlab::ImportExport::RelationFactory.create( - relation_sym: relation_key.to_sym, - relation_hash: relation_hash, - members_mapper: members_mapper, - merge_requests_mapping: merge_requests_mapping, - user: @user, - project: @project, - excluded_keys: excluded_keys_for_relation(relation_key)) - end.compact - - relation_hash_list.is_a?(Array) ? relation_array : relation_array.first + def group_model?(relation_object) + GROUP_MODELS.include?(relation_object.class) && relation_object.group_id end def reader diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb index ae8025c52ef..f8f6c3e6107 100644 --- a/lib/gitlab/import_export/relation_factory.rb +++ b/lib/gitlab/import_export/relation_factory.rb @@ -38,10 +38,13 @@ module Gitlab IMPORTED_OBJECT_MAX_RETRIES = 5.freeze - EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request].freeze + EXISTING_OBJECT_CHECK = %i[milestone milestones label labels project_label project_labels group_label group_labels project_feature merge_request ProjectCiCdSetting].freeze TOKEN_RESET_MODELS = %i[Project Namespace Ci::Trigger Ci::Build Ci::Runner ProjectHook].freeze + # This represents all relations that have unique key on `project_id` + UNIQUE_RELATIONS = %i[project_feature ProjectCiCdSetting].freeze + def self.create(*args) new(*args).create end @@ -274,7 +277,7 @@ module Gitlab end def setup_pipeline - @relation_hash.fetch('stages').each do |stage| + @relation_hash.fetch('stages', []).each do |stage| stage.statuses.each do |status| status.pipeline = imported_object end @@ -324,7 +327,8 @@ module Gitlab end def find_or_create_object! - return relation_class.find_or_create_by(project_id: @project.id) if @relation_name == :project_feature + return relation_class.find_or_create_by(project_id: @project.id) if UNIQUE_RELATIONS.include?(@relation_name) + return find_or_create_merge_request! if @relation_name == :merge_request # Can't use IDs as validation exists calling `group` or `project` attributes diff --git a/spec/fixtures/lib/gitlab/import_export/project.json b/spec/fixtures/lib/gitlab/import_export/complex/project.json index 864933ca1b4..86931d66472 100644 --- a/spec/fixtures/lib/gitlab/import_export/project.json +++ b/spec/fixtures/lib/gitlab/import_export/complex/project.json @@ -6680,6 +6680,25 @@ ] } ] + }, + { + "id": 41, + "project_id": 5, + "ref": "master", + "sha": "2ea1f3dec713d940208fb5ce4a38765ecb5d3f73", + "before_sha": null, + "push_data": null, + "created_at": "2016-03-22T15:20:35.763Z", + "updated_at": "2016-03-22T15:20:35.763Z", + "tag": null, + "yaml_errors": null, + "committed_at": null, + "status": "failed", + "started_at": null, + "finished_at": null, + "duration": null, + "stages": [ + ] } ], "triggers": [ diff --git a/spec/fixtures/lib/gitlab/import_export/project.group.json b/spec/fixtures/lib/gitlab/import_export/group/project.json index 47faf271cca..47faf271cca 100644 --- a/spec/fixtures/lib/gitlab/import_export/project.group.json +++ b/spec/fixtures/lib/gitlab/import_export/group/project.json diff --git a/spec/fixtures/lib/gitlab/import_export/project.light.json b/spec/fixtures/lib/gitlab/import_export/light/project.json index 2971ca0f0f8..2971ca0f0f8 100644 --- a/spec/fixtures/lib/gitlab/import_export/project.light.json +++ b/spec/fixtures/lib/gitlab/import_export/light/project.json diff --git a/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json b/spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json index b028147b5eb..b028147b5eb 100644 --- a/spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json +++ b/spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/frontend/vue_shared/components/commit_spec.js index f89627e727b..77d8e00cf00 100644 --- a/spec/javascripts/vue_shared/components/commit_spec.js +++ b/spec/frontend/vue_shared/components/commit_spec.js @@ -1,22 +1,27 @@ -import Vue from 'vue'; -import commitComp from '~/vue_shared/components/commit.vue'; -import mountComponent from '../../helpers/vue_mount_component_helper'; +import { shallowMount } from '@vue/test-utils'; +import CommitComponent from '~/vue_shared/components/commit.vue'; +import Icon from '~/vue_shared/components/icon.vue'; +import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue'; describe('Commit component', () => { let props; - let component; - let CommitComponent; + let wrapper; - beforeEach(() => { - CommitComponent = Vue.extend(commitComp); - }); + const findUserAvatar = () => wrapper.find(UserAvatarLink); + + const createComponent = propsData => { + wrapper = shallowMount(CommitComponent, { + propsData, + sync: false, + }); + }; afterEach(() => { - component.$destroy(); + wrapper.destroy(); }); it('should render a fork icon if it does not represent a tag', () => { - component = mountComponent(CommitComponent, { + createComponent({ tag: false, commitRef: { name: 'master', @@ -34,7 +39,12 @@ describe('Commit component', () => { }, }); - expect(component.$el.querySelector('.icon-container').children).toContain('svg'); + expect( + wrapper + .find('.icon-container') + .find(Icon) + .exists(), + ).toBe(true); }); describe('Given all the props', () => { @@ -56,68 +66,51 @@ describe('Commit component', () => { username: 'jschatz1', }, }; - - component = mountComponent(CommitComponent, props); + createComponent(props); }); it('should render a tag icon if it represents a tag', () => { - expect(component.$el.querySelector('.icon-container svg.ic-tag')).not.toBeNull(); + expect(wrapper.find('icon-stub[name="tag"]').exists()).toBe(true); }); it('should render a link to the ref url', () => { - expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual( - props.commitRef.ref_url, - ); + expect(wrapper.find('.ref-name').attributes('href')).toBe(props.commitRef.ref_url); }); it('should render the ref name', () => { - expect(component.$el.querySelector('.ref-name').textContent).toContain(props.commitRef.name); + expect(wrapper.find('.ref-name').text()).toContain(props.commitRef.name); }); it('should render the commit short sha with a link to the commit url', () => { - expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual( - props.commitUrl, - ); + expect(wrapper.find('.commit-sha').attributes('href')).toEqual(props.commitUrl); - expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha); + expect(wrapper.find('.commit-sha').text()).toContain(props.shortSha); }); it('should render icon for commit', () => { - expect( - component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'), - ).toContain('commit'); + expect(wrapper.find('icon-stub[name="commit"]').exists()).toBe(true); }); describe('Given commit title and author props', () => { it('should render a link to the author profile', () => { - expect( - component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), - ).toEqual(props.author.path); + const userAvatar = findUserAvatar(); + + expect(userAvatar.props('linkHref')).toBe(props.author.path); }); it('Should render the author avatar with title and alt attributes', () => { - expect( - component.$el - .querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip') - .textContent.trim(), - ).toContain(props.author.username); - - expect( - component.$el - .querySelector('.commit-title .avatar-image-container img') - .getAttribute('alt'), - ).toContain(`${props.author.username}'s avatar`); + const userAvatar = findUserAvatar(); + + expect(userAvatar.exists()).toBe(true); + + expect(userAvatar.props('imgAlt')).toBe(`${props.author.username}'s avatar`); }); }); it('should render the commit title', () => { - expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual( - props.commitUrl, - ); + expect(wrapper.find('.commit-row-message').attributes('href')).toEqual(props.commitUrl); - expect(component.$el.querySelector('a.commit-row-message').textContent).toContain( - props.title, - ); + expect(wrapper.find('.commit-row-message').text()).toContain(props.title); }); }); @@ -136,9 +129,9 @@ describe('Commit component', () => { author: {}, }; - component = mountComponent(CommitComponent, props); + createComponent(props); - expect(component.$el.querySelector('.commit-title span').textContent).toContain( + expect(wrapper.find('.commit-title span').text()).toContain( "Can't find HEAD commit for this branch", ); }); @@ -159,16 +152,16 @@ describe('Commit component', () => { author: {}, }; - component = mountComponent(CommitComponent, props); - const refEl = component.$el.querySelector('.ref-name'); + createComponent(props); + const refEl = wrapper.find('.ref-name'); - expect(refEl.textContent).toContain('master'); + expect(refEl.text()).toContain('master'); - expect(refEl.href).toBe(props.commitRef.ref_url); + expect(refEl.attributes('href')).toBe(props.commitRef.ref_url); - expect(refEl.getAttribute('data-original-title')).toBe(props.commitRef.name); + expect(refEl.attributes('data-original-title')).toBe(props.commitRef.name); - expect(component.$el.querySelector('.icon-container .ic-branch')).not.toBeNull(); + expect(wrapper.find('icon-stub[name="branch"]').exists()).toBe(true); }); }); @@ -192,16 +185,16 @@ describe('Commit component', () => { author: {}, }; - component = mountComponent(CommitComponent, props); - const refEl = component.$el.querySelector('.ref-name'); + createComponent(props); + const refEl = wrapper.find('.ref-name'); - expect(refEl.textContent).toContain('1234'); + expect(refEl.text()).toContain('1234'); - expect(refEl.href).toBe(props.mergeRequestRef.path); + expect(refEl.attributes('href')).toBe(props.mergeRequestRef.path); - expect(refEl.getAttribute('data-original-title')).toBe(props.mergeRequestRef.title); + expect(refEl.attributes('data-original-title')).toBe(props.mergeRequestRef.title); - expect(component.$el.querySelector('.icon-container .ic-git-merge')).not.toBeNull(); + expect(wrapper.find('icon-stub[name="git-merge"]').exists()).toBe(true); }); }); @@ -226,9 +219,9 @@ describe('Commit component', () => { showRefInfo: false, }; - component = mountComponent(CommitComponent, props); + createComponent(props); - expect(component.$el.querySelector('.ref-name')).toBeNull(); + expect(wrapper.find('.ref-name').exists()).toBe(false); }); }); }); diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js new file mode 100644 index 00000000000..2f87359a4a6 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js @@ -0,0 +1,108 @@ +import { shallowMount } from '@vue/test-utils'; +import { placeholderImage } from '~/lazy_loader'; +import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; +import defaultAvatarUrl from 'images/no_avatar.png'; + +jest.mock('images/no_avatar.png', () => 'default-avatar-url'); + +const DEFAULT_PROPS = { + size: 99, + imgSrc: 'myavatarurl.com', + imgAlt: 'mydisplayname', + cssClasses: 'myextraavatarclass', + tooltipText: 'tooltip text', + tooltipPlacement: 'bottom', +}; + +describe('User Avatar Image Component', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Initialization', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...DEFAULT_PROPS, + }, + sync: false, + }); + }); + + it('should have <img> as a child element', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.exists()).toBe(true); + expect(imageElement.attributes('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + expect(imageElement.attributes('alt')).toBe(DEFAULT_PROPS.imgAlt); + }); + + it('should properly render img css', () => { + const classes = wrapper.find('img').classes(); + expect(classes).toEqual(expect.arrayContaining(['avatar', 's99', DEFAULT_PROPS.cssClasses])); + expect(classes).not.toContain('lazy'); + }); + }); + + describe('Initialization when lazy', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { + propsData: { + ...DEFAULT_PROPS, + lazy: true, + }, + sync: false, + }); + }); + + it('should add lazy attributes', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.classes()).toContain('lazy'); + expect(imageElement.attributes('src')).toBe(placeholderImage); + expect(imageElement.attributes('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); + }); + }); + + describe('Initialization without src', () => { + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { sync: false }); + }); + + it('should have default avatar image', () => { + const imageElement = wrapper.find('img'); + + expect(imageElement.attributes('src')).toBe(`${defaultAvatarUrl}?width=20`); + }); + }); + + describe('dynamic tooltip content', () => { + const props = DEFAULT_PROPS; + const slots = { + default: ['Action!'], + }; + + beforeEach(() => { + wrapper = shallowMount(UserAvatarImage, { propsData: { props }, slots, sync: false }); + }); + + it('renders the tooltip slot', () => { + expect(wrapper.find('.js-user-avatar-image-toolip').exists()).toBe(true); + }); + + it('renders the tooltip content', () => { + expect(wrapper.find('.js-user-avatar-image-toolip').text()).toContain(slots.default[0]); + }); + + it('does not render tooltip data attributes for on avatar image', () => { + const avatarImg = wrapper.find('img'); + + expect(avatarImg.attributes('data-original-title')).toBeFalsy(); + expect(avatarImg.attributes('data-placement')).not.toBeDefined(); + expect(avatarImg.attributes('data-container')).not.toBeDefined(); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js new file mode 100644 index 00000000000..fc2eb6329b0 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -0,0 +1,186 @@ +import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue'; +import { mount } from '@vue/test-utils'; + +const DEFAULT_PROPS = { + loaded: true, + user: { + username: 'root', + name: 'Administrator', + location: 'Vienna', + bio: null, + organization: null, + status: null, + }, +}; + +describe('User Popover Component', () => { + const fixtureTemplate = 'merge_requests/diff_comment.html'; + preloadFixtures(fixtureTemplate); + + let wrapper; + + beforeEach(() => { + loadFixtures(fixtureTemplate); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('Empty', () => { + beforeEach(() => { + wrapper = mount(UserPopover, { + propsData: { + target: document.querySelector('.js-user-link'), + user: { + name: null, + username: null, + location: null, + bio: null, + organization: null, + status: null, + }, + }, + sync: false, + }); + }); + + it('should return skeleton loaders', () => { + expect(wrapper.findAll('.animation-container').length).toBe(4); + }); + }); + + describe('basic data', () => { + it('should show basic fields', () => { + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name); + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username); + expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location); + }); + + it('shows icon for location', () => { + const iconEl = wrapper.find('.js-location svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('location'); + }); + }); + + describe('job data', () => { + it('should show only bio if no organization is available', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Engineer'; + + wrapper = mount(UserPopover, { + propsData: { + ...testProps, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Engineer'); + }); + + it('should show only organization if no bio is available', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.organization = 'GitLab'; + + wrapper = mount(UserPopover, { + propsData: { + ...testProps, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('GitLab'); + }); + + it('should display bio and organization in separate lines', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Engineer'; + testProps.user.organization = 'GitLab'; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.find('.js-bio').text()).toContain('Engineer'); + expect(wrapper.find('.js-organization').text()).toContain('GitLab'); + }); + + it('should not encode special characters in bio and organization', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.bio = 'Manager & Team Lead'; + testProps.user.organization = 'Me & my <funky> Company'; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead'); + expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company'); + }); + + it('shows icon for bio', () => { + const iconEl = wrapper.find('.js-bio svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('profile'); + }); + + it('shows icon for organization', () => { + const iconEl = wrapper.find('.js-organization svg'); + + expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('work'); + }); + }); + + describe('status data', () => { + it('should show only message', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.status = { message_html: 'Hello World' }; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Hello World'); + }); + + it('should show message and emoji', () => { + const testProps = Object.assign({}, DEFAULT_PROPS); + testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; + + wrapper = mount(UserPopover, { + propsData: { + ...DEFAULT_PROPS, + target: document.querySelector('.js-user-link'), + status: { emoji: 'basketball_player', message_html: 'Hello World' }, + }, + sync: false, + }); + + expect(wrapper.text()).toContain('Hello World'); + expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js deleted file mode 100644 index c5045afc5b0..00000000000 --- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js +++ /dev/null @@ -1,120 +0,0 @@ -import Vue from 'vue'; -import { placeholderImage } from '~/lazy_loader'; -import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue'; -import mountComponent, { mountComponentWithSlots } from 'spec/helpers/vue_mount_component_helper'; -import defaultAvatarUrl from '~/../images/no_avatar.png'; - -const DEFAULT_PROPS = { - size: 99, - imgSrc: 'myavatarurl.com', - imgAlt: 'mydisplayname', - cssClasses: 'myextraavatarclass', - tooltipText: 'tooltip text', - tooltipPlacement: 'bottom', -}; - -describe('User Avatar Image Component', function() { - let vm; - let UserAvatarImage; - - beforeEach(() => { - UserAvatarImage = Vue.extend(userAvatarImage); - }); - - describe('Initialization', function() { - beforeEach(function() { - vm = mountComponent(UserAvatarImage, { - ...DEFAULT_PROPS, - }).$mount(); - }); - - it('should return a defined Vue component', function() { - expect(vm).toBeDefined(); - }); - - it('should have <img> as a child element', function() { - const imageElement = vm.$el.querySelector('img'); - - expect(imageElement).not.toBe(null); - expect(imageElement.getAttribute('src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - expect(imageElement.getAttribute('alt')).toBe(DEFAULT_PROPS.imgAlt); - }); - - it('should properly compute avatarSizeClass', function() { - expect(vm.avatarSizeClass).toBe('s99'); - }); - - it('should properly render img css', function() { - const { classList } = vm.$el.querySelector('img'); - const containsAvatar = classList.contains('avatar'); - const containsSizeClass = classList.contains('s99'); - const containsCustomClass = classList.contains(DEFAULT_PROPS.cssClasses); - const lazyClass = classList.contains('lazy'); - - expect(containsAvatar).toBe(true); - expect(containsSizeClass).toBe(true); - expect(containsCustomClass).toBe(true); - expect(lazyClass).toBe(false); - }); - }); - - describe('Initialization when lazy', function() { - beforeEach(function() { - vm = mountComponent(UserAvatarImage, { - ...DEFAULT_PROPS, - lazy: true, - }).$mount(); - }); - - it('should add lazy attributes', function() { - const imageElement = vm.$el.querySelector('img'); - const lazyClass = imageElement.classList.contains('lazy'); - - expect(lazyClass).toBe(true); - expect(imageElement.getAttribute('src')).toBe(placeholderImage); - expect(imageElement.getAttribute('data-src')).toBe(`${DEFAULT_PROPS.imgSrc}?width=99`); - }); - }); - - describe('Initialization without src', function() { - beforeEach(function() { - vm = mountComponent(UserAvatarImage); - }); - - it('should have default avatar image', function() { - const imageElement = vm.$el.querySelector('img'); - - expect(imageElement.getAttribute('src')).toBe(defaultAvatarUrl); - }); - }); - - describe('dynamic tooltip content', () => { - const props = DEFAULT_PROPS; - const slots = { - default: ['Action!'], - }; - - beforeEach(() => { - vm = mountComponentWithSlots(UserAvatarImage, { props, slots }).$mount(); - }); - - it('renders the tooltip slot', () => { - expect(vm.$el.querySelector('.js-user-avatar-image-toolip')).not.toBe(null); - }); - - it('renders the tooltip content', () => { - expect(vm.$el.querySelector('.js-user-avatar-image-toolip').textContent).toContain( - slots.default[0], - ); - }); - - it('does not render tooltip data attributes for on avatar image', () => { - const avatarImg = vm.$el.querySelector('img'); - - expect(avatarImg.dataset.originalTitle).not.toBeDefined(); - expect(avatarImg.dataset.placement).not.toBeDefined(); - expect(avatarImg.dataset.container).not.toBeDefined(); - }); - }); -}); diff --git a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js b/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js deleted file mode 100644 index c7e0d806d80..00000000000 --- a/spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js +++ /dev/null @@ -1,167 +0,0 @@ -import Vue from 'vue'; -import userPopover from '~/vue_shared/components/user_popover/user_popover.vue'; -import mountComponent from 'spec/helpers/vue_mount_component_helper'; - -const DEFAULT_PROPS = { - loaded: true, - user: { - username: 'root', - name: 'Administrator', - location: 'Vienna', - bio: null, - organization: null, - status: null, - }, -}; - -const UserPopover = Vue.extend(userPopover); - -describe('User Popover Component', () => { - const fixtureTemplate = 'merge_requests/diff_comment.html'; - preloadFixtures(fixtureTemplate); - - let vm; - - beforeEach(() => { - loadFixtures(fixtureTemplate); - }); - - afterEach(() => { - vm.$destroy(); - }); - - describe('Empty', () => { - beforeEach(() => { - vm = mountComponent(UserPopover, { - target: document.querySelector('.js-user-link'), - user: { - name: null, - username: null, - location: null, - bio: null, - organization: null, - status: null, - }, - }); - }); - - it('should return skeleton loaders', () => { - expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4); - }); - }); - - describe('basic data', () => { - it('should show basic fields', () => { - vm = mountComponent(UserPopover, { - ...DEFAULT_PROPS, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name); - expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username); - expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location); - }); - - it('shows icon for location', () => { - const iconEl = vm.$el.querySelector('.js-location svg'); - - expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('location'); - }); - }); - - describe('job data', () => { - it('should show only bio if no organization is available', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.bio = 'Engineer'; - - vm = mountComponent(UserPopover, { - ...testProps, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.textContent).toContain('Engineer'); - }); - - it('should show only organization if no bio is available', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.organization = 'GitLab'; - - vm = mountComponent(UserPopover, { - ...testProps, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.textContent).toContain('GitLab'); - }); - - it('should display bio and organization in separate lines', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.bio = 'Engineer'; - testProps.user.organization = 'GitLab'; - - vm = mountComponent(UserPopover, { - ...DEFAULT_PROPS, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.querySelector('.js-bio').textContent).toContain('Engineer'); - expect(vm.$el.querySelector('.js-organization').textContent).toContain('GitLab'); - }); - - it('should not encode special characters in bio and organization', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.bio = 'Manager & Team Lead'; - testProps.user.organization = 'Me & my <funky> Company'; - - vm = mountComponent(UserPopover, { - ...DEFAULT_PROPS, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.querySelector('.js-bio').textContent).toContain('Manager & Team Lead'); - expect(vm.$el.querySelector('.js-organization').textContent).toContain( - 'Me & my <funky> Company', - ); - }); - - it('shows icon for bio', () => { - const iconEl = vm.$el.querySelector('.js-bio svg'); - - expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('profile'); - }); - - it('shows icon for organization', () => { - const iconEl = vm.$el.querySelector('.js-organization svg'); - - expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('work'); - }); - }); - - describe('status data', () => { - it('should show only message', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.status = { message_html: 'Hello World' }; - - vm = mountComponent(UserPopover, { - ...DEFAULT_PROPS, - target: document.querySelector('.js-user-link'), - }); - - expect(vm.$el.textContent).toContain('Hello World'); - }); - - it('should show message and emoji', () => { - const testProps = Object.assign({}, DEFAULT_PROPS); - testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; - - vm = mountComponent(UserPopover, { - ...DEFAULT_PROPS, - target: document.querySelector('.js-user-link'), - status: { emoji: 'basketball_player', message_html: 'Hello World' }, - }); - - expect(vm.$el.textContent).toContain('Hello World'); - expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"'); - }); - }); -}); diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 88a7fbea1d1..1269fac7f48 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' include ImportExport::CommonUtil describe Gitlab::ImportExport::ProjectTreeRestorer do + include ImportExport::CommonUtil + let(:shared) { project.import_export_shared } describe 'restore project tree' do @@ -16,7 +18,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do RSpec::Mocks.with_temporary_scope do @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') @shared = @project.import_export_shared - allow(@shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/') + + setup_import_export_config('complex') allow_any_instance_of(Repository).to receive(:fetch_source_branch!).and_return(true) allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) @@ -257,9 +260,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end it 'has the correct number of pipelines and statuses' do - expect(@project.ci_pipelines.size).to eq(5) + expect(@project.ci_pipelines.size).to eq(6) - @project.ci_pipelines.zip([2, 2, 2, 2, 2]) + @project.ci_pipelines.zip([0, 2, 2, 2, 2, 2]) .each do |(pipeline, expected_status_size)| expect(pipeline.statuses.size).to eq(expected_status_size) end @@ -268,7 +271,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do context 'when restoring hierarchy of pipeline, stages and jobs' do it 'restores pipelines' do - expect(Ci::Pipeline.all.count).to be 5 + expect(Ci::Pipeline.all.count).to be 6 end it 'restores pipeline stages' do @@ -314,21 +317,33 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end - context 'Light JSON' do + context 'project.json file access check' do let(:user) { create(:user) } let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restored_project_json) { project_tree_restorer.restore } - before do - allow(shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/') + it 'does not read a symlink' do + Dir.mktmpdir do |tmpdir| + setup_symlink(tmpdir, 'project.json') + allow(shared).to receive(:export_path).and_call_original + + expect(project_tree_restorer.restore).to eq(false) + expect(shared.errors).to include('Incorrect JSON format') + end end + end + + context 'Light JSON' do + let(:user) { create(:user) } + let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } + let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } + let(:restored_project_json) { project_tree_restorer.restore } context 'with a simple project' do before do - project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json") - - restored_project_json + setup_import_export_config('light') + expect(restored_project_json).to eq(true) end it_behaves_like 'restores project correctly', @@ -339,19 +354,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do first_issue_labels: 1, services: 1 - context 'project.json file access check' do - it 'does not read a symlink' do - Dir.mktmpdir do |tmpdir| - setup_symlink(tmpdir, 'project.json') - allow(shared).to receive(:export_path).and_call_original - - restored_project_json - - expect(shared.errors).to be_empty - end - end - end - context 'when there is an existing build with build token' do before do create(:ci_build, token: 'abcd') @@ -367,6 +369,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'when the project has overridden params in import data' do + before do + setup_import_export_config('light') + end + it 'handles string versions of visibility_level' do # Project needs to be in a group for visibility level comparison # to happen @@ -375,24 +381,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } }) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL) end it 'overwrites the params stored in the JSON' do project.create_import_data(data: { override_params: { description: "Overridden" } }) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.description).to eq("Overridden") end it 'does not allow setting params that are excluded from import_export settings' do project.create_import_data(data: { override_params: { lfs_enabled: true } }) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.lfs_enabled).to be_falsey end @@ -408,7 +411,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do project.create_import_data(data: { override_params: disabled_access_levels }) - restored_project_json + expect(restored_project_json).to eq(true) aggregate_failures do access_level_keys.each do |key| @@ -429,9 +432,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end before do - project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.group.json") - - restored_project_json + setup_import_export_config('group') + expect(restored_project_json).to eq(true) end it_behaves_like 'restores project correctly', @@ -463,11 +465,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end before do - project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json") + setup_import_export_config('light') end it 'does not import any templated services' do - restored_project_json + expect(restored_project_json).to eq(true) expect(project.services.where(template: true).count).to eq(0) end @@ -477,8 +479,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.labels.count).to eq(1) end @@ -487,8 +488,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.group.milestones.count).to eq(1) expect(project.milestones.count).to eq(0) end @@ -504,13 +504,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do group: create(:group)) end - it 'preserves the project milestone IID' do - project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json") + before do + setup_import_export_config('milestone-iid') + end + it 'preserves the project milestone IID' do expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.milestones.count).to eq(2) expect(Milestone.find_by_title('Another milestone').iid).to eq(1) expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2) @@ -518,19 +519,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end context 'with external authorization classification labels' do + before do + setup_import_export_config('light') + end + it 'converts empty external classification authorization labels to nil' do project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } }) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.external_authorization_classification_label).to be_nil end it 'preserves valid external classification authorization labels' do project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } }) - restored_project_json - + expect(restored_project_json).to eq(true) expect(project.external_authorization_classification_label).to eq("foobar") end end diff --git a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb index 472bf55d37e..843de27df1a 100644 --- a/spec/lib/gitlab/import_export/relation_rename_service_spec.rb +++ b/spec/lib/gitlab/import_export/relation_rename_service_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' describe Gitlab::ImportExport::RelationRenameService do + include ImportExport::CommonUtil + let(:renames) do { 'example_relation1' => 'new_example_relation1', @@ -21,12 +23,12 @@ describe Gitlab::ImportExport::RelationRenameService do context 'when importing' do let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) } - let(:import_path) { 'spec/fixtures/lib/gitlab/import_export' } - let(:file_content) { IO.read("#{import_path}/project.json") } - let!(:json_file) { ActiveSupport::JSON.decode(file_content) } + let(:file_content) { IO.read(File.join(shared.export_path, 'project.json')) } + let(:json_file) { ActiveSupport::JSON.decode(file_content) } before do - allow(shared).to receive(:export_path).and_return(import_path) + setup_import_export_config('complex') + allow(ActiveSupport::JSON).to receive(:decode).and_call_original allow(ActiveSupport::JSON).to receive(:decode).with(file_content).and_return(json_file) end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 71b999048b4..052651bdf50 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -3342,22 +3342,6 @@ describe Project do end end - describe '#append_or_update_attribute' do - let(:project) { create(:project) } - - it 'shows full error updating an invalid MR' do - expect { project.append_or_update_attribute(:merge_requests, [create(:merge_request)]) } - .to raise_error(ActiveRecord::RecordInvalid, /Failed to set merge_requests:/) - end - - it 'updates the project successfully' do - merge_request = create(:merge_request, target_project: project, source_project: project) - - expect { project.append_or_update_attribute(:merge_requests, [merge_request]) } - .not_to raise_error - end - end - describe '#update' do let(:project) { create(:project) } diff --git a/spec/support/import_export/common_util.rb b/spec/support/import_export/common_util.rb index ac6840dbcfc..4e149c9fa54 100644 --- a/spec/support/import_export/common_util.rb +++ b/spec/support/import_export/common_util.rb @@ -8,5 +8,12 @@ module ImportExport File.open("#{tmpdir}/test", 'w') { |file| file.write("test") } FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}") end + + def setup_import_export_config(name, prefix = nil) + export_path = [prefix, 'spec', 'fixtures', 'lib', 'gitlab', 'import_export', name].compact + export_path = File.join(*export_path) + + allow_any_instance_of(Gitlab::ImportExport).to receive(:export_path) { export_path } + end end end diff --git a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb index 4ebb5e35e0e..4978a403324 100644 --- a/spec/support/shared_examples/models/concern/issuable_shared_examples.rb +++ b/spec/support/shared_examples/models/concern/issuable_shared_examples.rb @@ -4,7 +4,7 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do it 'fails fast for long strings' do # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823 expect do - Timeout.timeout(3.seconds) { mentionable.matches_cross_reference_regex? } + Timeout.timeout(6.seconds) { mentionable.matches_cross_reference_regex? } end.not_to raise_error end end diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore index 259148fa18f..259148fa18f 100644..100755 --- a/vendor/gitignore/C++.gitignore +++ b/vendor/gitignore/C++.gitignore diff --git a/vendor/gitignore/Java.gitignore b/vendor/gitignore/Java.gitignore index a1c2a238a96..a1c2a238a96 100644..100755 --- a/vendor/gitignore/Java.gitignore +++ b/vendor/gitignore/Java.gitignore |