summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-31 00:07:00 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-31 00:07:00 +0000
commitfd0691c6c0dc16ddada016af08c47a28614e888d (patch)
tree17e3da2d636967070bbd7b2039d3a5ce1adf1770
parent0f0a8be306e7e0cd5693f57414de351808c41db9 (diff)
downloadgitlab-ce-fd0691c6c0dc16ddada016af08c47a28614e888d.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/models/project.rb21
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb198
-rw-r--r--lib/gitlab/import_export/relation_factory.rb10
-rw-r--r--spec/fixtures/lib/gitlab/import_export/complex/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.json)19
-rw-r--r--spec/fixtures/lib/gitlab/import_export/group/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.group.json)0
-rw-r--r--spec/fixtures/lib/gitlab/import_export/light/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.light.json)0
-rw-r--r--spec/fixtures/lib/gitlab/import_export/milestone-iid/project.json (renamed from spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json)0
-rw-r--r--spec/frontend/vue_shared/components/commit_spec.js (renamed from spec/javascripts/vue_shared/components/commit_spec.js)115
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_image_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js186
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js120
-rw-r--r--spec/javascripts/vue_shared/components/user_popover/user_popover_spec.js167
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb97
-rw-r--r--spec/lib/gitlab/import_export/relation_rename_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb16
-rw-r--r--spec/support/import_export/common_util.rb7
-rw-r--r--spec/support/shared_examples/models/concern/issuable_shared_examples.rb2
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/C++.gitignore0
-rwxr-xr-x[-rw-r--r--]vendor/gitignore/Java.gitignore0
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