Skip to content
Snippets Groups Projects
Commit 88a08249 authored by GitLab Bot's avatar GitLab Bot
Browse files

Add latest changes from gitlab-org/gitlab@master

parent 7d19df2d
No related branches found
No related tags found
No related merge requests found
Showing
with 685 additions and 354 deletions
Loading
Loading
@@ -25,11 +25,78 @@ You should then be able to see the **Packages** section on the left sidebar.
Next, you must configure your project to authorize with the GitLab Maven
repository.
 
## Authenticating to the GitLab Maven Repository
## Getting Started
 
If a project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization. Support is available for
[personal access tokens](#authenticating-with-a-personal-access-token) and
This section will cover installing Maven and building a package. This is a
quickstart to help if you're new to building Maven packages. If you're already
using Maven and understand how to build your own packages, move onto the
[next section](#adding-the-gitlab-package-registry-as-a-maven-remote).
### Installing Maven
Follow the instructions at [maven.apache.org](https://maven.apache.org/install.html)
to download and install Maven for your local development environment. Once
installation is complete, verify you can use Maven in your terminal by running:
```shell
mvn --version
```
You should see something similar to the below printed in the output:
```shell
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-04T20:00:29+01:00)
Maven home: /Users/<your_user>/apache-maven-3.6.1
Java version: 12.0.2, vendor: Oracle Corporation, runtime: /Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home
Default locale: en_GB, platform encoding: UTF-8
OS name: "mac os x", version: "10.15.2", arch: "x86_64", family: "mac"
```
### Creating a project
Understanding how to create a full Java project is outside the scope of this
guide but you can follow the steps below to create a new project that can be
published to the GitLab Package Registry.
Start by opening your terminal and creating a directory where you would like to
store the project in your environment. From inside the directory, you can run
the following Maven command to initalize a new package:
```shell
mvn archetype:generate -DgroupId=com.mycompany.mydepartment -DartifactId=my-project -DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false
```
The arguments are as follows:
- `DgroupId`: A unique string that identifies your package. You should follow
the [Maven naming conventions](https://maven.apache.org/guides/mini/guide-naming-conventions.html).
- `DartifactId`: The name of the JAR, appended to the end of the `DgroupId`.
- `DarchetypeArtifactId`: The archetype used to create the intial structure of
the project.
- `DinteractiveMode`: Create the project using batch mode (optional).
After running the command, you should see the following message, indicating that
your project has been set up successfully:
```shell
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 3.429 s
[INFO] Finished at: 2020-01-28T11:47:04Z
[INFO] ------------------------------------------------------------------------
```
You should see a new directory where you ran this command matching your
`DartifactId` parameter (in this case it should be `my-project`).
## Adding the GitLab Package Registry as a Maven remote
The next step is to add the GitLab Package Registry as a Maven remote. If a
project is private or you want to upload Maven artifacts to GitLab,
credentials will need to be provided for authorization too. Support is available
for [personal access tokens](#authenticating-with-a-personal-access-token) and
[CI job tokens](#authenticating-with-a-ci-job-token) only.
[Deploy tokens](../../project/deploy_tokens/index.md) and regular username/password
credentials do not work.
Loading
Loading
@@ -92,7 +159,9 @@ You can read more on
## Configuring your project to use the GitLab Maven repository URL
 
To download and upload packages from GitLab, you need a `repository` and
`distributionManagement` section in your `pom.xml` file.
`distributionManagement` section in your `pom.xml` file. If you're following the
steps from above, then you'll need to add the following information to your
`my-project/pom.xml` file.
 
Depending on your workflow and the amount of Maven packages you have, there are
3 ways you can configure your project to use the GitLab endpoint for Maven packages:
Loading
Loading
@@ -133,7 +202,7 @@ would look like:
```
 
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
 
Replace `PROJECT_ID` with your project ID which can be found on the home page
of your project.
Loading
Loading
@@ -186,7 +255,7 @@ the `distributionManagement` section:
```
 
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
 
Replace `my-group` with your group name and `PROJECT_ID` with your project ID
which can be found on the home page of your project.
Loading
Loading
@@ -241,7 +310,7 @@ the `distributionManagement` section:
```
 
The `id` must be the same with what you
[defined in `settings.xml`](#authenticating-to-the-gitlab-maven-repository).
[defined in `settings.xml`](#adding-the-gitlab-package-registry-as-a-maven-remote).
 
Replace `PROJECT_ID` with your project ID which can be found on the home page
of your project.
Loading
Loading
@@ -257,17 +326,85 @@ project's ID can be used for uploading.
 
## Uploading packages
 
Once you have set up the [authentication](#authenticating-to-the-gitlab-maven-repository)
and [configuration](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
Once you have set up the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
and [configured your project](#configuring-your-project-to-use-the-gitlab-maven-repository-url),
test to upload a Maven artifact from a project of yours:
 
```shell
mvn deploy
```
 
If the deploy is successful, you should see the build success message again:
```shell
...
[INFO] BUILD SUCCESS
...
```
You should also see that the upload was uploaded to the correct registry:
```shell
Uploading to gitlab-maven: https://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.jar
```
You can then navigate to your project's **Packages** page and see the uploaded
artifacts or even delete them.
 
## Installing a package
Installing a package from the GitLab Package Registry requires that you set up
the [remote and authentication](#adding-the-gitlab-package-registry-as-a-maven-remote)
as above. Once this is completed, there are two ways for installaing a package.
### Install with `mvn install`
Add the dependency manually to your project `pom.xml` file. To add the example
created above, the XML would look like:
```xml
<dependency>
<groupId>com.mycompany.mydepartment</groupId>
<artifactId>my-project</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
```
Then, inside your project, run the following:
```shell
mvn install
```
Provided everything is set up correctly, you should see the dependency
downloaded from the GitLab Package Registry:
```shell
Downloading from gitlab-maven: http://gitlab.com/api/v4/projects/PROJECT_ID/packages/maven/com/mycompany/mydepartment/my-project/1.0-SNAPSHOT/my-project-1.0-20200128.120857-1.pom
```
### Install with `mvn dependency:get`
The second way to install packages is to use the Maven commands directly.
Inside your project directory, run:
```shell
mvn dependency:get -Dartifact=com.nickkipling.app:nick-test-app:1.1-SNAPSHOT
```
You should see the same downloading message confirming that the project was
retrieved from the GitLab Package Registry.
TIP: **Tip:**
Both the XML block and Maven command are readily copy and pastable from the
Package details page, allowing for quick and easy installation.
## Removing a package
In the packages view of your project page, you can delete packages by clicking
the red trash icons or by clicking the **Delete** button on the package details
page.
## Creating Maven packages with GitLab CI/CD
 
Once you have your repository configured to use the GitLab Maven Repository,
Loading
Loading
Loading
Loading
@@ -10,12 +10,54 @@ module Gitlab
class CurrentUserMode
NotRequestedError = Class.new(StandardError)
 
# RequestStore entries
CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY = { res: :current_user_mode, data: :bypass_session_admin_id }.freeze
CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY = { res: :current_user_mode, data: :current_admin }.freeze
# SessionStore entries
SESSION_STORE_KEY = :current_user_mode
ADMIN_MODE_START_TIME_KEY = 'admin_mode'
ADMIN_MODE_REQUESTED_TIME_KEY = 'admin_mode_requested'
ADMIN_MODE_START_TIME_KEY = :admin_mode
ADMIN_MODE_REQUESTED_TIME_KEY = :admin_mode_requested
MAX_ADMIN_MODE_TIME = 6.hours
ADMIN_MODE_REQUESTED_GRACE_PERIOD = 5.minutes
 
class << self
# Admin mode activation requires storing a flag in the user session. Using this
# method when scheduling jobs in Sidekiq will bypass the session check for a
# user that was already in admin mode
def bypass_session!(admin_id)
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY] = admin_id
Gitlab::AppLogger.debug("Bypassing session in admin mode for: #{admin_id}")
yield
ensure
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY)
end
def bypass_session_admin_id
Gitlab::SafeRequestStore[CURRENT_REQUEST_BYPASS_SESSION_ADMIN_ID_RS_KEY]
end
# Store in the current request the provided user model (only if in admin mode)
# and yield
def with_current_admin(admin)
return yield unless self.new(admin).admin_mode?
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY] = admin
Gitlab::AppLogger.debug("Admin mode active for: #{admin.username}")
yield
ensure
Gitlab::SafeRequestStore.delete(CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY)
end
def current_admin
Gitlab::SafeRequestStore[CURRENT_REQUEST_ADMIN_MODE_USER_RS_KEY]
end
end
def initialize(user)
@user = user
end
Loading
Loading
@@ -42,7 +84,7 @@ module Gitlab
 
raise NotRequestedError unless admin_mode_requested?
 
reset_request_store
reset_request_store_cache_entries
 
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
Loading
Loading
@@ -55,7 +97,7 @@ module Gitlab
def disable_admin_mode!
return unless user&.admin?
 
reset_request_store
reset_request_store_cache_entries
 
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
Loading
Loading
@@ -64,7 +106,7 @@ module Gitlab
def request_admin_mode!
return unless user&.admin?
 
reset_request_store
reset_request_store_cache_entries
 
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end
Loading
Loading
@@ -73,10 +115,12 @@ module Gitlab
 
attr_reader :user
 
# RequestStore entry to cache #admin_mode? result
def admin_mode_rs_key
@admin_mode_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode? }
end
 
# RequestStore entry to cache #admin_mode_requested? result
def admin_mode_requested_rs_key
@admin_mode_requested_rs_key ||= { res: :current_user_mode, user: user.id, method: :admin_mode_requested? }
end
Loading
Loading
@@ -86,6 +130,7 @@ module Gitlab
end
 
def any_session_with_admin_mode?
return true if bypass_session?
return true if current_session_data.initiated? && current_session_data[ADMIN_MODE_START_TIME_KEY].to_i > MAX_ADMIN_MODE_TIME.ago.to_i
 
all_sessions.any? do |session|
Loading
Loading
@@ -103,7 +148,11 @@ module Gitlab
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
end
 
def reset_request_store
def bypass_session?
user&.id && user.id == self.class.bypass_session_admin_id
end
def reset_request_store_cache_entries
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
end
Loading
Loading
Loading
Loading
@@ -11,7 +11,7 @@ module Gitlab
 
module Storage
# Class that returns the disk path for a project using hashed storage
class HashedProject
class Hashed
attr_accessor :project
 
ROOT_PATH_PREFIX = '@hashed'
Loading
Loading
@@ -121,7 +121,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
Storage::HashedProject.new(self)
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
Loading
Loading
Loading
Loading
@@ -46,7 +46,7 @@ module Gitlab
 
module Storage
# Class that returns the disk path for a project using hashed storage
class HashedProject
class Hashed
attr_accessor :project
 
ROOT_PATH_PREFIX = '@hashed'
Loading
Loading
@@ -176,7 +176,7 @@ module Gitlab
def storage
@storage ||=
if hashed_storage?
Storage::HashedProject.new(self)
Storage::Hashed.new(self)
else
Storage::LegacyProject.new(self)
end
Loading
Loading
Loading
Loading
@@ -17,6 +17,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::BatchLoader
chain.add Labkit::Middleware::Sidekiq::Server
chain.add Gitlab::SidekiqMiddleware::InstrumentationLogger
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
chain.add Gitlab::SidekiqStatus::ServerMiddleware
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Server
end
Loading
Loading
@@ -31,6 +32,7 @@ module Gitlab
chain.add Gitlab::SidekiqMiddleware::ClientMetrics
chain.add Gitlab::SidekiqMiddleware::WorkerContext::Client # needs to be before the Labkit middleware
chain.add Labkit::Middleware::Sidekiq::Client
chain.add Gitlab::SidekiqMiddleware::AdminMode::Client
end
end
end
Loading
Loading
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module AdminMode
# Checks if admin mode is enabled for the request creating the sidekiq job
# by examining if admin mode has been enabled for the user
# If enabled then it injects a job field that persists through the job execution
class Client
def call(_worker_class, job, _queue, _redis_pool)
return yield unless Feature.enabled?(:user_mode_in_session)
# Admin mode enabled in the original request or in a nested sidekiq job
admin_mode_user_id = find_admin_user_id
if admin_mode_user_id
job['admin_mode_user_id'] ||= admin_mode_user_id
Gitlab::AppLogger.debug("AdminMode::Client injected admin mode for job: #{job.inspect}")
end
yield
end
private
def find_admin_user_id
Gitlab::Auth::CurrentUserMode.current_admin&.id ||
Gitlab::Auth::CurrentUserMode.bypass_session_admin_id
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module SidekiqMiddleware
module AdminMode
class Server
def call(_worker, job, _queue)
return yield unless Feature.enabled?(:user_mode_in_session)
admin_mode_user_id = job['admin_mode_user_id']
# Do not bypass session if this job was not enabled with admin mode on
return yield unless admin_mode_user_id
Gitlab::Auth::CurrentUserMode.bypass_session!(admin_mode_user_id) do
Gitlab::AppLogger.debug("AdminMode::Server bypasses session for admin mode in job: #{job.inspect}")
yield
end
end
end
end
end
end
Loading
Loading
@@ -36,10 +36,7 @@ module MicrosoftTeams
 
attachments = options[:attachments]
unless attachments.blank?
result['sections'] << {
'title' => 'Details',
'facts' => [{ 'name' => 'Attachments', 'value' => attachments }]
}
result['sections'] << { text: attachments }
end
 
result.to_json
Loading
Loading
Loading
Loading
@@ -15,7 +15,7 @@ module QA
disable_optional_jobs(project)
end
 
describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: 'https://gitlab.com/gitlab-org/gitlab/issues/118481' do
describe 'Auto DevOps support', :orchestrated, :kubernetes do
context 'when rbac is enabled' do
let(:cluster) { Service::KubernetesCluster.new.create! }
 
Loading
Loading
Loading
Loading
@@ -91,7 +91,9 @@ module Trigger
'TOP_UPSTREAM_SOURCE_PROJECT' => ENV['CI_PROJECT_PATH'],
'TOP_UPSTREAM_SOURCE_JOB' => ENV['CI_JOB_URL'],
'TOP_UPSTREAM_SOURCE_SHA' => ENV['CI_COMMIT_SHA'],
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME']
'TOP_UPSTREAM_SOURCE_REF' => ENV['CI_COMMIT_REF_NAME'],
'TOP_UPSTREAM_MERGE_REQUEST_PROJECT_ID' => ENV['CI_MERGE_REQUEST_PROJECT_ID'],
'TOP_UPSTREAM_MERGE_REQUEST_IID' => ENV['CI_MERGE_REQUEST_IID']
}
end
 
Loading
Loading
# frozen_string_literal: true
require 'spec_helper'
# Test an operation that triggers background jobs requiring administrative rights
describe 'Admin mode for workers', :do_not_mock_admin_mode, :request_store, :clean_gitlab_redis_shared_state do
let(:user) { create(:user) }
let(:user_to_delete) { create(:user) }
before do
add_sidekiq_middleware
sign_in(user)
end
context 'as a regular user' do
it 'cannot delete user' do
visit admin_user_path(user_to_delete)
expect(page).to have_gitlab_http_status(:not_found)
end
end
context 'as an admin user' do
let(:user) { create(:admin) }
context 'when admin mode disabled' do
it 'cannot delete user', :js do
visit admin_user_path(user_to_delete)
expect(page).to have_content('Re-authentication required')
end
end
context 'when admin mode enabled', :delete do
before do
gitlab_enable_admin_mode_sign_in(user)
end
it 'can delete user', :js do
visit admin_user_path(user_to_delete)
click_button 'Delete user'
page.within '.modal-dialog' do
find("input[name='username']").send_keys(user_to_delete.name)
click_button 'Delete user'
wait_for_requests
end
expect(page).to have_content('The user is being deleted.')
# Perform jobs while logged out so that admin mode is only enabled in job metadata
execute_jobs_signed_out(user)
visit admin_user_path(user_to_delete)
expect(page).to have_title('Not Found')
end
end
end
def add_sidekiq_middleware
Sidekiq::Testing.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::AdminMode::Server
end
end
def execute_jobs_signed_out(user)
gitlab_sign_out
Sidekiq::Worker.drain_all
sign_in(user)
gitlab_enable_admin_mode_sign_in(user)
end
end
Loading
Loading
@@ -2,46 +2,64 @@
 
require 'spec_helper'
 
describe 'Admin uses repository checks' do
describe 'Admin uses repository checks', :request_store, :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV
 
let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(create(:admin))
sign_in(admin)
end
 
it 'to trigger a single check' do
project = create(:project)
visit_admin_project_page(project)
context 'when admin mode is disabled' do
it 'admin project page requires admin mode' do
project = create(:project)
visit_admin_project_page(project)
 
page.within('.repository-check') do
click_button 'Trigger repository check'
expect(page).not_to have_css('.repository-check')
expect(page).to have_content('Enter Admin Mode')
end
expect(page).to have_content('Repository check was triggered')
end
 
it 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
last_repository_check_at: Time.now
)
visit_admin_project_page(project)
context 'when admin mode is enabled' do
before do
gitlab_enable_admin_mode_sign_in(admin)
end
it 'to trigger a single check', :js do
project = create(:project)
visit_admin_project_page(project)
page.within('.repository-check') do
click_button 'Trigger repository check'
end
 
page.within('.alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
expect(page).to have_content('Repository check was triggered')
end
end
 
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
it 'to see a single failed repository check', :js do
project = create(:project)
project.update_columns(
last_repository_check_failed: true,
last_repository_check_at: Time.now
)
visit_admin_project_page(project)
page.within('.alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
 
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
 
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
 
expect(page).to have_content('Started asynchronous removal of all repository check states.')
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
end
 
def visit_admin_project_page(project)
Loading
Loading
Loading
Loading
@@ -30,6 +30,10 @@ describe 'AWS EKS Cluster', :js do
it 'user sees a form to create an EKS cluster' do
expect(page).to have_content('Create new cluster on EKS')
end
it 'highlights Amazon EKS logo' do
expect(page).to have_css('.js-create-aws-cluster-button.active')
end
end
end
end
Loading
Loading
@@ -38,6 +38,10 @@ describe 'Gcp Cluster', :js, :do_not_mock_admin_mode do
click_link 'Google GKE'
end
 
it 'highlights Google GKE logo' do
expect(page).to have_css('.js-create-gcp-cluster-button.active')
end
context 'when user filled form with valid parameters' do
subject { submit_form }
 
Loading
Loading
Loading
Loading
@@ -360,7 +360,7 @@ shared_examples 'Signup' do
InvisibleCaptcha.timestamp_enabled = true
stub_application_setting(recaptcha_enabled: true)
allow_next_instance_of(RegistrationsController) do |instance|
allow(instance).to receive(:verify_recaptcha).and_return(false)
allow(instance).to receive(:verify_recaptcha).and_return(true)
end
end
 
Loading
Loading
@@ -368,28 +368,53 @@ shared_examples 'Signup' do
InvisibleCaptcha.timestamp_enabled = false
end
 
it 'prevents from signing up' do
visit new_user_registration_path
context 'when reCAPTCHA detects malicious behaviour' do
before do
allow_next_instance_of(RegistrationsController) do |instance|
allow(instance).to receive(:verify_recaptcha).and_return(false)
end
end
 
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
it 'prevents from signing up' do
visit new_user_registration_path
 
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
end
fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.not_to change { User.count }
expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
end
end
 
fill_in 'new_user_password', with: new_user.password
context 'when invisible captcha detects malicious behaviour' do
it 'prevents from signing up' do
visit new_user_registration_path
 
expect { click_button 'Register' }.not_to change { User.count }
fill_in 'new_user_username', with: new_user.username
fill_in 'new_user_email', with: new_user.email
 
if Gitlab::Experimentation.enabled?(:signup_flow)
if Gitlab::Experimentation.enabled?(:signup_flow)
fill_in 'new_user_first_name', with: new_user.first_name
fill_in 'new_user_last_name', with: new_user.last_name
else
fill_in 'new_user_name', with: new_user.name
fill_in 'new_user_email_confirmation', with: new_user.email
end
fill_in 'new_user_password', with: new_user.password
expect { click_button 'Register' }.not_to change { User.count }
expect(page).to have_content('That was a bit too quick! Please resubmit.')
else
expect(page).to have_content('There was an error with the reCAPTCHA. Please solve the reCAPTCHA again.')
end
end
end
Loading
Loading
Loading
Loading
@@ -157,17 +157,21 @@ describe('Jobs Store Mutations', () => {
});
});
 
describe('STOP_POLLING_TRACE', () => {
it('sets isTraceComplete to true', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy);
describe('SET_TRACE_TIMEOUT', () => {
it('sets the traceTimeout id', () => {
const id = 7;
 
expect(stateCopy.isTraceComplete).toEqual(true);
expect(stateCopy.traceTimeout).not.toEqual(id);
mutations[types.SET_TRACE_TIMEOUT](stateCopy, id);
expect(stateCopy.traceTimeout).toEqual(id);
});
});
 
describe('RECEIVE_TRACE_ERROR', () => {
it('resets trace state and sets error to true', () => {
mutations[types.RECEIVE_TRACE_ERROR](stateCopy);
describe('STOP_POLLING_TRACE', () => {
it('sets isTraceComplete to true', () => {
mutations[types.STOP_POLLING_TRACE](stateCopy);
 
expect(stateCopy.isTraceComplete).toEqual(true);
});
Loading
Loading
import { shallowMount } from '@vue/test-utils';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
import expirationPolicyForm from '~/registry/shared/components/expiration_policy_form.vue';
import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
import { createStore } from '~/registry/settings/store/';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
Loading
Loading
@@ -14,14 +14,34 @@ describe('Settings Form', () => {
let store;
let dispatchSpy;
 
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const GlCard = {
name: 'gl-card-stub',
template: `
<div>
<slot name="header"></slot>
<slot></slot>
<slot name="footer"></slot>
</div>
`,
};
const trackingPayload = {
label: 'docker_container_retention_and_expiration_policies',
};
 
const findForm = () => wrapper.find(expirationPolicyForm);
const findForm = () => wrapper.find({ ref: 'form-element' });
const findFields = () => wrapper.find(expirationPolicyFields);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
 
const mountComponent = () => {
wrapper = shallowMount(component, {
stubs: {
GlCard,
GlLoadingIcon,
},
mocks: {
$toast: {
show: jest.fn(),
Loading
Loading
@@ -47,46 +67,50 @@ describe('Settings Form', () => {
let form;
beforeEach(() => {
form = findForm();
dispatchSpy.mockReturnValue();
});
 
describe('data binding', () => {
it('v-model change update the settings property', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('input', 'foo');
findFields().vm.$emit('input', 'foo');
expect(dispatchSpy).toHaveBeenCalledWith('updateSettings', { settings: 'foo' });
});
});
 
describe('form reset event', () => {
beforeEach(() => {
form.trigger('reset');
});
it('calls the appropriate function', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings');
});
 
it('tracks the reset event', () => {
dispatchSpy.mockReturnValue();
form.vm.$emit('reset');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
});
});
 
describe('form submit event ', () => {
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
it('dispatches the saveSettings action', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
});
 
it('tracks the submit event', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
});
 
it('show a success toast when submit succeed', () => {
dispatchSpy.mockResolvedValue();
form.vm.$emit('submit');
form.trigger('submit');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, {
type: 'success',
Loading
Loading
@@ -96,7 +120,7 @@ describe('Settings Form', () => {
 
it('show an error toast when submit fails', () => {
dispatchSpy.mockRejectedValue();
form.vm.$emit('submit');
form.trigger('submit');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, {
type: 'error',
Loading
Loading
@@ -105,4 +129,52 @@ describe('Settings Form', () => {
});
});
});
describe('form actions', () => {
describe('cancel button', () => {
beforeEach(() => {
store.commit('SET_SETTINGS', { foo: 'bar' });
});
it('has type reset', () => {
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when isEdited is false', () =>
wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
}));
it('is disabled isLoading is true', () => {
store.commit('TOGGLE_LOADING');
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
store.commit('TOGGLE_LOADING');
});
});
it('is enabled when isLoading is false and isEdited is true', () => {
store.commit('UPDATE_SETTINGS', { settings: { foo: 'baz' } });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('when isLoading is true', () => {
beforeEach(() => {
store.commit('TOGGLE_LOADING');
});
afterEach(() => {
store.commit('TOGGLE_LOADING');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button).exists()).toBe(true);
});
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<div
class="lh-2"
>
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Expiration Policy Form renders 1`] = `
<form
class="lh-2"
>
<div
class="card"
>
<!---->
<div
class="card-header"
>
Tag expiration policy
</div>
<div
class="card-body"
>
<!---->
<!---->
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Number of tags to retain:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
>
<option
value="foo"
>
Foo
</option>
<option
value="bar"
>
Bar
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Docker tags with names matching this regex pattern will expire:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
<div
class="card-footer"
>
<div
class="d-flex justify-content-end"
>
<glbutton-stub
class="mr-2 d-block"
size="md"
type="reset"
variant="secondary"
>
Cancel
</glbutton-stub>
<glbutton-stub
class="d-flex justify-content-center align-items-center js-no-auto-disable"
size="md"
type="submit"
variant="success"
>
Save expiration policy
<!---->
</glbutton-stub>
</div>
</div>
<!---->
</div>
</form>
`;
import { mount } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/shared/components/expiration_policy_form.vue';
import component from '~/registry/shared/components/expiration_policy_fields.vue';
 
import { NAME_REGEX_LENGTH } from '~/registry/shared/constants';
import { formOptions } from '../mock_data';
Loading
Loading
@@ -10,22 +10,14 @@ describe('Expiration Policy Form', () => {
 
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
 
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
const findFormGroup = name => wrapper.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}-group`);
const findFormElements = (name, parent = wrapper) =>
parent.find(`${FORM_ELEMENTS_ID_PREFIX}-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findForm = () => wrapper.find({ ref: 'form-element' });
const findLoadingIcon = (parent = wrapper) => parent.find(GlLoadingIcon);
 
const mountComponent = props => {
wrapper = mount(component, {
stubs: {
...stubChildren(component),
GlCard: false,
GlLoadingIcon,
},
propsData: {
formOptions,
Loading
Loading
@@ -114,77 +106,20 @@ describe('Expiration Policy Form', () => {
},
);
 
describe('form actions', () => {
describe('cancel button', () => {
it('has type reset', () => {
mountComponent();
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('is disabled when disableCancelButton is true', () => {
mountComponent({ disableCancelButton: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is disabled isLoading is true', () => {
mountComponent({ isLoading: true });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe('true');
});
});
it('is enabled when isLoading and disableCancelButton are false', () => {
mountComponent({ disableCancelButton: false, isLoading: false });
return wrapper.vm.$nextTick().then(() => {
expect(findCancelButton().attributes('disabled')).toBe(undefined);
});
});
});
describe('form cancel event', () => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('reset');
expect(wrapper.emitted('reset')).toBeTruthy();
});
});
it('save has type submit', () => {
mountComponent();
expect(findSaveButton().attributes('type')).toBe('submit');
});
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
});
it.each`
elementName
${'toggle'}
${'interval'}
${'schedule'}
${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
it('submit button is disabled and shows a spinner', () => {
const button = findSaveButton();
expect(button.attributes('disabled')).toBeTruthy();
expect(findLoadingIcon(button)).toExist();
});
describe('when isLoading is true', () => {
beforeEach(() => {
mountComponent({ isLoading: true });
});
 
describe('form submit event ', () => {
it('calls the appropriate function', () => {
mountComponent();
findForm().trigger('submit');
expect(wrapper.emitted('submit')).toBeTruthy();
});
it.each`
elementName
${'toggle'}
${'interval'}
${'schedule'}
${'latest'}
${'name-matching'}
`(`${FORM_ELEMENTS_ID_PREFIX}-$elementName is disabled`, ({ elementName }) => {
expect(findFormElements(elementName).attributes('disabled')).toBe('true');
});
});
 
Loading
Loading
@@ -196,20 +131,20 @@ describe('Expiration Policy Form', () => {
mountComponent({ value: { name_regex: invalidString } });
});
 
it('save btn is disabled', () => {
expect(findSaveButton().attributes('disabled')).toBeTruthy();
});
it('nameRegexState is false', () => {
expect(wrapper.vm.nameRegexState).toBe(false);
});
it('emit the @invalidated event', () => {
expect(wrapper.emitted('invalidated')).toBeTruthy();
});
});
 
it('if the user did not type validation is null', () => {
mountComponent({ value: { name_regex: '' } });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.nameRegexState).toBe(null);
expect(findSaveButton().attributes('disabled')).toBeFalsy();
expect(wrapper.emitted('validated')).toBeTruthy();
});
});
 
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment