Commit ca055120 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 6b833f1e
......@@ -82,9 +82,9 @@ GitLab is a Ruby on Rails application that runs on the following software:
- Ruby (MRI) 2.6.5
- Git 2.8.4+
- Redis 2.8+
- PostgreSQL (preferred) or MySQL
- PostgreSQL 9.6+
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
For more information please see the [architecture](https://docs.gitlab.com/ee/development/architecture.html) and [requirements](https://docs.gitlab.com/ee/install/requirements.html) documentation.
## UX design
......
<script>
import { GlButton, GlModal, GlModalDirective, GlLink, GlSprintf } from '@gitlab/ui';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import { s__ } from '~/locale';
export default {
components: {
GlButton,
GlLink,
GlModal,
GlSprintf,
ModalCopyButton,
},
directives: {
'gl-modal': GlModalDirective,
},
instructionText: {
step1: s__(
'EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}.',
),
step2: s__('EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:'),
step3: s__(
`EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file.`,
),
},
modalInfo: {
closeText: s__('EnableReviewApp|Close'),
copyToClipboardText: s__('EnableReviewApp|Copy snippet text'),
copyString: `deploy_review
stage: deploy
script:
- echo "Deploy a review app"
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_ENVIRONMENT_SLUG.example.com
only: branches
except: master`,
id: 'enable-review-app-info',
title: s__('ReviewApp|Enable Review App'),
},
};
</script>
<template>
<div>
<gl-button
v-gl-modal="$options.modalInfo.id"
variant="info"
category="secondary"
type="button"
class="js-enable-review-app-button"
>
{{ s__('Environments|Enable review app') }}
</gl-button>
<gl-modal
:modal-id="$options.modalInfo.id"
:title="$options.modalInfo.title"
size="lg"
class="text-2 ws-normal"
ok-only
ok-variant="light"
:ok-title="$options.modalInfo.closeText"
>
<p>
<gl-sprintf :message="$options.instructionText.step1">
<template #step="{ content }">
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link
href="https://docs.gitlab.com/ee/user/project/clusters/add_remove_clusters.html"
target="_blank"
>{{ content }}</gl-link
>
</template>
</gl-sprintf>
</p>
<div>
<p>
<gl-sprintf :message="$options.instructionText.step2">
<template #step="{ content }">
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
</p>
<div class="flex align-items-start">
<pre class="w-100"> {{ $options.modalInfo.copyString }} </pre>
<modal-copy-button
:title="$options.modalInfo.copyToClipboardText"
:text="$options.modalInfo.copyString"
:modal-id="$options.modalInfo.id"
css-classes="border-0"
/>
</div>
</div>
<p>
<gl-sprintf :message="$options.instructionText.step3">
<template #step="{ content }">
<strong>{{ content }}</strong>
</template>
<template #link="{ content }">
<gl-link href="blob/master/.gitlab-ci.yml" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
</gl-modal>
</div>
</template>
<script>
import { GlButton } from '@gitlab/ui';
import envrionmentsAppMixin from 'ee_else_ce/environments/mixins/environments_app_mixin';
import Flash from '../../flash';
import { s__ } from '../../locale';
import Flash from '~/flash';
import { s__ } from '~/locale';
import emptyState from './empty_state.vue';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import CIPaginationMixin from '~/vue_shared/mixins/ci_pagination_api_mixin';
import EnableReviewAppButton from './enable_review_app_button.vue';
import StopEnvironmentModal from './stop_environment_modal.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
export default {
components: {
ConfirmRollbackModal,
emptyState,
EnableReviewAppButton,
GlButton,
StopEnvironmentModal,
ConfirmRollbackModal,
},
mixins: [CIPaginationMixin, environmentsMixin, envrionmentsAppMixin],
......@@ -96,10 +100,16 @@ export default {
<div class="top-area">
<tabs :tabs="tabs" scope="environments" @onChangeTab="onChangeTab" />
<div v-if="canCreateEnvironment && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-success">
<div class="nav-controls">
<enable-review-app-button v-if="state.reviewAppDetails.can_setup_review_app" class="mr-2" />
<gl-button
v-if="canCreateEnvironment && !isLoading"
:href="newEnvironmentPath"
category="primary"
variant="success"
>
{{ s__('Environments|New environment') }}
</a>
</gl-button>
</div>
</div>
......
......@@ -52,6 +52,7 @@ export default {
this.store.storeAvailableCount(resp.data.available_count);
this.store.storeStoppedCount(resp.data.stopped_count);
this.store.storeEnvironments(resp.data.environments);
this.store.setReviewAppDetails(resp.data.review_app);
this.store.setPagination(resp.headers);
}
},
......
......@@ -14,6 +14,7 @@ export default class EnvironmentsStore {
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
this.state.paginationInformation = {};
this.state.reviewAppDetails = {};
return this;
}
......@@ -104,6 +105,11 @@ export default class EnvironmentsStore {
return paginationInformation;
}
setReviewAppDetails(details = {}) {
this.state.reviewAppDetails = details;
return details;
}
/**
* Stores the number of available environments.
*
......
......@@ -37,7 +37,6 @@ class ApplicationController < ActionController::Base
around_action :set_current_context
around_action :set_locale
around_action :set_session_storage
around_action :set_current_admin
after_action :set_page_title_header, if: :json_request?
after_action :limit_session_time, if: -> { !current_user }
......@@ -474,13 +473,6 @@ class ApplicationController < ActionController::Base
response.headers['Page-Title'] = URI.escape(page_title('GitLab'))
end
def set_current_admin(&block)
return yield unless Feature.enabled?(:user_mode_in_session)
return yield unless current_user
Gitlab::Auth::CurrentUserMode.with_current_admin(current_user, &block)
end
def html_request?
request.format.html?
end
......
---
title: Create conditional Enable Review App button
merge_request: 23703
author:
type: added
---
title: Add cycle analytics duration chart with median line
merge_request: 23971
author:
type: added
---
title: Admin mode support in sidekiq jobs
merge_request: 21792
author: Diego Louzán
type: changed
......@@ -347,6 +347,12 @@ reviewee.
of the contributed code. It's usually a good idea to ask another maintainer or
reviewer before doing it, but have the courage to do it when you believe it is
important.
- In the interest of [Iteration](https://about.gitlab.com/handbook/values/#iteration),
if, as a reviewer, your suggestions are non-blocking changes or personal preference
(not a documented or agreed requirement), consider approving the merge request
before passing it back to the author. This allows them to implement your suggestions
if they agree, or allows them to pass it onto the
maintainer for review straight away. This can help reduce our overall time-to-merge.
- There is a difference in doing things right and doing things right now.
Ideally, we should do the former, but in the real world we need the latter as
well. A good example is a security fix which should be released as soon as
......
......@@ -379,10 +379,6 @@ Rails migration example:
```ruby
add_column_with_default(:projects, :foo, :integer, default: 10, limit: 8)
# or
add_column(:projects, :foo, :integer, default: 10, limit: 8)
```
## Timestamp column type
......
# `ReactiveCaching`
> This doc refers to <https://gitlab.com/gitlab-org/gitlab/blob/master/app/models/concerns/reactive_caching.rb>.
The `ReactiveCaching` concern is used for fetching some data in the background and store it
in the Rails cache, keeping it up-to-date for as long as it is being requested. If the
data hasn't been requested for `reactive_cache_lifetime`, it will stop being refreshed,
and then be removed.
## Examples
```ruby
class Foo < ApplicationRecord
include ReactiveCaching
after_save :clear_reactive_cache!
def calculate_reactive_cache(param1, param2)
# Expensive operation here. The return value of this method is cached
end
def result
# Any arguments can be passed to `with_reactive_cache`. `calculate_reactive_cache`
# will be called with the same arguments.
with_reactive_cache(param1, param2) do |data|
# ...
end
end
end
```
In this example, the first time `#result` is called, it will return `nil`. However,
it will enqueue a background worker to call `#calculate_reactive_cache` and set an
initial cache lifetime of 10 min.
## How it works
The first time `#with_reactive_cache` is called, a background job is enqueued and
`with_reactive_cache` returns `nil`. The background job calls `#calculate_reactive_cache`
and stores its return value. It also re-enqueues the background job to run again after
`reactive_cache_refresh_interval`. Therefore, it will keep the stored value up to date.
Calculations never run concurrently.
Calling `#with_reactive_cache` while a value is cached will call the block given to
`#with_reactive_cache`, yielding the cached value. It will also extend the lifetime
of the cache by the `reactive_cache_lifetime` value.
Once the lifetime has expired, no more background jobs will be enqueued and calling
`#with_reactive_cache` will again return `nil` - starting the process all over again.
## When to use
- If we need to make a request to an external API (for example, requests to the k8s API).
It is not advisable to keep the application server worker blocked for the duration of
the external request.
- If a model needs to perform a lot of database calls or other time consuming
calculations.
## How to use
### In models and services
The ReactiveCaching concern can be used in models as well as `project_services`
(`app/models/project_services`).
1. Include the concern in your model or service.
When including in a model:
```ruby
include ReactiveCaching
```
or when including in a `project_service`:
```ruby
include ReactiveService
```
1. Implement the `calculate_reactive_cache` method in your model/service.
1. Call `with_reactive_cache` in your model/service where the cached value is needed.
### In controllers
Controller endpoints that call a model or service method that uses `ReactiveCaching` should
not wait until the background worker completes.
- An API that calls a model or service method that uses `ReactiveCaching` should return
`202 accepted` when the cache is being calculated (when `#with_reactive_cache` returns `nil`).
- It should also
[set the polling interval header](fe_guide/performance.md#realtime-components) with
`Gitlab::PollingInterval.set_header`.
- The consumer of the API is expected to poll the API.
- You can also consider implementing [ETag caching](polling.md) to reduce the server
load caused by polling.
### Methods to implement in a model or service
These are methods that should be implemented in the model/service that includes `ReactiveCaching`.
#### `#calculate_reactive_cache` (required)
- This method must be implemented. Its return value will be cached.
- It will be called by `ReactiveCaching` when it needs to populate the cache.
- Any arguments passed to `with_reactive_cache` will also be passed to `calculate_reactive_cache`.
#### `#reactive_cache_updated` (optional)
- This method can be implemented if needed.
- It is called by the `ReactiveCaching` concern whenever the cache is updated.
If the cache is being refreshed and the new cache value is the same as the old cache
value, this method will not be called. It is only called if a new value is stored in
the cache.
- It can be used to perform an action whenever the cache is updated.
### Methods called by a model or service
These are methods provided by `ReactiveCaching` and should be called in
the model/service.
#### `#with_reactive_cache` (required)
- `with_reactive_cache` must be called where the result of `calculate_reactive_cache`
is required.
- A block can be given to `with_reactive_cache`. `with_reactive_cache` can also take
any number of arguments. Any arguments passed to `with_reactive_cache` will be
passed to `calculate_reactive_cache`. The arguments passed to `with_reactive_cache`
will be appended to the cache key name.
- If `with_reactive_cache` is called when the result has already been cached, the
block will be called, yielding the cached value and the return value of the block
will be returned by `with_reactive_cache`. It will also reset the timeout of the
cache to the `reactive_cache_lifetime` value.
- If the result has not been cached as yet, `with_reactive_cache` will return nil.
It will also enqueue a background job, which will call `calculate_reactive_cache`
and cache the result.
- Once the background job has completed and the result is cached, the next call
to `with_reactive_cache` will pick up the cached value.
- In the example below, `data` is the cached value which is yielded to the block
given to `with_reactive_cache`.
```ruby
class Foo < ApplicationRecord
include ReactiveCaching
def calculate_reactive_cache(param1, param2)
# Expensive operation here. The return value of this method is cached
end
def result
with_reactive_cache(param1, param2) do |data|
# ...
end
end
end
```
#### `#clear_reactive_cache!` (optional)
- This method can be called when the cache needs to be expired/cleared. For example,
it can be called in an `after_save` callback in a model so that the cache is
cleared after the model is modified.
- This method should be called with the same parameters that are passed to
`with_reactive_cache` because the parameters are part of the cache key.
#### `#without_reactive_cache` (optional)
- This is a convenience method that can be used for debugging purposes.
- This method calls `calculate_reactive_cache` in the current process instead of
in a background worker.
### Configurable options
There are some `class_attribute` options which can be tweaked.
#### `self.reactive_cache_key`
- The value of this attribute is the prefix to the `data` and `alive` cache key names.
The parameters passed to `with_reactive_cache` form the rest of the cache key names.
- By default, this key uses the model's name and the ID of the record.
```ruby
self.reactive_cache_key = -> (record) { [model_name.singular, record.id] }
```
- The `data` and `alive` cache keys in this case will be `"ExampleModel:1:arg1:arg2"`
and `"ExampleModel:1:arg1:arg2:alive"` respectively, where `ExampleModel` is the
name of the model, `1` is the ID of the record, `arg1` and `arg2` are parameters
passed to `with_reactive_cache`.
- If you're including this concern in a service instead, you will need to override
the default by adding the following to your service:
```ruby
self.reactive_cache_key = ->(service) { [service.class.model_name.singular, service.project_id] }
```
If your reactive_cache_key is exactly like the above, you can use the existing
`ReactiveService` concern instead.
#### `self.reactive_cache_lease_timeout`
- `ReactiveCaching` uses `Gitlab::ExclusiveLease` to ensure that the cache calculation
is never run concurrently by multiple workers.
- This attribute is the timeout for the `Gitlab::ExclusiveLease`.
- It defaults to 2 minutes, but can be overriden if a different timeout is required.
```ruby
self.reactive_cache_lease_timeout = 2.minutes
```
#### `self.reactive_cache_refresh_interval`
- This is the interval at which the cache is refreshed.
- It defaults to 1 minute.
```ruby
self.reactive_cache_lease_timeout = 1.minute
```
#### `self.reactive_cache_lifetime`
- This is the duration after which the cache will be cleared if there are no requests.
- The default is 10 minutes. If there are no requests for this cache value for 10 minutes,
the cache will expire.
- If the cache value is requested before it expires, the timeout of the cache will
be reset to `reactive_cache_lifetime`.
```ruby
self.reactive_cache_lifetime = 10.minutes
```
#### `self.reactive_cache_worker_finder`
- This is the method used by the background worker to find or generate the object on
which `calculate_reactive_cache` can be called.
- By default it uses the model primary key to find the object:
```ruby
self.reactive_cache_worker_finder = ->(id, *_args) do
find_by(primary_key => id)
end
```
- The default behaviour can be overridden by defining a custom `reactive_cache_worker_finder`.
```ruby
class Foo < ApplicationRecord
include ReactiveCaching
self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
def self.from_cache(var1, var2)
# This method will be called by the background worker with "bar1" and
# "bar2" as arguments.
new(var1, var2)
end
def initialize(var1, var2)
# ...
end
def calculate_reactive_cache(var1, var2)
# Expensive operation here. The return value of this method is cached
end
def result
with_reactive_cache("bar1", "bar2") do |data|
# ...
end
end
end
```
- In this example, the primary key ID will be passed to `reactive_cache_worker_finder`
along with the parameters passed to `with_reactive_cache`.
- The custom `reactive_cache_worker_finder` calls `.from_cache` with the parameters
passed to `with_reactive_cache`.
This diff is collapsed.
......@@ -172,16 +172,23 @@ For example, if 30 days worth of data has been selected (for example, 2019-12-16
median line will represent the previous 30 days worth of data (2019-11-16 to 2019-12-16)
as a metric to compare against.
### Enabling chart
### Disabling chart
By default, this chart is disabled for self-managed instances. To enable it, ask an
administrator with Rails console access to run the following:
This chart is enabled by default. If you have a self-managed instance, an
administrator can open a Rails console and disable it with the following command:
```ruby
Feature.enable(:cycle_analytics_scatterplot_enabled)
Feature.disable(:cycle_analytics_scatterplot_enabled)
```
This chart is enabled by default on GitLab.com.
### Disabling chart median line
This chart median line is enabled by default. If you have a self-managed instance, an
administrator can open a Rails console and disable it with the following command:
```ruby
Feature.disable(:cycle_analytics_scatterplot_median_enabled)
```
## Permissions
......
......@@ -74,9 +74,9 @@ To enable this merge request approval rule:
1. Navigate to your project's **Settings > General** and expand
**Merge request approvals**.
1. Locate **All members with Developer role or higher and code owners (if any)** and click **Edit** to choose the number of approvals required.
1. Locate **Any eligible user** and choose the number of approvals required.
![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_4.png)
![MR approvals by Code Owners](img/mr_approvals_by_code_owners_v12_7.png)
Once set, merge requests can only be merged once approved by the
number of approvals you've set. GitLab will accept approvals from
......@@ -145,7 +145,7 @@ a rule is already defined.
When an [eligible approver](#eligible-approvers) approves a merge request, it will
reduce the number of approvals left for all rules that the approver belongs to.
![Approvals premium merge request widget](img/approvals_premium_mr_widget.png)
![Approvals premium merge request widget](img/approvals_premium_mr_widget_v12_7.png)
## Adding or removing an approval
......
......@@ -10,54 +10,12 @@ 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
......@@ -84,7 +42,7 @@ module Gitlab
raise NotRequestedError unless admin_mode_requested?
reset_request_store_cache_entries
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = Time.now
......@@ -97,7 +55,7 @@ module Gitlab
def disable_admin_mode!
return unless user&.admin?
reset_request_store_cache_entries
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = nil
current_session_data[ADMIN_MODE_START_TIME_KEY] = nil
......@@ -106,7 +64,7 @@ module Gitlab
def request_admin_mode!
return unless user&.admin?
reset_request_store_cache_entries
reset_request_store
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY] = Time.now
end
......@@ -115,12 +73,10 @@ 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
......@@ -130,7 +86,6 @@ 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|
......@@ -148,11 +103,7 @@ module Gitlab
current_session_data[ADMIN_MODE_REQUESTED_TIME_KEY].to_i > ADMIN_MODE_REQUESTED_GRACE_PERIOD.ago.to_i
end
def bypass_session?
user&.id && user.id == self.class.bypass_session_admin_id
end
def reset_request_store_cache_entries
def reset_request_store
Gitlab::SafeRequestStore.delete(admin_mode_rs_key)
Gitlab::SafeRequestStore.delete(admin_mode_requested_rs_key)
end
......
......@@ -93,6 +93,8 @@ module Gitlab
#
redis.expire(key, EXPIRATION)
end
record_metrics(redis.memory("USAGE", key))
end
# Subsequent read_file calls would need the latest cache.
......@@ -101,6 +103,10 @@ module Gitlab
clear_memoization(:cacheable_files)
end
def record_metrics(memory_usage)
self.class.gitlab_redis_diff_caching_memory_usage_bytes.observe({}, memory_usage)
end
def file_paths
strong_memoize(:file_paths) do
diff_files.collect(&:file_path)
......
......@@ -17,7 +17,6 @@ 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
......@@ -32,7 +31,6 @@ 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
......
# 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
......@@ -7137,6 +7137,21 @@ msgstr ""
msgid "Enable/disable your service desk. %{link_start}Learn more about service desk%{link_end}."
msgstr ""
msgid "EnableReviewApp|%{stepStart}Step 1%{stepEnd}. Ensure you have Kubernetes set up and have a base domain for your %{linkStart}cluster%{linkEnd}."
msgstr ""
msgid "EnableReviewApp|%{stepStart}Step 2%{stepEnd}. Copy the following snippet:"
msgstr ""
msgid "EnableReviewApp|%{stepStart}Step 3%{stepEnd}. Add it to the project %{linkStart}gitlab-ci.yml%{linkEnd} file."
msgstr ""
msgid "EnableReviewApp|Close"
msgstr ""
msgid "EnableReviewApp|Copy snippet text"
msgstr ""
msgid "Enabled"
msgstr ""
......@@ -7311,6 +7326,9 @@ msgstr ""
msgid "Environments|Deployment"
msgstr ""
msgid "Environments|Enable review app"
msgstr ""
msgid "Environments|Environment"
msgstr ""
......@@ -11529,6 +11547,9 @@ msgstr ""
msgid "Manage two-factor authentication"
msgstr ""
msgid "Manage your license"
msgstr ""
msgid "Manifest"
msgstr ""
......@@ -16189,6 +16210,9 @@ msgstr ""
msgid "Review time is defined as the time it takes from first comment until merged."
msgstr ""
msgid "ReviewApp|Enable Review App"
msgstr ""
msgid "Reviewing"
msgstr ""
......@@ -19205,6 +19229,9 @@ msgstr ""
msgid "This GitLab instance does not provide any shared Runners yet. Instance administrators can register shared Runners in the admin area."
msgstr ""
msgid "This GitLab instance is licensed at the %{insufficient_license} tier. Geo is only available for users who have at least a Premium license."
msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
......@@ -21868,9 +21895,6 @@ msgstr ""
msgid "You need a different license to enable FileLocks feature"
msgstr ""
msgid "You need a different license to use Geo replication."
msgstr ""
msgid "You need git-lfs version %{min_git_lfs_version} (or greater) to continue. Please visit https://git-lfs.github.com"
msgstr ""
......
......@@ -30,9 +30,9 @@ module QA::Page
element :pipeline_badges
end
def running?
def running?(wait: 0)
within('.ci-header-container') do
page.has_content?('running')
page.has_content?('running', wait: wait)
end
end
......
# frozen_string_literal: true
module QA
context 'Verify', :orchestrated, :docker do
context 'Verify', :docker do
describe 'Pipeline creation and processing' do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
after do
Service::DockerRun::GitlabRunner.new(executor).remove!
end
it 'users creates a pipeline which gets processed' do
Flow::Login.sign_in
project = Resource::Project.fabricate! do |project|
project.name = 'project-with-pipelines'
project.description = 'Project with CI/CD Pipelines.'
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
end
end
before do
Resource::Runner.fabricate! do |runner|
runner.project = project
runner.name = executor
runner.tags = %w[qa test]
runner.tags = [executor]
end
end
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = '.gitlab-ci.yml'
push.commit_message = 'Add .gitlab-ci.yml'
push.file_content = <<~EOF
test-success:
tags:
- qa
- test
script: echo 'OK'
test-failure:
tags:
- qa
- test
script:
- echo 'FAILURE'
- exit 1
test-tags:
tags:
- qa
- docker
script: echo 'NOOP'
after do
Service::DockerRun::GitlabRunner.new(executor).remove!
end
test-artifacts:
tags:
- qa
- test
script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt
artifacts:
paths:
- my-artifacts/
EOF
end.project.visit!
it 'users creates a pipeline which gets processed', :smoke do
Flow::Login.sign_in
expect(page).to have_content('Add .gitlab-ci.yml')
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files(
[
{
file_path: '.gitlab-ci.yml',
content: <<~YAML
test-success:
tags:
- #{executor}
script: echo 'OK'
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
test-failure:
tags:
- #{executor}
script:
- echo 'FAILURE'
- exit 1
expect(page).to have_content('All 1')
expect(page).to have_content('Add .gitlab-ci.yml')
test-tags:
tags:
- invalid
script: echo 'NOOP'
puts 'Waiting for the runner to process the pipeline'
sleep 15 # Runner should process all jobs within 15 seconds.
test-artifacts:
tags:
- #{executor}
script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt
artifacts:
paths:
- my-artifacts/
YAML
}
]
)
end.project.visit!
Page::Project::Menu.perform(&:click_ci_cd_pipelines)
Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline)
Page::Project::Pipeline::Show.perform do |pipeline|
expect(pipeline).to be_running
expect(pipeline).to be_running(wait: 30)
expect(pipeline).to have_build('test-success', status: :success)
expect(pipeline).to have_build('test-failure', status: :failed)
expect(pipeline).to have_build('test-tags', status: :pending)
......
# 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
......@@ -2,64 +2,46 @@
require 'spec_helper'
describe 'Admin uses repository checks', :request_store, :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
describe 'Admin uses repository checks' do
include StubENV
let(:admin) { create(:admin) }
before do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
sign_in(create(:admin))
end
context 'when admin mode is disabled' do
it 'admin project page requires admin mode' do
project = create(:project)
visit_admin_project_page(project)
it 'to trigger a single check' do
project = create(:project)
visit_admin_project_page(project)
expect(page).not_to have_css('.repository-check')
expect(page).to have_content('Enter Admin Mode')
page.within('.repository-check') do
click_button 'Trigger repository check'
end
end
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
expect(page).to have_content('Repository check was triggered')
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)
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
page.within('.alert') do
expect(page.text).to match(/Last repository check \(just now\) failed/)
end
end
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
it 'to clear all repository checks', :js do
visit repository_admin_application_settings_path
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
expect(RepositoryCheck::ClearWorker).to receive(:perform_async)
accept_confirm { find(:link, 'Clear all repository checks').send_keys(:return) }
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
expect(page).to have_content('Started asynchronous removal of all repository check states.')
end
def visit_admin_project_page(project)
......
import { shallowMount, mount } from '@vue/test-utils';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import EnableReviewAppButton from '~/environments/components/enable_review_app_button.vue';
describe('Enable Review App Button', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('renders button with text', () => {
beforeEach(() => {
wrapper = mount(EnableReviewAppButton);
});
it('renders Enable Review text', () => {
expect(wrapper.text()).toBe('Enable review app');
});
});
describe('renders the modal', () => {
beforeEach(() => {
wrapper = shallowMount(EnableReviewAppButton);
});
it('renders the copyToClipboard button', () => {
expect(wrapper.find(ModalCopyButton).exists()).toBe(true);
});
});
});
......@@ -55,6 +55,26 @@ describe('Environment', () => {
"You don't have any environments right now",
);
});
describe('when it is possible to enable a review app', () => {
beforeEach(done => {
mock
.onGet(mockData.endpoint)
.reply(200, { environments: [], review_app: { can_setup_review_app: true } });
component = mountComponent(EnvironmentsComponent, mockData);
setTimeout(() => {
done();
}, 0);
});
it('should render the enable review app button', () => {
expect(component.$el.querySelector('.js-enable-review-app-button').textContent).toContain(
'Enable review app',
);
});
});
});
describe('with paginated environments', () => {
......
......@@ -2,10 +2,10 @@
require 'spec_helper'
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store do
describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode do
include_context 'custom session'
let(:user) { build_stubbed(:user) }
let(:user) { build(:user) }
subject { described_class.new(user) }
......@@ -13,66 +13,54 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
allow(ActiveSession).to receive(:list_sessions).with(user).and_return([session])
end
shared_examples 'admin mode cannot be enabled' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false)
end
describe '#admin_mode?', :request_store do
context 'when the user is a regular user' do
it 'is false by default' do
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with empty params' do
subject.enable_admin_mode!
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
it 'disable has no effect' do
subject.enable_admin_mode!
subject.disable_admin_mode!
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(password: nil)
expect(subject.admin_mode?).to be(false)
end
expect(subject.admin_mode?).to be(false)
end
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
it 'cannot be enabled with empty params' do
subject.enable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
it 'disable has no effect' do
subject.enable_admin_mode!
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(false)
end
end
end
describe '#admin_mode?' do
context 'when the user is a regular user' do
it_behaves_like 'admin mode cannot be enabled'
context 'skipping password validation' do
it 'cannot be enabled with a valid password' do
subject.enable_admin_mode!(password: user.password, skip_password_validation: true)
context 'bypassing session' do
it_behaves_like 'admin mode cannot be enabled' do
around do |example|
described_class.bypass_session!(user.id) { example.run }
end
expect(subject.admin_mode?).to be(false)
end
it 'cannot be enabled with an invalid password' do
subject.enable_admin_mode!(skip_password_validation: true)
expect(subject.admin_mode?).to be(false)
end
end
end
context 'when the user is an admin' do
let(:user) { build_stubbed(:user, :admin) }
let(:user) { build(:user, :admin) }
context 'when admin mode not requested' do
it 'is false by default' do
......@@ -160,36 +148,11 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
end
end
end
context 'bypassing session' do
it 'is active by default' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(true)
end
end
it 'enable has no effect' do
described_class.bypass_session!(user.id) do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
expect(subject.admin_mode?).to be(true)
end
end
it 'disable has no effect' do
described_class.bypass_session!(user.id) do
subject.disable_admin_mode!
expect(subject.admin_mode?).to be(true)
end
end
end
end
end
describe '#enable_admin_mode!' do
let(:user) { build_stubbed(:user, :admin) }
let(:user) { build(:user, :admin) }
it 'creates a timestamp in the session' do
subject.request_admin_mode!
......@@ -200,7 +163,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
end
describe '#enable_sessionless_admin_mode!' do
let(:user) { build_stubbed(:user, :admin) }
let(:user) { build(:user, :admin) }
it 'enabled admin mode without password' do
subject.enable_sessionless_admin_mode!
......@@ -210,7 +173,7 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
end
describe '#disable_admin_mode!' do
let(:user) { build_stubbed(:user, :admin) }
let(:user) { build(:user, :admin) }
it 'sets the session timestamp to nil' do
subject.request_admin_mode!
......@@ -220,73 +183,6 @@ describe Gitlab::Auth::CurrentUserMode, :do_not_mock_admin_mode, :request_store
end
end
describe '.bypass_session!' do
context 'with a regular user' do
it 'admin mode is false' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(false)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
it 'admin mode is true' do
described_class.bypass_session!(user.id) do
expect(subject.admin_mode?).to be(true)
expect(described_class.bypass_session_admin_id).to be(user.id)
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
end
describe '.with_current_request_admin_mode' do
context 'with a regular user' do
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'with an admin user' do
let(:user) { build_stubbed(:user, :admin) }
context 'admin mode is disabled' do
it 'user is not available inside nor outside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be_nil
end
expect(described_class.bypass_session_admin_id).to be_nil
end
end
context 'admin mode is enabled' do
before do
subject.request_admin_mode!
subject.enable_admin_mode!(password: user.password)
end
it 'user is available only inside the yielded block' do
described_class.with_current_admin(user) do
expect(described_class.current_admin).to be(user)
end
expect(described_class.current_admin).to be_nil
end
end
end
end
def expected_session_entry(value_matcher)
{
Gitlab::Auth::CurrentUserMode::SESSION_STORE_KEY => a_hash_including(
......
......@@ -101,6 +101,13 @@ describe Gitlab::Diff::HighlightCache, :clean_gitlab_redis_cache do
let(:paths) { merge_request.diffs.raw_diff_files.select(&:text?).map(&:file_path) }
end
it 'updates memory usage metrics' do
expect(described_class.gitlab_redis_diff_caching_memory_usage_bytes)
.to receive(:observe).and_call_original
cache.send(:write_to_redis_hash, diff_hash)
end
context 'different diff_collections for the same diffable' do
before do
cache.write_if_empty
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SidekiqMiddleware::AdminMode::Client, :do_not_mock_admin_mode, :request_store do
include AdminModeHelper
let(:worker) do
Class.new do
def perform; end
end
end
let(:job) { {} }
let(:queue) { :test }
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, nil, &b)
end.to yield_control.once
end
context 'user is a regular user' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'user is an administrator' do
let(:admin) { create(:admin) }
context 'admin mode disabled' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'admin mode enabled' do
before do
enable_admin_mode!(admin)
end
context 'when sidekiq required context not set' do
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
context 'when user stored in current request' do
it 'has admin mode field in payload' do
Gitlab::Auth::CurrentUserMode.with_current_admin(admin) do
subject.call(worker, job, queue, nil) { nil }
expect(job).to include('admin_mode_user_id' => admin.id)
end
end
end
context 'when bypassing session' do
it 'has admin mode field in payload' do
Gitlab::Auth::CurrentUserMode.bypass_session!(admin.id) do
subject.call(worker, job, queue, nil) { nil }
expect(job).to include('admin_mode_user_id' => admin.id)
end
end
end
end
end
context 'admin mode feature disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, nil, &b)
end.to yield_control.once
end
it 'no admin mode field in payload' do
subject.call(worker, job, queue, nil) { nil }
expect(job).not_to include('admin_mode_user_id')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::SidekiqMiddleware::AdminMode::Server, :do_not_mock_admin_mode, :request_store do
include AdminModeHelper
let(:worker) do
Class.new do
def perform; end
end
end
let(:job) { {} }
let(:queue) { :test }
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, &b)
end.to yield_control.once
end
context 'job has no admin mode field' do
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
context 'job has admin mode field' do
let(:admin) { create(:admin) }
context 'nil admin mode id' do
let(:job) { { 'admin_mode_user_id' => nil } }
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
context 'valid admin mode id' do
let(:job) { { 'admin_mode_user_id' => admin.id } }
it 'session is bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be(admin.id)
end
end
end
end
context 'admin mode feature disabled' do
before do
stub_feature_flags(user_mode_in_session: false)
end
it 'yields block' do
expect do |b|
subject.call(worker, job, queue, &b)
end.to yield_control.once
end
it 'session is not bypassed' do
subject.call(worker, job, queue) do
expect(Gitlab::Auth::CurrentUserMode.bypass_session_admin_id).to be_nil
end
end
end
end
......@@ -45,8 +45,7 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqMiddleware::ArgumentsLogger,
Gitlab::SidekiqMiddleware::MemoryKiller,
Gitlab::SidekiqMiddleware::RequestStoreMiddleware,
Gitlab::SidekiqMiddleware::WorkerContext::Server,
Gitlab::SidekiqMiddleware::AdminMode::Server
Gitlab::SidekiqMiddleware::WorkerContext::Server
]
end
let(:enabled_sidekiq_middlewares) { all_sidekiq_middlewares - disabled_sidekiq_middlewares }
......@@ -116,8 +115,7 @@ describe Gitlab::SidekiqMiddleware do
Gitlab::SidekiqStatus::ClientMiddleware,
Gitlab::SidekiqMiddleware::ClientMetrics,
Gitlab::SidekiqMiddleware::WorkerContext::Client,
Labkit::Middleware::Sidekiq::Client,
Gitlab::SidekiqMiddleware::AdminMode::Client
Labkit::Middleware::Sidekiq::Client
]
end
......
......@@ -2985,9 +2985,9 @@ describe User, :do_not_mock_admin_mode do
end
end
describe '#can_read_all_resources?', :request_store do
describe '#can_read_all_resources?' do
it 'returns false for regular user' do
user = build_stubbed(:user)
user = build(:user)
expect(user.can_read_all_resources?).to be_falsy
end
......@@ -2995,7 +2995,7 @@ describe User, :do_not_mock_admin_mode do
context 'for admin user' do
include_context 'custom session'
let(:user) { build_stubbed(:user, :admin) }
let(:user) { build(:user, :admin) }
context 'when admin mode is disabled' do
it 'returns false' do
......
......@@ -23,8 +23,8 @@ describe BasePolicy, :do_not_mock_admin_mode do
end
describe 'read cross project' do
let(:current_user) { build_stubbed(:user) }
let(:user) { build_stubbed(:user) }
let(:current_user) { create(:user) }
let(:user) { create(:user) }
subject { described_class.new(current_user, [user]) }
......@@ -38,7 +38,7 @@ describe BasePolicy, :do_not_mock_admin_mode do
it { is_expected.not_to be_allowed(:read_cross_project) }
context 'for admins' do
let(:current_user) { build_stubbed(:admin) }
let(:current_user) { build(:admin) }
subject { described_class.new(current_user, nil) }
......@@ -56,14 +56,14 @@ describe BasePolicy, :do_not_mock_admin_mode do
end
describe 'full private access' do
let(:current_user) { build_stubbed(:user) }
let(:current_user) { create(:user) }
subject { described_class.new(current_user, nil) }
it { is_expected.not_to be_allowed(:read_all_resources) }
context 'for admins' do
let(:current_user) { build_stubbed(:admin) }
let(:current_user) { build(:admin) }
it 'allowed when in admin mode' do
enable_admin_mode!(current_user)
......
......@@ -8,44 +8,40 @@ RSpec.shared_examples 'thread comments' do |resource_name|
let(:submit_selector) { "#{form_selector} .js-comment-submit-button" }
let(:close_selector) { "#{form_selector} .btn-comment-and-close" }
let(:comments_selector) { '.timeline > .note.timeline-entry' }
let(:comment) { 'My comment' }
it 'clicking "Comment" will post a comment', :quarantine do
it 'clicking "Comment" will post a comment' do
expect(page).to have_selector toggle_selector
find("#{form_selector} .note-textarea").send_keys('a')
find("#{form_selector} .note-textarea").send_keys(comment)
find(submit_selector).click
click_button 'Comment'
wait_for_requests
expect(page).to have_content(comment)
find(comments_selector, match: :first)
new_comment = all(comments_selector).last
expect(new_comment).to have_content 'a'
expect(new_comment).not_to have_selector '.discussion'
end
if resource_name == 'issue'
it "clicking 'Comment & close #{resource_name}' will post a comment and close the #{resource_name}" do
find("#{form_selector} .note-textarea").send_keys('a')
find("#{form_selector} .note-textarea").send_keys(comment)
find(close_selector).click
wait_for_requests
click_button 'Comment & close issue'
find(comments_selector, match: :first)
find("#{comments_selector}.system-note")
entries = all(comments_selector)
close_note = entries.last
new_comment = entries[-2]
expect(page).to have_content(comment)
expect(page).to have_content "@#{user.username} closed"
new_comment = all(comments_selector).last
expect(close_note).to have_content 'closed'
expect(new_comment).not_to have_selector '.discussion'
end
end
describe 'when the toggle is clicked' do
before do
find("#{form_selector} .note-textarea").send_keys('a')
find("#{form_selector} .note-textarea").send_keys(comment)
find(toggle_selector).click
end
......@@ -153,10 +149,11 @@ RSpec.shared_examples 'thread comments' do |resource_name|
end
it 'clicking "Start thread" will post a thread' do
expect(page).to have_content(comment)
new_comment = all(comments_selector).last
expect(new_comment).to have_content 'a'
expect(new_comment).to have_selector '.discussion'
expect(new_comment).to have_selector('.discussion')
end
if resource_name =~ /(issue|merge request)/
......@@ -208,15 +205,13 @@ RSpec.shared_examples 'thread comments' do |resource_name|
if resource_name == 'issue'
it "clicking 'Start thread & close #{resource_name}' will post a thread and close the #{resource_name}" do
find(close_selector).click
click_button 'Start thread & close issue'
find(comments_selector, match: :first)
find("#{comments_selector}.system-note")
entries = all(comments_selector)
close_note = entries.last
new_discussion = entries[-2]
expect(page).to have_content(comment)
expect(page).to have_content "@#{user.username} closed"
new_discussion = all(comments_selector)[-2]
expect(close_note).to have_content 'closed'
expect(new_discussion).to have_selector '.discussion'
end
end
......
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