Commit cf2ba49f authored by Illya Klymov's avatar Illya Klymov

Merge branch 'group-migration' into 'master'

Bulk group migration e2e test

See merge request gitlab-org/gitlab!60482
parents 7328c3f5 3a5cd561
...@@ -242,7 +242,7 @@ export default { ...@@ -242,7 +242,7 @@ export default {
:description="s__('Check your source instance permissions.')" :description="s__('Check your source instance permissions.')"
/> />
<template v-else> <template v-else>
<table class="gl-w-full"> <table class="gl-w-full" data-qa-selector="import_table">
<thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"> <thead class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1">
<th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th> <th class="gl-py-4 import-jobs-from-col">{{ s__('BulkImport|From source group') }}</th>
<th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th> <th class="gl-py-4 import-jobs-to-col">{{ s__('BulkImport|To new group') }}</th>
......
...@@ -119,7 +119,11 @@ export default { ...@@ -119,7 +119,11 @@ export default {
</script> </script>
<template> <template>
<tr class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"> <tr
class="gl-border-gray-200 gl-border-0 gl-border-b-1 gl-border-solid"
data-qa-selector="import_item"
:data-qa-source-group="group.full_path"
>
<td class="gl-p-4"> <td class="gl-p-4">
<gl-link <gl-link
:href="group.web_url" :href="group.web_url"
...@@ -150,6 +154,7 @@ export default { ...@@ -150,6 +154,7 @@ export default {
:disabled="isAlreadyImported" :disabled="isAlreadyImported"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!" toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1" class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1"
data-qa-selector="target_namespace_selector_dropdown"
> >
<gl-dropdown-item @click="$emit('update-target-namespace', '')">{{ <gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
s__('BulkImport|No parent') s__('BulkImport|No parent')
...@@ -162,6 +167,8 @@ export default { ...@@ -162,6 +167,8 @@ export default {
<gl-dropdown-item <gl-dropdown-item
v-for="ns in availableNamespaces" v-for="ns in availableNamespaces"
:key="ns.full_path" :key="ns.full_path"
data-qa-selector="target_group_dropdown_item"
:data-qa-group-name="ns.full_path"
@click="$emit('update-target-namespace', ns.full_path)" @click="$emit('update-target-namespace', ns.full_path)"
> >
{{ ns.full_path }} {{ ns.full_path }}
...@@ -192,7 +199,7 @@ export default { ...@@ -192,7 +199,7 @@ export default {
</div> </div>
</div> </div>
</td> </td>
<td class="gl-p-4 gl-white-space-nowrap"> <td class="gl-p-4 gl-white-space-nowrap" data-qa-selector="import_status_indicator">
<import-status :status="group.progress.status" class="gl-mt-2" /> <import-status :status="group.progress.status" class="gl-mt-2" />
</td> </td>
<td class="gl-p-4"> <td class="gl-p-4">
...@@ -201,6 +208,7 @@ export default { ...@@ -201,6 +208,7 @@ export default {
:disabled="isInvalid" :disabled="isInvalid"
variant="confirm" variant="confirm"
category="secondary" category="secondary"
data-qa-selector="import_group_button"
@click="$emit('import-group')" @click="$emit('import-group')"
>{{ __('Import') }}</gl-button >{{ __('Import') }}</gl-button
> >
......
...@@ -18,7 +18,8 @@ ...@@ -18,7 +18,8 @@
= f.text_field :bulk_import_gitlab_url, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8', = f.text_field :bulk_import_gitlab_url, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8',
required: true, required: true,
title: s_('GroupsNew|Please fill in GitLab source URL.'), title: s_('GroupsNew|Please fill in GitLab source URL.'),
id: 'import_gitlab_url' id: 'import_gitlab_url',
data: { qa_selector: 'import_gitlab_url' }
.form-group.gl-display-flex.gl-flex-direction-column .form-group.gl-display-flex.gl-flex-direction-column
= f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token' = f.label :bulk_import_gitlab_access_token, s_('GroupsNew|Personal access token'), for: 'import_gitlab_token'
.gl-font-weight-normal .gl-font-weight-normal
...@@ -27,6 +28,7 @@ ...@@ -27,6 +28,7 @@
= f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8', = f.text_field :bulk_import_gitlab_access_token, placeholder: s_('GroupsNew|e.g. h8d3f016698e...'), class: 'gl-form-input gl-mt-3 col-xs-12 col-sm-8',
required: true, required: true,
title: s_('GroupsNew|Please fill in your personal access token.'), title: s_('GroupsNew|Please fill in your personal access token.'),
id: 'import_gitlab_token' id: 'import_gitlab_token',
data: { qa_selector: 'import_gitlab_token' }
.gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5 .gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5
= f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm' = f.submit s_('GroupsNew|Connect instance'), class: 'btn gl-button btn-confirm', data: { qa_selector: 'connect_instance_button' }
...@@ -13,10 +13,10 @@ ...@@ -13,10 +13,10 @@
= link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do = link_to explore_groups_path, data: { track_label: "groups_dropdown_explore_groups", track_event: "click_link" } do
= _('Explore groups') = _('Explore groups')
= nav_link(path: 'groups/new#create-group-pane', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do = nav_link(path: 'groups/new#create-group-pane', html_options: { class: 'gl-border-0 gl-border-t-1 gl-border-solid gl-border-gray-100' }) do
= link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link" } do = link_to new_group_path(anchor: 'create-group-pane'), data: { track_label: "groups_dropdown_create_group", track_event: "click_link", qa_selector: 'create_group_link' } do
= _('Create group') = _('Create group')
= nav_link(path: 'groups/new#import-group-pane') do = nav_link(path: 'groups/new#import-group-pane') do
= link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link" } do = link_to new_group_path(anchor: 'import-group-pane'), data: { track_label: "groups_dropdown_import_group", track_event: "click_link", qa_selector: 'import_group_link' } do
= _('Import group') = _('Import group')
.frequent-items-dropdown-content .frequent-items-dropdown-content
#js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } } #js-groups-dropdown{ data: { user_name: current_user.username, group: group_meta } }
...@@ -225,6 +225,7 @@ module QA ...@@ -225,6 +225,7 @@ module QA
autoload :Show, 'qa/page/group/show' autoload :Show, 'qa/page/group/show'
autoload :Menu, 'qa/page/group/menu' autoload :Menu, 'qa/page/group/menu'
autoload :Members, 'qa/page/group/members' autoload :Members, 'qa/page/group/members'
autoload :BulkImport, 'qa/page/group/bulk_import'
module Milestone module Milestone
autoload :Index, 'qa/page/group/milestone/index' autoload :Index, 'qa/page/group/milestone/index'
......
# frozen_string_literal: true
module QA
module Page
module Group
class BulkImport < Page::Base
view "app/assets/javascripts/import_entities/import_groups/components/import_table.vue" do
element :import_table
end
view "app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue" do
element :import_item
element :target_namespace_selector_dropdown
element :target_group_dropdown_item
element :import_status_indicator
element :import_group_button
end
# Import source group in to target group
#
# @param [String] source_group_name
# @param [String] target_group_name
# @return [void]
def import_group(source_group_name, target_group_name)
finished_loading?
within_element(:import_item, source_group: source_group_name) do
click_element(:target_namespace_selector_dropdown)
click_element(:target_group_dropdown_item, group_name: target_group_name)
click_element(:import_group_button)
end
end
# Check if import page has a successfully imported group
#
# @param [String] source_group_name
# @param [Integer] wait
# @return [Boolean]
def has_imported_group?(source_group_name, wait: QA::Support::WaitForRequests::DEFAULT_MAX_WAIT_TIME)
within_element(:import_item, source_group: source_group_name) do
has_element?(:import_status_indicator, text: "Complete", wait: wait)
end
end
end
end
end
end
...@@ -15,6 +15,12 @@ module QA ...@@ -15,6 +15,12 @@ module QA
element :create_group_button, "submit _('Create group')" # rubocop:disable QA/ElementWithPattern element :create_group_button, "submit _('Create group')" # rubocop:disable QA/ElementWithPattern
end end
view 'app/views/groups/_import_group_from_another_instance_panel.html.haml' do
element :import_gitlab_url
element :import_gitlab_token
element :connect_instance_button
end
def set_path(path) def set_path(path)
fill_element(:group_path_field, path) fill_element(:group_path_field, path)
fill_element(:group_name_field, path) fill_element(:group_name_field, path)
...@@ -23,6 +29,26 @@ module QA ...@@ -23,6 +29,26 @@ module QA
def create def create
click_button 'Create group' click_button 'Create group'
end end
def set_gitlab_url(url)
fill_element(:import_gitlab_url, url)
end
def set_gitlab_token(token)
fill_element(:import_gitlab_token, token)
end
# Connect gitlab instance
#
# @param [String] gitlab_url
# @param [String] gitlab_token
# @return [void]
def connect_gitlab_instance(gitlab_url, gitlab_token)
set_gitlab_url(gitlab_url)
set_gitlab_token(gitlab_token)
click_element(:connect_instance_button)
end
end end
end end
end end
......
...@@ -35,17 +35,24 @@ module QA ...@@ -35,17 +35,24 @@ module QA
element :your_projects_link element :your_projects_link
end end
view 'app/views/layouts/nav/groups_dropdown/_show.html.haml' do
element :create_group_link
element :import_group_link
end
view 'app/views/layouts/_search.html.haml' do view 'app/views/layouts/_search.html.haml' do
element :search_term_field element :search_term_field
end end
def go_to_groups def go_to_groups
within_top_menu do within_groups_menu do
click_element :groups_dropdown click_element :your_groups_link
end
end end
page.within('.qa-groups-dropdown-sidebar') do def go_to_import_group
click_element :your_groups_link within_groups_menu do
click_element :import_group_link
end end
end end
...@@ -173,6 +180,14 @@ module QA ...@@ -173,6 +180,14 @@ module QA
end end
end end
def within_groups_menu(&block)
within_top_menu do
click_element :groups_dropdown
end
page.within('.qa-groups-dropdown-sidebar', &block)
end
def click_admin_area def click_admin_area
within_top_menu { click_element :admin_area_link } within_top_menu { click_element :admin_area_link }
end end
......
...@@ -9,7 +9,7 @@ module QA ...@@ -9,7 +9,7 @@ module QA
attr_reader :unique_id attr_reader :unique_id
attr_writer :username, :password attr_writer :username, :password
attr_accessor :admin, :provider, :extern_uid, :expect_fabrication_success attr_accessor :admin, :provider, :extern_uid, :expect_fabrication_success, :hard_delete_on_api_removal
attribute :id attribute :id
attribute :name attribute :name
...@@ -19,6 +19,7 @@ module QA ...@@ -19,6 +19,7 @@ module QA
def initialize def initialize
@admin = false @admin = false
@hard_delete_on_api_removal = false
@unique_id = SecureRandom.hex(8) @unique_id = SecureRandom.hex(8)
@expect_fabrication_success = true @expect_fabrication_success = true
end end
...@@ -77,9 +78,7 @@ module QA ...@@ -77,9 +78,7 @@ module QA
def fabricate! def fabricate!
# Don't try to log-out if we're not logged-in # Don't try to log-out if we're not logged-in
if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
Page::Main::Menu.perform { |main| main.sign_out }
end
if credentials_given? if credentials_given?
Page::Main::Login.perform do |login| Page::Main::Login.perform do |login|
...@@ -103,9 +102,9 @@ module QA ...@@ -103,9 +102,9 @@ module QA
end end
def api_delete_path def api_delete_path
"/users/#{id}" "/users/#{id}?hard_delete=#{hard_delete_on_api_removal}"
rescue NoValueError rescue NoValueError
"/users/#{fetch_id(username)}" "/users/#{fetch_id(username)}?hard_delete=#{hard_delete_on_api_removal}"
end end
def api_get_path def api_get_path
...@@ -135,12 +134,12 @@ module QA ...@@ -135,12 +134,12 @@ module QA
def self.fabricate_or_use(username = nil, password = nil) def self.fabricate_or_use(username = nil, password = nil)
if Runtime::Env.signup_disabled? if Runtime::Env.signup_disabled?
self.fabricate_via_api! do |user| fabricate_via_api! do |user|
user.username = username user.username = username
user.password = password user.password = password
end end
else else
self.fabricate! do |user| fabricate! do |user|
user.username = username if username user.username = username if username
user.password = password if password user.password = password if password
end end
...@@ -149,11 +148,10 @@ module QA ...@@ -149,11 +148,10 @@ module QA
def block! def block!
response = post(Runtime::API::Request.new(api_client, api_block_path).url, nil) response = post(Runtime::API::Request.new(api_client, api_block_path).url, nil)
return if response.code == HTTP_STATUS_CREATED
unless response.code == HTTP_STATUS_CREATED
raise ResourceUpdateFailedError, "Failed to block user. Request returned (#{response.code}): `#{response}`." raise ResourceUpdateFailedError, "Failed to block user. Request returned (#{response.code}): `#{response}`."
end end
end
private private
......
# frozen_string_literal: true
module QA
RSpec.describe "Manage", :requires_admin do
describe "Group bulk import" do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:user) do
Resource::User.fabricate_via_api! do |usr|
usr.api_client = api_client
usr.hard_delete_on_api_removal = true
end
end
let!(:personal_access_token) { Runtime::API::Client.new(user: user).personal_access_token }
let!(:sandbox) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = api_client
end
end
let!(:source_group) do
Resource::Sandbox.fabricate_via_api! do |group|
group.api_client = api_client
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
end
end
let(:imported_group) do
Resource::Group.new.tap do |group|
group.api_client = api_client
group.path = source_group.path
end.reload!
rescue Resource::ApiFabricator::ResourceNotFoundError
nil
end
# Return subset of fields for comparing groups
#
# @param [Resource::Group, nil] group
# @return [Hash]
def comparable_group(group)
group&.api_resource&.except(
:id,
:web_url,
:visibility,
:full_name,
:full_path,
:created_at,
:parent_id,
:runners_token
)
end
before(:all) do
Runtime::Feature.enable(:bulk_import)
end
before do
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
source_group.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
Flow::Login.sign_in(as: user)
Page::Main::Menu.new.go_to_import_group
Page::Group::New.new.connect_gitlab_instance(Runtime::Scenario.gitlab_address, personal_access_token)
end
it(
"performs bulk group import from another gitlab instance",
testcase: "https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785",
# https://gitlab.com/gitlab-org/gitlab/-/issues/330344
exclude: { job: ["ce:relative_url", "ee:relative_url"] }
) do
Page::Group::BulkImport.perform do |import_page|
import_page.import_group(source_group.path, sandbox.path)
aggregate_failures do
expect(import_page).to have_imported_group(source_group.path, wait: 120)
expect(comparable_group(imported_group)).to eq(comparable_group(source_group))
end
end
end
after do
user.remove_via_api!
source_group.remove_via_api!
end
after(:all) do
Runtime::Feature.disable(:bulk_import)
end
end
end
end
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
require_relative '../qa' require_relative '../qa'
require 'rspec/retry' require 'rspec/retry'
require 'rspec-parameterized' require 'rspec-parameterized'
require 'active_support/core_ext/hash'
if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK'] if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK']
require 'knapsack' require 'knapsack'
......
# frozen_string_literal: true # frozen_string_literal: true
require 'active_support/core_ext/hash'
RSpec.describe QA::Specs::Runner do RSpec.describe QA::Specs::Runner do
shared_examples 'excludes orchestrated, transient, and geo' do shared_examples 'excludes orchestrated, transient, and geo' do
it 'excludes the orchestrated, transient, and geo tags, and includes default args' do it 'excludes the orchestrated, transient, and geo tags, and includes default args' do
......
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