Commit 2e302b5b authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'ce-to-ee-2018-01-23' into 'master'

CE upstream - Tuesday

Closes #4419, gitaly#936, and gitaly#890

See merge request gitlab-org/gitlab-ee!4210
parents 88c63106 672419bb
......@@ -422,7 +422,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.73.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.74.0', require: 'gitaly'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -309,7 +309,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
gitaly-proto (0.73.0)
gitaly-proto (0.74.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (4.7.6)
......@@ -1091,7 +1091,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
gitaly-proto (~> 0.73.0)
gitaly-proto (~> 0.74.0)
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-license (~> 1.0)
......
......@@ -118,14 +118,14 @@ export const showSubLevelItems = (el) => {
moveSubItemsToPosition(el, subItems);
};
export const mouseEnterTopItems = (el) => {
export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
if (currentOpenMenu) hideMenu(currentOpenMenu);
showSubLevelItems(el);
}, getHideSubItemsInterval());
}, timeout);
};
export const mouseLeaveTopItem = (el) => {
......
......@@ -210,7 +210,7 @@
</div>
<span class="help-block">{{ visibilityLevelDescription }}</span>
<label
v-if="visibilityLevel !== visibilityOptions.PUBLIC"
v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access"
>
<input
......
---
title: Fix missing "allow users to request access" option in public project permissions
merge_request: 16485
author:
type: fixed
worker_processes 2
timeout 60
before_fork do |server, worker|
if /darwin/ =~ RUBY_PLATFORM
require 'fiddle'
# Dynamically load Foundation.framework, ~implicitly~ initialising
# the Objective-C runtime before any forking happens in Unicorn
#
# From https://bugs.ruby-lang.org/issues/14009
Fiddle.dlopen '/System/Library/Frameworks/Foundation.framework/Foundation'
end
end
......@@ -27,10 +27,23 @@ This exported module should be used instead of directly using `axios` to ensure
});
```
## Mock axios response on tests
## Mock axios response in tests
To help us mock the responses we need we use [axios-mock-adapter][axios-mock-adapter]
To help us mock the responses we are using [axios-mock-adapter][axios-mock-adapter].
Advantages over [`spyOn()`]:
- no need to create response objects
- does not allow call through (which we want to avoid)
- simple API to test error cases
- provides `replyOnce()` to allow for different responses
We have also decided against using [axios interceptors] because they are not suitable for mocking.
[axios interceptors]: https://github.com/axios/axios#interceptors
[`spyOn()`]: https://jasmine.github.io/api/edge/global.html#spyOn
### Example
```javascript
import axios from '~/lib/utils/axios_utils';
......@@ -54,7 +67,7 @@ To help us mock the responses we need we use [axios-mock-adapter][axios-mock-ada
});
```
### Mock poll requests on tests with axios
### Mock poll requests in tests with axios
Because polling function requires a header object, we need to always include an object as the third argument:
......
......@@ -169,6 +169,30 @@ For Omnibus GitLab packages:
1. [Reconfigure GitLab] for the changes to take effect
#### Digital Ocean Spaces and other S3-compatible providers
Not all S3 providers are fully-compatible with the Fog library. For example,
if you see `411 Length Required` errors after attempting to upload, you may
need to downgrade the `aws_signature_version` value from the default value to
2 [due to this issue](https://github.com/fog/fog-aws/issues/428).
1. For example, with [Digital Ocean Spaces](https://www.digitalocean.com/products/spaces/),
this example configuration can be used for a bucket in Amsterdam (AMS3):
```ruby
gitlab_rails['backup_upload_connection'] = {
'provider' => 'AWS',
'region' => 'ams3',
'aws_access_key_id' => 'AKIAKIAKI',
'aws_secret_access_key' => 'secret123',
'aws_signature_version' => 2,
'endpoint' => 'https://ams3.digitaloceanspaces.com'
}
gitlab_rails['backup_upload_remote_directory'] = 'my.s3.bucket'
```
1. [Reconfigure GitLab] for the changes to take effect
---
For installations from source:
......
# Rack attack
# Rack Attack
To prevent abusive clients doing damage GitLab uses rack-attack gem.
Rack Attack, also known as Rack::Attack, is [a rubygem](https://github.com/kickstarter/rack-attack)
that is meant to protect GitLab with the ability to customize throttling and
blocking user IPs.
You can prevent brute-force passwords attacks, scrapers, or any other offenders
by throttling requests from IP addresses making large volumes of requests.
In case you find throttling is not enough to protect you against abusive clients,
Rack Attack offers IP whitelisting, blacklisting, Fail2ban style filtering and
tracking.
If you installed or upgraded GitLab by following the official guides this should be enabled by default.
By default, user sign-in, user sign-up (if enabled), and user password reset is
limited to 6 requests per minute. After trying for 6 times, the client will
have to wait for the next minute to be able to try again.
If you are missing `config/initializers/rack_attack.rb` the following steps need to be taken in order to enable protection for your GitLab instance:
If you installed or upgraded GitLab by following the [official guides](../install/README.md)
this should be enabled by default. If your instance is not exposed to any incoming
connections, it is recommended to disable Rack Attack.
1. In config/application.rb find and uncomment the following line:
For more information on how to use these options check out
[rack-attack README](https://github.com/kickstarter/rack-attack/blob/master/README.md).
config.middleware.use Rack::Attack
## Settings
1. Rename `config/initializers/rack_attack.rb.example` to `config/initializers/rack_attack.rb`.
**Omnibus GitLab**
1. Review the `paths_to_be_protected` and add any other path you need protecting.
1. Open `/etc/gitlab/gitlab.rb` with you editor
1. Add the following:
1. Restart GitLab instance.
```ruby
gitlab_rails['rack_attack_git_basic_auth'] = {
'enabled' => true,
'ip_whitelist' => ["127.0.0.1"],
'maxretry' => 10,
'findtime' => 60,
'bantime' => 3600
}
```
By default, user sign-in, user sign-up(if enabled) and user password reset is limited to 6 requests per minute. After trying for 6 times, client will have to wait for the next minute to be able to try again. These settings can be found in `config/initializers/rack_attack.rb`
3. Reconfigure GitLab:
If you want more restrictive/relaxed throttle rule change the `limit` or `period` values. For example, more relaxed throttle rule will be if you set limit: 3 and period: 1.second(this will allow 3 requests per second). You can also add other paths to the protected list by adding to `paths_to_be_protected` variable. If you change any of these settings do not forget to restart your GitLab instance.
```
sudo gitlab-ctl reconfigure
```
In case you find throttling is not enough to protect you against abusive clients, rack-attack gem offers IP whitelisting, blacklisting, Fail2ban style filter and tracking.
The following settings can be configured:
For more information on how to use these options check out [rack-attack README](https://github.com/kickstarter/rack-attack/blob/master/README.md).
- `enabled`: By default this is set to `true`. Set this to `false` to disable Rack Attack.
- `ip_whitelist`: Whitelist any IPs from being blocked. They must be formatted as strings within a ruby array.
For example, `["127.0.0.1", "127.0.0.2", "127.0.0.3"]`.
- `maxretry`: The maximum amount of times a request can be made in the
specified time.
- `findtime`: The maximum amount of time failed requests can count against an IP
before it's blacklisted.
- `bantime`: The total amount of time that a blacklisted IP will be blocked in
seconds.
**Installations from source**
These settings can be found in `config/initializers/rack_attack.rb`. If you are
missing `config/initializers/rack_attack.rb`, the following steps need to be
taken in order to enable protection for your GitLab instance:
1. In `config/application.rb` find and uncomment the following line:
```ruby
config.middleware.use Rack::Attack
```
1. Copy `config/initializers/rack_attack.rb.example` to `config/initializers/rack_attack.rb`
1. Open `config/initializers/rack_attack.rb`, review the
`paths_to_be_protected`, and add any other path you need protecting
1. Restart GitLab:
```sh
sudo service gitlab restart
```
If you want more restrictive/relaxed throttle rules, edit
`config/initializers/rack_attack.rb` and change the `limit` or `period` values.
For example, more relaxed throttle rules will be if you set
`limit: 3` and `period: 1.seconds` (this will allow 3 requests per second).
You can also add other paths to the protected list by adding to `paths_to_be_protected`
variable. If you change any of these settings do not forget to restart your
GitLab instance.
## Remove blocked IPs from Rack Attack via Redis
In case you want to remove a blocked IP, follow these steps:
1. Find the IPs that have been blocked in the production log:
```sh
grep "Rack_Attack" /var/log/gitlab/gitlab-rails/production.log
```
2. Since the blacklist is stored in Redis, you need to open up `redis-cli`:
```sh
/opt/gitlab/embedded/bin/redis-cli -s /var/opt/gitlab/redis/redis.socket
```
3. You can remove the block using the following syntax, replacing `<ip>` with
the actual IP that is blacklisted:
```
del cache:gitlab:rack::attack:allow2ban:ban:<ip>
```
4. Confirm that the key with the IP no longer shows up:
```
keys *rack::attack*
```
5. Optionally, add the IP to the whitelist to prevent it from being blacklisted
again (see [settings](#settings)).
## Troubleshooting
### Rack attack is blacklisting the load balancer
Rack Attack may block your load balancer if all traffic appears to come from
the load balancer. In that case, you will need to:
1. [Configure `nginx[real_ip_trusted_addresses]`](https://docs.gitlab.com/omnibus/settings/nginx.html#configuring-gitlab-trusted_proxies-and-the-nginx-real_ip-module).
This will keep users' IPs from being listed as the load balancer IPs.
2. Whitelist the load balancer's IP address(es) in the Rack Attack [settings](#settings).
3. Reconfigure GitLab:
```
sudo gitlab-ctl reconfigure
```
4. [Remove the block via Redis.](#remove-blocked-ips-from-rack-attack-via-redis)
......@@ -1268,32 +1268,15 @@ module Gitlab
author_email: nil, author_name: nil,
start_branch_name: nil, start_repository: self)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
index = Gitlab::Git::Index.new(self)
parents = []
if start_commit
index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
gitaly_migrate(:operation_user_commit_files) do |is_enabled|
if is_enabled
gitaly_operation_client.user_commit_files(user, branch_name,
message, actions, author_email, author_name,
start_branch_name, start_repository)
else
rugged_multi_action(user, branch_name, message, actions,
author_email, author_name, start_branch_name, start_repository)
end
actions.each { |opts| index.apply(opts.delete(:action), opts) }
committer = user_to_committer(user)
author = Gitlab::Git.committer_hash(email: author_email, name: author_name) || committer
options = {
tree: index.write_tree,
message: message,
parents: parents,
author: author,
committer: committer
}
create_commit(options)
end
end
# rubocop:enable Metrics/ParameterLists
......@@ -2079,6 +2062,39 @@ module Gitlab
remove_remote(remote_name)
end
def rugged_multi_action(
user, branch_name, message, actions, author_email, author_name,
start_branch_name, start_repository)
OperationService.new(user, self).with_branch(
branch_name,
start_branch_name: start_branch_name,
start_repository: start_repository
) do |start_commit|
index = Gitlab::Git::Index.new(self)
parents = []
if start_commit
index.read_tree(start_commit.rugged_commit.tree)
parents = [start_commit.sha]
end
actions.each { |opts| index.apply(opts.delete(:action), opts) }
committer = user_to_committer(user)
author = Gitlab::Git.committer_hash(email: author_email, name: author_name) || committer
options = {
tree: index.write_tree,
message: message,
parents: parents,
author: author,
committer: committer
}
create_commit(options)
end
end
def fetch_remote(remote_name = 'origin', env: nil)
run_git(['fetch', remote_name], env: env).last.zero?
end
......
......@@ -3,6 +3,8 @@ module Gitlab
class OperationService
include Gitlab::EncodingHelper
MAX_MSG_SIZE = 128.kilobytes.freeze
def initialize(repository)
@gitaly_repo = repository.gitaly_repository
@repository = repository
......@@ -175,6 +177,49 @@ module Gitlab
end
end
def user_commit_files(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
req_enum = Enumerator.new do |y|
header = user_commit_files_request_header(user, branch_name,
commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
y.yield Gitaly::UserCommitFilesRequest.new(header: header)
actions.each do |action|
action_header = user_commit_files_action_header(action)
y.yield Gitaly::UserCommitFilesRequest.new(
action: Gitaly::UserCommitFilesAction.new(header: action_header)
)
reader = binary_stringio(action[:content])
until reader.eof?
chunk = reader.read(MAX_MSG_SIZE)
y.yield Gitaly::UserCommitFilesRequest.new(
action: Gitaly::UserCommitFilesAction.new(content: chunk)
)
end
end
end
response = GitalyClient.call(@repository.storage, :operation_service,
:user_commit_files, req_enum, remote_storage: start_repository.storage)
if (pre_receive_error = response.pre_receive_error.presence)
raise Gitlab::Git::HooksService::PreReceiveError, pre_receive_error
end
if (index_error = response.index_error.presence)
raise Gitlab::Git::Index::IndexError, index_error
end
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
private
def call_cherry_pick_or_revert(rpc, user:, commit:, branch_name:, message:, start_branch_name:, start_repository:)
......@@ -212,6 +257,33 @@ module Gitlab
Gitlab::Git::OperationService::BranchUpdate.from_gitaly(response.branch_update)
end
end
def user_commit_files_request_header(
user, branch_name, commit_message, actions, author_email, author_name,
start_branch_name, start_repository)
Gitaly::UserCommitFilesRequestHeader.new(
repository: @gitaly_repo,
user: Gitlab::Git::User.from_gitlab(user).to_gitaly,
branch_name: encode_binary(branch_name),
commit_message: encode_binary(commit_message),
commit_author_name: encode_binary(author_name),
commit_author_email: encode_binary(author_email),
start_branch_name: encode_binary(start_branch_name),
start_repository: start_repository.gitaly_repository
)
end
def user_commit_files_action_header(action)
Gitaly::UserCommitFilesActionHeader.new(
action: action[:action].upcase.to_sym,
file_path: encode_binary(action[:file_path]),
previous_path: encode_binary(action[:previous_path]),
base64_content: action[:encoding] == 'base64'
)
rescue RangeError
raise ArgumentError, "Unknown action '#{action[:action]}'"
end
end
end
end
......@@ -6,3 +6,4 @@ gem 'capybara-screenshot', '~> 1.0.18'
gem 'rake', '~> 12.3.0'
gem 'rspec', '~> 3.7'
gem 'selenium-webdriver', '~> 3.8.0'
gem 'airborne', '~> 0.2.13'
GEM
remote: https://rubygems.org/
specs:
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
addressable (2.5.2)
public_suffix (>= 2.0.2, < 4.0)
airborne (0.2.13)
activesupport
rack
rack-test (~> 0.6, >= 0.6.2)
rest-client (>= 1.7.3, < 3.0)
rspec (~> 3.1)
byebug (9.1.0)
capybara (2.16.1)
addressable
......@@ -17,13 +28,25 @@ GEM
childprocess (0.8.0)
ffi (~> 1.0, >= 1.0.11)
coderay (1.1.2)
concurrent-ruby (1.0.5)
diff-lcs (1.3)
domain_name (0.5.20170404)
unf (>= 0.0.5, < 1.0.0)
ffi (1.9.18)
http-cookie (1.0.3)
domain_name (~> 0.5)
i18n (0.9.1)
concurrent-ruby (~> 1.0)
launchy (2.4.3)
addressable (~> 2.3)
method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_mime (1.0.0)
mini_portile2 (2.3.0)
minitest (5.11.1)
netrc (0.11.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
pry (0.11.3)
......@@ -37,11 +60,15 @@ GEM
rack-test (0.8.2)
rack (>= 1.0, < 3)
rake (12.3.0)
rest-client (2.0.2)
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.0)
rspec-core (3.7.1)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
......@@ -54,6 +81,12 @@ GEM
selenium-webdriver (3.8.0)
childprocess (~> 0.5)
rubyzip (~> 1.0)
thread_safe (0.3.6)
tzinfo (1.2.4)
thread_safe (~> 0.1)
unf (0.1.4)
unf_ext
unf_ext (0.0.7.4)
xpath (2.1.0)
nokogiri (~> 1.3)
......@@ -61,6 +94,7 @@ PLATFORMS
ruby
DEPENDENCIES
airborne (~> 0.2.13)
capybara (~> 2.16.1)
capybara-screenshot (~> 1.0.18)
pry-byebug (~> 3.5.1)
......@@ -69,4 +103,4 @@ DEPENDENCIES
selenium-webdriver (~> 3.8.0)
BUNDLED WITH
1.16.0
1.16.1
......@@ -11,6 +11,8 @@ module QA
autoload :Scenario, 'qa/runtime/scenario'
autoload :Browser, 'qa/runtime/browser'
autoload :Env, 'qa/runtime/env'
autoload :Address, 'qa/runtime/address'
autoload :API, 'qa/runtime/api'
end
##
......@@ -26,6 +28,7 @@ module QA
autoload :Group, 'qa/factory/resource/group'
autoload :Project, 'qa/factory/resource/project'
autoload :DeployKey, 'qa/factory/resource/deploy_key'
autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token'
end
module Repository
......@@ -85,6 +88,7 @@ module QA
autoload :Main, 'qa/page/menu/main'
autoload :Side, 'qa/page/menu/side'
autoload :Admin, 'qa/page/menu/admin'
autoload :Profile, 'qa/page/menu/profile'
end
module Dashboard
......@@ -108,6 +112,10 @@ module QA
end
end
module Profile
autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens'
end
module Admin
autoload :Settings, 'qa/page/admin/settings'
end
......
module QA
module Factory
module Resource
##
# Create a personal access token that can be used by the api
#
class PersonalAccessToken < Factory::Base
attr_accessor :name
product :access_token do
Page::Profile::PersonalAccessTokens.act { created_access_token }
end
def fabricate!
Page::Menu::Main.act { go_to_profile_settings }
Page::Menu::Profile.act { click_access_tokens }
Page::Profile::PersonalAccessTokens.perform do |page|
page.fill_token_name(name || 'api-test-token')
page.check_api
page.create_token
end
end
end
end
end
end
......@@ -77,7 +77,7 @@ module Page
view 'app/views/devise/sessions/_new_base.html.haml' do
element :login_field, 'text_field :login'
element :passowrd_field, 'password_field :password'
element :password_field, 'password_field :password'
element :sign_in_button, 'submit "Sign in"'
end
......@@ -103,6 +103,16 @@ view 'app/views/my/view.html.haml' do
end
```
## Running the test locally
During development, you can run the `qa:selectors` test by running
```shell
bin/qa Test::Sanity::Selectors
```
from within the `qa` directory.
## Where to ask for help?
If you need more information, ask for help on `#qa` channel on Slack (GitLab
......
......@@ -7,6 +7,7 @@ module QA
element :user_avatar
element :user_menu, '.dropdown-menu-nav'
element :user_sign_out_link, 'link_to "Sign out"'
element :settings_link, 'link_to "Settings"'
end
view 'app/views/layouts/nav/_dashboard.html.haml' do
......@@ -40,7 +41,13 @@ module QA
def sign_out
within_user_menu do
click_link('Sign out')
click_link 'Sign out'
end
end
def go_to_profile_settings
within_user_menu do
click_link 'Settings'
end
end
......
module QA
module Page
module Menu
class Profile < Page::Base
view 'app/views/layouts/nav/sidebar/_profile.html.haml' do
element :access_token_link, 'link_to profile_personal_access_tokens_path'
element :access_token_title, 'Access Tokens'
element :top_level_items, '.sidebar-top-level-items'
end
def click_access_tokens
within_sidebar do
click_link('Access Tokens')
end
end
private
def within_sidebar
page.within('.sidebar-top-level-items') do
yield
end
end
end
end
end
end
module QA
module Page
module Profile
class PersonalAccessTokens < Page::Base
view 'app/views/shared/_personal_access_tokens_form.html.haml' do
element :personal_access_token_name_field, 'text_field :name'
element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable Lint/InterpolationCheck
element :scopes_api_radios, "label :scopes"
end
view 'app/views/profiles/personal_access_tokens/index.html.haml' do
element :create_token_field, "text_field_tag 'created-personal-access-token'"
end
def fill_token_name(name)
fill_in 'personal_access_token_name', with: name
end
def check_api
check 'personal_access_token_scopes_api'
end
def create_token
click_on 'Create personal access token'
end
def created_access_token
page.find('#created-personal-access-token').value
end
end
end
end
end
module QA
module Runtime
class Address
attr_reader :address
def initialize(instance, page = nil)
@instance = instance
@address = host + (page.is_a?(String) ? page : page&.path)
end
def host
if @instance.is_a?(Symbol)
Runtime::Scenario.send("#{@instance}_address")
else
@instance.to_s
end
end
end
end
end
require 'airborne'
module QA
module Runtime
module API
class Client
attr_reader :address
def initialize(address = :gitlab)
@address = address
end
def personal_access_token
@personal_access_token ||= get_personal_access_token
end
def get_personal_access_token
# you can set the environment variable PERSONAL_ACCESS_TOKEN
# to use a specific access token rather than create one from the UI
if Runtime::Env.personal_access_token
Runtime::Env.personal_access_token
else
create_personal_access_token
end
end
private
def create_personal_access_token
Runtime::Browser.visit(@address, Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::PersonalAccessToken.fabricate!.access_token
end
end
end
class Request
API_VERSION = 'v4'.freeze
def initialize(api_client, path, personal_access_token: nil)
personal_access_token ||= api_client.personal_access_token
request_path = request_path(path, personal_access_token: personal_access_token)
@session_address = Runtime::Address.new(api_client.address, request_path)
end
def url
@session_address.address
end
# Prepend a request path with the path to the API
#
# path - Path to append
#
# Examples
#
# >> request_path('/issues')
# => "/api/v4/issues"
#
# >> request_path('/issues', personal_access_token: 'sometoken)
# => "/api/v4/issues?private_token=..."
#
# Returns the relative path to the requested API resource
def request_path(path, version: API_VERSION, personal_access_token: nil, oauth_access_token: nil)
full_path = File.join('/api', version, path)
if oauth_access_token
query_string = "access_token=#{oauth_access_token}"
elsif personal_access_token
query_string = "private_token=#{personal_access_token}"
end
if query_string
full_path << (path.include?('?') ? '&' : '?')
full_path << query_string
end
full_path
end
end
end
end
end
......@@ -24,9 +24,7 @@ module QA
# based on `Runtime::Scenario#something_address`.
#
def visit(address, page, &block)
Browser::Session.new(address, page).tap do |session|
session.perform(&block)
end
Browser::Session.new(address, page).perform(&block)
end
def self.visit(address, page, &block)
......@@ -94,20 +92,15 @@ module QA
include Capybara::DSL
def initialize(instance, page = nil)
@instance = instance
@address = host + page&.path
@session_address = Runtime::Address.new(instance, page)
end
def host
if @instance.is_a?(Symbol)
Runtime::Scenario.send("#{@instance}_address")
else
@instance.to_s
end
def url
@session_address.address
end
def perform(&block)
visit(@address)
visit(url)
yield if block_given?
rescue
......@@ -130,7 +123,7 @@ module QA
# See gitlab-org/gitlab-qa#102
#
def clear!
visit(@address)
visit(url)
reset_session!
end
end
......
......@@ -3,6 +3,7 @@ module QA
module Env
extend self
# set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
(ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0
end
......@@ -10,6 +11,11 @@ module QA
def running_in_ci?
ENV['CI'] || ENV['CI_SERVER']
end
# specifies token that can be used for the api
def personal_access_token
ENV['PERSONAL_ACCESS_TOKEN']
end
end
end
end
module QA
feature 'API users', :core do
before(:context) do
@api_client = Runtime::API::Client.new(:gitlab)
end
context 'when authenticated' do
let(:request) { Runtime::API::Request.new(@api_client, '/users') }
scenario 'get list of users' do
get request.url
expect_status(200)
end
scenario 'submit request with a valid user name' do
get request.url, { params: { username: 'root' } }
expect_status(200)
expect(json_body).to be_an Array
expect(json_body.size).to eq(1)
expect(json_body.first[:username]).to eq Runtime::User.name
end
scenario 'submit request with an invalid user name' do
get request.url, { params: { username: 'invalid' } }
expect_status(200)
expect(json_body).to be_an Array
expect(json_body.size).to eq(0)
end
end
scenario 'submit request with an invalid token' do
request = Runtime::API::Request.new(@api_client, '/users', personal_access_token: 'invalid')
get request.url
expect_status(401)
end
end
end
describe QA::Runtime::API::Client do
include Support::StubENV
describe 'initialization' do
it 'defaults to :gitlab address' do
expect(described_class.new.address).to eq :gitlab
end
it 'uses specified address' do
client = described_class.new('http:///example.com')
expect(client.address).to eq 'http:///example.com'
end
end
describe '#get_personal_access_token' do
it 'returns specified token from env' do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
expect(described_class.new.get_personal_access_token).to eq 'a_token'
end
it 'returns a created token' do
allow_any_instance_of(described_class)
.to receive(:create_personal_access_token).and_return('created_token')
expect(described_class.new.get_personal_access_token).to eq 'created_token'
end
end
end
describe QA::Runtime::API::Request do
include Support::StubENV
before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
end
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
describe '#url' do
it 'returns the full api request url' do
expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
end
end
describe '#request_path' do
it 'prepends the api path' do
expect(request.request_path('/users')).to eq '/api/v4/users'
end
it 'adds the personal access token' do
expect(request.request_path('/users', personal_access_token: 'token'))
.to eq '/api/v4/users?private_token=token'
end
it 'adds the oauth access token' do
expect(request.request_path('/users', oauth_access_token: 'otoken'))
.to eq '/api/v4/users?access_token=otoken'
end
it 'respects query parameters' do
expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1'
expect(request.request_path('/users?page=1', personal_access_token: 'token'))
.to eq '/api/v4/users?page=1&private_token=token'
end
it 'uses a different api version' do
expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users'
end
end
end
describe QA::Runtime::Env do
before do
allow(ENV).to receive(:[]).and_call_original
end
include Support::StubENV
describe '.chrome_headless?' do
context 'when there is an env variable set' do
......@@ -57,8 +55,4 @@ describe QA::Runtime::Env do
end
end
end
def stub_env(name, value)
allow(ENV).to receive(:[]).with(name).and_return(value)
end
end
require_relative '../qa'
Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f }
RSpec.configure do |config|
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
......
# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb
module Support
module StubENV
def stub_env(key_or_hash, value = nil)
init_stub unless env_stubbed?
if key_or_hash.is_a? Hash
key_or_hash.each { |k, v| add_stubbed_value(k, v) }
else
add_stubbed_value key_or_hash, value
end
end
private
STUBBED_KEY = '__STUBBED__'.freeze
def add_stubbed_value(key, value)
allow(ENV).to receive(:[]).with(key).and_return(value)
allow(ENV).to receive(:key?).with(key).and_return(true)
allow(ENV).to receive(:fetch).with(key).and_return(value)
allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val|
value || default_val
end
end
def env_stubbed?
ENV[STUBBED_KEY]
end
def init_stub
allow(ENV).to receive(:[]).and_call_original
allow(ENV).to receive(:key?).and_call_original
allow(ENV).to receive(:fetch).and_call_original
add_stubbed_value(STUBBED_KEY, true)
end
end
end
......@@ -167,30 +167,26 @@ describe('Fly out sidebar navigation', () => {
describe('mouseEnterTopItems', () => {
beforeEach(() => {
jasmine.clock().install();
el.innerHTML = '<div class="sidebar-sub-level-items" style="position: absolute; top: 0; left: 100px; height: 200px;"></div>';
});
afterEach(() => {
jasmine.clock().uninstall();
});
it('shows sub-items after 0ms if no menu is open', () => {
it('shows sub-items after 0ms if no menu is open', (done) => {
mouseEnterTopItems(el);
expect(
getHideSubItemsInterval(),
).toBe(0);
jasmine.clock().tick(0);
setTimeout(() => {
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
done();
});
});
it('shows sub-items after 300ms if a menu is currently open', () => {
it('shows sub-items after 300ms if a menu is currently open', (done) => {
documentMouseMove({
clientX: el.getBoundingClientRect().left,
clientY: el.getBoundingClientRect().top,
......@@ -203,17 +199,19 @@ describe('Fly out sidebar navigation', () => {
clientY: el.getBoundingClientRect().top + 10,
});
mouseEnterTopItems(el);
mouseEnterTopItems(el, 0);
expect(
getHideSubItemsInterval(),
).toBe(300);
jasmine.clock().tick(300);
setTimeout(() => {
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
expect(
el.querySelector('.sidebar-sub-level-items').style.display,
).toBe('block');
done();
});
});
});
......
......@@ -3,7 +3,7 @@
import './class_spec_helper';
describe('ClassSpecHelper', () => {
describe('itShouldBeAStaticMethod', function () {
describe('itShouldBeAStaticMethod', () => {
beforeEach(() => {
class TestClass {
instanceMethod() { this.prop = 'val'; }
......@@ -14,23 +14,5 @@ describe('ClassSpecHelper', () => {
});
ClassSpecHelper.itShouldBeAStaticMethod(ClassSpecHelper, 'itShouldBeAStaticMethod');
it('should have a defined spec', () => {
expect(ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod').description).toBe('should be a static method');
});
it('should pass for a static method', () => {
const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'staticMethod');
expect(spec.status()).toBe('passed');
});
it('should fail for an instance method', (done) => {
const spec = ClassSpecHelper.itShouldBeAStaticMethod(this.TestClass, 'instanceMethod');
spec.resultCallback = (result) => {
expect(result.status).toBe('failed');
done();
};
spec.execute();
});
});
});
......@@ -25,7 +25,7 @@ describe('Modal', () => {
});
describe('with id', () => {
it('does not render a primary button', () => {
describe('does not render a primary button', () => {
beforeEach(() => {
vm = mountComponent(modalComponent, {
id: 'my-modal',
......
......@@ -21,22 +21,21 @@ describe('collapsedGroupedDatePicker', () => {
});
});
it('toggleCollapse events', () => {
const toggleCollapse = jasmine.createSpy();
describe('toggleCollapse events', () => {
beforeEach((done) => {
spyOn(vm, 'toggleSidebar');
vm.minDate = new Date('07/17/2016');
Vue.nextTick(done);
});
it('should emit when sidebar is toggled', () => {
vm.$el.querySelector('.gutter-toggle').click();
expect(toggleCollapse).toHaveBeenCalled();
expect(vm.toggleSidebar).toHaveBeenCalled();
});
it('should emit when collapsed-calendar-icon is clicked', () => {
vm.$el.querySelector('.sidebar-collapsed-icon').click();
expect(toggleCollapse).toHaveBeenCalled();
expect(vm.toggleSidebar).toHaveBeenCalled();
});
});
......
......@@ -219,6 +219,9 @@ describe Gitlab::Git::GitlabProjects do
before do
FileUtils.mkdir_p(dest_repos_path)
# Undo spec_helper stub that deletes hooks
allow_any_instance_of(described_class).to receive(:fork_repository).and_call_original
end
after do
......
......@@ -72,13 +72,15 @@ describe Files::UpdateService do
end
end
context 'when target branch is different than source branch' do
let(:branch_name) { "#{project.default_branch}-new" }
context 'with gitaly disabled', :skip_gitaly_mock do
context 'when target branch is different than source branch' do
let(:branch_name) { "#{project.default_branch}-new" }
it 'fires hooks only once' do
expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original
it 'fires hooks only once' do
expect(Gitlab::Git::HooksService).to receive(:new).once.and_call_original
subject.execute
subject.execute
end
end
end
end
......
......@@ -114,6 +114,16 @@ RSpec.configure do |config|
config.before(:example) do
# Skip pre-receive hook check so we can use the web editor and merge.
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil])
allow_any_instance_of(Gitlab::Git::GitlabProjects).to receive(:fork_repository).and_wrap_original do |m, *args|
m.call(*args)
shard_path, repository_relative_path = args
# We can't leave the hooks in place after a fork, as those would fail in tests
# The "internal" API is not available
FileUtils.rm_rf(File.join(shard_path, repository_relative_path, 'hooks'))
end
# Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true }
end
......
......@@ -5,10 +5,16 @@ module CycleAnalyticsHelpers
end
def create_commit(message, project, user, branch_name, count: 1)
oldrev = project.repository.commit(branch_name).sha
repository = project.repository
oldrev = repository.commit(branch_name).sha
if Timecop.frozen? && Gitlab::GitalyClient.feature_enabled?(:operation_user_commit_files)
mock_gitaly_multi_action_dates(repository.raw)
end
commit_shas = Array.new(count) do |index|
commit_sha = project.repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
project.repository.commit(commit_sha)
commit_sha = repository.create_file(user, generate(:branch), "content", message: message, branch_name: branch_name)
repository.commit(commit_sha)
commit_sha
end
......@@ -98,6 +104,25 @@ module CycleAnalyticsHelpers
pipeline: dummy_pipeline,
protected: false)
end
def mock_gitaly_multi_action_dates(raw_repository)
allow(raw_repository).to receive(:multi_action).and_wrap_original do |m, *args|
new_date = Time.now
branch_update = m.call(*args)
if branch_update.newrev
_, opts = args
commit = raw_repository.commit(branch_update.newrev).rugged_commit
branch_update.newrev = commit.amend(
update_ref: "#{Gitlab::Git::BRANCH_REF_PREFIX}#{opts[:branch_name]}",
author: commit.author.merge(time: new_date),
committer: commit.committer.merge(time: new_date)
)
end
branch_update
end
end
end
RSpec.configure do |config|
......
......@@ -38,10 +38,6 @@ module ProjectForksHelper
# so we have to explicitely call this method to clear the @exists variable.
# of the instance we're returning here.
forked_project.repository.after_import
# We can't leave the hooks in place after a fork, as those would fail in tests
# The "internal" API is not available
FileUtils.rm_rf("#{forked_project.repository.path}/hooks")
end
forked_project
......
......@@ -345,12 +345,6 @@ production:
replicas="$new_replicas"
fi
if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
secret_name='gitlab-registry'
else
secret_name=''
fi
helm upgrade --install \
--wait \
--set service.enabled="$service_enabled" \
......@@ -358,7 +352,6 @@ production:
--set image.repository="$CI_APPLICATION_REPOSITORY" \
--set image.tag="$CI_APPLICATION_TAG" \
--set image.pullPolicy=IfNotPresent \
--set image.secrets[0].name="$secret_name" \
--set application.track="$track" \
--set application.database_url="$DATABASE_URL" \
--set service.url="$CI_ENVIRONMENT_URL" \
......@@ -488,9 +481,6 @@ production:
function create_secret() {
echo "Create secret..."
if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then
return
fi
kubectl create secret -n "$KUBE_NAMESPACE" \
docker-registry gitlab-registry \
......
......@@ -4185,9 +4185,9 @@ isurl@^1.0.0-alpha5:
has-to-string-tag-x "^1.2.0"
is-object "^1.0.1"
jasmine-core@^2.6.3:
version "2.6.3"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.3.tgz#45072950e4a42b1e322fe55c001100a465d77815"
jasmine-core@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.9.0.tgz#bfbb56defcd30789adec5a3fbba8504233289c72"
jasmine-jquery@^2.1.1:
version "2.1.1"
......@@ -4269,7 +4269,7 @@ json-stable-stringify@~0.0.0:
dependencies:
jsonify "~0.0.0"
json-stringify-safe@5.0.x, json-stringify-safe@^5.0.1, json-stringify-safe@~5.0.1:
json-stringify-safe@5.0.x, json-stringify-safe@~5.0.1:
version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
......@@ -6063,11 +6063,9 @@ raphael@^2.2.7:
dependencies:
eve-raphael "0.5.0"
raven-js@^3.14.0:
version "3.14.0"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.14.0.tgz#94dda81d975fdc4a42f193db437cf70021d654e0"
dependencies:
json-stringify-safe "^5.0.1"
raven-js@^3.22.1:
version "3.22.1"
resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.22.1.tgz#1117f00dfefaa427ef6e1a7d50bbb1fb998a24da"
raw-body@2:
version "2.3.2"
......
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