Commit 78c30dfd authored by Stan Hu's avatar Stan Hu

Merge branch 'master' into ce-to-ee-2018-10-15

parents 8b919b8c a5c7e0a3
<script>
import $ from 'jquery';
import IssueCardWeight from 'ee/boards/components/issue_card_weight.vue';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../eventhub';
import tooltip from '../../vue_shared/directives/tooltip';
......@@ -10,6 +11,7 @@
components: {
UserAvatarLink,
IssueCardWeight,
Icon,
},
directives: {
tooltip,
......@@ -142,11 +144,11 @@
<div>
<div class="board-card-header">
<h4 class="board-card-title">
<i
<icon
v-if="issue.confidential"
class="fa fa-eye-slash confidential-icon"
aria-hidden="true"
></i>
name="eye-slash"
class="confidential-icon"
/>
<a
:href="issue.path"
:title="issue.title"
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store';
import IssueCardInner from '../issue_card_inner.vue';
......@@ -6,6 +7,7 @@
export default {
components: {
IssueCardInner,
Icon,
},
props: {
issueLinkBase: {
......@@ -147,13 +149,13 @@
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"/>
<span
<icon
v-if="issue.selected"
:aria-label="'Issue #' + issue.id + ' selected'"
name="mobile-issue-close"
aria-checked="true"
class="issue-card-selected text-center">
<i class="fa fa-check"></i>
</span>
class="issue-card-selected text-center"
/>
</div>
</div>
</div>
......
<script>
import { Link } from '@gitlab-org/gitlab-ui';
import Icon from '~/vue_shared/components/icon.vue';
import ModalStore from '../../stores/modal_store';
import boardsStore from '../../stores/boards_store';
export default {
components: {
'gl-link': Link,
Icon,
},
data() {
return {
......@@ -35,7 +37,9 @@ export default {
class="dropdown-label-box">
</span>
{{ selected.title }}
<i class="fa fa-chevron-down"></i>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-drop-up">
<ul>
......
<script>
import $ from 'jquery';
import _ from 'underscore';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../eventhub';
import Api from '../../api';
export default {
name: 'BoardProjectSelect',
components: {
Icon,
},
props: {
groupId: {
type: Number,
......@@ -78,11 +82,9 @@ export default {
aria-expanded="false"
>
{{ selectedProjectName }}
<i
class="fa fa-chevron-down"
aria-hidden="true"
>
</i>
<icon
name="chevron-down"
/>
</button>
<div class="dropdown-menu dropdown-menu-selectable dropdown-menu-full-width">
<div class="dropdown-title">
......@@ -92,12 +94,11 @@ export default {
type="button"
class="dropdown-title-button dropdown-menu-close"
>
<i
aria-hidden="true"
<icon
name="merge-request-close-m"
data-hidden="true"
class="fa fa-times dropdown-menu-close-icon"
>
</i>
class="dropdown-menu-close-icon"
/>
</button>
</div>
<div class="dropdown-input">
......@@ -106,12 +107,11 @@ export default {
type="search"
placeholder="Search projects"
/>
<i
aria-hidden="true"
<icon
name="search"
class="dropdown-input-search"
data-hidden="true"
class="fa fa-search dropdown-input-search"
>
</i>
/>
</div>
<div class="dropdown-content"></div>
<div class="dropdown-loading">
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
import iconCycleAnalyticsSplash from 'icons/_icon_cycle_analytics_splash.svg';
export default {
components: {
Icon,
},
props: {
documentationLink: {
type: String,
......@@ -28,10 +32,9 @@
type="button"
@click="dismissOverviewDialog"
>
<i
class="fa fa-times"
aria-hidden="true">
</i>
<icon
name="close"
/>
</button>
<div
class="svg-container"
......
......@@ -115,7 +115,7 @@ export default {
<span>
{{ selectedVersionName }}
</span>
<Icon
<icon
:size="12"
name="angle-down"
class="position-absolute"
......
......@@ -60,11 +60,9 @@ export default {
>
<span>
<icon name="play" />
<i
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<icon
name="chevron-down"
/>
<gl-loading-icon v-if="isLoading" />
</span>
</button>
......
......@@ -4,6 +4,7 @@ import _ from 'underscore';
import tooltip from '~/vue_shared/directives/tooltip';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { humanize } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import ActionsComponent from './environment_actions.vue';
import ExternalUrlComponent from './environment_external_url.vue';
import StopComponent from './environment_stop.vue';
......@@ -24,6 +25,7 @@ export default {
components: {
UserAvatarLink,
CommitComponent,
Icon,
ActionsComponent,
ExternalUrlComponent,
StopComponent,
......@@ -448,6 +450,14 @@ export default {
this.canRetry
);
},
folderIconName() {
return this.model.isOpen ? 'chevron-down' : 'chevron-right';
},
deployIconName() {
return this.model.isDeployBoardVisible ? 'chevron-down' : 'chevron-right';
},
},
methods: {
......@@ -481,23 +491,15 @@ export default {
>
{{ s__("Environments|Environment") }}
</div>
<span
v-if="model.hasDeployBoard"
class="deploy-board-icon"
@click="toggleDeployBoard">
<i
v-show="!model.isDeployBoardVisible"
class="fa fa-caret-right"
aria-hidden="true">
</i>
<i
v-show="model.isDeployBoardVisible"
class="fa fa-caret-down"
aria-hidden="true">
</i>
@click="toggleDeployBoard"
>
<icon :name="deployIconName" />
</span>
<span
v-if="!model.isFolder"
class="environment-name table-mobile-content">
......@@ -520,27 +522,15 @@ export default {
role="button"
@click="onClickFolder">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true"
>
</i>
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"
>
</i>
</span>
<icon
:name="folderIconName"
class="folder-icon"
/>
<span class="folder-icon">
<i
class="fa fa-folder"
aria-hidden="true">
</i>
</span>
<icon
name="folder"
class="folder-icon"
/>
<span>
{{ model.folderName }}
......
<script>
import _ from 'underscore';
import { mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import eventHub from '../event_hub';
import frequentItemsMixin from './frequent_items_mixin';
export default {
components: {
Icon,
},
mixins: [frequentItemsMixin],
data() {
return {
......@@ -45,11 +49,10 @@ export default {
type="search"
class="form-control"
/>
<i
<icon
v-if="!searchQuery"
class="search-icon fa fa-fw fa-search"
aria-hidden="true"
>
</i>
name="search"
class="search-icon"
/>
</div>
</template>
......@@ -102,7 +102,6 @@ export default {
>
<icon
:name="lockIcon"
aria-hidden="true"
class="sidebar-item-icon is-active"
/>
</div>
......@@ -134,7 +133,6 @@ export default {
<icon
:size="16"
name="lock"
aria-hidden="true"
class="sidebar-item-icon inline is-active"
/>
{{ __('Locked') }}
......@@ -147,7 +145,6 @@ export default {
<icon
:size="16"
name="lock-open"
aria-hidden="true"
class="sidebar-item-icon inline"
/>
{{ __('Unlocked') }}
......
......@@ -105,6 +105,7 @@ export default {
:x="x"
:y="y"
:tabindex="tabIndex"
aria-hidden="true"
>
<use v-bind="{ 'xlink:href':spriteHref }"/>
</svg>
......
......@@ -37,7 +37,6 @@
:name="warningIcon"
:size="16"
class="icon inline"
aria-hidden="true"
/>
<span v-if="isLockedAndConfidential">
......
......@@ -109,7 +109,7 @@ export default {
class="system-note-commit-list-toggler flex-row"
@click="expanded = !expanded"
>
<Icon
<icon
:name="toggleIcon"
:size="8"
class="append-right-5"
......
......@@ -283,18 +283,20 @@
.dismiss-button {
position: absolute;
right: 6px;
top: 6px;
right: $gl-padding-8;
top: $gl-padding-8;
cursor: pointer;
color: $blue-300;
color: $blue-500;
z-index: 1;
border: 0;
background-color: transparent;
padding: $gl-padding-8;
line-height: 0;
&:hover,
&:focus {
border: 0;
color: $blue-400;
color: $blue-700;
}
}
......
......@@ -144,6 +144,13 @@
top: 11px;
right: 8px;
}
.ic-chevron-down {
position: absolute;
top: $gl-padding-8;
right: $gl-padding-8;
color: $gray-darkest;
}
}
@mixin dropdown-item-hover {
......@@ -561,6 +568,10 @@
top: -1px;
}
.dropdown-menu-close-icon {
vertical-align: middle;
}
.dropdown-menu-back {
left: 7px;
top: 2px;
......@@ -572,9 +583,10 @@
padding: 0 10px;
.fa,
.input-icon {
.input-icon,
.ic-search {
position: absolute;
top: 10px;
top: $gl-padding-8;
right: 20px;
color: $dropdown-input-fa-color;
font-size: 12px;
......
......@@ -350,8 +350,7 @@
}
.confidential-icon {
position: relative;
top: 1px;
vertical-align: text-top;
margin-right: 5px;
}
}
......
......@@ -21,7 +21,7 @@
}
}
svg {
.svg-container svg {
width: 136px;
height: 136px;
}
......
......@@ -90,6 +90,7 @@
margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
vertical-align: text-top;
.fa:nth-child(1) {
margin-right: 3px;
......
......@@ -240,7 +240,7 @@ table.pipeline-project-metrics tr td {
}
}
svg {
.svg-container svg {
width: 62px;
height: 50px;
}
......
......@@ -10,7 +10,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
before_action :authorize_update_build!, only: [:keep]
before_action :extract_ref_name_and_path
before_action :set_request_format, only: [:file]
before_action :validate_artifacts!
before_action :validate_artifacts!, except: [:download]
before_action :entry, only: [:file]
def download
......@@ -102,7 +102,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def artifacts_file
@artifacts_file ||= build.artifacts_file_for_type(params[:file_type] || :archive)
@artifacts_file ||= build&.artifacts_file_for_type(params[:file_type] || :archive)
end
def entry
......
......@@ -6,7 +6,7 @@ class Projects::BuildArtifactsController < Projects::ApplicationController
before_action :authorize_read_build!
before_action :extract_ref_name_and_path
before_action :validate_artifacts!
before_action :validate_artifacts!, except: [:download]
def download
redirect_to download_project_job_artifacts_path(project, job)
......
......@@ -19,7 +19,7 @@ module Clusters
def set_initial_status
return unless not_installable?
if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip
if cluster&.application_ingress_available? && cluster.application_ingress.external_ip
self.status = 'installable'
end
end
......
......@@ -45,8 +45,9 @@ module Clusters
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :rbac?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_helm, prefix: true, allow_nil: true
delegate :available?, to: :application_ingress, prefix: true, allow_nil: true
delegate :available?, to: :application_prometheus, prefix: true, allow_nil: true
enum platform_type: {
kubernetes: 1
......
......@@ -15,7 +15,7 @@ module Clusters
def set_initial_status
return unless not_installable?
self.status = 'installable' if cluster&.application_helm_installed?
self.status = 'installable' if cluster&.application_helm_available?
end
def self.application_name
......
......@@ -66,6 +66,10 @@ module Clusters
end
end
end
def available?
installed? || updated?
end
end
end
end
......@@ -26,7 +26,7 @@ class PrometheusService < MonitoringService
end
def editable?
manual_configuration? || !prometheus_installed?
manual_configuration? || !prometheus_available?
end
def title
......@@ -75,17 +75,17 @@ class PrometheusService < MonitoringService
RestClient::Resource.new(api_url) if api_url && manual_configuration? && active?
end
def prometheus_installed?
def prometheus_available?
return false if template?
return false unless project
project.clusters.enabled.any? { |cluster| cluster.application_prometheus&.installed? }
project.clusters.enabled.any? { |cluster| cluster.application_prometheus_available? }
end
private
def synchronize_service_state
self.active = prometheus_installed? || manual_configuration?
self.active = prometheus_available? || manual_configuration?
true
end
......
......@@ -212,7 +212,7 @@ module SystemNoteService
# "closed via bc17db76"
#
# Returns the created Note object
def change_status(noteable, project, author, status, source)
def change_status(noteable, project, author, status, source = nil)
body = status.dup
body << " via #{source.gfm_reference(project)}" if source
......
......@@ -2,7 +2,7 @@
= form_errors(@group)
= render 'shared/group_form', f: f
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
= render_if_exists 'shared/old_repository_size_limit_setting', form: f, type: :group
= render_if_exists 'admin/namespace_plan', f: f
.form-group.row.group-description-holder
......
......@@ -13,7 +13,7 @@
- if cookies[:explore_groups_landing_dismissed] != 'true'
.explore-groups.landing.content-block.js-explore-groups-landing.hide
%button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= icon('times')
%button.dismiss-button{ type: 'button', 'aria-label' => _('Dismiss') }= sprite_icon('close', size: 16)
.svg-container
= custom_icon('icon_explore_groups_splash')
.inner-content
......
......@@ -10,7 +10,7 @@
%br/
%span.descr This setting can be overridden in each project.
= render partial: 'groups/ee/project_creation_level', locals: { form: f, group: @group }
= render partial: 'groups/ee/old_project_creation_level', locals: { form: f, group: @group }
.form-group.row
= f.label :require_two_factor_authentication, 'Two-factor authentication', class: 'col-form-label col-sm-2 pt-0'
......
......@@ -13,11 +13,11 @@
= f.text_field :id, class: 'form-control w-auto', readonly: true
.row.prepend-top-8
.form-group.col-md-9.append-bottom-0
.form-group.col-md-9
= f.label :description, _('Group description (optional)'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.row= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.form-group.prepend-top-default.append-bottom-20
.avatar-container.s90
......
......@@ -18,6 +18,7 @@
%span.descr.text-muted= share_with_group_lock_help_text(@group)
= render 'groups/settings/lfs', f: f
= render partial: 'groups/ee/project_creation_level', locals: { form: f, group: @group }
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
......
......@@ -7,7 +7,7 @@
- else
.container-fluid
.row
- if service.prometheus_installed?
- if service.prometheus_available?
.col-sm-2
.svg-container
= image_tag 'illustrations/monitoring/getting_started.svg'
......
---
title: Show available clusters when installed or updated
merge_request: 22356
author:
type: fixed
---
title: Replace i to icons in vue components
merge_request: 20748
author: George Tsiolis
type: changed
......@@ -273,6 +273,67 @@ edit existing comments. Non-team members are restricted from adding or editing c
Additionally locked issues can not be reopened.
## Merge Request Reviews **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/4213) in GitLab 11.4.
When looking at a Merge Request diff, you are able to start a review.
This allows you to create comments inside a Merge Request that are **only visible to you** until published,
in order to allow you to submit them all as a single action.
### Starting a review
In order to start a review, simply write a comment on a diff as normal under the **Changes** tab
in an MR and click on the **Start a review** button.
![Starting a review](img/mr_review_start.png)
Once a review is started, you will see any comments that are part of this review marked `Pending`.
All comments that are part of a review show two buttons:
- **Submit review**: Submits all comments that are part of the review, making them visible to other users.
- **Add comment now**: Submits the specific comment as a regular comment instead of as part of the review.
![A comment that is part of a review](img/pending_review_comment.png)
You can use [quick actions] inside review comments. The comment will show the actions that will be performed once published.
![A review comment with quick actions](img/review_comment_quickactions.png)
To add more comments to a review, start writing a comment as normal and click the **Add to review** button.
![Adding a second comment to a review](img/mr_review_second_comment.png)
This will add the comment to the review.
![Second review comment](img/mr_review_second_comment_added.png)
### Resolving/Unresolving discussions
Review comments can also resolve/unresolve [resolvable discussions](#resolvable-discussions).
When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve
the discussion once published.
![Resolve checkbox](img/mr_review_resolve.png)
![Unresolve checkbox](img/mr_review_unresolve.png)
If a particular pending comment will resolve or unresolve the discussion, this will be shown on the pending
comment itself.
![Resolve status](img/mr_review_resolve2.png)
![Unresolve status](img/mr_review_unresolve2.png)
### Submitting a review
If you have any comments that have not been submitted, you will see a bar at the bottom of the screen with two buttons:
- **Discard**: Discards all comments that have not been submitted.
- **Submit review**: All comments are published. Any quick actions submitted are performed at this time.
Alternatively, every pending comment has a button to submit the entire review.
![MR Review bar](img/mr_review_bar.png)
[ce-5022]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5022
[ce-7125]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7125
[ce-7527]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7527
......
......@@ -86,10 +86,11 @@ website with GitLab Pages
- [Cycle Analytics](cycle_analytics.md): Review your development lifecycle
- [Security Dashboard](security_dashboard.md): Security Dashboard
- [Syntax highlighting](highlighting.md): An alternative to customize
your code blocks, overriding GitLab's default choice of language
your code blocks, overriding GitLab's default choice of language
- [Badges](badges.md): Badges for the project overview
- [Maven packages](packages/maven_repository.md): Your private Maven repository in GitLab **[PREMIUM]**
- [Code owners](code_owners.md): Specify code owners for certain files **[STARTER]**
- [License Management](merge_requests/license_management.md): Approve and blacklist licenses for projects **[ULTIMATE]**
### Project's integrations
......
......@@ -33,6 +33,7 @@ With GitLab merge requests, you can:
With **[GitLab Enterprise Edition][ee]**, you can also:
- Prepare a full review and submit it once it's ready with [Merge Request Reviews](../../discussions/index.md#merge-request-reviews) **[PREMIUM]**
- View the deployment process across projects with [Multi-Project Pipelines](../../../ci/multi_project_pipelines.md#multi-project-pipeline-graphs) **[PREMIUM]**
- Request [approvals](merge_request_approvals.md) from your managers **[STARTER]**
- Analyze the impact of your changes with [Code Quality reports](code_quality.md) **[STARTER]**
......@@ -149,6 +150,14 @@ you hide discussions that are no longer relevant.
[Read more about resolving discussion comments in merge requests reviews.](../../discussions/index.md)
## Perform a Review **[PREMIUM]**
Start a review in order to create multiple comments on a diff and publish them once you're ready.
Starting a review allows you to get all your thoughts in order and ensure you haven't missed anything
before submitting all your comments.
[Learn more about Merge Request Reviews](../../discussions/index.md#merge-request-reviews)
## Squash and merge
GitLab allows you to squash all changes present in a merge request into a single
......
......@@ -5,16 +5,17 @@
## Overview
If you are using [GitLab CI/CD][ci], you can search your project dependencies for their licenses
using License Management, either by
including the CI job in your [existing `.gitlab-ci.yml` file][cc-docs] or
by implicitly using [Auto License Management](../../../topics/autodevops/index.md#auto-dependency-scanning)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
using License Management by:
Going a step further, GitLab can show the licenses list right in the merge
- Including the CI job in your [existing `.gitlab-ci.yml` file][cc-docs].
- Implicitly using [Auto License Management](../../../topics/autodevops/index.md#auto-dependency-scanning)
that is provided by [Auto DevOps](../../../topics/autodevops/index.md).
In addition, you can [manually approve or blacklist](#manual-license-management) licenses in the project's settings.
GitLab can show the licenses list right in the merge
request widget area, highlighting the presence of licenses you don't want to use, or new
ones that need a decision.
Licenses can be accepted or blacklisted in the project settings, or directly from the
merge request widget.
## Use cases
......@@ -37,7 +38,7 @@ The following languages and package managers are supported.
## How it works
First of all, you need to define a job named `license_management` in your
First, you need to define a job named `license_management` in your
`.gitlab-ci.yml` file. [Check how the `license_management` job should look like][cc-docs].
In order for the report to show in the merge request, there are two
......@@ -50,7 +51,7 @@ prerequisites:
>**Note:**
If the license management report doesn't have anything to compare to, no information
will be displayed in the merge request area. That is the case when you add the
`license_management` job in your `.gitlab-ci.yml` for the very first time.
`license_management` job in your `.gitlab-ci.yml` for the first time.
Consecutive merge requests will have something to compare to and the license
management report will be shown properly.
......@@ -67,10 +68,31 @@ the choice to approve it or blacklist it.
![License approval decision](img/license_management_decision.png)
The list of licenses and their status can also be managed from the project settings.
From the project's settings:
- The list of licenses and their status can be managed.
- Licenses can be [manually approved or blacklisted](#manual-license-management).
![License Management Settings](img/license_management_settings.png)
### Manual license management
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/5940) in [GitLab Ultimate][ee] 11.4.
Licenses can be manually approved or blacklisted in a project's settings.
To approve or blacklist a license:
1. Navigate to the project's **Settings > CI/CD**.
1. Expand the **License Management** section and click the **Add a license** button.
1. In the **License name** dropdown, either:
- Select one of the available licenses. You can search for licenses in the field
at the top of the list.
- Enter arbitrary text in the field at the top of the list. This will cause the text to be
added as a license name to the list.
1. Select the **Approve** or **Blacklist** radio button to approve or blacklist respectively
the selected license.
## License Management report under pipelines
> [Introduced][ee-5491] in [GitLab Ultimate][ee] 11.2.
......
......@@ -22,7 +22,7 @@ module EE
private
def prometheus_adapter
return unless cluster&.application_prometheus&.installed?
return unless cluster&.application_prometheus_available?
cluster.application_prometheus
end
......
......@@ -29,37 +29,6 @@ module EE
group_epic_path(entity.group, entity, *args)
end
def sast_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.sast_artifact,
path: Ci::Build::SAST_FILE)
end
def dependency_scanning_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
end
# sast_container_artifact_url is deprecated and replaced with container_scanning_artifact_url (#5778)
def sast_container_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.sast_container_artifact,
path: Ci::Build::SAST_CONTAINER_FILE)
end
def container_scanning_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.container_scanning_artifact,
path: Ci::Build::CONTAINER_SCANNING_FILE)
end
def dast_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.dast_artifact,
path: Ci::Build::DAST_FILE)
end
def license_management_artifact_url(pipeline)
raw_project_build_artifacts_url(pipeline.project,
pipeline.license_management_artifact,
......
......@@ -148,21 +148,12 @@ module EE
can_create_issue: "false"
}
else
# Handle old job and artifact names for container scanning
sast_container_head_path = if pipeline.expose_sast_container_data?
sast_container_artifact_url(pipeline)
elsif pipeline.expose_container_scanning_data?
container_scanning_artifact_url(pipeline)
else
nil
end
{
head_blob_path: project_blob_path(project, pipeline.sha),
sast_head_path: pipeline.expose_sast_data? ? sast_artifact_url(pipeline) : nil,
dependency_scanning_head_path: pipeline.expose_dependency_scanning_data? ? dependency_scanning_artifact_url(pipeline) : nil,
dast_head_path: pipeline.expose_dast_data? ? dast_artifact_url(pipeline) : nil,
sast_container_head_path: sast_container_head_path,
sast_head_path: pipeline.downloadable_path_for_report_type(:sast),
dependency_scanning_head_path: pipeline.downloadable_path_for_report_type(:dependency_scanning),
dast_head_path: pipeline.downloadable_path_for_report_type(:dast),
sast_container_head_path: pipeline.downloadable_path_for_report_type(:container_scanning),
vulnerability_feedback_path: project_vulnerability_feedback_index_path(project),
pipeline_id: pipeline.id,
vulnerability_feedback_help_path: help_page_path("user/project/merge_requests/index", anchor: "interacting-with-security-reports-ultimate"),
......
......@@ -7,14 +7,8 @@ module EE
module Build
extend ActiveSupport::Concern
DEPENDENCY_SCANNING_FILE = 'gl-dependency-scanning-report.json'.freeze
LICENSE_MANAGEMENT_FILE = 'gl-license-management-report.json'.freeze
SAST_FILE = 'gl-sast-report.json'.freeze
PERFORMANCE_FILE = 'performance.json'.freeze
# SAST_CONTAINER_FILE is deprecated and replaced with CONTAINER_SCANNING_FILE (#5778)
SAST_CONTAINER_FILE = 'gl-sast-container-report.json'.freeze
CONTAINER_SCANNING_FILE = 'gl-container-scanning-report.json'.freeze
DAST_FILE = 'gl-dast-report.json'.freeze
prepended do
after_save :stick_build_if_status_changed
......@@ -36,37 +30,11 @@ module EE
has_artifact?(PERFORMANCE_FILE)
end
def has_sast_json?
name_in?('sast') &&
has_artifact?(SAST_FILE)
end
def has_dependency_scanning_json?
name_in?('dependency_scanning') &&
has_artifact?(DEPENDENCY_SCANNING_FILE)
end
def has_license_management_json?
name_in?('license_management') &&
has_artifact?(LICENSE_MANAGEMENT_FILE)
end
# has_sast_container_json? is deprecated and replaced with has_container_scanning_json? (#5778)
def has_sast_container_json?
name_in?(%w[sast:container container_scanning]) &&
has_artifact?(SAST_CONTAINER_FILE)
end
def has_container_scanning_json?
name_in?(%w[sast:container container_scanning]) &&
has_artifact?(CONTAINER_SCANNING_FILE)
end
def has_dast_json?
name_in?('dast') &&
has_artifact?(DAST_FILE)
end
def log_geo_deleted_event
# It is not needed to generate a Geo deleted event
# since Legacy Artifacts are migrated to multi-build artifacts
......
......@@ -17,6 +17,16 @@ module EE
joins(:artifacts).where(ci_builds: { name: %w[sast dependency_scanning sast:container container_scanning dast] })
}
# This structure describes feature levels
# to access the file types for given reports
LEGACY_REPORT_LICENSED_FEATURES = {
codequality: nil,
sast: :sast,
dependency_scanning: :dependency_scanning,
container_scanning: :sast_container,
dast: :dast
}.freeze
# Deprecated, to be removed in 12.0
# A hash of Ci::JobArtifact file_types
# With mapping to the legacy job names,
......@@ -25,15 +35,39 @@ module EE
codequality: {
names: %w(codeclimate codequality code_quality),
files: %w(codeclimate.json gl-code-quality-report.json)
},
sast: {
names: %w(deploy sast),
files: %w(gl-sast-report.json)
},
dependency_scanning: {
names: %w(dependency_scanning),
files: %w(gl-dependency-scanning-report.json)
},
container_scanning: {
names: %w(sast:container container_scanning),
files: %w(gl-sast-container-report.json gl-container-scanning-report.json)
},
dast: {
names: %w(dast),
files: %w(gl-dast-report.json)
}
}.freeze
end
def artifact_for_file_type(file_type)
def any_report_artifact_for_type(file_type)
report_artifact_for_file_type(file_type) || legacy_report_artifact_for_file_type(file_type)
end
def report_artifact_for_file_type(file_type)
return unless available_licensed_report_type?(file_type)
job_artifacts.where(file_type: ::Ci::JobArtifact.file_types[file_type]).last
end
def legacy_report_artifact_for_file_type(file_type)
return unless available_licensed_report_type?(file_type)
legacy_names = LEGACY_REPORT_FORMATS[file_type]
return unless legacy_names
......@@ -53,106 +87,35 @@ module EE
@performance_artifact ||= artifacts_with_files.find(&:has_performance_json?)
end
def sast_artifact
@sast_artifact ||= artifacts_with_files.find(&:has_sast_json?)
end
def dependency_scanning_artifact
@dependency_scanning_artifact ||= artifacts_with_files.find(&:has_dependency_scanning_json?)
end
def license_management_artifact
@license_management_artifact ||= artifacts_with_files.find(&:has_license_management_json?)
end
# sast_container_artifact is deprecated and replaced with container_scanning_artifact (#5778)
def sast_container_artifact
@sast_container_artifact ||= artifacts_with_files.find(&:has_sast_container_json?)
end
def container_scanning_artifact
@container_scanning_artifact ||= artifacts_with_files.find(&:has_container_scanning_json?)
end
def dast_artifact
@dast_artifact ||= artifacts_with_files.find(&:has_dast_json?)
end
def has_sast_data?
sast_artifact&.success?
end
def has_dependency_scanning_data?
dependency_scanning_artifact&.success?
end
def has_license_management_data?
license_management_artifact&.success?
end
# has_sast_container_data? is deprecated and replaced with has_container_scanning_data? (#5778)
def has_sast_container_data?
sast_container_artifact&.success?
end
def has_container_scanning_data?
container_scanning_artifact&.success?
end
def has_dast_data?
dast_artifact&.success?
end
def has_performance_data?
performance_artifact&.success?
end
def expose_sast_data?
project.feature_available?(:sast) &&
has_sast_data?
end
def expose_dependency_scanning_data?
project.feature_available?(:dependency_scanning) &&
has_dependency_scanning_data?
end
def expose_license_management_data?
project.feature_available?(:license_management) &&
has_license_management_data?
end
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
def expose_sast_container_data?
project.feature_available?(:sast_container) &&
has_sast_container_data?
end
def expose_container_scanning_data?
project.feature_available?(:sast_container) &&
has_container_scanning_data?
end
def expose_dast_data?
project.feature_available?(:dast) &&
has_dast_data?
end
def expose_performance_data?
project.feature_available?(:merge_request_performance_metrics) &&
has_performance_data?
end
def expose_security_dashboard?
expose_sast_data? ||
expose_dependency_scanning_data? ||
expose_dast_data? ||
expose_sast_container_data? ||
expose_container_scanning_data?
end
private
def available_licensed_report_type?(file_type)
feature_name = LEGACY_REPORT_LICENSED_FEATURES.fetch(file_type)
feature_name.nil? || project.feature_available?(feature_name)
end
def artifacts_with_files
@artifacts_with_files ||= artifacts.includes(:job_artifacts_metadata, :job_artifacts_archive).to_a
end
......
......@@ -13,35 +13,12 @@ module EE
delegate :performance_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :performance_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :dependency_scanning_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :dependency_scanning_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :license_management_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :license_management_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
# sast_container_artifact is deprecated and replaced with container_scanning_artifact (#5778)
delegate :sast_container_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :sast_container_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :container_scanning_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :container_scanning_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :dast_artifact, to: :head_pipeline, prefix: :head, allow_nil: true
delegate :dast_artifact, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :sha, to: :head_pipeline, prefix: :head_pipeline, allow_nil: true
delegate :sha, to: :base_pipeline, prefix: :base_pipeline, allow_nil: true
delegate :has_sast_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_dependency_scanning_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_license_management_data?, to: :base_pipeline, prefix: :base, allow_nil: true
# has_sast_container_data? is deprecated and replaced with has_container_scanning_data? (#5778)
delegate :has_sast_container_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_container_scanning_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :has_dast_data?, to: :base_pipeline, prefix: :base, allow_nil: true
delegate :expose_sast_data?, to: :head_pipeline, allow_nil: true
delegate :expose_dependency_scanning_data?, to: :head_pipeline, allow_nil: true
delegate :expose_license_management_data?, to: :head_pipeline, allow_nil: true
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
delegate :expose_sast_container_data?, to: :head_pipeline, allow_nil: true
delegate :expose_container_scanning_data?, to: :head_pipeline, allow_nil: true
delegate :expose_dast_data?, to: :head_pipeline, allow_nil: true
delegate :merge_requests_author_approval?, to: :target_project, allow_nil: true
participant :participant_approvers
......
......@@ -6,10 +6,17 @@ module EE
size_limit_exceeded: 'Pipeline size limit exceeded!'
}.freeze
def downloadable_url_for_report_type(file_type)
if (job_artifact = artifact_for_file_type(file_type)) &&
def expose_security_dashboard?
any_report_artifact_for_type(:sast) ||
any_report_artifact_for_type(:dependency_scanning) ||
any_report_artifact_for_type(:dast) ||
any_report_artifact_for_type(:container_scanning)
end
def downloadable_path_for_report_type(file_type)
if (job_artifact = report_artifact_for_file_type(file_type)) &&
can?(current_user, :read_build, job_artifact.job)
return download_project_build_artifacts_url(
return download_project_job_artifacts_path(
job_artifact.project,
job_artifact.job,
file_type: file_type)
......@@ -17,7 +24,7 @@ module EE
if (build_artifact = legacy_report_artifact_for_file_type(file_type)) &&
can?(current_user, :read_build, build_artifact.build)
return raw_project_build_artifacts_url(
return raw_project_job_artifacts_path(
build_artifact.build.project,
build_artifact.build,
path: build_artifact.path)
......
......@@ -16,13 +16,13 @@ module EE
end
end
expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_url_for_report_type(:codequality) } do
expose :codeclimate, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:codequality) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_url_for_report_type(:codequality)
head_pipeline_downloadable_path_for_report_type(:codequality)
end
expose :base_path do |merge_request|
base_pipeline_downloadable_url_for_report_type(:codequality)
base_pipeline_downloadable_path_for_report_type(:codequality)
end
end
......@@ -40,31 +40,23 @@ module EE
end
end
expose :sast, if: -> (mr, _) { mr.expose_sast_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_sast_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_sast_artifact,
path: Ci::Build::SAST_FILE)
expose :sast, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:sast) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:sast)
end
expose :base_path, if: -> (mr, _) { mr.base_has_sast_data? && can?(current_user, :read_build, mr.base_sast_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_sast_artifact,
path: Ci::Build::SAST_FILE)
expose :base_path do |merge_request|
base_pipeline_downloadable_path_for_report_type(:sast)
end
end
expose :dependency_scanning, if: -> (mr, _) { mr.expose_dependency_scanning_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_dependency_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
expose :dependency_scanning, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:dependency_scanning) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:dependency_scanning)
end
expose :base_path, if: -> (mr, _) { mr.base_has_dependency_scanning_data? && can?(current_user, :read_build, mr.base_dependency_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_dependency_scanning_artifact,
path: Ci::Build::DEPENDENCY_SCANNING_FILE)
expose :base_path do |merge_request|
base_pipeline_downloadable_path_for_report_type(:dependency_scanning)
end
end
......@@ -98,47 +90,23 @@ module EE
end
end
# expose_sast_container_data? is deprecated and replaced with expose_container_scanning_data? (#5778)
expose :sast_container, if: -> (mr, _) { mr.expose_sast_container_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_sast_container_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_sast_container_artifact,
path: Ci::Build::SAST_CONTAINER_FILE)
end
expose :base_path, if: -> (mr, _) { mr.base_has_sast_container_data? && can?(current_user, :read_build, mr.base_sast_container_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_sast_container_artifact,
path: Ci::Build::SAST_CONTAINER_FILE)
end
end
# We still expose it as `sast_container` to keep compatibility with Frontend (#5778)
expose :sast_container, if: -> (mr, _) { mr.expose_container_scanning_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_container_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_container_scanning_artifact,
path: Ci::Build::CONTAINER_SCANNING_FILE)
expose :sast_container, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:container_scanning) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:container_scanning)
end
expose :base_path, if: -> (mr, _) { mr.base_has_container_scanning_data? && can?(current_user, :read_build, mr.base_container_scanning_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_container_scanning_artifact,
path: Ci::Build::CONTAINER_SCANNING_FILE)
expose :base_path do |merge_request|
base_pipeline_downloadable_path_for_report_type(:container_scanning)
end
end
expose :dast, if: -> (mr, _) { mr.expose_dast_data? } do
expose :head_path, if: -> (mr, _) { can?(current_user, :read_build, mr.head_dast_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.source_project,
merge_request.head_dast_artifact,
path: Ci::Build::DAST_FILE)
expose :dast, if: -> (mr, _) { head_pipeline_downloadable_path_for_report_type(:dast) } do
expose :head_path do |merge_request|
head_pipeline_downloadable_path_for_report_type(:dast)
end
expose :base_path, if: -> (mr, _) { mr.base_has_dast_data? && can?(current_user, :read_build, mr.base_dast_artifact) } do |merge_request|
raw_project_build_artifacts_url(merge_request.target_project,
merge_request.base_dast_artifact,
path: Ci::Build::DAST_FILE)
expose :base_path do |merge_request|
base_pipeline_downloadable_path_for_report_type(:dast)
end
end
......@@ -170,12 +138,14 @@ module EE
private
def head_pipeline_downloadable_url_for_report_type(file_type)
object.head_pipeline&.present(current_user: current_user)&.downloadable_url_for_report_type(file_type)
def head_pipeline_downloadable_path_for_report_type(file_type)
object.head_pipeline&.present(current_user: current_user)
&.downloadable_path_for_report_type(file_type)
end
def base_pipeline_downloadable_url_for_report_type(file_type)
object.base_pipeline&.present(current_user: current_user)&.downloadable_url_for_report_type(file_type)
def base_pipeline_downloadable_path_for_report_type(file_type)
object.base_pipeline&.present(current_user: current_user)
&.downloadable_path_for_report_type(file_type)
end
end
end
......@@ -2,10 +2,11 @@
class VulnerabilitySummaryEntity < Grape::Entity
Vulnerabilities::Occurrence::REPORT_TYPES.each do |report_type_name, report_type|
report_key = Gitlab.rails5? ? report_type_name : report_type
expose report_type_name do
Vulnerabilities::Occurrence::LEVELS.each do |severity_name, severity|
expose severity_name do |group|
grouped_vulnerabilities[[report_type, severity]] || 0
grouped_vulnerabilities[[report_key, severity]] || 0
end
end
end
......
......@@ -13,6 +13,7 @@ module Epics
def close_epic(epic)
if epic.close
epic.update(closed_by: current_user)
SystemNoteService.change_status(epic, nil, current_user, epic.state)
end
end
end
......
......@@ -5,8 +5,15 @@ module Epics
def execute(epic)
return epic unless can?(current_user, :update_epic, epic)
epic.reopen
epic
reopen_epic(epic)
end
private
def reopen_epic(epic)
if epic.reopen
SystemNoteService.change_status(epic, nil, current_user, epic.state)
end
end
end
end
- return unless can?(current_user, :admin_group, group) && License.feature_available?(:member_lock)
%hr
.form-group.row
= f.label :membership_lock, class: 'col-form-label col-sm-2' do
Member lock
.col-sm-10
%h5= _('Member lock')
.form-group
.form-check
= f.check_box :membership_lock, class: 'form-check-input'
%span.descr Prevent adding new members to project membership within this group
= f.label :membership_lock, class: 'form-check-label' do
%span= _('Prevent adding new members to project membership within this group')
- return unless group.feature_available?(:project_creation_level)
- form = local_assigns.fetch(:form)
- group = local_assigns.fetch(:group)
.form-group.row
= form.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'col-form-label col-sm-2'
.col-sm-10
= form.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, group.project_creation_level), {}, class: 'form-control'
- return unless group.feature_available?(:project_creation_level)
- form = local_assigns.fetch(:form)
- group = local_assigns.fetch(:group)
.form-group.row
= form.label s_('ProjectCreationLevel|Allowed to create projects'), class: 'col-form-label col-sm-2'
.col-sm-10
.form-group.col-md-9.row.prepend-top-8
= form.label :description, s_('ProjectCreationLevel|Allowed to create projects'), class: 'label-bold'
= form.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, group.project_creation_level), {}, class: 'form-control'
......@@ -2,7 +2,7 @@
%section.settings.no-animate.expanded.cluster-health-graphs#cluster-health
%h4= s_('ClusterIntegration|Kubernetes cluster health')
- if @cluster&.application_prometheus&.installed?
- if @cluster&.application_prometheus_available?
#prometheus-graphs{ data: { "settings-path": edit_project_service_path(@project, 'prometheus'),
"clusters-path": project_clusters_path(@project),
"documentation-path": help_page_path('administration/monitoring/prometheus/index.md'),
......
- pipeline = local_assigns.fetch(:pipeline)
- project = local_assigns.fetch(:project)
- sast_endpoint = pipeline.expose_sast_data? ? sast_artifact_url(pipeline) : nil
- dependency_scanning_endpoint = pipeline.expose_dependency_scanning_data? ? dependency_scanning_artifact_url(pipeline) : nil
- dast_endpoint = pipeline.expose_dast_data? ? dast_artifact_url(pipeline) : nil
- sast_container_endpoint = pipeline.expose_sast_container_data? ? sast_container_artifact_url(pipeline) : pipeline.expose_container_scanning_data? ? container_scanning_artifact_url(pipeline) : nil
- sast_endpoint = pipeline.downloadable_path_for_report_type(:sast)
- dependency_scanning_endpoint = pipeline.downloadable_path_for_report_type(:dependency_scanning)
- dast_endpoint = pipeline.downloadable_path_for_report_type(:dast)
- sast_container_endpoint = pipeline.downloadable_path_for_report_type(:container_scanning)
- blob_path = project_blob_path(project, pipeline.sha)
- license_management_settings_path = can?(current_user, :admin_software_license_policy, project) ? license_management_settings_path(project) : nil
......
- return unless current_user.admin? && License.feature_available?(:repository_size_limit)
- form = local_assigns.fetch(:form)
- type = local_assigns.fetch(:type)
- label_class = (type == :project) ? 'label-bold' : 'col-form-label col-sm-2'
.form-group.row
= form.label :repository_size_limit, class: label_class do
Repository size limit (MB)
- if type == :project
= form.number_field :repository_size_limit, value: form.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.form-text.text-muted#repository_size_limit_help_block
= size_limit_message(@project)
- elsif type == :group
.col-sm-10
= form.number_field :repository_size_limit, value: form.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.form-text.text-muted#repository_size_limit_help_block
= size_limit_message_for_group(@group)
......@@ -2,17 +2,11 @@
- form = local_assigns.fetch(:form)
- type = local_assigns.fetch(:type)
- label_class = (type == :project) ? 'label-bold' : 'col-form-label col-sm-2'
- form_group_class = type === :group ? 'col-md-9' : ''
.form-group.row
= form.label :repository_size_limit, class: label_class do
.form-group{ class: form_group_class }
= form.label :repository_size_limit, class: 'label-bold' do
Repository size limit (MB)
- if type == :project
= form.number_field :repository_size_limit, value: form.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.form-text.text-muted#repository_size_limit_help_block
= size_limit_message(@project)
- elsif type == :group
.col-sm-10
= form.number_field :repository_size_limit, value: form.object.repository_size_limit.try(:to_mb), class: 'form-control', min: 0
%span.form-text.text-muted#repository_size_limit_help_block
= size_limit_message_for_group(@group)
= type === :project ? size_limit_message(@project) : size_limit_message_for_group(@group)
---
title: Create system notes for epic close and reopen
merge_request: 7850
author:
type: added
---
title: 'Rails5: fix VulnerabilitySummaryEntity'
merge_request: 7893
author: Jasper Maes
type: other
---
title: Refactor test reports to use new artifact architecture.
merge_request: 7827
author:
type: changed
......@@ -23,7 +23,7 @@ describe Projects::PipelinesController do
pipeline: pipeline,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......
......@@ -23,7 +23,7 @@ describe Projects::Security::DashboardController do
pipeline: pipeline_1,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......
......@@ -26,7 +26,7 @@ describe 'Pipeline', :js do
pipeline: pipeline,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......
......@@ -120,30 +120,9 @@ describe Ci::Build do
filename: Ci::Build::PERFORMANCE_FILE,
job_names: %w[performance deploy]
},
has_sast_json?: {
filename: Ci::Build::SAST_FILE,
job_names: %w[sast]
},
has_dependency_scanning_json?: {
filename: Ci::Build::DEPENDENCY_SCANNING_FILE,
job_names: %w[dependency_scanning]
},
has_license_management_json?: {
filename: Ci::Build::LICENSE_MANAGEMENT_FILE,
job_names: %w[license_management]
},
# has_sast_container_json? is deprecated and replaced with has_container_scanning_json (#5778)
has_sast_container_json?: {
filename: Ci::Build::SAST_CONTAINER_FILE,
job_names: %w[sast:container container_scanning]
},
has_container_scanning_json?: {
filename: Ci::Build::CONTAINER_SCANNING_FILE,
job_names: %w[sast:container container_scanning]
},
has_dast_json?: {
filename: Ci::Build::DAST_FILE,
job_names: %w[dast]
}
}
......
......@@ -20,15 +20,7 @@ describe Ci::Pipeline do
PIPELINE_ARTIFACTS_METHODS = [
{ method: :performance_artifact, options: [Ci::Build::PERFORMANCE_FILE, 'performance'] },
{ method: :sast_artifact, options: [Ci::Build::SAST_FILE, 'sast'] },
{ method: :dependency_scanning_artifact, options: [Ci::Build::DEPENDENCY_SCANNING_FILE, 'dependency_scanning'] },
{ method: :license_management_artifact, options: [Ci::Build::LICENSE_MANAGEMENT_FILE, 'license_management'] },
# sast_container_artifact is deprecated and replaced with container_scanning_artifact (#5778)
{ method: :sast_container_artifact, options: [Ci::Build::SAST_CONTAINER_FILE, 'sast:container'] },
{ method: :sast_container_artifact, options: [Ci::Build::SAST_CONTAINER_FILE, 'container_scanning'] },
{ method: :container_scanning_artifact, options: [Ci::Build::CONTAINER_SCANNING_FILE, 'sast:container'] },
{ method: :container_scanning_artifact, options: [Ci::Build::CONTAINER_SCANNING_FILE, 'container_scanning'] },
{ method: :dast_artifact, options: [Ci::Build::DAST_FILE, 'dast'] }
{ method: :license_management_artifact, options: [Ci::Build::LICENSE_MANAGEMENT_FILE, 'license_management'] }
].freeze
PIPELINE_ARTIFACTS_METHODS.each do |method_test|
......@@ -64,7 +56,7 @@ describe Ci::Pipeline do
end
end
%w(sast dependency_scanning dast performance sast_container container_scanning).each do |type|
%w(performance license_management).each do |type|
method = "has_#{type}_data?"
describe "##{method}" do
......@@ -78,7 +70,7 @@ describe Ci::Pipeline do
end
end
%w(sast dependency_scanning dast performance sast_container container_scanning).each do |type|
%w(performance license_management).each do |type|
method = "expose_#{type}_data?"
describe "##{method}" do
......@@ -107,7 +99,7 @@ describe Ci::Pipeline do
pipeline: pipeline_1,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......@@ -119,7 +111,7 @@ describe Ci::Pipeline do
pipeline: pipeline_2,
options: {
artifacts: {
paths: [Ci::Build::DEPENDENCY_SCANNING_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:dependency_scanning]]
}
}
)
......@@ -131,7 +123,7 @@ describe Ci::Pipeline do
pipeline: pipeline_3,
options: {
artifacts: {
paths: [Ci::Build::CONTAINER_SCANNING_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:container_scanning]]
}
}
)
......@@ -143,7 +135,7 @@ describe Ci::Pipeline do
pipeline: pipeline_4,
options: {
artifacts: {
paths: [Ci::Build::DAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:dast]]
}
}
)
......@@ -152,12 +144,7 @@ describe Ci::Pipeline do
:success,
:artifacts,
name: 'foobar',
pipeline: pipeline_5,
options: {
artifacts: {
paths: ['foobar-report.json']
}
}
pipeline: pipeline_5
)
end
......@@ -166,12 +153,12 @@ describe Ci::Pipeline do
end
end
describe '#artifact_for_file_type' do
describe '#report_artifact_for_file_type' do
let(:file_type) { :codequality }
let!(:build) { create(:ci_build, pipeline: pipeline) }
let!(:artifact) { create(:ci_job_artifact, :codequality, job: build) }
subject { pipeline.artifact_for_file_type(file_type) }
subject { pipeline.report_artifact_for_file_type(file_type) }
it 'returns the artifact' do
expect(subject).to eq(artifact)
......@@ -221,12 +208,12 @@ describe Ci::Pipeline do
end
it 'does not perform extra queries when calling pipeline artifacts methods after the first' do
create_build('sast', Ci::Build::SAST_FILE)
create_build('dependency_scanning', 'gl-dependency-scanning-report.json')
create_build('performance', 'performance.json')
create_build('license_management', 'gl-license-management-report.json')
pipeline.sast_artifact
pipeline.performance_artifact
expect { pipeline.dependency_scanning_artifact }.not_to exceed_query_limit(0)
expect { pipeline.license_management_artifact }.not_to exceed_query_limit(0)
end
end
end
......@@ -63,11 +63,26 @@ describe MergeRequest do
end
end
%w(sast dast sast_container container_scanning).each do |type|
it { is_expected.to delegate_method(:"expose_#{type}_data?").to(:head_pipeline) }
it { is_expected.to delegate_method(:"has_#{type}_data?").to(:base_pipeline).with_prefix(:base) }
it { is_expected.to delegate_method(:"#{type}_artifact").to(:head_pipeline).with_prefix(:head) }
it { is_expected.to delegate_method(:"#{type}_artifact").to(:base_pipeline).with_prefix(:base) }
describe '#base_license_management_artifact' do
before do
allow(subject.base_pipeline).to receive(:license_management_artifact)
.and_return(1)
end
it 'delegates to merge request diff' do
expect(subject.base_license_management_artifact).to eq(1)
end
end
describe '#head_license_management_artifact' do
before do
allow(subject.head_pipeline).to receive(:license_management_artifact)
.and_return(1)
end
it 'delegates to merge request diff' do
expect(subject.head_license_management_artifact).to eq(1)
end
end
describe '#expose_performance_data?' do
......@@ -86,4 +101,15 @@ describe MergeRequest do
it { expect(subject.expose_performance_data?).to be_falsey }
end
end
describe '#expose_license_management_data?' do
before do
allow(subject.head_pipeline).to receive(:expose_license_management_data?)
.and_return(1)
end
it 'delegates to merge request diff' do
expect(subject.expose_license_management_data?).to eq(1)
end
end
end
......@@ -1541,7 +1541,7 @@ describe Project do
pipeline: pipeline_1,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......@@ -1553,7 +1553,7 @@ describe Project do
pipeline: pipeline_2,
options: {
artifacts: {
paths: [Ci::Build::SAST_FILE]
paths: [Ci::JobArtifact::DEFAULT_FILE_NAMES[:sast]]
}
}
)
......
......@@ -26,7 +26,22 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json[:blob_path]).to include(:head_path)
end
describe 'codeclimate' do
it 'sets approvals_before_merge to 0 if nil' do
expect(subject.as_json[:approvals_before_merge]).to eq(0)
end
describe 'test report artifacts' do
using RSpec::Parameterized::TableSyntax
where(:json_entry, :artifact_type) do
:codeclimate | :codequality
:sast | :sast
:dependency_scanning | :dependency_scanning
:sast_container | :container_scanning
:dast | :dast
end
with_them do
before do
allow(merge_request).to receive_messages(
base_pipeline: pipeline,
......@@ -34,26 +49,29 @@ describe MergeRequestWidgetEntity do
)
end
context 'with codeclimate data' do
context 'when feature is available' do
before do
allow(pipeline).to receive(:available_licensed_report_type?).and_return(true)
end
context "with data" do
before do
job = create(:ci_build, pipeline: pipeline)
create(:ci_job_artifact, :codequality, job: job)
create(:ci_job_artifact, file_type: artifact_type, file_format: Ci::JobArtifact::TYPE_AND_FORMAT_PAIRS[artifact_type], job: job)
end
it 'has codeclimate data entry' do
expect(subject.as_json).to include(:codeclimate)
it "has data entry" do
expect(subject.as_json).to include(json_entry)
end
end
context 'without codeclimate data' do
it 'does not have codeclimate data entry' do
expect(subject.as_json).not_to include(:codeclimate)
context "without data" do
it "does not have data entry" do
expect(subject.as_json).not_to include(json_entry)
end
end
end
end
it 'sets approvals_before_merge to 0 if nil' do
expect(subject.as_json[:approvals_before_merge]).to eq(0)
end
it 'has performance data' do
......@@ -61,7 +79,6 @@ describe MergeRequestWidgetEntity do
allow(merge_request).to receive_messages(
expose_performance_data?: true,
expose_security_dashboard?: false,
base_performance_artifact: build,
head_performance_artifact: build
)
......@@ -69,45 +86,12 @@ describe MergeRequestWidgetEntity do
expect(subject.as_json).to include(:performance)
end
it 'has sast data' do
build = create(:ci_build, name: 'sast', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_sast_data?: true,
expose_security_dashboard?: true,
base_has_sast_data?: true,
base_sast_artifact: build,
head_sast_artifact: build
)
expect(subject.as_json).to include(:sast)
expect(subject.as_json[:sast]).to include(:head_path)
expect(subject.as_json[:sast]).to include(:base_path)
end
it 'has dependency_scanning data' do
build = create(:ci_build, name: 'dependency_scanning', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_dependency_scanning_data?: true,
expose_security_dashboard?: true,
base_has_dependency_scanning_data?: true,
base_dependency_scanning_artifact: build,
head_dependency_scanning_artifact: build
)
expect(subject.as_json).to include(:dependency_scanning)
expect(subject.as_json[:dependency_scanning]).to include(:head_path)
expect(subject.as_json[:dependency_scanning]).to include(:base_path)
end
describe '#license_management' do
before do
build = create(:ci_build, name: 'license_management', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_license_management_data?: true,
expose_security_dashboard?: false,
base_has_license_management_data?: true,
base_license_management_artifact: build,
head_license_management_artifact: build,
......@@ -143,55 +127,6 @@ describe MergeRequestWidgetEntity do
end
end
# methods for old artifact are deprecated and replaced with ones for the new name (#5779)
it 'has sast_container data (with old artifact name gl-sast-container-report.json)' do
build = create(:ci_build, name: 'container_scanning', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_sast_container_data?: true,
expose_security_dashboard?: true,
base_has_sast_container_data?: true,
base_sast_container_artifact: build,
head_sast_container_artifact: build
)
expect(subject.as_json).to include(:sast_container)
expect(subject.as_json[:sast_container]).to include(:head_path)
expect(subject.as_json[:sast_container]).to include(:base_path)
end
it 'has sast_container data (with new artifact name gl-container-scanning-report.json)' do
build = create(:ci_build, name: 'container_scanning', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_container_scanning_data?: true,
expose_security_dashboard?: true,
base_has_container_scanning_data?: true,
base_container_scanning_artifact: build,
head_container_scanning_artifact: build
)
expect(subject.as_json).to include(:sast_container)
expect(subject.as_json[:sast_container]).to include(:head_path)
expect(subject.as_json[:sast_container]).to include(:base_path)
end
it 'has dast data' do
build = create(:ci_build, name: 'dast', pipeline: pipeline)
allow(merge_request).to receive_messages(
expose_dast_data?: true,
expose_security_dashboard?: true,
base_has_dast_data?: true,
base_dast_artifact: build,
head_dast_artifact: build
)
expect(subject.as_json).to include(:dast)
expect(subject.as_json[:dast]).to include(:head_path)
expect(subject.as_json[:dast]).to include(:base_path)
end
it 'has vulnerability feedbacks path' do
expect(subject.as_json).to include(:vulnerability_feedback_path)
end
......
......@@ -41,6 +41,15 @@ describe Epics::CloseService do
it 'changes closed_at' do
expect { subject.execute(epic) }.to change { epic.closed_at }
end
it 'creates a system note about epic close' do
expect { subject.execute(epic) }.to change { epic.notes.count }.by(1)
note = epic.notes.last
expect(note.note).to eq('closed')
expect(note.system_note_metadata.action).to eq('closed')
end
end
context 'when trying to close a closed epic' do
......@@ -59,6 +68,10 @@ describe Epics::CloseService do
it 'does not change closed_by' do
expect { subject.execute(epic) }.not_to change { epic.closed_by }
end
it 'does not create a system note' do
expect { subject.execute(epic) }.not_to change { epic.notes.count }
end
end
end
......
......@@ -41,6 +41,15 @@ describe Epics::ReopenService do
it 'removes closed_at' do
expect { subject.execute(epic) }.to change { epic.closed_at }.to(nil)
end
it 'creates a system note about epic reopen' do
expect { subject.execute(epic) }.to change { epic.notes.count }.by(1)
note = epic.notes.last
expect(note.note).to eq('opened')
expect(note.system_note_metadata.action).to eq('opened')
end
end
context 'when trying to reopen an opened epic' do
......@@ -59,6 +68,10 @@ describe Epics::ReopenService do
it 'does not change closed_by' do
expect { subject.execute(epic) }.not_to change { epic.closed_by }
end
it 'does not create a system note' do
expect { subject.execute(epic) }.not_to change { epic.notes.count }
end
end
end
......
......@@ -4820,6 +4820,9 @@ msgstr ""
msgid "Median"
msgstr ""
msgid "Member lock"
msgstr ""
msgid "Member since %{date}"
msgstr ""
......@@ -5817,6 +5820,9 @@ msgstr ""
msgid "Press Enter or click to search"
msgstr ""
msgid "Prevent adding new members to project membership within this group"
msgstr ""
msgid "Preview"
msgstr ""
......
......@@ -54,12 +54,10 @@ describe('Environment table', () => {
});
expect(vm.$el.querySelector('.js-deploy-board-row')).toBeDefined();
expect(
vm.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
expect(vm.$el.querySelector('.deploy-board-icon')).not.toBeNull();
});
it('should toggle deploy board visibility when arrow is clicked', () => {
it('should toggle deploy board visibility when arrow is clicked', (done) => {
const mockItem = {
name: 'review',
size: 1,
......@@ -80,6 +78,7 @@ describe('Environment table', () => {
eventHub.$on('toggleDeployBoard', (env) => {
expect(env.id).toEqual(mockItem.id);
done();
});
vm = mountComponent(Component, {
......
......@@ -113,9 +113,7 @@ describe('Environment', () => {
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i.fa-caret-right'),
).toBeDefined();
expect(component.$el.querySelector('.deploy-board-icon.ic-chevron-right')).toBeDefined();
done();
}, 0);
});
......@@ -169,12 +167,7 @@ describe('Environment', () => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(
component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
).toContain('display: none');
expect(
component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
).not.toContain('display: none');
expect(component.$el.querySelector('.folder-icon.ic-chevron-right')).toBe(null);
done();
});
}, 0);
......@@ -190,12 +183,7 @@ describe('Environment', () => {
component.$el.querySelector('.folder-name').click();
Vue.nextTick(() => {
expect(
component.$el.querySelector('.folder-icon i.fa-caret-down').getAttribute('style'),
).toContain('display: none');
expect(
component.$el.querySelector('.folder-icon i.fa-caret-right').getAttribute('style'),
).not.toContain('display: none');
expect(component.$el.querySelector('.folder-icon.ic-chevron-down')).toBe(null);
done();
});
});
......
......@@ -125,9 +125,7 @@ describe('Environments Folder View', () => {
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
expect(component.$el.querySelector('.folder-icon.ic-chevron-right')).not.toBeNull();
done();
}, 0);
});
......
......@@ -4,7 +4,7 @@ describe Clusters::Applications::Ingress do
let(:ingress) { create(:clusters_applications_ingress) }
include_examples 'cluster application core specs', :clusters_applications_ingress
include_examples 'cluster application status specs', :cluster_application_ingress
include_examples 'cluster application status specs', :clusters_applications_ingress
before do
allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
......
......@@ -4,7 +4,7 @@ describe Clusters::Applications::Prometheus do
include KubernetesHelpers
include_examples 'cluster application core specs', :clusters_applications_prometheus
include_examples 'cluster application status specs', :cluster_application_prometheus
include_examples 'cluster application status specs', :clusters_applications_prometheus
describe '.installed' do
subject { described_class.installed }
......
......@@ -4,7 +4,7 @@ describe Clusters::Applications::Runner do
let(:ci_runner) { create(:ci_runner) }
include_examples 'cluster application core specs', :clusters_applications_runner
include_examples 'cluster application status specs', :cluster_application_runner
include_examples 'cluster application status specs', :clusters_applications_runner
it { is_expected.to belong_to(:runner) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::Cluster do
......@@ -15,8 +17,9 @@ describe Clusters::Cluster do
it { is_expected.to delegate_method(:on_creation?).to(:provider) }
it { is_expected.to delegate_method(:active?).to(:platform_kubernetes).with_prefix }
it { is_expected.to delegate_method(:rbac?).to(:platform_kubernetes).with_prefix }
it { is_expected.to delegate_method(:installed?).to(:application_helm).with_prefix }
it { is_expected.to delegate_method(:installed?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_helm).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_ingress).with_prefix }
it { is_expected.to delegate_method(:available?).to(:application_prometheus).with_prefix }
it { is_expected.to respond_to :project }
describe '.enabled' do
......
# frozen_string_literal: true
require 'spec_helper'
describe PrometheusService, :use_clean_rails_memory_store_caching do
......@@ -83,13 +85,22 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
end
end
describe '#prometheus_installed?' do
describe '#prometheus_available?' do
context 'clusters with installed prometheus' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
it 'returns true' do
expect(service.prometheus_installed?).to be(true)
expect(service.prometheus_available?).to be(true)
end
end
context 'clusters with updated prometheus' do
let!(:cluster) { create(:cluster, projects: [project]) }
let!(:prometheus) { create(:clusters_applications_prometheus, :updated, cluster: cluster) }
it 'returns true' do
expect(service.prometheus_available?).to be(true)
end
end
......@@ -98,7 +109,7 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
it 'returns false' do
expect(service.prometheus_installed?).to be(false)
expect(service.prometheus_available?).to be(false)
end
end
......@@ -106,13 +117,13 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
let(:cluster) { create(:cluster, projects: [project]) }
it 'returns false' do
expect(service.prometheus_installed?).to be(false)
expect(service.prometheus_available?).to be(false)
end
end
context 'no clusters' do
it 'returns false' do
expect(service.prometheus_installed?).to be(false)
expect(service.prometheus_available?).to be(false)
end
end
end
......@@ -150,7 +161,7 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do
context 'with prometheus installed in the cluster' do
before do
allow(service).to receive(:prometheus_installed?).and_return(true)
allow(service).to receive(:prometheus_available?).and_return(true)
end
context 'when service is inactive' do
......
......@@ -11,60 +11,4 @@ shared_examples 'cluster application core specs' do |application_name|
expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
end
end
describe 'status state machine' do
describe '#make_installing' do
subject { create(application_name, :scheduled) }
it 'is installing' do
subject.make_installing!
expect(subject).to be_installing
end
end
describe '#make_installed' do
subject { create(application_name, :installing) }
it 'is installed' do
subject.make_installed
expect(subject).to be_installed
end
end
describe '#make_errored' do
subject { create(application_name, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
subject.make_errored(reason)
expect(subject).to be_errored
expect(subject.status_reason).to eq(reason)
end
end
describe '#make_scheduled' do
subject { create(application_name, :installable) }
it 'is scheduled' do
subject.make_scheduled
expect(subject).to be_scheduled
end
describe 'when was errored' do
subject { create(application_name, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
end
end
......@@ -28,4 +28,87 @@ shared_examples 'cluster application status specs' do |application_name|
end
end
end
describe 'status state machine' do
describe '#make_installing' do
subject { create(application_name, :scheduled) }
it 'is installing' do
subject.make_installing!
expect(subject).to be_installing
end
end
describe '#make_installed' do
subject { create(application_name, :installing) }
it 'is installed' do
subject.make_installed
expect(subject).to be_installed
end
end
describe '#make_errored' do
subject { create(application_name, :installing) }
let(:reason) { 'some errors' }
it 'is errored' do
subject.make_errored(reason)
expect(subject).to be_errored
expect(subject.status_reason).to eq(reason)
end
end
describe '#make_scheduled' do
subject { create(application_name, :installable) }
it 'is scheduled' do
subject.make_scheduled
expect(subject).to be_scheduled
end
describe 'when was errored' do
subject { create(application_name, :errored) }
it 'clears #status_reason' do
expect(subject.status_reason).not_to be_nil
subject.make_scheduled!
expect(subject.status_reason).to be_nil
end
end
end
end
describe '#available?' do
using RSpec::Parameterized::TableSyntax
where(:trait, :available) do
:not_installable | false
:installable | false
:scheduled | false
:installing | false
:installed | true
:updating | false
:updated | true
:errored | false
:update_errored | false
:timeouted | false
end
with_them do
subject { build(application_name, trait) }
if params[:available]
it { is_expected.to be_available }
else
it { is_expected.not_to be_available }
end
end
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