Commit ec3150db authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 221b1159d254
......@@ -100,7 +100,7 @@ def api_delete
url = Runtime::API::Request.new(api_client, api_delete_path).url
response = delete(url)
unless response.code == HTTP_STATUS_NO_CONTENT
unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
raise ResourceNotDeletedError, "Resource at #{url} could not be deleted (#{response.code}): `#{response}`."
end
......
......@@ -70,6 +70,10 @@ def api_post_body
}
end
def api_delete_path
"/groups/#{id}"
end
def full_path
sandbox.path + ' / ' + path
end
......
# frozen_string_literal: true
require 'socket'
module QA
module Runtime
module IPAddress
include Support::Api
HostUnreachableError = Class.new(StandardError)
LOOPBACK_ADDRESS = '127.0.0.1'
PUBLIC_IP_ADDRESS_API = "https://api.ipify.org"
def fetch_current_ip_address
# When running on CI against a live environment such as staging.gitlab.com,
# we use the public facing IP address
ip_address = if Env.running_in_ci? && !URI.parse(Scenario.gitlab_address).host.include?('test')
response = get(PUBLIC_IP_ADDRESS_API)
raise HostUnreachableError, "#{PUBLIC_IP_ADDRESS_API} is unreachable" unless response.code == Support::Api::HTTP_STATUS_OK
response.body
elsif page.current_host.include?('localhost')
LOOPBACK_ADDRESS
else
Socket.ip_address_list.detect { |intf| intf.ipv4_private? }.ip_address
end
QA::Runtime::Logger.info "Current IP address: #{ip_address}"
ip_address
end
end
end
end
......@@ -6,6 +6,7 @@ module Api
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
HTTP_STATUS_NO_CONTENT = 204
HTTP_STATUS_ACCEPTED = 202
def post(url, payload)
RestClient::Request.execute(
......
......@@ -17,7 +17,7 @@
get :new
expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
......@@ -28,7 +28,21 @@
get :new
expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
context 'already in admin mode' do
before do
controller.current_user_mode.request_admin_mode!
controller.current_user_mode.enable_admin_mode!(password: user.password)
end
it 'redirects to original location' do
get :new
expect(response).to redirect_to(admin_root_path)
expect(controller.current_user_mode.admin_mode?).to be(true)
end
end
end
end
......@@ -39,7 +53,7 @@
post :create
expect(response).to have_gitlab_http_status(:not_found)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
......@@ -47,24 +61,60 @@
let(:user) { create(:admin) }
it 'sets admin mode with a valid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
post :create, params: { password: user.password }
expect(response).to redirect_to admin_root_path
expect(controller.send(:current_user_mode).admin_mode?).to be(true)
expect(controller.current_user_mode.admin_mode?).to be(true)
end
it 'fails with an invalid password' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
post :create, params: { password: '' }
expect(response).to render_template :new
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
it 'fails if not requested first' do
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# do not trigger the auth form
post :create, params: { password: user.password }
expect(response).to redirect_to(new_admin_session_path)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
it 'fails if request period expired' do
expect(controller.current_user_mode.admin_mode?).to be(false)
controller.store_location_for(:redirect, admin_root_path)
# triggering the auth form will request admin mode
get :new
Timecop.freeze(Gitlab::Auth::CurrentUserMode::ADMIN_MODE_REQUESTED_GRACE_PERIOD.from_now) do
post :create, params: { password: user.password }
expect(response).to redirect_to(new_admin_session_path)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
end
end
......@@ -75,7 +125,7 @@
get :destroy
expect(response).to have_gitlab_http_status(404)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
......@@ -83,15 +133,17 @@
let(:user) { create(:admin) }
it 'disables admin mode and redirects to main page' do
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
get :new
post :create, params: { password: user.password }
expect(controller.send(:current_user_mode).admin_mode?).to be(true)
expect(controller.current_user_mode.admin_mode?).to be(true)
get :destroy
expect(response).to have_gitlab_http_status(:found)
expect(response).to redirect_to(root_path)
expect(controller.send(:current_user_mode).admin_mode?).to be(false)
expect(controller.current_user_mode.admin_mode?).to be(false)
end
end
end
......
......@@ -814,6 +814,7 @@ def index
context 'that re-authenticated' do
before do
Gitlab::Auth::CurrentUserMode.new(user).request_admin_mode!
Gitlab::Auth::CurrentUserMode.new(user).enable_admin_mode!(password: user.password)
end
......
......@@ -62,6 +62,12 @@
end
end
context 'Helpers' do
it 'current_user_mode available' do
expect(subject.current_user_mode).not_to be_nil
end
end
def disable_user_oauth
allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false)
end
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe OmniauthCallbacksController, type: :controller do
describe OmniauthCallbacksController, type: :controller, do_not_mock_admin_mode: true do
include LoginHelpers
describe 'omniauth' do
......@@ -336,4 +336,109 @@ def stub_last_request_id(id)
end
end
end
describe 'enable admin mode' do
include_context 'custom session'
let(:provider) { :auth0 }
let(:extern_uid) { 'my-uid' }
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider) }
def reauthenticate_and_check_admin_mode(expected_admin_mode:)
# Initially admin mode disabled
expect(subject.current_user_mode.admin_mode?).to be(false)
# Trigger OmniAuth admin mode flow and expect admin mode status
post provider
expect(request.env['warden']).to be_authenticated
expect(subject.current_user_mode.admin_mode?).to be(expected_admin_mode)
end
context 'user and admin mode requested by the same user' do
before do
sign_in user
mock_auth_hash(provider.to_s, extern_uid, user.email, additional_info: {})
stub_omniauth_provider(provider, context: request)
end
context 'with a regular user' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(root_path)
end
end
context 'with an admin user' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
context 'when requested first' do
before do
subject.current_user_mode.request_admin_mode!
end
it 'can be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: true)
expect(response).to redirect_to(admin_root_path)
end
end
context 'when not requested first' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(root_path)
end
end
end
end
context 'user and admin mode requested by different users' do
let(:reauth_extern_uid) { 'another_uid' }
let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider) }
before do
sign_in user
mock_auth_hash(provider.to_s, reauth_extern_uid, reauth_user.email, additional_info: {})
stub_omniauth_provider(provider, context: request)
end
context 'with a regular user' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(profile_account_path)
end
end
context 'with an admin user' do
let(:user) { create(:omniauth_user, extern_uid: extern_uid, provider: provider, access_level: :admin) }
let(:reauth_user) { create(:omniauth_user, extern_uid: reauth_extern_uid, provider: provider, access_level: :admin) }
context 'when requested first' do
before do
subject.current_user_mode.request_admin_mode!
end
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(new_admin_session_path)
end
end
context 'when not requested first' do
it 'cannot be enabled' do
reauthenticate_and_check_admin_mode(expected_admin_mode: false)
expect(response).to redirect_to(profile_account_path)
end
end
end
end
end
end
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe 'Gcp Cluster', :js do
describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
include GoogleApi::CloudPlatformHelpers
let(:project) { create(:project) }
......
......@@ -49,11 +49,11 @@ def stop_button_selector
it 'renders second page of pipelines' do
visit_environments(project, scope: 'available')
find('.js-next-button').click
find('.page-link.next-page-item').click
wait_for_requests
expect(page).to have_selector('.gl-pagination .page', count: 2)
expect(find('.gl-pagination .page-item.active .page-link').text).to eq("2")
expect(page).to have_selector('.gl-pagination .page-link', count: 4)
expect(find('.gl-pagination .page-link.active').text).to eq("2")
end
end
......
......@@ -592,15 +592,15 @@
visit project_pipelines_path(project, page: '2')
wait_for_requests
expect(page).to have_selector('.gl-pagination .page', count: 2)
expect(page).to have_selector('.gl-pagination .page-link', count: 4)
end
it 'shows updated content' do
visit project_pipelines_path(project)
wait_for_requests
page.find('.js-next-button .page-link').click
page.find('.page-link.next-page-item').click
expect(page).to have_selector('.gl-pagination .page', count: 2)
expect(page).to have_selector('.gl-pagination .page-link', count: 4)
end
end
end
......
import { shallowMount } from '@vue/test-utils';
import $ from 'jquery';
import { GlIcon } from '@gitlab/ui';
import ClusterFormDropdown from '~/create_cluster/eks_cluster/components/cluster_form_dropdown.vue';
......@@ -169,4 +170,14 @@ describe('ClusterFormDropdown', () => {
expect(vm.findAll('.js-dropdown-item').length).toEqual(1);
expect(vm.find('.js-dropdown-item').text()).toEqual(secondItem.name);
});
it('focuses dropdown search input when dropdown is displayed', () => {
const dropdownEl = vm.find('.dropdown').element;
expect(vm.find(DropdownSearchInput).props('focused')).toBe(false);
$(dropdownEl).trigger('shown.bs.dropdown');
expect(vm.find(DropdownSearchInput).props('focused')).toBe(true);
});
});
import { mount } from '@vue/test-utils';
import DropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
describe('DropdownSearchInputComponent', () => {
let wrapper;
const defaultProps = {
placeholderText: 'Search something',
};
const buildVM = (propsData = defaultProps) => {
wrapper = mount(DropdownSearchInputComponent, {
propsData,
});
};
const findInputEl = () => wrapper.find('.dropdown-input-field');
beforeEach(() => {
buildVM();
});
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
it('renders input element with type `search`', () => {
expect(findInputEl().exists()).toBe(true);
expect(findInputEl().attributes('type')).toBe('search');
});
it('renders search icon element', () => {
expect(wrapper.find('.fa-search.dropdown-input-search').exists()).toBe(true);
});
it('renders clear search icon element', () => {
expect(wrapper.find('.fa-times.dropdown-input-clear.js-dropdown-input-clear').exists()).toBe(
true,
);
});
it('displays custom placeholder text', () => {
expect(findInputEl().attributes('placeholder')).toBe(defaultProps.placeholderText);
});
it('focuses input element when focused property equals true', () => {
const inputEl = findInputEl().element;
jest.spyOn(inputEl, 'focus');
wrapper.setProps({ focused: true });
expect(inputEl.focus).toHaveBeenCalled();
});
});
});
import { shallowMount } from '@vue/test-utils';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { GlPagination } from '@gitlab/ui';
describe('Pagination component', () => {
let wrapper;
......@@ -12,15 +13,6 @@ describe('Pagination component', () => {
});
};
const findFirstButtonLink = () => wrapper.find('.js-first-button .page-link');
const findPreviousButton = () => wrapper.find('.js-previous-button');
const findPreviousButtonLink = () => wrapper.find('.js-previous-button .page-link');
const findNextButton = () => wrapper.find('.js-next-button');
const findNextButtonLink = () => wrapper.find('.js-next-button .page-link');
const findLastButtonLink = () => wrapper.find('.js-last-button .page-link');
const findPages = () => wrapper.findAll('.page');
const findSeparator = () => wrapper.find('.separator');
beforeEach(() => {
spy = jest.fn();
});
......@@ -46,290 +38,54 @@ describe('Pagination component', () => {
expect(wrapper.isEmpty()).toBe(true);
});
describe('prev button', () => {
it('should be disabled and non clickable', () => {
mountComponent({
pageInfo: {
nextPage: 2,
page: 1,
perPage: 20,
previousPage: NaN,
total: 84,
totalPages: 5,
},
change: spy,
});
expect(findPreviousButton().classes()).toContain('disabled');
findPreviousButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be disabled and non clickable when total and totalPages are NaN', () => {
mountComponent({
pageInfo: {
nextPage: 2,
page: 1,
perPage: 20,
previousPage: NaN,
total: NaN,
totalPages: NaN,
},
change: spy,
});
expect(findPreviousButton().classes()).toContain('disabled');
findPreviousButtonLink().trigger('click');
expect(spy).not.toHaveBeenCalled();
});
it('should be enabled and clickable', () => {
mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
perPage: 20,
previousPage: 1,
total: 84,
totalPages: 5,
},
change: spy,
});
findPreviousButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
it('should be enabled and clickable when total and totalPages are NaN', () => {
mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
perPage: 20,
previousPage: 1,
total: NaN,
totalPages: NaN,
},
change: spy,
});
findPreviousButtonLink().trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
});
describe('first button', () => {
it('should call the change callback with the first page', () => {
mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
perPage: 20,
previousPage: 1,
total: 84,
totalPages: 5,
},
change: spy,
});
const button = findFirstButtonLink();
expect(button.text().trim()).toEqual('« First');
button.trigger('click');
expect(spy).toHaveBeenCalledWith(1);
});
it('should call the change callback with the first page when total and totalPages are NaN', () => {
mountComponent({
pageInfo: {
nextPage: 3,
page: 2,
perPage: 20,
previousPage: 1,
total: NaN,
totalPages: NaN,
},
change: spy,
});