Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-05-19 21:08:05 +00:00
parent d84f18d66c
commit 680d188025
50 changed files with 533 additions and 292 deletions

View File

@ -19,7 +19,7 @@ See [the general developer security release guidelines](https://gitlab.com/gitla
- [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security` - [ ] A [CHANGELOG entry](https://docs.gitlab.com/ee/development/changelog.html) is added without a `merge_request` value, with `type` set to `security`
- [ ] Assign to a reviewer and maintainer, per our [Code Review process]. - [ ] Assign to a reviewer and maintainer, per our [Code Review process].
- [ ] For the MR targeting `master`: - [ ] For the MR targeting `master`:
- [ ] Ping appsec team member who created the issue and ask for a non-blocking review with `Please review this MR`. - [ ] Ask for a non-blocking review from the AppSec team member associated to the issue in the [Canonical repository](https://gitlab.com/gitlab-org/gitlab). If you're unsure who to ping, ask on `#sec-appsec` Slack channel.
- [ ] Ensure it's approved according to our [Approval Guidelines]. - [ ] Ensure it's approved according to our [Approval Guidelines].
- [ ] Merge request _must not_ close the corresponding security issue, _unless_ it targets `master`. - [ ] Merge request _must not_ close the corresponding security issue, _unless_ it targets `master`.

View File

@ -359,9 +359,6 @@ RSpec/LeakyConstantDeclaration:
- 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb' - 'spec/lib/gitlab/database/obsolete_ignored_columns_spec.rb'
- 'spec/lib/gitlab/database/with_lock_retries_spec.rb' - 'spec/lib/gitlab/database/with_lock_retries_spec.rb'
- 'spec/lib/gitlab/git/diff_collection_spec.rb' - 'spec/lib/gitlab/git/diff_collection_spec.rb'
- 'spec/lib/gitlab/graphql/pagination/keyset/connection_spec.rb'
- 'spec/lib/gitlab/health_checks/master_check_spec.rb'
- 'spec/lib/gitlab/import_export/attribute_configuration_spec.rb'
- 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb' - 'spec/lib/gitlab/import_export/import_test_coverage_spec.rb'
- 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb' - 'spec/lib/gitlab/import_export/project/relation_factory_spec.rb'
- 'spec/lib/gitlab/jira_import/issues_importer_spec.rb' - 'spec/lib/gitlab/jira_import/issues_importer_spec.rb'
@ -372,17 +369,14 @@ RSpec/LeakyConstantDeclaration:
- 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb' - 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
- 'spec/lib/gitlab/view/presenter/factory_spec.rb' - 'spec/lib/gitlab/view/presenter/factory_spec.rb'
- 'spec/lib/marginalia_spec.rb' - 'spec/lib/marginalia_spec.rb'
- 'spec/lib/system_check/simple_executor_spec.rb'
- 'spec/mailers/notify_spec.rb' - 'spec/mailers/notify_spec.rb'
- 'spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb' - 'spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb'
- 'spec/migrations/encrypt_plaintext_attributes_on_application_settings_spec.rb'
- 'spec/models/concerns/batch_destroy_dependent_associations_spec.rb' - 'spec/models/concerns/batch_destroy_dependent_associations_spec.rb'
- 'spec/models/concerns/bulk_insert_safe_spec.rb' - 'spec/models/concerns/bulk_insert_safe_spec.rb'
- 'spec/models/concerns/bulk_insertable_associations_spec.rb' - 'spec/models/concerns/bulk_insertable_associations_spec.rb'
- 'spec/models/concerns/triggerable_hooks_spec.rb' - 'spec/models/concerns/triggerable_hooks_spec.rb'
- 'spec/models/repository_spec.rb' - 'spec/models/repository_spec.rb'
- 'spec/requests/api/graphql/tasks/task_completion_status_spec.rb' - 'spec/requests/api/graphql/tasks/task_completion_status_spec.rb'
- 'spec/rubocop/cop/rspec/env_assignment_spec.rb'
- 'spec/serializers/commit_entity_spec.rb' - 'spec/serializers/commit_entity_spec.rb'
- 'spec/services/clusters/applications/check_installation_progress_service_spec.rb' - 'spec/services/clusters/applications/check_installation_progress_service_spec.rb'
- 'spec/services/clusters/applications/check_uninstall_progress_service_spec.rb' - 'spec/services/clusters/applications/check_uninstall_progress_service_spec.rb'

View File

@ -1,22 +1,32 @@
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { GlTable, GlLink, GlLoadingIcon, GlBadge } from '@gitlab/ui'; import { GlBadge, GlLink, GlLoadingIcon, GlPagination, GlTable } from '@gitlab/ui';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
import { CLUSTER_TYPES, STATUSES } from '../constants'; import { CLUSTER_TYPES, STATUSES } from '../constants';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
export default { export default {
components: { components: {
GlTable, GlBadge,
GlLink, GlLink,
GlLoadingIcon, GlLoadingIcon,
GlBadge, GlPagination,
GlTable,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
computed: { computed: {
...mapState(['clusters', 'loading']), ...mapState(['clusters', 'clustersPerPage', 'loading', 'page', 'totalCulsters']),
currentPage: {
get() {
return this.page;
},
set(newVal) {
this.setPage(newVal);
this.fetchClusters();
},
},
fields() { fields() {
return [ return [
{ {
@ -47,12 +57,15 @@ export default {
}, },
]; ];
}, },
hasClusters() {
return this.clustersPerPage > 0;
},
}, },
mounted() { mounted() {
this.fetchClusters(); this.fetchClusters();
}, },
methods: { methods: {
...mapActions(['fetchClusters']), ...mapActions(['fetchClusters', 'setPage']),
statusClass(status) { statusClass(status) {
const iconClass = STATUSES[status] || STATUSES.default; const iconClass = STATUSES[status] || STATUSES.default;
return iconClass.className; return iconClass.className;
@ -67,33 +80,46 @@ export default {
<template> <template>
<gl-loading-icon v-if="loading" size="md" class="mt-3" /> <gl-loading-icon v-if="loading" size="md" class="mt-3" />
<gl-table v-else :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
<template #cell(name)="{ item }">
<div class="d-flex flex-row-reverse flex-md-row js-status">
<gl-link data-qa-selector="cluster" :data-qa-cluster-name="item.name" :href="item.path">
{{ item.name }}
</gl-link>
<gl-loading-icon <section v-else>
v-if="item.status === 'deleting'" <gl-table :items="clusters" :fields="fields" stacked="md" class="qa-clusters-table">
v-tooltip <template #cell(name)="{ item }">
:title="statusTitle(item.status)" <div class="d-flex flex-row-reverse flex-md-row js-status">
size="sm" <gl-link data-qa-selector="cluster" :data-qa-cluster-name="item.name" :href="item.path">
class="mr-2 ml-md-2" {{ item.name }}
/> </gl-link>
<div
v-else <gl-loading-icon
v-tooltip v-if="item.status === 'deleting'"
class="cluster-status-indicator rounded-circle align-self-center gl-w-4 gl-h-4 mr-2 ml-md-2" v-tooltip
:class="statusClass(item.status)" :title="statusTitle(item.status)"
:title="statusTitle(item.status)" size="sm"
></div> class="mr-2 ml-md-2"
</div> />
</template> <div
<template #cell(cluster_type)="{value}"> v-else
<gl-badge variant="light"> v-tooltip
{{ value }} class="cluster-status-indicator rounded-circle align-self-center gl-w-4 gl-h-4 mr-2 ml-md-2"
</gl-badge> :class="statusClass(item.status)"
</template> :title="statusTitle(item.status)"
</gl-table> ></div>
</div>
</template>
<template #cell(cluster_type)="{value}">
<gl-badge variant="light">
{{ value }}
</gl-badge>
</template>
</gl-table>
<gl-pagination
v-if="hasClusters"
v-model="currentPage"
:per-page="clustersPerPage"
:total-items="totalCulsters"
:prev-text="__('Prev')"
:next-text="__('Next')"
align="center"
/>
</section>
</template> </template>

View File

@ -2,18 +2,22 @@ import Poll from '~/lib/utils/poll';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export const fetchClusters = ({ state, commit }) => { export const fetchClusters = ({ state, commit }) => {
const poll = new Poll({ const poll = new Poll({
resource: { resource: {
fetchClusters: endpoint => axios.get(endpoint), fetchClusters: paginatedEndPoint => axios.get(paginatedEndPoint),
}, },
data: state.endpoint, data: `${state.endpoint}?page=${state.page}`,
method: 'fetchClusters', method: 'fetchClusters',
successCallback: ({ data }) => { successCallback: ({ data, headers }) => {
if (data.clusters) { if (data.clusters) {
commit(types.SET_CLUSTERS_DATA, data); const normalizedHeaders = normalizeHeaders(headers);
const paginationInformation = parseIntPagination(normalizedHeaders);
commit(types.SET_CLUSTERS_DATA, { data, paginationInformation });
commit(types.SET_LOADING_STATE, false); commit(types.SET_LOADING_STATE, false);
poll.stop(); poll.stop();
} }
@ -24,5 +28,9 @@ export const fetchClusters = ({ state, commit }) => {
poll.makeRequest(); poll.makeRequest();
}; };
export const setPage = ({ commit }, page) => {
commit(types.SET_PAGE, page);
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};

View File

@ -1,2 +1,3 @@
export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA'; export const SET_CLUSTERS_DATA = 'SET_CLUSTERS_DATA';
export const SET_LOADING_STATE = 'SET_LOADING_STATE'; export const SET_LOADING_STATE = 'SET_LOADING_STATE';
export const SET_PAGE = 'SET_PAGE';

View File

@ -4,10 +4,15 @@ export default {
[types.SET_LOADING_STATE](state, value) { [types.SET_LOADING_STATE](state, value) {
state.loading = value; state.loading = value;
}, },
[types.SET_CLUSTERS_DATA](state, data) { [types.SET_CLUSTERS_DATA](state, { data, paginationInformation }) {
Object.assign(state, { Object.assign(state, {
clusters: data.clusters, clusters: data.clusters,
clustersPerPage: paginationInformation.perPage,
hasAncestorClusters: data.has_ancestor_clusters, hasAncestorClusters: data.has_ancestor_clusters,
totalCulsters: paginationInformation.total,
}); });
}, },
[types.SET_PAGE](state, value) {
state.page = Number(value) || 1;
},
}; };

View File

@ -3,4 +3,7 @@ export default (initialState = {}) => ({
hasAncestorClusters: false, hasAncestorClusters: false,
loading: true, loading: true,
clusters: [], clusters: [],
clustersPerPage: 0,
page: 1,
totalCulsters: 0,
}); });

View File

@ -1,5 +1,6 @@
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default { export default {
@ -7,6 +8,10 @@ export default {
timeAgoTooltip, timeAgoTooltip,
GitlabTeamMemberBadge: () => GitlabTeamMemberBadge: () =>
import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'), import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
}, },
props: { props: {
author: { author: {
@ -44,6 +49,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
isConfidential: {
type: Boolean,
required: false,
default: false,
},
}, },
data() { data() {
return { return {
@ -174,6 +184,15 @@ export default {
</a> </a>
<time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" /> <time-ago-tooltip v-else ref="noteTimestamp" :time="createdAt" tooltip-placement="bottom" />
</template> </template>
<gl-icon
v-if="isConfidential"
v-gl-tooltip:tooltipcontainer.bottom
data-testid="confidentialIndicator"
name="eye-slash"
:size="14"
:title="s__('Notes|Private comments are accessible by internal staff only')"
class="gl-ml-1 gl-text-gray-800 align-middle"
/>
<slot name="extra-controls"></slot> <slot name="extra-controls"></slot>
<i <i
v-if="showSpinner" v-if="showSpinner"

View File

@ -255,7 +255,13 @@ export default {
</div> </div>
<div class="timeline-content"> <div class="timeline-content">
<div class="note-header"> <div class="note-header">
<note-header v-once :author="author" :created-at="note.created_at" :note-id="note.id"> <note-header
v-once
:author="author"
:created-at="note.created_at"
:note-id="note.id"
:is-confidential="note.confidential"
>
<slot slot="note-header-info" name="note-header-info"></slot> <slot slot="note-header-info" name="note-header-info"></slot>
<span v-if="commit" v-html="actionText"></span> <span v-if="commit" v-html="actionText"></span>
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span> <span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>

View File

@ -68,7 +68,7 @@ export default {
footer-primary-button-variant="warning" footer-primary-button-variant="warning"
@submit="onSubmit" @submit="onSubmit"
> >
<template slot="title"> <template #title>
{{ title }} {{ title }}
</template> </template>
<div> <div>

View File

@ -316,6 +316,7 @@ class ProjectPolicy < BasePolicy
enable :update_deployment enable :update_deployment
enable :create_release enable :create_release
enable :update_release enable :update_release
enable :daily_statistics
enable :create_metrics_dashboard_annotation enable :create_metrics_dashboard_annotation
enable :delete_metrics_dashboard_annotation enable :delete_metrics_dashboard_annotation
enable :update_metrics_dashboard_annotation enable :update_metrics_dashboard_annotation
@ -358,7 +359,6 @@ class ProjectPolicy < BasePolicy
enable :create_environment_terminal enable :create_environment_terminal
enable :destroy_release enable :destroy_release
enable :destroy_artifacts enable :destroy_artifacts
enable :daily_statistics
enable :admin_operations enable :admin_operations
enable :read_deploy_token enable :read_deploy_token
enable :create_deploy_token enable :create_deploy_token

View File

@ -2,8 +2,32 @@
module Snippets module Snippets
class BaseService < ::BaseService class BaseService < ::BaseService
include SpamCheckMethods
CreateRepositoryError = Class.new(StandardError)
attr_reader :uploaded_files
def initialize(project, user = nil, params = {})
super
@uploaded_files = Array(@params.delete(:files).presence)
filter_spam_check_params
end
private private
def visibility_allowed?(snippet, visibility_level)
Gitlab::VisibilityLevel.allowed_for?(current_user, visibility_level)
end
def error_forbidden_visibility(snippet)
deny_visibility_level(snippet)
snippet_error_response(snippet, 403)
end
def snippet_error_response(snippet, http_status) def snippet_error_response(snippet, http_status)
ServiceResponse.error( ServiceResponse.error(
message: snippet.errors.full_messages.to_sentence, message: snippet.errors.full_messages.to_sentence,

View File

@ -2,25 +2,11 @@
module Snippets module Snippets
class CreateService < Snippets::BaseService class CreateService < Snippets::BaseService
include SpamCheckMethods
CreateRepositoryError = Class.new(StandardError)
def execute def execute
filter_spam_check_params @snippet = build_from_params
@files = Array(params.delete(:files).presence) unless visibility_allowed?(@snippet, @snippet.visibility_level)
return error_forbidden_visibility(@snippet)
@snippet = if project
project.snippets.build(params)
else
PersonalSnippet.new(params)
end
unless Gitlab::VisibilityLevel.allowed_for?(current_user, @snippet.visibility_level)
deny_visibility_level(@snippet)
return snippet_error_response(@snippet, 403)
end end
@snippet.author = current_user @snippet.author = current_user
@ -41,6 +27,14 @@ module Snippets
private private
def build_from_params
if project
project.snippets.build(params)
else
PersonalSnippet.new(params)
end
end
def save_and_commit def save_and_commit
snippet_saved = @snippet.save snippet_saved = @snippet.save
@ -91,7 +85,7 @@ module Snippets
def move_temporary_files def move_temporary_files
return unless @snippet.is_a?(PersonalSnippet) return unless @snippet.is_a?(PersonalSnippet)
@files.each do |file| uploaded_files.each do |file|
FileMover.new(file, from_model: current_user, to_model: @snippet).execute FileMover.new(file, from_model: current_user, to_model: @snippet).execute
end end
end end

View File

@ -2,26 +2,15 @@
module Snippets module Snippets
class UpdateService < Snippets::BaseService class UpdateService < Snippets::BaseService
include SpamCheckMethods
COMMITTABLE_ATTRIBUTES = %w(file_name content).freeze COMMITTABLE_ATTRIBUTES = %w(file_name content).freeze
UpdateError = Class.new(StandardError) UpdateError = Class.new(StandardError)
CreateRepositoryError = Class.new(StandardError)
def execute(snippet) def execute(snippet)
# check that user is allowed to set specified visibility_level if visibility_changed?(snippet) && !visibility_allowed?(snippet, visibility_level)
new_visibility = visibility_level return error_forbidden_visibility(snippet)
if new_visibility && new_visibility.to_i != snippet.visibility_level
unless Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
deny_visibility_level(snippet, new_visibility)
return snippet_error_response(snippet, 403)
end
end end
filter_spam_check_params
snippet.assign_attributes(params) snippet.assign_attributes(params)
spam_check(snippet, current_user) spam_check(snippet, current_user)
@ -36,6 +25,10 @@ module Snippets
private private
def visibility_changed?(snippet)
visibility_level && visibility_level.to_i != snippet.visibility_level
end
def save_and_commit(snippet) def save_and_commit(snippet)
return false unless snippet.save return false unless snippet.save

View File

@ -27,7 +27,7 @@
%td %td
- if token.expires? - if token.expires?
%span{ class: ('text-warning' if token.expires_soon?) } %span{ class: ('text-warning' if token.expires_soon?) }
In #{distance_of_time_in_words_to_now(token.expires_at)} = _('In %{time_to_now}') % { time_to_now: distance_of_time_in_words_to_now(token.expires_at) }
- else - else
%span.token-never-expires-label= _('Never') %span.token-never-expires-label= _('Never')
%td= token.scopes.present? ? token.scopes.join(', ') : _('<no scopes selected>') %td= token.scopes.present? ? token.scopes.join(', ') : _('<no scopes selected>')

View File

@ -22,14 +22,14 @@
- elsif event.target - elsif event.target
= link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title = link_to event.target.to_reference, [event.project.namespace.becomes(Namespace), event.project, event.target], class: 'has-tooltip', title: event.target_title
at = s_('UserProfile|at')
%strong %strong
- if event.project - if event.project
= link_to_project(event.project) = link_to_project(event.project)
- else - else
= event.resource_parent_name = event.resource_parent_name
- else - else
made a private contribution = s_('UserProfile|made a private contribution')
- else - else
%p %p
= _('No contributions were found') = _('No contributions were found')

View File

@ -0,0 +1,5 @@
---
title: Add confidential status support for comment and replies
merge_request: 31622
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/shared/_personal_access_tokens_table.html.haml
merge_request: 32116
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: Externalize i18n strings from ./app/views/users/calendar_activities.html.haml
merge_request: 32094
author: Gilang Gumilar
type: changed

View File

@ -0,0 +1,5 @@
---
title: 'Cluster index refactor: Add missing pagination'
merge_request: 32338
author:
type: changed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue in env assignment spec
merge_request: 32040
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue in application settings encrypt spec
merge_request: 32066
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue in simple executor spec
merge_request: 32082
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Fix leaky constant issue connection, master check and attr config spec
merge_request: 32144
author: Rajendra Kadam
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Correct the permission according to docs
merge_request: 28657
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Validate package types in package metadatum models
merge_request: 32091
author: Sashi Kumar
type: other

View File

@ -0,0 +1,5 @@
---
title: Update deprecated slot syntax in ./app/assets/javascripts/pages/milestones/shared/components/promote_milestone_modal.vue
merge_request: 31980
author: Gilang Gumilar
type: other

View File

@ -62,7 +62,7 @@ The following settings are:
| Setting | Description | Default | | Setting | Description | Default |
|---------|-------------|---------| |---------|-------------|---------|
| `enabled` | Enable/disable object storage | `true` | | `enabled` | Enable/disable object storage | `false` |
| `remote_directory` | The bucket name where Terraform state files will be stored | | | `remote_directory` | The bucket name where Terraform state files will be stored | |
| `connection` | Various connection options described below | | | `connection` | Various connection options described below | |

View File

@ -2,7 +2,7 @@
type: howto type: howto
--- ---
# Installing GitLab HA on Amazon Web Services (AWS) # Installing GitLab on Amazon Web Services (AWS)
This page offers a walkthrough of a common configuration This page offers a walkthrough of a common configuration
for GitLab on AWS. You should customize it to accommodate your needs. for GitLab on AWS. You should customize it to accommodate your needs.
@ -16,7 +16,7 @@ GitLab on AWS can leverage many of the services that are already
configurable. These services offer a great deal of configurable. These services offer a great deal of
flexibility and can be adapted to the needs of most companies. flexibility and can be adapted to the needs of most companies.
In this guide, we'll go through a basic HA setup where we'll start by In this guide, we'll go through a multi-node setup where we'll start by
configuring our Virtual Private Cloud and subnets to later integrate configuring our Virtual Private Cloud and subnets to later integrate
services such as RDS for our database server and ElastiCache as a Redis services such as RDS for our database server and ElastiCache as a Redis
cluster to finally manage them within an auto scaling group with custom cluster to finally manage them within an auto scaling group with custom
@ -306,7 +306,7 @@ Now, it's time to create the database:
1. Under **Settings**, set a DB instance identifier, a master username, and a master password. We'll use `gitlab-db-ha`, `gitlab`, and a very secure password respectively. Make a note of these as we'll need them later. 1. Under **Settings**, set a DB instance identifier, a master username, and a master password. We'll use `gitlab-db-ha`, `gitlab`, and a very secure password respectively. Make a note of these as we'll need them later.
1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown menu. We'll use a `db.m4.large` instance. 1. For the DB instance size, select **Standard classes** and select an instance size that meets your requirements from the dropdown menu. We'll use a `db.m4.large` instance.
1. Under **Storage**, configure the following: 1. Under **Storage**, configure the following:
1. Select **Provisioned IOPS (SSD)** from the storage type dropdown menu. Provisioned IOPS (SSD) storage is best suited for HA (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html). 1. Select **Provisioned IOPS (SSD)** from the storage type dropdown menu. Provisioned IOPS (SSD) storage is best suited for this use (though you can choose General Purpose (SSD) to reduce the costs). Read more about it at [Storage for Amazon RDS](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html).
1. Allocate storage and set provisioned IOPS. We'll use the minimum values, `100` and `1000`, respectively. 1. Allocate storage and set provisioned IOPS. We'll use the minimum values, `100` and `1000`, respectively.
1. Enable storage autoscaling (optional) and set a maximum storage threshold. 1. Enable storage autoscaling (optional) and set a maximum storage threshold.
1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different [Availability Zone](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html). 1. Under **Availability & durability**, select **Create a standby instance** to have a standby RDS instance provisioned in a different [Availability Zone](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html).

View File

@ -83,7 +83,7 @@ The following table depicts the various user permission levels in a project.
| See a container registry | | ✓ | ✓ | ✓ | ✓ | | See a container registry | | ✓ | ✓ | ✓ | ✓ |
| See environments | | ✓ | ✓ | ✓ | ✓ | | See environments | | ✓ | ✓ | ✓ | ✓ |
| See a list of merge requests | | ✓ | ✓ | ✓ | ✓ | | See a list of merge requests | | ✓ | ✓ | ✓ | ✓ |
| View project statistics | | | ✓ | ✓ | ✓ | | View project statistics | | | ✓ | ✓ | ✓ |
| View Error Tracking list | | ✓ | ✓ | ✓ | ✓ | | View Error Tracking list | | ✓ | ✓ | ✓ | ✓ |
| Create new merge request | | ✓ | ✓ | ✓ | ✓ | | Create new merge request | | ✓ | ✓ | ✓ | ✓ |
| View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ | | View metrics dashboard annotations | | ✓ | ✓ | ✓ | ✓ |

View File

@ -11589,6 +11589,9 @@ msgstr ""
msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition." msgid "Improve search with Advanced Global Search and GitLab Enterprise Edition."
msgstr "" msgstr ""
msgid "In %{time_to_now}"
msgstr ""
msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}." msgid "In order to enable instance-level analytics, please ask an admin to enable %{usage_ping_link_start}usage ping%{usage_ping_link_end}."
msgstr "" msgstr ""
@ -14519,6 +14522,9 @@ msgstr ""
msgid "Notes|Collapse replies" msgid "Notes|Collapse replies"
msgstr "" msgstr ""
msgid "Notes|Private comments are accessible by internal staff only"
msgstr ""
msgid "Notes|Show all activity" msgid "Notes|Show all activity"
msgstr "" msgstr ""
@ -14965,6 +14971,9 @@ msgstr ""
msgid "Package type must be NuGet" msgid "Package type must be NuGet"
msgstr "" msgstr ""
msgid "Package type must be PyPi"
msgstr ""
msgid "Package was removed" msgid "Package was removed"
msgstr "" msgstr ""
@ -23775,6 +23784,12 @@ msgstr ""
msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice." msgid "UserProfile|Your projects can be available publicly, internally, or privately, at your choice."
msgstr "" msgstr ""
msgid "UserProfile|at"
msgstr ""
msgid "UserProfile|made a private contribution"
msgstr ""
msgid "Username (optional)" msgid "Username (optional)"
msgstr "" msgstr ""

View File

@ -57,6 +57,10 @@ module QA
click_element :download_export_link click_element :download_export_link
end end
def has_download_export_link?
has_element? :download_export_link
end
def archive_project def archive_project
page.accept_alert("Are you sure that you want to archive this project?") do page.accept_alert("Are you sure that you want to archive this project?") do
click_element :archive_project_link click_element :archive_project_link

View File

@ -4,7 +4,7 @@ import ClusterStore from '~/clusters_list/store';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { apiData } from '../mock_data'; import { apiData } from '../mock_data';
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlTable, GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon, GlTable, GlPagination } from '@gitlab/ui';
describe('Clusters', () => { describe('Clusters', () => {
let mock; let mock;
@ -14,11 +14,12 @@ describe('Clusters', () => {
const endpoint = 'some/endpoint'; const endpoint = 'some/endpoint';
const findLoader = () => wrapper.find(GlLoadingIcon); const findLoader = () => wrapper.find(GlLoadingIcon);
const findPaginatedButtons = () => wrapper.find(GlPagination);
const findTable = () => wrapper.find(GlTable); const findTable = () => wrapper.find(GlTable);
const findStatuses = () => findTable().findAll('.js-status'); const findStatuses = () => findTable().findAll('.js-status');
const mockPollingApi = (response, body, header) => { const mockPollingApi = (response, body, header) => {
mock.onGet(endpoint).reply(response, body, header); mock.onGet(`${endpoint}?page=${header['x-page']}`).reply(response, body, header);
}; };
const mountWrapper = () => { const mountWrapper = () => {
@ -29,7 +30,11 @@ describe('Clusters', () => {
beforeEach(() => { beforeEach(() => {
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mockPollingApi(200, apiData, {}); mockPollingApi(200, apiData, {
'x-total': apiData.clusters.length,
'x-per-page': 20,
'x-page': 1,
});
return mountWrapper(); return mountWrapper();
}); });
@ -93,4 +98,47 @@ describe('Clusters', () => {
} }
}); });
}); });
describe('pagination', () => {
const perPage = apiData.clusters.length;
const totalFirstPage = 100;
const totalSecondPage = 500;
beforeEach(() => {
mockPollingApi(200, apiData, {
'x-total': totalFirstPage,
'x-per-page': perPage,
'x-page': 1,
});
return mountWrapper();
});
it('should load to page 1 with header values', () => {
const buttons = findPaginatedButtons();
expect(buttons.props('perPage')).toBe(perPage);
expect(buttons.props('totalItems')).toBe(totalFirstPage);
expect(buttons.props('value')).toBe(1);
});
describe('when updating currentPage', () => {
beforeEach(() => {
mockPollingApi(200, apiData, {
'x-total': totalSecondPage,
'x-per-page': perPage,
'x-page': 2,
});
wrapper.setData({ currentPage: 2 });
return axios.waitForAll();
});
it('should change pagination when currentPage changes', () => {
const buttons = findPaginatedButtons();
expect(buttons.props('perPage')).toBe(perPage);
expect(buttons.props('totalItems')).toBe(totalSecondPage);
expect(buttons.props('value')).toBe(2);
});
});
});
}); });

View File

@ -19,14 +19,29 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore()); afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', done => { it('should commit SET_CLUSTERS_DATA with received response', done => {
mock.onGet().reply(200, apiData); const headers = {
'x-total': apiData.clusters.length,
'x-per-page': 20,
'x-page': 1,
};
const paginationInformation = {
nextPage: NaN,
page: 1,
perPage: 20,
previousPage: NaN,
total: apiData.clusters.length,
totalPages: NaN,
};
mock.onGet().reply(200, apiData, headers);
testAction( testAction(
actions.fetchClusters, actions.fetchClusters,
{ endpoint: apiData.endpoint }, { endpoint: apiData.endpoint },
{}, {},
[ [
{ type: types.SET_CLUSTERS_DATA, payload: apiData }, { type: types.SET_CLUSTERS_DATA, payload: { data: apiData, paginationInformation } },
{ type: types.SET_LOADING_STATE, payload: false }, { type: types.SET_LOADING_STATE, payload: false },
], ],
[], [],

View File

@ -39,7 +39,7 @@ describe('Dashboard', () => {
const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' }); const findEnvironmentsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem); const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
const setSearchTerm = searchTerm => { const setSearchTerm = searchTerm => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm); store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
}; };
const createShallowWrapper = (props = {}, options = {}) => { const createShallowWrapper = (props = {}, options = {}) => {
@ -135,7 +135,7 @@ describe('Dashboard', () => {
it('hides the group panels when showPanels is false', () => { it('hides the group panels when showPanels is false', () => {
createMountedWrapper({ hasMetrics: true, showPanels: false }); createMountedWrapper({ hasMetrics: true, showPanels: false });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.showEmptyState).toEqual(false); expect(wrapper.vm.showEmptyState).toEqual(false);
@ -148,7 +148,7 @@ describe('Dashboard', () => {
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
wrapper.vm.$store.commit( store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData, environmentData,
); );
@ -188,7 +188,7 @@ describe('Dashboard', () => {
); );
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', { expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
@ -205,7 +205,7 @@ describe('Dashboard', () => {
setSearch(''); setSearch('');
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).not.toHaveBeenCalledWith( expect(store.dispatch).not.toHaveBeenCalledWith(
@ -228,7 +228,7 @@ describe('Dashboard', () => {
); );
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
@ -328,7 +328,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -436,7 +436,7 @@ describe('Dashboard', () => {
it('hides the environments dropdown list when there is no environments', () => { it('hides the environments dropdown list when there is no environments', () => {
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithDashboard(wrapper.vm.$store); setupStoreWithDashboard(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
@ -446,7 +446,7 @@ describe('Dashboard', () => {
it('renders the datetimepicker dropdown', () => { it('renders the datetimepicker dropdown', () => {
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find(DateTimePicker).exists()).toBe(true); expect(wrapper.find(DateTimePicker).exists()).toBe(true);
@ -456,7 +456,7 @@ describe('Dashboard', () => {
it('renders the refresh dashboard button', () => { it('renders the refresh dashboard button', () => {
createMountedWrapper({ hasMetrics: true }); createMountedWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' });
@ -469,8 +469,8 @@ describe('Dashboard', () => {
describe('variables section', () => { describe('variables section', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
setupStoreWithVariable(wrapper.vm.$store); setupStoreWithVariable(store);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -486,7 +486,7 @@ describe('Dashboard', () => {
describe('when the panel is not expanded', () => { describe('when the panel is not expanded', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -524,14 +524,14 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } }); createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
const { panelGroups } = wrapper.vm.$store.state.monitoringDashboard.dashboard; const { panelGroups } = store.state.monitoringDashboard.dashboard;
group = panelGroups[0].group; group = panelGroups[0].group;
[panel] = panelGroups[0].panels; [panel] = panelGroups[0].panels;
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, { store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
group, group,
panel, panel,
}); });
@ -593,10 +593,8 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
const { $store } = wrapper.vm; setupStoreWithDashboard(store);
setMetricResult({ store, result: [], panel: 2 });
setupStoreWithDashboard($store);
setMetricResult({ $store, result: [], panel: 2 });
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -622,7 +620,7 @@ describe('Dashboard', () => {
beforeEach(() => { beforeEach(() => {
createMountedWrapper({ hasMetrics: true }, { attachToDocument: true }); createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -673,7 +671,7 @@ describe('Dashboard', () => {
}); });
it('shows loading element when environments fetch is still loading', () => { it('shows loading element when environments fetch is still loading', () => {
wrapper.vm.$store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`); store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
return wrapper.vm return wrapper.vm
.$nextTick() .$nextTick()
@ -681,7 +679,7 @@ describe('Dashboard', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(true); expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownLoading' }).exists()).toBe(true);
}) })
.then(() => { .then(() => {
wrapper.vm.$store.commit( store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData, environmentData,
); );
@ -703,7 +701,7 @@ describe('Dashboard', () => {
store.dispatch.mockRestore(); store.dispatch.mockRestore();
createShallowWrapper({ hasMetrics: true }); createShallowWrapper({ hasMetrics: true });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -791,7 +789,7 @@ describe('Dashboard', () => {
createShallowWrapper({ hasMetrics: true, showHeader: false }); createShallowWrapper({ hasMetrics: true, showHeader: false });
// all_dashboards is not defined in health dashboards // all_dashboards is not defined in health dashboards
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined); store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
@ -917,7 +915,7 @@ describe('Dashboard', () => {
customMetricsPath: '/endpoint', customMetricsPath: '/endpoint',
customMetricsAvailable: true, customMetricsAvailable: true,
}); });
setupStoreWithData(wrapper.vm.$store); setupStoreWithData(store);
origPage = document.body.dataset.page; origPage = document.body.dataset.page;
document.body.dataset.page = 'projects:environments:metrics'; document.body.dataset.page = 'projects:environments:metrics';

View File

@ -47,7 +47,7 @@ describe('Metrics dashboard/variables section component', () => {
it('shows the variables section', () => { it('shows the variables section', () => {
createShallowWrapper(); createShallowWrapper();
wrapper.vm.$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables); store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, sampleVariables);
return wrapper.vm.$nextTick(() => { return wrapper.vm.$nextTick(() => {
const allInputs = findTextInput().length + findCustomInput().length; const allInputs = findTextInput().length + findCustomInput().length;

View File

@ -2,48 +2,48 @@ import * as types from '~/monitoring/stores/mutation_types';
import { metricsResult, environmentData, dashboardGitResponse } from './mock_data'; import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data'; import { metricsDashboardPayload } from './fixture_data';
export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => { export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
const { dashboard } = $store.state.monitoringDashboard; const { dashboard } = store.state.monitoringDashboard;
const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric]; const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric];
$store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, { store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
metricId, metricId,
result, result,
}); });
}; };
const setEnvironmentData = $store => { const setEnvironmentData = store => {
$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData); store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
}; };
export const setupAllDashboards = $store => { export const setupAllDashboards = store => {
$store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse); store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
}; };
export const setupStoreWithDashboard = $store => { export const setupStoreWithDashboard = store => {
$store.commit( store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload, metricsDashboardPayload,
); );
$store.commit( store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload, metricsDashboardPayload,
); );
}; };
export const setupStoreWithVariable = $store => { export const setupStoreWithVariable = store => {
$store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, { store.commit(`monitoringDashboard/${types.SET_VARIABLES}`, {
label1: 'pod', label1: 'pod',
}); });
}; };
export const setupStoreWithData = $store => { export const setupStoreWithData = store => {
setupAllDashboards($store); setupAllDashboards(store);
setupStoreWithDashboard($store); setupStoreWithDashboard(store);
setMetricResult({ $store, result: [], panel: 0 }); setMetricResult({ store, result: [], panel: 0 });
setMetricResult({ $store, result: metricsResult, panel: 1 }); setMetricResult({ store, result: metricsResult, panel: 1 });
setMetricResult({ $store, result: metricsResult, panel: 2 }); setMetricResult({ store, result: metricsResult, panel: 2 });
setEnvironmentData($store); setEnvironmentData(store);
}; };

View File

@ -18,6 +18,7 @@ describe('NoteHeader component', () => {
const findActionText = () => wrapper.find({ ref: 'actionText' }); const findActionText = () => wrapper.find({ ref: 'actionText' });
const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' }); const findTimestampLink = () => wrapper.find({ ref: 'noteTimestampLink' });
const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' }); const findTimestamp = () => wrapper.find({ ref: 'noteTimestamp' });
const findConfidentialIndicator = () => wrapper.find('[data-testid="confidentialIndicator"]');
const findSpinner = () => wrapper.find({ ref: 'spinner' }); const findSpinner = () => wrapper.find({ ref: 'spinner' });
const author = { const author = {
@ -231,4 +232,15 @@ describe('NoteHeader component', () => {
}); });
}); });
}); });
describe('with confidentiality indicator', () => {
it.each`
status | condition
${true} | ${'shows'}
${false} | ${'hides'}
`('$condition icon indicator when isConfidential is $status', ({ status }) => {
createComponent({ isConfidential: status });
expect(findConfidentialIndicator().exists()).toBe(status);
});
});
}); });

View File

@ -9,6 +9,14 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)} let(:schema) { GraphQL::Schema.define(query: query_type, mutation: nil)}
let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) } let(:context) { GraphQL::Query::Context.new(query: OpenStruct.new(schema: schema), values: nil, object: nil) }
before do
stub_const('NoPrimaryKey', Class.new(ActiveRecord::Base))
NoPrimaryKey.class_eval do
self.table_name = 'no_primary_key'
self.primary_key = nil
end
end
subject(:connection) do subject(:connection) do
described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments)) described_class.new(nodes, { context: context, max_page_size: 3 }.merge(arguments))
end end
@ -303,9 +311,4 @@ describe Gitlab::Graphql::Pagination::Keyset::Connection do
end end
end end
end end
class NoPrimaryKey < ActiveRecord::Base
self.table_name = 'no_primary_key'
self.primary_key = nil
end
end end

View File

@ -6,10 +6,9 @@ require_relative './simple_check_shared'
describe Gitlab::HealthChecks::MasterCheck do describe Gitlab::HealthChecks::MasterCheck do
let(:result_class) { Gitlab::HealthChecks::Result } let(:result_class) { Gitlab::HealthChecks::Result }
SUCCESS_CODE = 100
FAILURE_CODE = 101
before do before do
stub_const('SUCCESS_CODE', 100)
stub_const('FAILURE_CODE', 101)
described_class.register_master described_class.register_master
end end

View File

@ -43,7 +43,4 @@ describe 'Import/Export attribute configuration' do
IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file} IMPORT_EXPORT_CONFIG: #{Gitlab::ImportExport.config_file}
MSG MSG
end end
class Author < User
end
end end

View File

@ -4,99 +4,109 @@ require 'spec_helper'
require 'rake_helper' require 'rake_helper'
describe SystemCheck::SimpleExecutor do describe SystemCheck::SimpleExecutor do
class SimpleCheck < SystemCheck::BaseCheck before do
set_name 'my simple check' stub_const('SimpleCheck', Class.new(SystemCheck::BaseCheck))
stub_const('OtherCheck', Class.new(SystemCheck::BaseCheck))
stub_const('SkipCheck', Class.new(SystemCheck::BaseCheck))
stub_const('DynamicSkipCheck', Class.new(SystemCheck::BaseCheck))
stub_const('MultiCheck', Class.new(SystemCheck::BaseCheck))
stub_const('SkipMultiCheck', Class.new(SystemCheck::BaseCheck))
stub_const('RepairCheck', Class.new(SystemCheck::BaseCheck))
stub_const('BugousCheck', Class.new(SystemCheck::BaseCheck))
def check? SimpleCheck.class_eval do
true set_name 'my simple check'
end
end
class OtherCheck < SystemCheck::BaseCheck def check?
set_name 'other check' true
end
def check?
false
end end
def show_error OtherCheck.class_eval do
$stdout.puts 'this is an error text' set_name 'other check'
end
end
class SkipCheck < SystemCheck::BaseCheck def check?
set_name 'skip check' false
set_skip_reason 'this is a skip reason' end
def skip? def show_error
true $stdout.puts 'this is an error text'
end
end end
def check? SkipCheck.class_eval do
raise 'should not execute this' set_name 'skip check'
end set_skip_reason 'this is a skip reason'
end
class DynamicSkipCheck < SystemCheck::BaseCheck def skip?
set_name 'dynamic skip check' true
set_skip_reason 'this is a skip reason' end
def skip? def check?
self.skip_reason = 'this is a dynamic skip reason' raise 'should not execute this'
true end
end end
def check? DynamicSkipCheck.class_eval do
raise 'should not execute this' set_name 'dynamic skip check'
end set_skip_reason 'this is a skip reason'
end
class MultiCheck < SystemCheck::BaseCheck def skip?
set_name 'multi check' self.skip_reason = 'this is a dynamic skip reason'
true
end
def multi_check def check?
$stdout.puts 'this is a multi output check' raise 'should not execute this'
end
end end
def check? MultiCheck.class_eval do
raise 'should not execute this' set_name 'multi check'
end
end
class SkipMultiCheck < SystemCheck::BaseCheck def multi_check
set_name 'skip multi check' $stdout.puts 'this is a multi output check'
end
def skip? def check?
true raise 'should not execute this'
end
end end
def multi_check SkipMultiCheck.class_eval do
raise 'should not execute this' set_name 'skip multi check'
end
end
class RepairCheck < SystemCheck::BaseCheck def skip?
set_name 'repair check' true
end
def check? def multi_check
false raise 'should not execute this'
end
end end
def repair! RepairCheck.class_eval do
true set_name 'repair check'
def check?
false
end
def repair!
true
end
def show_error
$stdout.puts 'this is an error message'
end
end end
def show_error BugousCheck.class_eval do
$stdout.puts 'this is an error message' set_name 'my bugous check'
end
end
class BugousCheck < SystemCheck::BaseCheck def check?
CustomError = Class.new(StandardError) raise StandardError, 'omg'
set_name 'my bugous check' end
def check?
raise CustomError, 'omg'
end end
end end

View File

@ -8,7 +8,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
let(:application_settings) { table(:application_settings) } let(:application_settings) { table(:application_settings) }
let(:plaintext) { 'secret-token' } let(:plaintext) { 'secret-token' }
PLAINTEXT_ATTRIBUTES = %w[ plaintext_attributes = %w[
akismet_api_key akismet_api_key
elasticsearch_aws_secret_access_key elasticsearch_aws_secret_access_key
recaptcha_private_key recaptcha_private_key
@ -21,7 +21,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
it 'encrypts token and saves it' do it 'encrypts token and saves it' do
application_setting = application_settings.create application_setting = application_settings.create
application_setting.update_columns( application_setting.update_columns(
PLAINTEXT_ATTRIBUTES.each_with_object({}) do |plaintext_attribute, attributes| plaintext_attributes.each_with_object({}) do |plaintext_attribute, attributes|
attributes[plaintext_attribute] = plaintext attributes[plaintext_attribute] = plaintext
end end
) )
@ -29,7 +29,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
migration.up migration.up
application_setting.reload application_setting.reload
PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute| plaintext_attributes.each do |plaintext_attribute|
expect(application_setting[plaintext_attribute]).not_to be_nil expect(application_setting[plaintext_attribute]).not_to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}"]).not_to be_nil expect(application_setting["encrypted_#{plaintext_attribute}"]).not_to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).not_to be_nil expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).not_to be_nil
@ -40,7 +40,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
describe '#down' do describe '#down' do
it 'decrypts encrypted token and saves it' do it 'decrypts encrypted token and saves it' do
application_setting = application_settings.create( application_setting = application_settings.create(
PLAINTEXT_ATTRIBUTES.each_with_object({}) do |plaintext_attribute, attributes| plaintext_attributes.each_with_object({}) do |plaintext_attribute, attributes|
attributes[plaintext_attribute] = plaintext attributes[plaintext_attribute] = plaintext
end end
) )
@ -48,7 +48,7 @@ describe EncryptPlaintextAttributesOnApplicationSettings do
migration.down migration.down
application_setting.reload application_setting.reload
PLAINTEXT_ATTRIBUTES.each do |plaintext_attribute| plaintext_attributes.each do |plaintext_attribute|
expect(application_setting[plaintext_attribute]).to eq(plaintext) expect(application_setting[plaintext_attribute]).to eq(plaintext)
expect(application_setting["encrypted_#{plaintext_attribute}"]).to be_nil expect(application_setting["encrypted_#{plaintext_attribute}"]).to be_nil
expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).to be_nil expect(application_setting["encrypted_#{plaintext_attribute}_iv"]).to be_nil

View File

@ -42,7 +42,7 @@ describe ProjectPolicy do
admin_tag admin_milestone admin_merge_request update_merge_request create_commit_status admin_tag admin_milestone admin_merge_request update_merge_request create_commit_status
update_commit_status create_build update_build create_pipeline update_commit_status create_build update_build create_pipeline
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image destroy_container_image resolve_note create_container_image update_container_image destroy_container_image daily_statistics
create_environment update_environment create_deployment update_deployment create_release update_release create_environment update_environment create_deployment update_deployment create_release update_release
create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation create_metrics_dashboard_annotation delete_metrics_dashboard_annotation update_metrics_dashboard_annotation
] ]
@ -54,7 +54,7 @@ describe ProjectPolicy do
admin_snippet admin_project_member admin_note admin_wiki admin_project admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics read_deploy_token create_deploy_token destroy_deploy_token read_deploy_token create_deploy_token destroy_deploy_token
admin_terraform_state admin_terraform_state
] ]
end end

View File

@ -3,23 +3,23 @@
require 'spec_helper' require 'spec_helper'
describe API::ProjectStatistics do describe API::ProjectStatistics do
let(:maintainer) { create(:user) } let_it_be(:developer) { create(:user) }
let(:public_project) { create(:project, :public) } let_it_be(:public_project) { create(:project, :public) }
before do before do
public_project.add_maintainer(maintainer) public_project.add_developer(developer)
end end
describe 'GET /projects/:id/statistics' do describe 'GET /projects/:id/statistics' do
let!(:fetch_statistics1) { create(:project_daily_statistic, project: public_project, fetch_count: 30, date: 29.days.ago) } let_it_be(:fetch_statistics1) { create(:project_daily_statistic, project: public_project, fetch_count: 30, date: 29.days.ago) }
let!(:fetch_statistics2) { create(:project_daily_statistic, project: public_project, fetch_count: 4, date: 3.days.ago) } let_it_be(:fetch_statistics2) { create(:project_daily_statistic, project: public_project, fetch_count: 4, date: 3.days.ago) }
let!(:fetch_statistics3) { create(:project_daily_statistic, project: public_project, fetch_count: 3, date: 2.days.ago) } let_it_be(:fetch_statistics3) { create(:project_daily_statistic, project: public_project, fetch_count: 3, date: 2.days.ago) }
let!(:fetch_statistics4) { create(:project_daily_statistic, project: public_project, fetch_count: 2, date: 1.day.ago) } let_it_be(:fetch_statistics4) { create(:project_daily_statistic, project: public_project, fetch_count: 2, date: 1.day.ago) }
let!(:fetch_statistics5) { create(:project_daily_statistic, project: public_project, fetch_count: 1, date: Date.today) } let_it_be(:fetch_statistics5) { create(:project_daily_statistic, project: public_project, fetch_count: 1, date: Date.today) }
let!(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) } let_it_be(:fetch_statistics_other_project) { create(:project_daily_statistic, project: create(:project), fetch_count: 29, date: 29.days.ago) }
it 'returns the fetch statistics of the last 30 days' do it 'returns the fetch statistics of the last 30 days' do
get api("/projects/#{public_project.id}/statistics", maintainer) get api("/projects/#{public_project.id}/statistics", developer)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches'] fetches = json_response['fetches']
@ -32,7 +32,7 @@ describe API::ProjectStatistics do
it 'excludes the fetch statistics older than 30 days' do it 'excludes the fetch statistics older than 30 days' do
create(:project_daily_statistic, fetch_count: 31, project: public_project, date: 30.days.ago) create(:project_daily_statistic, fetch_count: 31, project: public_project, date: 30.days.ago)
get api("/projects/#{public_project.id}/statistics", maintainer) get api("/projects/#{public_project.id}/statistics", developer)
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
fetches = json_response['fetches'] fetches = json_response['fetches']
@ -41,11 +41,11 @@ describe API::ProjectStatistics do
expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s }) expect(fetches['days'].last).to eq({ 'count' => fetch_statistics1.fetch_count, 'date' => fetch_statistics1.date.to_s })
end end
it 'responds with 403 when the user is not a maintainer of the repository' do it 'responds with 403 when the user is not a developer of the repository' do
developer = create(:user) guest = create(:user)
public_project.add_developer(developer) public_project.add_guest(guest)
get api("/projects/#{public_project.id}/statistics", developer) get api("/projects/#{public_project.id}/statistics", guest)
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
expect(json_response['message']).to eq('403 Forbidden') expect(json_response['message']).to eq('403 Forbidden')

View File

@ -10,8 +10,8 @@ require_relative '../../../../rubocop/cop/rspec/env_assignment'
describe RuboCop::Cop::RSpec::EnvAssignment do describe RuboCop::Cop::RSpec::EnvAssignment do
include CopHelper include CopHelper
OFFENSE_CALL_SINGLE_QUOTES_KEY = %(ENV['FOO'] = 'bar').freeze offense_call_single_quotes_key = %(ENV['FOO'] = 'bar').freeze
OFFENSE_CALL_DOUBLE_QUOTES_KEY = %(ENV["FOO"] = 'bar').freeze offense_call_double_quotes_key = %(ENV["FOO"] = 'bar').freeze
let(:source_file) { 'spec/foo_spec.rb' } let(:source_file) { 'spec/foo_spec.rb' }
@ -36,12 +36,12 @@ describe RuboCop::Cop::RSpec::EnvAssignment do
end end
context 'with a key using single quotes' do context 'with a key using single quotes' do
it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY it_behaves_like 'an offensive ENV#[]= call', offense_call_single_quotes_key
it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_SINGLE_QUOTES_KEY, %(stub_env('FOO', 'bar')) it_behaves_like 'an autocorrected ENV#[]= call', offense_call_single_quotes_key, %(stub_env('FOO', 'bar'))
end end
context 'with a key using double quotes' do context 'with a key using double quotes' do
it_behaves_like 'an offensive ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY it_behaves_like 'an offensive ENV#[]= call', offense_call_double_quotes_key
it_behaves_like 'an autocorrected ENV#[]= call', OFFENSE_CALL_DOUBLE_QUOTES_KEY, %(stub_env("FOO", 'bar')) it_behaves_like 'an autocorrected ENV#[]= call', offense_call_double_quotes_key, %(stub_env("FOO", 'bar'))
end end
end end

View File

@ -74,47 +74,6 @@ describe Snippets::CreateService do
end end
end end
shared_examples 'spam check is performed' do
shared_examples 'marked as spam' do
it 'marks a snippet as spam' do
expect(snippet).to be_spam
end
it 'invalidates the snippet' do
expect(snippet).to be_invalid
end
it 'creates a new spam_log' do
expect { snippet }
.to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
end
it 'assigns a spam_log to an issue' do
expect(snippet.spam_log).to eq(SpamLog.last)
end
end
let(:extra_opts) do
{ visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
end
before do
expect_next_instance_of(Spam::AkismetService) do |akismet_service|
expect(akismet_service).to receive_messages(spam?: true)
end
end
[true, false, nil].each do |allow_possible_spam|
context "when recaptcha_disabled flag is #{allow_possible_spam.inspect}" do
before do
stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
end
it_behaves_like 'marked as spam'
end
end
end
shared_examples 'snippet create data is tracked' do shared_examples 'snippet create data is tracked' do
let(:counter) { Gitlab::UsageDataCounters::SnippetCounter } let(:counter) { Gitlab::UsageDataCounters::SnippetCounter }
@ -280,7 +239,7 @@ describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet' it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply' it_behaves_like 'public visibility level restrictions apply'
it_behaves_like 'spam check is performed' it_behaves_like 'snippets spam check is performed'
it_behaves_like 'snippet create data is tracked' it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails' it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files' it_behaves_like 'creates repository and files'
@ -306,7 +265,7 @@ describe Snippets::CreateService do
it_behaves_like 'a service that creates a snippet' it_behaves_like 'a service that creates a snippet'
it_behaves_like 'public visibility level restrictions apply' it_behaves_like 'public visibility level restrictions apply'
it_behaves_like 'spam check is performed' it_behaves_like 'snippets spam check is performed'
it_behaves_like 'snippet create data is tracked' it_behaves_like 'snippet create data is tracked'
it_behaves_like 'an error service response when save fails' it_behaves_like 'an error service response when save fails'
it_behaves_like 'creates repository and files' it_behaves_like 'creates repository and files'

View File

@ -7,7 +7,7 @@ describe Snippets::UpdateService do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:admin) { create :user, admin: true } let_it_be(:admin) { create :user, admin: true }
let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE } let(:visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
let(:options) do let(:base_opts) do
{ {
title: 'Test snippet', title: 'Test snippet',
file_name: 'snippet.rb', file_name: 'snippet.rb',
@ -15,6 +15,8 @@ describe Snippets::UpdateService do
visibility_level: visibility_level visibility_level: visibility_level
} }
end end
let(:extra_opts) { {} }
let(:options) { base_opts.merge(extra_opts) }
let(:updater) { user } let(:updater) { user }
let(:service) { Snippets::UpdateService.new(project, updater, options) } let(:service) { Snippets::UpdateService.new(project, updater, options) }
@ -85,7 +87,7 @@ describe Snippets::UpdateService do
end end
context 'when update fails' do context 'when update fails' do
let(:options) { { title: '' } } let(:extra_opts) { { title: '' } }
it 'does not increment count' do it 'does not increment count' do
expect { subject }.not_to change { counter.read(:update) } expect { subject }.not_to change { counter.read(:update) }
@ -273,7 +275,7 @@ describe Snippets::UpdateService do
shared_examples 'committable attributes' do shared_examples 'committable attributes' do
context 'when file_name is updated' do context 'when file_name is updated' do
let(:options) { { file_name: 'snippet.rb' } } let(:extra_opts) { { file_name: 'snippet.rb' } }
it 'commits to repository' do it 'commits to repository' do
expect(service).to receive(:create_commit) expect(service).to receive(:create_commit)
@ -282,7 +284,7 @@ describe Snippets::UpdateService do
end end
context 'when content is updated' do context 'when content is updated' do
let(:options) { { content: 'puts "hello world"' } } let(:extra_opts) { { content: 'puts "hello world"' } }
it 'commits to repository' do it 'commits to repository' do
expect(service).to receive(:create_commit) expect(service).to receive(:create_commit)
@ -314,6 +316,11 @@ describe Snippets::UpdateService do
it_behaves_like 'updates repository content' it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails' it_behaves_like 'commit operation fails'
it_behaves_like 'committable attributes' it_behaves_like 'committable attributes'
it_behaves_like 'snippets spam check is performed' do
before do
subject
end
end
context 'when snippet does not have a repository' do context 'when snippet does not have a repository' do
let!(:snippet) { create(:project_snippet, author: user, project: project) } let!(:snippet) { create(:project_snippet, author: user, project: project) }
@ -333,6 +340,11 @@ describe Snippets::UpdateService do
it_behaves_like 'updates repository content' it_behaves_like 'updates repository content'
it_behaves_like 'commit operation fails' it_behaves_like 'commit operation fails'
it_behaves_like 'committable attributes' it_behaves_like 'committable attributes'
it_behaves_like 'snippets spam check is performed' do
before do
subject
end
end
context 'when snippet does not have a repository' do context 'when snippet does not have a repository' do
let!(:snippet) { create(:personal_snippet, author: user, project: project) } let!(:snippet) { create(:personal_snippet, author: user, project: project) }

View File

@ -39,7 +39,7 @@ RSpec.shared_context 'ProjectPolicy context' do
update_pipeline create_merge_request_from create_wiki push_code update_pipeline create_merge_request_from create_wiki push_code
resolve_note create_container_image update_container_image resolve_note create_container_image update_container_image
create_environment create_deployment update_deployment create_release update_release create_environment create_deployment update_deployment create_release update_release
update_environment update_environment daily_statistics
] ]
end end
@ -49,7 +49,6 @@ RSpec.shared_context 'ProjectPolicy context' do
admin_snippet admin_project_member admin_note admin_wiki admin_project admin_snippet admin_project_member admin_note admin_wiki admin_project
admin_commit_status admin_build admin_container_image admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics
] ]
end end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
RSpec.shared_examples 'snippets spam check is performed' do
shared_examples 'marked as spam' do
it 'marks a snippet as spam' do
expect(snippet).to be_spam
end
it 'invalidates the snippet' do
expect(snippet).to be_invalid
end
it 'creates a new spam_log' do
expect { snippet }
.to have_spam_log(title: snippet.title, noteable_type: snippet.class.name)
end
it 'assigns a spam_log to an issue' do
expect(snippet.spam_log).to eq(SpamLog.last)
end
end
let(:extra_opts) do
{ visibility_level: Gitlab::VisibilityLevel::PUBLIC, request: double(:request, env: {}) }
end
before do
expect_next_instance_of(Spam::AkismetService) do |akismet_service|
expect(akismet_service).to receive_messages(spam?: true)
end
end
[true, false, nil].each do |allow_possible_spam|
context "when allow_possible_spam flag is #{allow_possible_spam.inspect}" do
before do
stub_feature_flags(allow_possible_spam: allow_possible_spam) unless allow_possible_spam.nil?
end
it_behaves_like 'marked as spam'
end
end
end