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
(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
......
......@@ -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
......
......@@ -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": [
......
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);
});
});
});
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 mountComponent from 'spec/helpers/vue_mount_component_helper';
import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import { mount } from '@vue/test-utils';
const DEFAULT_PROPS = {
loaded: true,
......@@ -14,58 +13,62 @@ const DEFAULT_PROPS = {
},
};
const UserPopover = Vue.extend(userPopover);
describe('User Popover Component', () => {
const fixtureTemplate = 'merge_requests/diff_comment.html';
preloadFixtures(fixtureTemplate);
let vm;
let wrapper;
beforeEach(() => {
loadFixtures(fixtureTemplate);
});
afterEach(() => {
vm.$destroy();
wrapper.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,
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(vm.$el.querySelectorAll('.animation-container').length).toBe(4);
expect(wrapper.findAll('.animation-container').length).toBe(4);
});
});
describe('basic data', () => {
it('should show basic fields', () => {
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
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);
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 = 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', () => {
const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.bio = 'Engineer';
vm = mountComponent(UserPopover, {
...testProps,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...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', () => {
const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.organization = 'GitLab';
vm = mountComponent(UserPopover, {
...testProps,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...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', () => {
......@@ -99,13 +108,16 @@ describe('User Popover Component', () => {
testProps.user.bio = 'Engineer';
testProps.user.organization = 'GitLab';
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(vm.$el.querySelector('.js-bio').textContent).toContain('Engineer');
expect(vm.$el.querySelector('.js-organization').textContent).toContain('GitLab');
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', () => {
......@@ -113,27 +125,28 @@ describe('User Popover Component', () => {
testProps.user.bio = 'Manager & Team Lead';
testProps.user.organization = 'Me & my <funky> Company';
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
},
sync: false,
});
expect(vm.$el.querySelector('.js-bio').textContent).toContain('Manager & Team Lead');
expect(vm.$el.querySelector('.js-organization').textContent).toContain(
'Me & my <funky> Company',
);
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 = 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', () => {
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', () => {
const testProps = Object.assign({}, DEFAULT_PROPS);
testProps.user.status = { message_html: 'Hello World' };
vm = mountComponent(UserPopover, {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
wrapper = mount(UserPopover, {
propsData: {
...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', () => {
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' },
wrapper = mount(UserPopover, {
propsData: {
...DEFAULT_PROPS,
target: document.querySelector('.js-user-link'),
status: { emoji: 'basketball_player', message_html: 'Hello World' },
},
sync: false,
});
expect(vm.$el.textContent).toContain('Hello World');
expect(vm.$el.innerHTML).toContain('<gl-emoji data-name="basketball_player"');
expect(wrapper.text()).toContain('Hello World');
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'
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
......
......@@ -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
......
......@@ -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) }
......
......@@ -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
......@@ -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
......
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