Commit fd0691c6 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 0f0a8be3
...@@ -1966,27 +1966,6 @@ class Project < ApplicationRecord ...@@ -1966,27 +1966,6 @@ class Project < ApplicationRecord
(auto_devops || build_auto_devops)&.predefined_variables (auto_devops || build_auto_devops)&.predefined_variables
end 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 # 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 # @return [Boolean] true when set to read_only or false when an existing git transfer is in progress
......
...@@ -15,7 +15,6 @@ module Gitlab ...@@ -15,7 +15,6 @@ module Gitlab
@user = user @user = user
@shared = shared @shared = shared
@project = project @project = project
@saved = true
end end
def restore def restore
...@@ -33,7 +32,8 @@ module Gitlab ...@@ -33,7 +32,8 @@ module Gitlab
ActiveRecord::Base.uncached do ActiveRecord::Base.uncached do
ActiveRecord::Base.no_touching do ActiveRecord::Base.no_touching do
update_project_params! update_project_params!
create_relations create_project_relations!
post_import!
end end
end end
...@@ -69,77 +69,75 @@ module Gitlab ...@@ -69,77 +69,75 @@ module Gitlab
# in the DB. The structure and relationships between models are guessed from # in the DB. The structure and relationships between models are guessed from
# the configuration yaml file too. # the configuration yaml file too.
# Finally, it updates each attribute in the newly imported project. # Finally, it updates each attribute in the newly imported project.
def create_relations def create_project_relations!
project_relations.each do |relation_key, relation_definition| project_relations.each(&method(
relation_key_s = relation_key.to_s :process_project_relation!))
end
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 post_import!
@project.merge_requests.set_latest_merge_request_diff_ids! @project.merge_requests.set_latest_merge_request_diff_ids!
@saved
end end
def save_relation_hash(relation_key, relation_hash_batch) def process_project_relation!(relation_key, relation_definition)
relation_hash = create_relation(relation_key, relation_hash_batch) 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 end
# Older, serialized CI pipeline exports may only have a # Older, serialized CI pipeline exports may only have a
# merge_request_id and not the full hash of the merge request. To # merge_request_id and not the full hash of the merge request. To
# import these pipelines, we need to preserve the mapping between # import these pipelines, we need to preserve the mapping between
# the old and new the merge request ID. # 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' return unless relation_key == 'merge_requests'
relation_hash = Array(relation_hash) merge_requests_mapping[data_hash['id']] = relation_object.id
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
end end
def project_relations 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 end
def update_project_params! def update_project_params!
Gitlab::Timeless.timeless(@project) do project_params = @tree_hash.reject do |key, value|
project_params = @tree_hash.reject do |key, value| project_relations.include?(key)
project_relations.include?(key.to_sym) end
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.assign_attributes(project_params)
project_params = Gitlab::ImportExport::AttributeCleaner.clean( @project.drop_visibility_level!
relation_hash: project_params,
relation_class: Project,
excluded_keys: excluded_keys_for_relation(:project))
@project.assign_attributes(project_params) Gitlab::Timeless.timeless(@project) do
@project.drop_visibility_level!
@project.save! @project.save!
end end
end end
...@@ -156,73 +154,61 @@ module Gitlab ...@@ -156,73 +154,61 @@ module Gitlab
@project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {} @project_override_params ||= @project.import_data&.data&.fetch('override_params', nil) || {}
end end
# Given a relation hash containing one or more models and its relationships, def build_relations(relation_key, relation_definition, data_hashes)
# loops through each model and each object from a model type and data_hashes.map do |data_hash|
# and assigns its correspondent attributes hash from +tree_hash+ build_relation(relation_key, relation_definition, data_hash)
# Example: end.compact
# +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
end end
def process_sub_relation(relation_key, relation_definition, relation_item) def build_relation(relation_key, relation_definition, data_hash)
relation_definition.each do |sub_relation_key, sub_relation_definition| # TODO: This is hack to not create relation for the author
# We just use author to get the user ID, do not attempt to create an instance. # Rather make `RelationFactory#set_note_author` to take care of that
next if sub_relation_key == :author 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 Gitlab::ImportExport::RelationFactory.create(
if sub_relation_definition.present? relation_sym: relation_key.to_sym,
create_sub_relations(sub_relation_key_s, sub_relation_definition, relation_item, save: false) 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 end
# transform relation hash to actual object # persist object(s) or delete from relation
sub_relation_hash = relation_item[sub_relation_key_s] if sub_data_hash
if sub_relation_hash.present? data_hash[sub_relation_key] = sub_data_hash
relation_item[sub_relation_key_s] = create_relation(sub_relation_key, sub_relation_hash) else
end data_hash.delete(sub_relation_key)
end end
end end
def create_relation(relation_key, relation_hash_list) def group_model?(relation_object)
relation_array = [relation_hash_list].flatten.map do |relation_hash| GROUP_MODELS.include?(relation_object.class) && relation_object.group_id
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
end end
def reader def reader
......
...@@ -38,10 +38,13 @@ module Gitlab ...@@ -38,10 +38,13 @@ module Gitlab
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze 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 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) def self.create(*args)
new(*args).create new(*args).create
end end
...@@ -274,7 +277,7 @@ module Gitlab ...@@ -274,7 +277,7 @@ module Gitlab
end end
def setup_pipeline def setup_pipeline
@relation_hash.fetch('stages').each do |stage| @relation_hash.fetch('stages', []).each do |stage|
stage.statuses.each do |status| stage.statuses.each do |status|
status.pipeline = imported_object status.pipeline = imported_object
end end
...@@ -324,7 +327,8 @@ module Gitlab ...@@ -324,7 +327,8 @@ module Gitlab
end end
def find_or_create_object! 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 return find_or_create_merge_request! if @relation_name == :merge_request
# Can't use IDs as validation exists calling `group` or `project` attributes # Can't use IDs as validation exists calling `group` or `project` attributes
......
...@@ -6680,6 +6680,25 @@ ...@@ -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": [ "triggers": [
......
import Vue from 'vue'; import { shallowMount } from '@vue/test-utils';
import commitComp from '~/vue_shared/components/commit.vue'; import CommitComponent from '~/vue_shared/components/commit.vue';
import mountComponent from '../../helpers/vue_mount_component_helper'; import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
describe('Commit component', () => { describe('Commit component', () => {
let props; let props;
let component; let wrapper;
let CommitComponent;
beforeEach(() => { const findUserAvatar = () => wrapper.find(UserAvatarLink);
CommitComponent = Vue.extend(commitComp);
}); const createComponent = propsData => {
wrapper = shallowMount(CommitComponent, {
propsData,
sync: false,
});
};
afterEach(() => { afterEach(() => {
component.$destroy(); wrapper.destroy();
}); });
it('should render a fork icon if it does not represent a tag', () => { it('should render a fork icon if it does not represent a tag', () => {
component = mountComponent(CommitComponent, { createComponent({
tag: false, tag: false,
commitRef: { commitRef: {
name: 'master', name: 'master',
...@@ -34,7 +39,12 @@ describe('Commit component', () => { ...@@ -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', () => { describe('Given all the props', () => {
...@@ -56,68 +66,51 @@ describe('Commit component', () => { ...@@ -56,68 +66,51 @@ describe('Commit component', () => {
username: 'jschatz1', username: 'jschatz1',
}, },
}; };
createComponent(props);
component = mountComponent(CommitComponent, props);
}); });
it('should render a tag icon if it represents a tag', () => { 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', () => { it('should render a link to the ref url', () => {
expect(component.$el.querySelector('.ref-name').getAttribute('href')).toEqual( expect(wrapper.find('.ref-name').attributes('href')).toBe(props.commitRef.ref_url);
props.commitRef.ref_url,
);
}); });
it('should render the ref name', () => { 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', () => { it('should render the commit short sha with a link to the commit url', () => {
expect(component.$el.querySelector('.commit-sha').getAttribute('href')).toEqual( expect(wrapper.find('.commit-sha').attributes('href')).toEqual(props.commitUrl);
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', () => { it('should render icon for commit', () => {
expect( expect(wrapper.find('icon-stub[name="commit"]').exists()).toBe(true);
component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'),
).toContain('commit');
}); });
describe('Given commit title and author props', () => { describe('Given commit title and author props', () => {
it('should render a link to the author profile', () => { it('should render a link to the author profile', () => {
expect( const userAvatar = findUserAvatar();
component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
).toEqual(props.author.path); expect(userAvatar.props('linkHref')).toBe(props.author.path);
}); });
it('Should render the author avatar with title and alt attributes', () => { it('Should render the author avatar with title and alt attributes', () => {
expect( const userAvatar = findUserAvatar();
component.$el
.querySelector('.commit-title .avatar-image-container .js-user-avatar-image-toolip') expect(userAvatar.exists()).toBe(true);
.textContent.trim(),
).toContain(props.author.username); expect(userAvatar.props('imgAlt')).toBe(`${props.author.username}'s avatar`);
expect(
component.$el
.querySelector('.commit-title .avatar-image-container img')
.getAttribute('alt'),
).toContain(`${props.author.username}'s avatar`);
}); });
}); });
it('should render the commit title', () => { it('should render the commit title', () => {
expect(component.$el.querySelector('a.commit-row-message').getAttribute('href')).toEqual( expect(wrapper.find('.commit-row-message').attributes('href')).toEqual(props.commitUrl);
props.commitUrl,
);
expect(component.$el.querySelector('a.commit-row-message').textContent).toContain( expect(wrapper.find('.commit-row-message').text()).toContain(props.title);
props.title,
);
}); });
}); });
...@@ -136,9 +129,9 @@ describe('Commit component', () => { ...@@ -136,9 +129,9 @@ describe('Commit component', () => {
author: {}, 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", "Can't find HEAD commit for this branch",
); );
}); });
...@@ -159,16 +152,16 @@ describe('Commit component', () => { ...@@ -159,16 +152,16 @@ describe('Commit component', () => {
author: {}, author: {},
}; };
component = mountComponent(CommitComponent, props); createComponent(props);
const refEl = component.$el.querySelector('.ref-name'); 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', () => { ...@@ -192,16 +185,16 @@ describe('Commit component', () => {
author: {}, author: {},
}; };
component = mountComponent(CommitComponent, props); createComponent(props);
const refEl = component.$el.querySelector('.ref-name'); 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', () => { ...@@ -226,9 +219,9 @@ describe('Commit component', () => {
showRefInfo: false, showRefInfo: false,
}; };
component = mountComponent(CommitComponent, props); createComponent(props);
expect(component.$el.querySelector('.ref-name')).toBeNull(); expect(wrapper.find('.ref-name').exists()).toBe(false);
}); });
}); });
}); });
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();
});
});
});
import Vue from 'vue'; import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import userPopover from '~/vue_shared/components/user_popover/user_popover.vue'; import { mount } from '@vue/test-utils';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = { const DEFAULT_PROPS = {
loaded: true, loaded: true,
...@@ -14,58 +13,62 @@ const DEFAULT_PROPS = { ...@@ -14,58 +13,62 @@ const DEFAULT_PROPS = {
}, },
}; };
const UserPopover = Vue.extend(userPopover);
describe('User Popover Component', () => { describe('User Popover Component', () => {
const fixtureTemplate = 'merge_requests/diff_comment.html'; const fixtureTemplate = 'merge_requests/diff_comment.html';
preloadFixtures(fixtureTemplate); preloadFixtures(fixtureTemplate);
let vm; let wrapper;
beforeEach(() => { beforeEach(() => {
loadFixtures(fixtureTemplate); loadFixtures(fixtureTemplate);
}); });
afterEach(() => { afterEach(() => {
vm.$destroy(); wrapper.destroy();
}); });
describe('Empty', () => { describe('Empty', () => {
beforeEach(() => { beforeEach(() => {
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
target: document.querySelector('.js-user-link'), propsData: {
user: { target: document.querySelector('.js-user-link'),
name: null, user: {
username: null, name: null,
location: null, username: null,
bio: null, location: null,
organization: null, bio: null,
status: null, organization: null,
status: null,
},
}, },
sync: false,
}); });
}); });
it('should return skeleton loaders', () => { it('should return skeleton loaders', () => {
expect(vm.$el.querySelectorAll('.animation-container').length).toBe(4); expect(wrapper.findAll('.animation-container').length).toBe(4);
}); });
}); });
describe('basic data', () => { describe('basic data', () => {
it('should show basic fields', () => { it('should show basic fields', () => {
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...DEFAULT_PROPS, propsData: {
target: document.querySelector('.js-user-link'), ...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.name); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.username); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
expect(vm.$el.textContent).toContain(DEFAULT_PROPS.user.location); expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
}); });
it('shows icon for location', () => { it('shows icon for location', () => {
const iconEl = vm.$el.querySelector('.js-location svg'); const iconEl = wrapper.find('.js-location svg');
expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('location'); expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('location');
}); });
}); });
...@@ -74,24 +77,30 @@ describe('User Popover Component', () => { ...@@ -74,24 +77,30 @@ describe('User Popover Component', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.bio = 'Engineer'; testProps.user.bio = 'Engineer';
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...testProps, propsData: {
target: document.querySelector('.js-user-link'), ...testProps,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.textContent).toContain('Engineer'); expect(wrapper.text()).toContain('Engineer');
}); });
it('should show only organization if no bio is available', () => { it('should show only organization if no bio is available', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.organization = 'GitLab'; testProps.user.organization = 'GitLab';
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...testProps, propsData: {
target: document.querySelector('.js-user-link'), ...testProps,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.textContent).toContain('GitLab'); expect(wrapper.text()).toContain('GitLab');
}); });
it('should display bio and organization in separate lines', () => { it('should display bio and organization in separate lines', () => {
...@@ -99,13 +108,16 @@ describe('User Popover Component', () => { ...@@ -99,13 +108,16 @@ describe('User Popover Component', () => {
testProps.user.bio = 'Engineer'; testProps.user.bio = 'Engineer';
testProps.user.organization = 'GitLab'; testProps.user.organization = 'GitLab';
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...DEFAULT_PROPS, propsData: {
target: document.querySelector('.js-user-link'), ...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.querySelector('.js-bio').textContent).toContain('Engineer'); expect(wrapper.find('.js-bio').text()).toContain('Engineer');
expect(vm.$el.querySelector('.js-organization').textContent).toContain('GitLab'); expect(wrapper.find('.js-organization').text()).toContain('GitLab');
}); });
it('should not encode special characters in bio and organization', () => { it('should not encode special characters in bio and organization', () => {
...@@ -113,27 +125,28 @@ describe('User Popover Component', () => { ...@@ -113,27 +125,28 @@ describe('User Popover Component', () => {
testProps.user.bio = 'Manager & Team Lead'; testProps.user.bio = 'Manager & Team Lead';
testProps.user.organization = 'Me & my <funky> Company'; testProps.user.organization = 'Me & my <funky> Company';
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...DEFAULT_PROPS, propsData: {
target: document.querySelector('.js-user-link'), ...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.querySelector('.js-bio').textContent).toContain('Manager & Team Lead'); expect(wrapper.find('.js-bio').text()).toContain('Manager & Team Lead');
expect(vm.$el.querySelector('.js-organization').textContent).toContain( expect(wrapper.find('.js-organization').text()).toContain('Me & my <funky> Company');
'Me & my <funky> Company',
);
}); });
it('shows icon for bio', () => { it('shows icon for bio', () => {
const iconEl = vm.$el.querySelector('.js-bio svg'); const iconEl = wrapper.find('.js-bio svg');
expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('profile'); expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('profile');
}); });
it('shows icon for organization', () => { it('shows icon for organization', () => {
const iconEl = vm.$el.querySelector('.js-organization svg'); const iconEl = wrapper.find('.js-organization svg');
expect(iconEl.querySelector('use').getAttribute('xlink:href')).toContain('work'); expect(iconEl.find('use').element.getAttribute('xlink:href')).toContain('work');
}); });
}); });
...@@ -142,26 +155,32 @@ describe('User Popover Component', () => { ...@@ -142,26 +155,32 @@ describe('User Popover Component', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.status = { message_html: 'Hello World' }; testProps.user.status = { message_html: 'Hello World' };
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...DEFAULT_PROPS, propsData: {
target: document.querySelector('.js-user-link'), ...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
}); });
expect(vm.$el.textContent).toContain('Hello World'); expect(wrapper.text()).toContain('Hello World');
}); });
it('should show message and emoji', () => { it('should show message and emoji', () => {
const testProps = Object.assign({}, DEFAULT_PROPS); const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' }; testProps.user.status = { emoji: 'basketball_player', message_html: 'Hello World' };
vm = mountComponent(UserPopover, { wrapper = mount(UserPopover, {
...DEFAULT_PROPS, propsData: {
target: document.querySelector('.js-user-link'), ...DEFAULT_PROPS,
status: { emoji: 'basketball_player', message_html: 'Hello World' }, target: document.querySelector('.js-user-link'),
status: { emoji: 'basketball_player', message_html: 'Hello World' },
},
sync: false,
}); });
expect(vm.$el.textContent).toContain('Hello World'); expect(wrapper.text()).toContain('Hello World');
expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"'); expect(wrapper.html()).toContain('<gl-emoji data-name="basketball_player"');
}); });
}); });
}); });
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();
});
});
});
...@@ -2,6 +2,8 @@ require 'spec_helper' ...@@ -2,6 +2,8 @@ require 'spec_helper'
include ImportExport::CommonUtil include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer do describe Gitlab::ImportExport::ProjectTreeRestorer do
include ImportExport::CommonUtil
let(:shared) { project.import_export_shared } let(:shared) { project.import_export_shared }
describe 'restore project tree' do describe 'restore project tree' do
...@@ -16,7 +18,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -16,7 +18,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
RSpec::Mocks.with_temporary_scope do RSpec::Mocks.with_temporary_scope do
@project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project') @project = create(:project, :builds_enabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared @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(Repository).to receive(:fetch_source_branch!).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
...@@ -257,9 +260,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -257,9 +260,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
it 'has the correct number of pipelines and statuses' do 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)| .each do |(pipeline, expected_status_size)|
expect(pipeline.statuses.size).to eq(expected_status_size) expect(pipeline.statuses.size).to eq(expected_status_size)
end end
...@@ -268,7 +271,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -268,7 +271,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'when restoring hierarchy of pipeline, stages and jobs' do context 'when restoring hierarchy of pipeline, stages and jobs' do
it 'restores pipelines' do it 'restores pipelines' do
expect(Ci::Pipeline.all.count).to be 5 expect(Ci::Pipeline.all.count).to be 6
end end
it 'restores pipeline stages' do it 'restores pipeline stages' do
...@@ -314,21 +317,33 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -314,21 +317,33 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
end end
context 'Light JSON' do context 'project.json file access check' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } 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(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
before do it 'does not read a symlink' do
allow(shared).to receive(:export_path).and_return('spec/fixtures/lib/gitlab/import_export/') 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
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 context 'with a simple project' do
before do before do
project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json") setup_import_export_config('light')
expect(restored_project_json).to eq(true)
restored_project_json
end end
it_behaves_like 'restores project correctly', it_behaves_like 'restores project correctly',
...@@ -339,19 +354,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -339,19 +354,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
first_issue_labels: 1, first_issue_labels: 1,
services: 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 context 'when there is an existing build with build token' do
before do before do
create(:ci_build, token: 'abcd') create(:ci_build, token: 'abcd')
...@@ -367,6 +369,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -367,6 +369,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
context 'when the project has overridden params in import data' do 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 it 'handles string versions of visibility_level' do
# Project needs to be in a group for visibility level comparison # Project needs to be in a group for visibility level comparison
# to happen # to happen
...@@ -375,24 +381,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -375,24 +381,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project.create_import_data(data: { override_params: { visibility_level: Gitlab::VisibilityLevel::INTERNAL.to_s } }) 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) expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end end
it 'overwrites the params stored in the JSON' do it 'overwrites the params stored in the JSON' do
project.create_import_data(data: { override_params: { description: "Overridden" } }) 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") expect(project.description).to eq("Overridden")
end end
it 'does not allow setting params that are excluded from import_export settings' do it 'does not allow setting params that are excluded from import_export settings' do
project.create_import_data(data: { override_params: { lfs_enabled: true } }) 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 expect(project.lfs_enabled).to be_falsey
end end
...@@ -408,7 +411,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -408,7 +411,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
project.create_import_data(data: { override_params: disabled_access_levels }) project.create_import_data(data: { override_params: disabled_access_levels })
restored_project_json expect(restored_project_json).to eq(true)
aggregate_failures do aggregate_failures do
access_level_keys.each do |key| access_level_keys.each do |key|
...@@ -429,9 +432,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -429,9 +432,8 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
before do before do
project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.group.json") setup_import_export_config('group')
expect(restored_project_json).to eq(true)
restored_project_json
end end
it_behaves_like 'restores project correctly', it_behaves_like 'restores project correctly',
...@@ -463,11 +465,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -463,11 +465,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
before do before do
project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.light.json") setup_import_export_config('light')
end end
it 'does not import any templated services' do 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) expect(project.services.where(template: true).count).to eq(0)
end end
...@@ -477,8 +479,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -477,8 +479,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) 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) expect(project.labels.count).to eq(1)
end end
...@@ -487,8 +488,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -487,8 +488,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect_any_instance_of(Gitlab::ImportExport::Shared).not_to receive(:error) 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.group.milestones.count).to eq(1)
expect(project.milestones.count).to eq(0) expect(project.milestones.count).to eq(0)
end end
...@@ -504,13 +504,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -504,13 +504,14 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
group: create(:group)) group: create(:group))
end end
it 'preserves the project milestone IID' do before do
project_tree_restorer.instance_variable_set(:@path, "spec/fixtures/lib/gitlab/import_export/project.milestone-iid.json") 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) 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(project.milestones.count).to eq(2)
expect(Milestone.find_by_title('Another milestone').iid).to eq(1) expect(Milestone.find_by_title('Another milestone').iid).to eq(1)
expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2) expect(Milestone.find_by_title('Group-level milestone').iid).to eq(2)
...@@ -518,19 +519,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -518,19 +519,21 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
end end
context 'with external authorization classification labels' do 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 it 'converts empty external classification authorization labels to nil' do
project.create_import_data(data: { override_params: { external_authorization_classification_label: "" } }) 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 expect(project.external_authorization_classification_label).to be_nil
end end
it 'preserves valid external classification authorization labels' do it 'preserves valid external classification authorization labels' do
project.create_import_data(data: { override_params: { external_authorization_classification_label: "foobar" } }) 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") expect(project.external_authorization_classification_label).to eq("foobar")
end end
end end
......
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::RelationRenameService do describe Gitlab::ImportExport::RelationRenameService do
include ImportExport::CommonUtil
let(:renames) do let(:renames) do
{ {
'example_relation1' => 'new_example_relation1', 'example_relation1' => 'new_example_relation1',
...@@ -21,12 +23,12 @@ describe Gitlab::ImportExport::RelationRenameService do ...@@ -21,12 +23,12 @@ describe Gitlab::ImportExport::RelationRenameService do
context 'when importing' do context 'when importing' do
let(:project_tree_restorer) { Gitlab::ImportExport::ProjectTreeRestorer.new(user: user, shared: shared, project: project) } 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(File.join(shared.export_path, 'project.json')) }
let(:file_content) { IO.read("#{import_path}/project.json") } let(:json_file) { ActiveSupport::JSON.decode(file_content) }
let!(:json_file) { ActiveSupport::JSON.decode(file_content) }
before do 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).and_call_original
allow(ActiveSupport::JSON).to receive(:decode).with(file_content).and_return(json_file) allow(ActiveSupport::JSON).to receive(:decode).with(file_content).and_return(json_file)
end end
......
...@@ -3342,22 +3342,6 @@ describe Project do ...@@ -3342,22 +3342,6 @@ describe Project do
end end
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 describe '#update' do
let(:project) { create(:project) } let(:project) { create(:project) }
......
...@@ -8,5 +8,12 @@ module ImportExport ...@@ -8,5 +8,12 @@ module ImportExport
File.open("#{tmpdir}/test", 'w') { |file| file.write("test") } File.open("#{tmpdir}/test", 'w') { |file| file.write("test") }
FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}") FileUtils.ln_s("#{tmpdir}/test", "#{tmpdir}/#{symlink_name}")
end 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
end end
...@@ -4,7 +4,7 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do ...@@ -4,7 +4,7 @@ shared_examples_for 'matches_cross_reference_regex? fails fast' do
it 'fails fast for long strings' 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 # took well under 1 second in CI https://dev.gitlab.org/gitlab/gitlabhq/merge_requests/3267#note_172823
expect do 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.not_to raise_error
end end
end end
......
File mode changed from 100644 to 100755
File mode changed from 100644 to 100755
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment