Commit 1a0d6dbd authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent b11f7057
......@@ -217,7 +217,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/helpers/auth_helper_spec.rb
- ee/spec/lib/gitlab/gl_repository_spec.rb
- ee/spec/models/namespace_spec.rb
- ee/spec/serializers/environment_entity_spec.rb
- ee/spec/services/issues/create_service_spec.rb
- ee/spec/services/merge_requests/create_service_spec.rb
- ee/spec/services/merge_requests/refresh_service_spec.rb
......@@ -225,7 +224,6 @@ Gitlab/DuplicateSpecLocation:
- ee/spec/services/system_hooks_service_spec.rb
- ee/spec/helpers/ee/auth_helper_spec.rb
- ee/spec/models/ee/namespace_spec.rb
- ee/spec/serializers/ee/environment_entity_spec.rb
- ee/spec/services/ee/issues/create_service_spec.rb
- ee/spec/services/ee/merge_requests/create_service_spec.rb
- ee/spec/services/ee/merge_requests/refresh_service_spec.rb
......@@ -414,15 +412,12 @@ RSpec/RepeatedExample:
- 'spec/models/concerns/issuable_spec.rb'
- 'spec/models/member_spec.rb'
- 'spec/models/project_services/chat_message/pipeline_message_spec.rb'
- 'spec/models/user_spec.rb'
- 'spec/models/wiki_page_spec.rb'
- 'spec/requests/api/merge_requests_spec.rb'
- 'spec/routing/admin_routing_spec.rb'
- 'spec/rubocop/cop/migration/update_large_table_spec.rb'
- 'spec/services/notification_service_spec.rb'
- 'spec/services/web_hook_service_spec.rb'
- 'ee/spec/models/group_spec.rb'
- 'ee/spec/requests/api/merge_request_approvals_spec.rb'
- 'ee/spec/services/boards/lists/update_service_spec.rb'
- 'ee/spec/services/geo/repository_verification_primary_service_spec.rb'
- 'ee/spec/workers/geo/file_download_dispatch_worker_spec.rb'
......@@ -41,6 +41,8 @@ const Api = {
createBranchPath: '/api/:version/projects/:id/repository/branches',
releasesPath: '/api/:version/projects/:id/releases',
releasePath: '/api/:version/projects/:id/releases/:tag_name',
releaseLinksPath: '/api/:version/projects/:id/releases/:tag_name/assets/links',
releaseLinkPath: '/api/:version/projects/:id/releases/:tag_name/assets/links/:link_id',
mergeRequestsPipeline: '/api/:version/projects/:id/merge_requests/:merge_request_iid/pipelines',
adminStatisticsPath: '/api/:version/application/statistics',
pipelineSinglePath: '/api/:version/projects/:id/pipelines/:pipeline_id',
......@@ -460,6 +462,23 @@ const Api = {
return axios.put(url, release);
},
createReleaseLink(projectPath, tagName, link) {
const url = Api.buildUrl(this.releaseLinksPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':tag_name', encodeURIComponent(tagName));
return axios.post(url, link);
},
deleteReleaseLink(projectPath, tagName, linkId) {
const url = Api.buildUrl(this.releaseLinkPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':tag_name', encodeURIComponent(tagName))
.replace(':link_id', encodeURIComponent(linkId));
return axios.delete(url);
},
adminStatistics() {
const url = Api.buildUrl(this.adminStatisticsPath);
return axios.get(url);
......
......@@ -7,6 +7,8 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { BACK_URL_PARAM } from '~/releases/constants';
import { getParameterByName } from '~/lib/utils/common_utils';
import AssetLinksForm from './asset_links_form.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
name: 'ReleaseEditApp',
......@@ -16,10 +18,12 @@ export default {
GlButton,
GlLink,
MarkdownField,
AssetLinksForm,
},
directives: {
autofocusonshow,
},
mixins: [glFeatureFlagsMixin()],
computed: {
...mapState('detail', [
'isFetchingRelease',
......@@ -80,6 +84,9 @@ export default {
cancelPath() {
return getParameterByName(BACK_URL_PARAM) || this.releasesPagePath;
},
showAssetLinksForm() {
return this.glFeatures.releaseAssetLinkEditing;
},
},
created() {
this.fetchRelease();
......@@ -153,6 +160,8 @@ export default {
</div>
</gl-form-group>
<asset-links-form v-if="showAssetLinksForm" />
<div class="d-flex pt-3">
<gl-button
class="mr-auto js-submit-button"
......
<script>
import { mapState, mapActions } from 'vuex';
import {
GlSprintf,
GlLink,
GlFormGroup,
GlButton,
GlIcon,
GlTooltipDirective,
GlFormInput,
} from '@gitlab/ui';
export default {
name: 'AssetLinksForm',
components: { GlSprintf, GlLink, GlFormGroup, GlButton, GlIcon, GlFormInput },
directives: { GlTooltip: GlTooltipDirective },
computed: {
...mapState('detail', ['release', 'releaseAssetsDocsPath']),
},
created() {
this.addEmptyAssetLink();
},
methods: {
...mapActions('detail', [
'addEmptyAssetLink',
'updateAssetLinkUrl',
'updateAssetLinkName',
'removeAssetLink',
]),
onAddAnotherClicked() {
this.addEmptyAssetLink();
},
onRemoveClicked(linkId) {
this.removeAssetLink(linkId);
},
onUrlInput(linkIdToUpdate, newUrl) {
this.updateAssetLinkUrl({ linkIdToUpdate, newUrl });
},
onLinkTitleInput(linkIdToUpdate, newName) {
this.updateAssetLinkName({ linkIdToUpdate, newName });
},
},
};
</script>
<template>
<div class="d-flex flex-column release-assets-links-form">
<h2 class="text-4">{{ __('Release assets') }}</h2>
<p class="m-0">
<gl-sprintf
:message="
__(
'Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence.',
)
"
>
<template #link="{ content }">
<gl-link
:href="releaseAssetsDocsPath"
target="_blank"
:aria-label="__('Release assets documentation')"
>
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
<h3 class="text-3">{{ __('Links') }}</h3>
<p>
{{
__(
'Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance.',
)
}}
</p>
<div
v-for="(link, index) in release.assets.links"
:key="link.id"
class="d-flex flex-column flex-sm-row align-items-stretch align-items-sm-end"
>
<gl-form-group
class="url-field form-group flex-grow-1 mr-sm-4"
:label="__('URL')"
:label-for="`asset-url-${index}`"
>
<gl-form-input
:id="`asset-url-${index}`"
:value="link.url"
type="text"
class="form-control"
@change="onUrlInput(link.id, $event)"
/>
</gl-form-group>
<gl-form-group
class="link-title-field flex-grow-1 mr-sm-4"
:label="__('Link title')"
:label-for="`asset-link-name-${index}`"
>
<gl-form-input
:id="`asset-link-name-${index}`"
:value="link.name"
type="text"
class="form-control"
@change="onLinkTitleInput(link.id, $event)"
/>
</gl-form-group>
<gl-button
v-gl-tooltip
class="mb-5 mb-sm-3 flex-grow-0 flex-shrink-0 remove-button"
:aria-label="__('Remove asset link')"
:title="__('Remove asset link')"
@click="onRemoveClicked(link.id)"
>
<gl-icon class="m-0" name="remove" />
<span class="d-inline d-sm-none">{{ __('Remove asset link') }}</span>
</gl-button>
</div>
<gl-button variant="link" class="align-self-end mb-5 mb-sm-0" @click="onAddAnotherClicked">
{{ __('Add another link') }}
</gl-button>
</div>
</template>
......@@ -41,20 +41,74 @@ export const receiveUpdateReleaseError = ({ commit }, error) => {
createFlash(s__('Release|Something went wrong while saving the release details'));
};
export const updateRelease = ({ dispatch, state }) => {
export const updateRelease = ({ dispatch, state, getters }) => {
dispatch('requestUpdateRelease');
return api
const { release } = state;
return (
api
.updateRelease(state.projectId, state.tagName, {
name: state.release.name,
description: state.release.description,
name: release.name,
description: release.description,
})
/**
* Currently, we delete all existing links and then
* recreate new ones on each edit. This is because the
* REST API doesn't support bulk updating of Release links,
* and updating individual links can lead to validation
* race conditions (in particular, the "URLs must be unique")
* constraint.
*
* This isn't ideal since this is no longer an atomic
* operation - parts of it can fail while others succeed,
* leaving the Release in an inconsistent state.
*
* This logic should be refactored to use GraphQL once
* https://gitlab.com/gitlab-org/gitlab/-/issues/208702
* is closed.
*/
.then(() => {
// Delete all links currently associated with this Release
return Promise.all(
getters.releaseLinksToDelete.map(l =>
api.deleteReleaseLink(state.projectId, release.tagName, l.id),
),
);
})
.then(() => {
// Create a new link for each link in the form
return Promise.all(
getters.releaseLinksToCreate.map(l =>
api.createReleaseLink(state.projectId, release.tagName, l),
),
);
})
.then(() => dispatch('receiveUpdateReleaseSuccess'))
.catch(error => {
dispatch('receiveUpdateReleaseError', error);
});
})
);
};
export const navigateToReleasesPage = ({ state }) => {
redirectTo(state.releasesPagePath);
};
export const addEmptyAssetLink = ({ commit }) => {
commit(types.ADD_EMPTY_ASSET_LINK);
};
export const updateAssetLinkUrl = ({ commit }, { linkIdToUpdate, newUrl }) => {
commit(types.UPDATE_ASSET_LINK_URL, { linkIdToUpdate, newUrl });
};
export const updateAssetLinkName = ({ commit }, { linkIdToUpdate, newName }) => {
commit(types.UPDATE_ASSET_LINK_NAME, { linkIdToUpdate, newName });
};
export const removeAssetLink = ({ commit }, linkIdToRemove) => {
commit(types.REMOVE_ASSET_LINK, linkIdToRemove);
};
/**
* @returns {Boolean} `true` if the release link is empty, i.e. it has
* empty (or whitespace-only) values for both `url` and `name`.
* Otherwise, `false`.
*/
const isEmptyReleaseLink = l => !/\S/.test(l.url) && !/\S/.test(l.name);
/** Returns all release links that aren't empty */
export const releaseLinksToCreate = state => {
if (!state.release) {
return [];
}
return state.release.assets.links.filter(l => !isEmptyReleaseLink(l));
};
/** Returns all release links that should be deleted */
export const releaseLinksToDelete = state => {
if (!state.originalRelease) {
return [];
}
return state.originalRelease.assets.links;
};
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
export default initialState => ({
namespaced: true,
actions,
getters,
mutations,
state: createState(initialState),
});
......@@ -8,3 +8,8 @@ export const UPDATE_RELEASE_NOTES = 'UPDATE_RELEASE_NOTES';
export const REQUEST_UPDATE_RELEASE = 'REQUEST_UPDATE_RELEASE';
export const RECEIVE_UPDATE_RELEASE_SUCCESS = 'RECEIVE_UPDATE_RELEASE_SUCCESS';
export const RECEIVE_UPDATE_RELEASE_ERROR = 'RECEIVE_UPDATE_RELEASE_ERROR';
export const ADD_EMPTY_ASSET_LINK = 'ADD_EMPTY_ASSET_LINK';
export const UPDATE_ASSET_LINK_URL = 'UPDATE_ASSET_LINK_URL';
export const UPDATE_ASSET_LINK_NAME = 'UPDATE_ASSET_LINK_NAME';
export const REMOVE_ASSET_LINK = 'REMOVE_ASSET_LINK';
import * as types from './mutation_types';
import { uniqueId, cloneDeep } from 'lodash';
const findReleaseLink = (release, id) => {
return release.assets.links.find(l => l.id === id);
};
export default {
[types.REQUEST_RELEASE](state) {
......@@ -8,6 +13,7 @@ export default {
state.fetchError = undefined;
state.isFetchingRelease = false;
state.release = data;
state.originalRelease = Object.freeze(cloneDeep(state.release));
},
[types.RECEIVE_RELEASE_ERROR](state, error) {
state.fetchError = error;
......@@ -33,4 +39,26 @@ export default {
state.updateError = error;
state.isUpdatingRelease = false;
},
[types.ADD_EMPTY_ASSET_LINK](state) {
state.release.assets.links.push({
id: uniqueId('new-link-'),
url: '',
name: '',
});
},
[types.UPDATE_ASSET_LINK_URL](state, { linkIdToUpdate, newUrl }) {
const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
linkToUpdate.url = newUrl;
},
[types.UPDATE_ASSET_LINK_NAME](state, { linkIdToUpdate, newName }) {
const linkToUpdate = findReleaseLink(state.release, linkIdToUpdate);
linkToUpdate.name = newName;
},
[types.REMOVE_ASSET_LINK](state, linkIdToRemove) {
state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkIdToRemove);
},
};
......@@ -5,6 +5,7 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
}) => ({
projectId,
tagName,
......@@ -12,9 +13,18 @@ export default ({
markdownDocsPath,
markdownPreviewPath,
updateReleaseApiDocsPath,
releaseAssetsDocsPath,
/** The Release object */
release: null,
/**
* A deep clone of the Release object above.
* Used when editing this Release so that
* changes can be computed.
*/
originalRelease: null,
isFetchingRelease: false,
fetchError: null,
......
......@@ -8,6 +8,7 @@ module Groups
before_action :authorize_update_max_artifacts_size!, only: [:update]
before_action do
push_frontend_feature_flag(:new_variables_ui, @group)
push_frontend_feature_flag(:ajax_new_deploy_token, @group)
end
before_action :define_variables, only: [:show, :create_deploy_token]
......@@ -42,13 +43,30 @@ module Groups
end
def create_deploy_token
@new_deploy_token = Groups::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
result = Projects::DeployTokens::CreateService.new(@group, current_user, deploy_token_params).execute
@new_deploy_token = result[:deploy_token]
if @new_deploy_token.persisted?
if result[:status] == :success
respond_to do |format|
format.json do
# IMPORTANT: It's a security risk to expose the token value more than just once here!
json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
render json: json, status: result[:http_status]
end
format.html do
flash.now[:notice] = s_('DeployTokens|Your new group deploy token has been created.')
render :show
end
end
else
respond_to do |format|
format.json { render json: { message: result[:message] }, status: result[:http_status] }
format.html do
flash.now[:alert] = result[:message]
render :show
end
end
end
render 'show'
end
private
......
......@@ -9,6 +9,7 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_issue_summary, project, default_enabled: true)
push_frontend_feature_flag(:release_evidence_collection, project, default_enabled: true)
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
push_frontend_feature_flag(:release_asset_link_editing, project)
end
before_action :authorize_update_release!, only: %i[edit update]
......
......@@ -7,6 +7,7 @@ module Projects
before_action :define_variables
before_action do
push_frontend_feature_flag(:new_variables_ui, @project)
push_frontend_feature_flag(:ajax_new_deploy_token, @project)
end
def show
......@@ -47,13 +48,30 @@ module Projects
end
def create_deploy_token
@new_deploy_token = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
result = Projects::DeployTokens::CreateService.new(@project, current_user, deploy_token_params).execute
@new_deploy_token = result[:deploy_token]
if @new_deploy_token.persisted?
if result[:status] == :success
respond_to do |format|
format.json do
# IMPORTANT: It's a security risk to expose the token value more than just once here!
json = API::Entities::DeployTokenWithToken.represent(@new_deploy_token).as_json
render json: json, status: result[:http_status]
end
format.html do
flash.now[:notice] = s_('DeployTokens|Your new project deploy token has been created.')
render :show
end
end
else
respond_to do |format|
format.json { render json: { message: result[:message] }, status: result[:http_status] }
format.html do
flash.now[:alert] = result[:message]
render :show
end
end
end
render 'show'
end
private
......
......@@ -14,6 +14,7 @@
# milestone_title: string
# release_tag: string
# author_id: integer
# author_username: string
# assignee_id: integer
# search: string
# in: 'title', 'description', or a string joining them with comma
......
......@@ -8,8 +8,8 @@ module ReleasesHelper
image_path(IMAGE_PATH)
end
def help_page
help_page_path(DOCUMENTATION_PATH)
def help_page(anchor: nil)
help_page_path(DOCUMENTATION_PATH, anchor: anchor)
end
def data_for_releases_page
......@@ -29,7 +29,8 @@ module ReleasesHelper
markdown_preview_path: preview_markdown_path(@project),
markdown_docs_path: help_page_path('user/markdown'),
releases_page_path: project_releases_path(@project, anchor: @release.tag),
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release')
update_release_api_docs_path: help_page_path('api/releases/index.md', anchor: 'update-a-release'),
release_assets_docs_path: help_page(anchor: 'release-assets')
}
end
end
......@@ -318,6 +318,7 @@ class ProjectPolicy < BasePolicy
enable :read_pod_logs
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
end
rule { (mirror_available & can?(:admin_project)) | admin }.enable :admin_remote_mirror
......
......@@ -6,7 +6,13 @@ module Groups
include DeployTokenMethods
def execute
create_deploy_token_for(@group, params)
deploy_token = create_deploy_token_for(@group, params)
if deploy_token.persisted?
success(deploy_token: deploy_token, http_status: :ok)
else
error(deploy_token.errors.full_messages.to_sentence, :bad_request)
end
end
end
end
......
......@@ -6,7 +6,13 @@ module Projects
include DeployTokenMethods
def execute
create_deploy_token_for(@project, params)
deploy_token = create_deploy_token_for(@project, params)
if deploy_token.persisted?
success(deploy_token: deploy_token, http_status: :ok)
else
error(deploy_token.errors.full_messages.to_sentence, :bad_request)
end
end
end
end
......
%p.profile-settings-content
= s_("DeployTokens|Pick a name for the application, and we'll give you a unique deploy token.")
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post do |f|
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
= form_errors(token)
.form-group
......
---
title: Accept `author_username` as a param in Merge Requests API
merge_request: 28100
author:
type: changed
---
title: Conditional mocking of admin mode in specs by directory
merge_request: 28420
author: Diego Louzán
type: other
---
title: Remove repeated examples in user model specs
merge_request: 28450
author: Rajendra Kadam
type: changed
# frozen_string_literal: true
class AddIndexForGroupVsmUsagePing < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
INDEX_NAME = 'index_analytics_cycle_analytics_group_stages_custom_only'
disable_ddl_transaction!
def up
add_concurrent_index :analytics_cycle_analytics_group_stages, :id, where: 'custom = true', name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :analytics_cycle_analytics_group_stages, INDEX_NAME
end
end
......@@ -8520,6 +8520,8 @@ CREATE INDEX index_analytics_ca_project_stages_on_relative_position ON public.an
CREATE INDEX index_analytics_ca_project_stages_on_start_event_label_id ON public.analytics_cycle_analytics_project_stages USING btree (start_event_label_id);
CREATE INDEX index_analytics_cycle_analytics_group_stages_custom_only ON public.analytics_cycle_analytics_group_stages USING btree (id) WHERE (custom = true);
CREATE INDEX index_application_settings_on_custom_project_templates_group_id ON public.application_settings USING btree (custom_project_templates_group_id);
CREATE INDEX index_application_settings_on_file_template_project_id ON public.application_settings USING btree (file_template_project_id);
......@@ -12909,5 +12911,6 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326135443
20200326144443
20200326145443
20200330074719
\.
......@@ -541,6 +541,12 @@ Particular attention should be shown to:
gitlab-ctl reconfigure
```
1. Verify each `gitlab-shell` on each Gitaly instance can reach GitLab. On each Gitaly instance run:
```shell
/opt/gitlab/embedded/service/gitlab-shell/bin/check -config /opt/gitlab/embedded/service/gitlab-shell/config.yml
```
1. Verify that GitLab can reach Praefect:
```shell
......
......@@ -30,6 +30,7 @@ GET /merge_requests?state=all
GET /merge_requests?milestone=release
GET /merge_requests?labels=bug,reproduced
GET /merge_requests?author_id=5
GET /merge_requests?author_username=gitlab-bot
GET /merge_requests?my_reaction_emoji=star
GET /merge_requests?scope=assigned_to_me
GET /merge_requests?search=foo&in=title
......@@ -51,7 +52,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`. Defaults to `created_by_me`<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead. |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned_to_me` |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. Combine with `scope=all` or `scope=assigned_to_me`
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
......@@ -230,7 +232,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> For versions before 11.0, use the now deprecated `created-by-me` or `assigned-to-me` scopes instead.<br> _([Introduced][ce-13060] in GitLab 9.5. [Changed to snake_case][ce-18935] in GitLab 11.0)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
......@@ -392,7 +395,8 @@ Parameters:
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created_by_me`, `assigned_to_me` or `all`.<br> |
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Mutually exclusive with `author_username`. _([Introduced][ce-13060] in GitLab 9.5)_
| `author_username` | string | no | Returns merge requests created by the given `username`. Mutually exclusive with `author_id`. _([Introduced][ce-13060] in GitLab 12.10)_ | |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id`. `None` returns unassigned merge requests. `Any` returns merge requests with an assignee. _([Introduced][ce-13060] in GitLab 9.5)_ |
| `approver_ids` **(STARTER)** | integer array | no | Returns merge requests which have specified all the users with the given `id`s as individual approvers. `None` returns merge requests without approvers. `Any` returns merge requests with an approver. |
| `approved_by_ids` **(STARTER)** | integer array | no | Returns merge requests which have been approved by all the users with the given `id`s (Max: 5). `None` returns merge requests with no approvals. `Any` returns merge requests with an approval. |
......
......@@ -243,12 +243,35 @@ Do not include the same information in multiple places. [Link to a SSOT instead.
## Language
- Use inclusive language and avoid jargon, as well as uncommon
words. The docs should be clear and easy to understand.
- Do not write in the first person singular. Instead of "I" or "me," use "we," "you," "us," or "one."
- Be clear, concise, and stick to the goal of the doc.
GitLab documentation should be clear and easy to understand.
- Be clear, concise, and stick to the goal of the documentation.
- Write in US English with US grammar.
- Use inclusive language.
- Avoid jargon.
- Avoid uncommon words.
- Don't write in the first person singular.
- Instead of "I" or "me," use "we," "you," "us," or "one."
### Point of view
In most cases, it’s appropriate to use the second-person (you, yours) point of view,
because it’s friendly and easy to understand.
<!-- How do we harmonize the second person in Pajamas with our first person plural in our doc guide? -->
### Capitalization
- Capitalize "G" and "L" in GitLab.
- Use sentence case for:
- Titles.
- Labels.
- Menu items.
- Buttons.
- Headings. Don't capitalize other words in the title, unless
it refers to a product feature. For example:
- Capitalizing "issues" is acceptable in
`## What you can do with GitLab Issues`, but not in `## Closing multiple issues`.
- Use title case when referring to:
- [GitLab Features](https://about.gitlab.com/features/). For example, Issue Board,
Geo, and Runner.
......@@ -335,8 +358,6 @@ as even native users of English might misunderstand them.
- [Write in Markdown](#markdown).
- Splitting long lines (preferably up to 100 characters) can make it easier to provide feedback on small chunks of text.
- Insert an empty line for new paragraphs.
- Add a new line by ending a line with two spaces. [Using a backslash](../../user/markdown.md#newlines) doesn't work in the docs site.
- Use sentence case for titles, headings, labels, menu items, and buttons.
- Insert an empty line between different markups (for example, after every paragraph, header, list, and so on). Example:
```md
......@@ -572,13 +593,10 @@ For other punctuation rules, please refer to the
- Leave exactly one blank line before and after a heading.
- Do not use links in headings.
- Add the corresponding [product badge](#product-badges) according to the tier the feature belongs.
- Use sentence case in headings. Do not capitalize the words of the title, unless
it refers to a product feature. For example, capitalizing "issues" is acceptable in
`## What you can do with GitLab Issues`, but not in `## Closing multiple issues`.
- Our docs site search engine prioritizes headings, therefore, make sure to write
headings that contextualize the subject and help to take the user to the right
document. For example, `## Examples` is a bad heading; `## GitLab Pages examples`
is a better one. It's not an exact science, but please consider this carefully.
- Our docs site search engine prioritizes words used in headings and subheadings.
Make you subheading titles clear, descriptive, and complete to help users find the
right example, as shown in the section on [heading titles](#heading-titles).
- See [Capitalization](#capitalization) for guidelines on capitalizing headings.
### Heading titles
......@@ -589,6 +607,9 @@ Keep heading titles clear and direct. Make every word count. To accommodate sear
| Configure GDK | Configuring GDK |
| GitLab Release and Maintenance Policy | This section covers GitLab's Release and Maintenance Policy |
| Backport to older releases | Backporting to older releases |
| GitLab Pages examples | Examples |
For guidelines on capitalizing headings, see the section on [capitalization](#capitalization).
NOTE: **Note:**
If you change an existing title, be careful. Any such changes may affect not only [links](#anchor-links)
......
......@@ -38,11 +38,9 @@ The current stages are:
## Default image
The default image is currently
`registry.gitlab.com/gitlab-org/gitlab-build-images:ruby-2.6.5-golang-1.12-git-2.24-lfs-2.9-chrome-73.0-node-12.x-yarn-1.21-postgresql-9.6-graphicsmagick-1.3.34`.
The default image is defined in <https://gitlab.com/gitlab-org/gitlab/blob/master/.gitlab-ci.yml>.
It includes Ruby 2.6.5, Go 1.12, Git 2.24, Git LFS 2.9, Chrome 73, Node 12, Yarn 1.21,
PostgreSQL 9.6, and Graphics Magick 1.3.34.
It includes Ruby, Go, Git, Git LFS, Chrome, Node, Yarn, PostgreSQL, and Graphics Magick.
The images used in our pipelines are configured in the
[`gitlab-org/gitlab-build-images`](https://gitlab.com/gitlab-org/gitlab-build-images)
......
......@@ -68,7 +68,10 @@ A self-managed subscription uses a hybrid model. You pay for a subscription acco
Every occupied seat, whether by person, job, or bot is counted in the subscription, with the following exceptions:
- Blocked users who are blocked prior to the renewal of a subscription won't be counted as active users for the renewal subscription. They may count as active users in the subscription period in which they were originally added.
- [Deactivated](../user/admin_area/activating_deactivating_users.md#deactivating-a-user) and
[blocked](../user/admin_area/blocking_unblocking_users.md) users who are restricted prior to the
renewal of a subscription won't be counted as active users for the renewal subscription. They may
count as active users in the subscription period in which they were originally added.
- Members with Guest permissions on an Ultimate subscription.
- GitLab-created service accounts: `Ghost User` and `Support Bot`.
......
......@@ -230,7 +230,7 @@ Container Scanning can be executed on an offline GitLab Ultimate installation by
```
1. If your local Docker container registry is running securely over `HTTPS`, but you're using a
self-signed certificate, then you must set `DOCKER_INSECURE: true` in the above
self-signed certificate, then you must set `DOCKER_INSECURE: "true"` in the above
`container_scanning` section of your `.gitlab-ci.yml`.
It may be worthwhile to set up a [scheduled pipeline](../../../ci/pipelines/schedules.md) to automatically build a new version of the vulnerabilities database on a preset schedule. You can use the following `.gitlab-yml.ci` as a template:
......
......@@ -9,25 +9,28 @@ type: reference
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/5164) and later, the epic bars show their title, progress, and completed weight percentage.
An Epic within a group containing **Start date** and/or **Due date**
can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page
can be visualized in a form of a timeline (a Gantt chart). The Epics Roadmap page
shows such a visualization for all the epics which are under a group and/or its subgroups.
On the epic bars, you can see their title, progress, and completed weight percentage.
When you hover over an epic bar, a popover appears with its title, start and due dates, and weight
completed.
You can expand epics that contain child epics to show their child epics in the roadmap.
You can click the chevron **{chevron-down}** next to the epic title to expand and collapse the child epics.
![roadmap view](img/roadmap_view_v12_9.png)
A dropdown allows you to show only open or closed epics. By default, all epics are shown.
A dropdown menu allows you to show only open or closed epics. By default, all epics are shown.
![epics state dropdown](img/epics_state_dropdown.png)
Epics in the view can be sorted by:
You can sort epics in the Roadmap view by:
- **Created date**
- **Last updated**
- **Start date**
- **Due date**
- Created date
- Last updated
- Start date
- Due date
Each option contains a button that toggles the sort order between **ascending** and **descending**. The sort option and order will be persisted when browsing Epics,
including the [epics list view](../epics/index.md).
......
......@@ -637,6 +637,15 @@ clicking on the context menu in the upper-right corner.
If you use the **Timeline zoom** function at the bottom of the chart, logs will narrow down to the time range you selected.
### Timeline zoom and URL sharing
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/198910) in GitLab 12.8.
You can use the **Timeline zoom** function at the bottom of a chart to zoom in
on a date and time of your choice. When you click and drag the sliders to select
a different beginning or end date of data to display, GitLab adds your selected start
and end times to the URL, enabling you to share specific timeframes more easily.
### Downloading data as CSV
Data from Prometheus charts on the metrics dashboard can be downloaded as CSV.
......
......@@ -172,6 +172,7 @@ module API
mount ::API::ProjectSnippets
mount ::API::ProjectStatistics
mount ::API::ProjectTemplates
mount ::API::Terraform::State
mount ::API::ProtectedBranches
mount ::API::ProtectedTags
mount ::API::Releases
......
......@@ -46,6 +46,10 @@ module API
prepend_if_ee('EE::API::APIGuard::HelperMethods') # rubocop: disable Cop/InjectEnterpriseEditionModule
include Gitlab::Auth::AuthFinders
def access_token
super || find_personal_access_token_from_http_basic_auth
end
def find_current_user!
user = find_user_from_sources
return unless user
......
......@@ -65,11 +65,15 @@ module API
post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_project)
deploy_token = ::Projects::DeployTokens::CreateService.new(
result = ::Projects::DeployTokens::CreateService.new(
user_project, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute
present deploy_token, with: Entities::DeployTokenWithToken
if result[:status] == :success
present result[:deploy_token], with: Entities::DeployTokenWithToken
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Delete a project deploy token' do
......@@ -126,11 +130,15 @@ module API
post ':id/deploy_tokens' do
authorize!(:create_deploy_token, user_group)
deploy_token = ::Groups::DeployTokens::CreateService.new(
result = ::Groups::DeployTokens::CreateService.new(
user_group, current_user, scope_params.merge(declared(params, include_missing: false, include_parent_namespaces: false))
).execute
present deploy_token, with: Entities::DeployTokenWithToken
if result[:status] == :success
present result[:deploy_token], with: Entities::DeployTokenWithToken
else
render_api_error!(result[:message], result[:http_status])
end
end
desc 'Delete a group deploy token' do
......
......@@ -36,7 +36,11 @@ module API
type: String,
values: %w[simple],
desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
optional :author_username, type: String, desc: 'Return merge requests which are authored by the user with the given username'
mutually_exclusive :author_id, :author_username
optional :assignee_id,
types: [Integer, String],
integer_none_any: true,
......
......@@ -207,11 +207,8 @@ module API
status 202
header 'Job-Status', job.status
header 'Range', "0-#{stream_size}"
if Feature.enabled?(:runner_job_trace_update_interval_header, default_enabled: true)
header 'X-GitLab-Trace-Update-Interval', job.trace.update_interval.to_s
end
end
desc 'Authorize artifacts uploading for job' do
http_codes [[200, 'Upload allowed'],
......
# frozen_string_literal: true
module API
module Terraform
class State < Grape::API
before { authenticate! }
before { authorize! :admin_terraform_state, user_project }
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
params do
requires :name, type: String, desc: 'The name of a terraform state'
end
namespace ':id/terraform/state/:name' do
desc 'Get a terraform state by its name'
route_setting :authentication, basic_auth_personal_access_token: true
get do
status 501
content_type 'text/plain'
body 'not implemented'
end
desc 'Add a new terraform state or update an existing one'
route_setting :authentication, basic_auth_personal_access_token: true
post do
status 501
content_type 'text/plain'
body 'not implemented'
end
desc 'Delete a terraform state of certain name'
route_setting :authentication, basic_auth_personal_access_token: true
delete do
status 501
content_type 'text/plain'
body 'not implemented'
end
end
end
end
end
end
......@@ -167,6 +167,14 @@ module Gitlab
oauth_token
end
def find_personal_access_token_from_http_basic_auth
return unless route_authentication_setting[:basic_auth_personal_access_token]
return unless has_basic_credentials?(current_request)
_username, password = user_name_and_password(current_request)
PersonalAccessToken.find_by_token(password)
end
def parsed_oauth_token
Doorkeeper::OAuth::Token.from_request(current_request, *Doorkeeper.configuration.access_token_methods)
end
......
......@@ -76,6 +76,11 @@ msgid_plural "%d changed files"
msgstr[0] ""
msgstr[1] ""
msgid "%d child epic"
msgid_plural "%d child epics"
msgstr[0] ""
msgstr[1] ""
msgid "%d code quality issue"
msgid_plural "%d code quality issues"
msgstr[0] ""
......@@ -390,6 +395,9 @@ msgstr ""
msgid "%{openedIssues} open, %{closedIssues} closed"
msgstr ""
msgid "%{percentage}%% weight completed"
msgstr ""
msgid "%{percent}%% complete"
msgstr ""
......@@ -1060,6 +1068,9 @@ msgid_plural "Add %d issues"
msgstr[0] ""
msgstr[1] ""
msgid "Add %{linkStart}assets%{linkEnd} to your Release. GitLab automatically includes read-only assets, like source code and release evidence."
msgstr ""
msgid "Add CHANGELOG"
msgstr ""
......@@ -1138,6 +1149,9 @@ msgstr ""
msgid "Add an issue"
msgstr ""
msgid "Add another link"
msgstr ""
msgid "Add approval rule"
msgstr ""
......@@ -4986,6 +5000,9 @@ msgstr ""
msgid "Collapse approvers"
msgstr ""
msgid "Collapse child epics"
msgstr ""
msgid "Collapse sidebar"
msgstr ""
......@@ -8307,6 +8324,9 @@ msgstr ""
msgid "Expand approvers"
msgstr ""
msgid "Expand child epics"
msgstr ""
msgid "Expand down"
msgstr ""
......@@ -11994,6 +12014,9 @@ msgstr ""
msgid "Link copied"
msgstr ""
msgid "Link title"
msgstr ""
msgid "Linked emails (%{email_count})"
msgstr ""
......@@ -14795,6 +14818,9 @@ msgstr ""
msgid "Pods in use"
msgstr ""
msgid "Point to any links you like: documentation, built binaries, or other related materials. These can be internal or external links from your GitLab instance."
msgstr ""
msgid "Preferences"
msgstr ""
......@@ -16539,6 +16565,12 @@ msgid_plural "Releases"
msgstr[0] ""
msgstr[1] ""
msgid "Release assets"
msgstr ""
msgid "Release assets documentation"
msgstr ""
msgid "Release does not have the same project as the milestone"
msgstr ""
......@@ -16608,6 +16640,9 @@ msgstr ""
msgid "Remove approvers?"
msgstr ""
msgid "Remove asset link"
msgstr ""
msgid "Remove assignee"
msgstr ""
......@@ -20745,19 +20780,25 @@ msgstr ""
msgid "ThreatMonitoring|Anomalous Requests"
msgstr ""
msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data."
msgid "ThreatMonitoring|Application firewall not detected"
msgstr ""
msgid "ThreatMonitoring|Container Network Policy"
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies are not installed or has been disabled. To view this data, ensure you NetworkPolicies are installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|Container NetworkPolicies not detected"
msgstr ""
msgid "ThreatMonitoring|Dropped Packets"
msgstr ""
msgid "ThreatMonitoring|Environment"
msgstr ""
msgid "ThreatMonitoring|No traffic to display"
msgid "ThreatMonitoring|No environments detected"
msgstr ""
msgid "ThreatMonitoring|Operations Per Second"
......@@ -20778,6 +20819,9 @@ msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch statistics"
msgstr ""
msgid "ThreatMonitoring|The firewall is not installed or has been disabled. To view this data, ensure you firewall is installed and enabled for your cluster."
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
msgstr ""
......@@ -20787,13 +20831,10 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
msgid "ThreatMonitoring|Threat monitoring is not enabled"
msgstr ""
msgid "ThreatMonitoring|Threat monitoring provides security monitoring and rules to protect production applications."
msgid "ThreatMonitoring|Time"
msgstr ""
msgid "ThreatMonitoring|Time"
msgid "ThreatMonitoring|To view this data, ensure you have configured an environment for this project and that at least one threat monitoring feature is enabled."
msgstr ""
msgid "ThreatMonitoring|Total Packets"
......@@ -20802,10 +20843,10 @@ msgstr ""
msgid "ThreatMonitoring|Total Requests"
msgstr ""
msgid "ThreatMonitoring|Web Application Firewall"
msgid "ThreatMonitoring|View documentation"
msgstr ""
msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the Network Policies correctly."
msgid "ThreatMonitoring|Web Application Firewall"
msgstr ""
msgid "ThreatMonitoring|While it's rare to have no traffic coming to your application, it can happen. In any event, we ask that you double check your settings to make sure you've set up the WAF correctly."
......
......@@ -212,13 +212,85 @@ describe Groups::Settings::CiCdController do
end
describe 'POST create_deploy_token' do
context 'when ajax_new_deploy_token feature flag is disabled for the project' do
before do
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: group })
entity.add_owner(user)
end
it_behaves_like 'a created deploy token' do
let(:entity) { group }
let(:create_entity_params) { { group_id: group } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:group_type] }
end
end
context 'when ajax_new_deploy_token feature flag is enabled for the project' do
let(:good_deploy_token_params) do
{
name: 'name',
expires_at: 1.day.from_now.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:group_type]
}
end
let(:request_params) do
{
group_id: group.to_param,
deploy_token: deploy_token_params
}
end
before do
entity.add_owner(user)
group.add_owner(user)
end
subject { post :create_deploy_token, params: request_params, format: :json }
context('a good request') do
let(:deploy_token_params) { good_deploy_token_params }
let(:expected_response) do
{
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
end
}
end
it 'creates the deploy token' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/deploy_token')
expect(json_response).to match(expected_response)
end
end
context('a bad request') do
let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
let(:expected_response) { { 'message' => "Scopes can't be blank" } }
it 'does not create the deploy token' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to match(expected_response)
end
end
context('an invalid request') do
let(:deploy_token_params) { good_deploy_token_params.except(:name) }
it 'raises a validation error' do
expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
end
end
end
......
......@@ -249,10 +249,82 @@ describe Projects::Settings::CiCdController do
end
describe 'POST create_deploy_token' do
context 'when ajax_new_deploy_token feature flag is disabled for the project' do
before do
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
end
it_behaves_like 'a created deploy token' do
let(:entity) { project }
let(:create_entity_params) { { namespace_id: project.namespace, project_id: project } }
let(:deploy_token_type) { DeployToken.deploy_token_types[:project_type] }
end
end
context 'when ajax_new_deploy_token feature flag is enabled for the project' do
let(:good_deploy_token_params) do
{
name: 'name',
expires_at: 1.day.from_now.to_s,
username: 'deployer',
read_repository: '1',
deploy_token_type: DeployToken.deploy_token_types[:project_type]
}
end
let(:request_params) do
{
namespace_id: project.namespace.to_param,
project_id: project.to_param,
deploy_token: deploy_token_params
}
end
subject { post :create_deploy_token, params: request_params, format: :json }
context('a good request') do
let(:deploy_token_params) { good_deploy_token_params }
let(:expected_response) do
{
'id' => be_a(Integer),
'name' => deploy_token_params[:name],
'username' => deploy_token_params[:username],
'expires_at' => Time.parse(deploy_token_params[:expires_at]),
'token' => be_a(String),
'scopes' => deploy_token_params.inject([]) do |scopes, kv|
key, value = kv
key.to_s.start_with?('read_') && !value.to_i.zero? ? scopes << key.to_s : scopes
end
}
end
it 'creates the deploy token' do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('public_api/v4/deploy_token')
expect(json_response).to match(expected_response)
end
end
context('a bad request') do
let(:deploy_token_params) { good_deploy_token_params.except(:read_repository) }
let(:expected_response) { { 'message' => "Scopes can't be blank" } }
it 'does not create the deploy token' do
subject
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response).to match(expected_response)
end
end
context('an invalid request') do
let(:deploy_token_params) { good_deploy_token_params.except(:name) }
it 'raises a validation error' do
expect { subject }.to raise_error(ActiveRecord::StatementInvalid)
end
end
end
end
end
......@@ -14,6 +14,7 @@ describe 'Projects > Settings > CI / CD settings' do
project.add_role(user, role)
sign_in(user)
stub_container_registry_config(enabled: true)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit project_settings_ci_cd_path(project)
end
......
......@@ -11,6 +11,7 @@ describe 'Repository Settings > User sees revoke deploy token modal', :js do
before do
project.add_role(user, role)
sign_in(user)
stub_feature_flags(ajax_new_deploy_token: { enabled: false, thing: project })
visit(project_settings_ci_cd_path(project))
click_link('Revoke')
end
......
......@@ -570,4 +570,65 @@ describe('Api', () => {
.catch(done.fail);
});
});
describe('createReleaseLink', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const dummyReleaseTag = 'v1.3';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
)}/releases/${dummyReleaseTag}/assets/links`;
const expectedLink = {
url: 'https://example.com',
name: 'An example link',
};
describe('when the Release is successfully created', () => {
it('resolves the Promise', () => {
mock.onPost(expectedUrl, expectedLink).replyOnce(201);
return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).then(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
describe('when an error occurs while creating the Release', () => {
it('rejects the Promise', () => {
mock.onPost(expectedUrl, expectedLink).replyOnce(500);
return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).catch(() => {
expect(mock.history.post).toHaveLength(1);
});
});
});
});
describe('deleteReleaseLink', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
const dummyReleaseTag = 'v1.3';
const dummyLinkId = '4';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
)}/releases/${dummyReleaseTag}/assets/links/${dummyLinkId}`;
describe('when the Release is successfully deleted', () => {
it('resolves the Promise', () => {
mock.onDelete(expectedUrl).replyOnce(200);
return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).then(() => {
expect(mock.history.delete).toHaveLength(1);
});
});
});
describe('when an error occurs while deleting the Release', () => {
it('rejects the Promise', () => {
mock.onDelete(expectedUrl).replyOnce(500);
return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).catch(() => {
expect(mock.history.delete).toHaveLength(1);
});
});
});
});
});
......@@ -4,6 +4,7 @@ import ReleaseEditApp from '~/releases/components/app_edit.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
describe('Release edit component', () => {
let wrapper;
......@@ -11,7 +12,7 @@ describe('Release edit component', () => {
let actions;
let state;
const factory = () => {
const factory = (featureFlags = {}) => {
state = {
release,
markdownDocsPath: 'path/to/markdown/docs',
......@@ -22,6 +23,7 @@ describe('Release edit component', () => {
actions = {
fetchRelease: jest.fn(),
updateRelease: jest.fn(),
addEmptyAssetLink: jest.fn(),
};
const store = new Vuex.Store({
......@@ -36,6 +38,9 @@ describe('Release edit component', () => {
wrapper = mount(ReleaseEditApp, {
store,
provide: {
glFeatures: featureFlags,
},
});
};
......@@ -132,4 +137,28 @@ describe('Release edit component', () => {
expect(cancelButton.attributes().href).toBe(backUrl);
});
});
describe('asset links form', () => {
const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
describe('when the release_asset_link_editing feature flag is disabled', () => {
beforeEach(() => {
factory({ releaseAssetLinkEditing: false });
});
it('does not render the asset links portion of the form', () => {
expect(findAssetLinksForm().exists()).toBe(false);
});
});
describe('when the release_asset_link_editing feature flag is enabled', () => {
beforeEach(() => {
factory({ releaseAssetLinkEditing: true });
});
it('renders the asset links portion of the form', () => {
expect(findAssetLinksForm().exists()).toBe(true);
});
});
});
});
......@@ -9,6 +9,7 @@ import createState from '~/releases/stores/modules/detail/state';
import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import api from '~/api';
jest.mock('~/flash', () => jest.fn());
......@@ -179,40 +180,92 @@ describe('Release detail actions', () => {
});
describe('updateRelease', () => {
let getReleaseUrl;
let getters;
let dispatch;
let callOrder;
beforeEach(() => {
state.release = release;
state.release = convertObjectPropsToCamelCase(release);
state.projectId = '18';
state.tagName = 'v1.3';
getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
state.tagName = state.release.tagName;
getters = {
releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
};
dispatch = jest.fn();
callOrder = [];
jest.spyOn(api, 'updateRelease').mockImplementation(() => {
callOrder.push('updateRelease');
return Promise.resolve();
});
jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
callOrder.push('deleteReleaseLink');
return Promise.resolve();
});
jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
callOrder.push('createReleaseLink');
return Promise.resolve();
});
});
it(`dispatches requestUpdateRelease and receiveUpdateReleaseSuccess`, () => {
mock.onPut(getReleaseUrl).replyOnce(200);
it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => {
return actions.updateRelease({ dispatch, state, getters }).then(() => {
expect(dispatch.mock.calls).toEqual([
['requestUpdateRelease'],
['receiveUpdateReleaseSuccess'],
]);
});
});
return testAction(
actions.updateRelease,
undefined,
state,
[],
[{ type: 'requestUpdateRelease' }, { type: 'receiveUpdateReleaseSuccess' }],
);
it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
return actions.updateRelease({ dispatch, state, getters }).then(() => {
expect(dispatch.mock.calls).toEqual([
['requestUpdateRelease'],
['receiveUpdateReleaseError', error],
]);
});
});
it(`dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object`, () => {
mock.onPut(getReleaseUrl).replyOnce(500);
it('updates the Release, then deletes all existing links, and then recreates new links', () => {
return actions.updateRelease({ dispatch, state, getters }).then(() => {
expect(callOrder).toEqual([
'updateRelease',
'deleteReleaseLink',
'deleteReleaseLink',
'createReleaseLink',
'createReleaseLink',
]);
return testAction(
actions.updateRelease,
undefined,
state,
[],
expect(api.updateRelease.mock.calls).toEqual([
[
{ type: 'requestUpdateRelease' },
{ type: 'receiveUpdateReleaseError', payload: expect.anything() },
state.projectId,
state.tagName,
{
name: state.release.name,
description: state.release.description,
},
],
]);
expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length);
getters.releaseLinksToDelete.forEach(link => {
expect(api.deleteReleaseLink).toHaveBeenCalledWith(
state.projectId,
state.tagName,
link.id,
);
});
expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length);
getters.releaseLinksToCreate.forEach(link => {
expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link);
});
});
});
});
});
import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => {
describe('releaseLinksToCreate', () => {
it("returns an empty array if state.release doesn't exist", () => {
const state = {};
expect(getters.releaseLinksToCreate(state)).toEqual([]);
});
it("returns all release links that aren't empty", () => {
const emptyLinks = [
{ url: '', name: '' },
{ url: ' ', name: '' },
{ url: ' ', name: ' ' },
{ url: '\r\n', name: '\t' },
];
const nonEmptyLinks = [
{ url: 'https://example.com/1', name: 'Example 1' },
{ url: '', name: 'Example 2' },
{ url: 'https://example.com/3', name: '' },
];
const state = {
release: {
assets: {
links: [...emptyLinks, ...nonEmptyLinks],
},
},
};
expect(getters.releaseLinksToCreate(state)).toEqual(nonEmptyLinks);
});
});
describe('releaseLinksToDelete', () => {
it("returns an empty array if state.originalRelease doesn't exist", () => {
const state = {};
expect(getters.releaseLinksToDelete(state)).toEqual([]);
});
it('returns all links associated with the original release', () => {
const originalLinks = [
{ url: 'https://example.com/1', name: 'Example 1' },
{ url: 'https://example.com/2', name: 'Example 2' },
];
const state = {
originalRelease: {
assets: {
links: originalLinks,
},
},
};
expect(getters.releaseLinksToDelete(state)).toEqual(originalLinks);
});
});
});
......@@ -8,11 +8,12 @@
import createState from '~/releases/stores/modules/detail/state';
import mutations from '~/releases/stores/modules/detail/mutations';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release } from '../../../mock_data';
import { release as originalRelease } from '../../../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
describe('Release detail mutations', () => {
let state;
let releaseClone;
let release;
beforeEach(() => {
state = createState({
......@@ -23,7 +24,7 @@ describe('Release detail mutations', () => {
markdownPreviewPath: 'path/to/markdown/preview',
updateReleaseApiDocsPath: 'path/to/api/docs',
});
releaseClone = JSON.parse(JSON.stringify(release));
release = convertObjectPropsToCamelCase(originalRelease);
});
describe(types.REQUEST_RELEASE, () => {
......@@ -36,13 +37,15 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_RELEASE_SUCCESS](state, releaseClone);
mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
expect(state.fetchError).toEqual(undefined);
expect(state.isFetchingRelease).toEqual(false);
expect(state.release).toEqual(releaseClone);
expect(state.release).toEqual(release);
expect(state.originalRelease).toEqual(release);
});
});
......@@ -61,7 +64,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_TITLE, () => {
it("updates the release's title", () => {
state.release = releaseClone;
state.release = release;
const newTitle = 'The new release title';
mutations[types.UPDATE_RELEASE_TITLE](state, newTitle);
......@@ -71,7 +74,7 @@ describe('Release detail mutations', () => {
describe(types.UPDATE_RELEASE_NOTES, () => {
it("updates the release's notes", () => {
state.release = releaseClone;
state.release = release;
const newNotes = 'The new release notes';
mutations[types.UPDATE_RELEASE_NOTES](state, newNotes);
......@@ -89,7 +92,7 @@ describe('Release detail mutations', () => {
describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, releaseClone);
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
expect(state.updateError).toEqual(undefined);
......@@ -107,4 +110,65 @@ describe('Release detail mutations', () => {
expect(state.updateError).toEqual(error);
});
});
describe(types.ADD_EMPTY_ASSET_LINK, () => {
it('adds a new, empty link object to the release', () => {
state.release = release;
const linksBefore = [...state.release.assets.links];
mutations[types.ADD_EMPTY_ASSET_LINK](state);
expect(state.release.assets.links).toEqual([
...linksBefore,
{
id: expect.stringMatching(/^new-link-/),
url: '',
name: '',
},
]);
});
});
describe(types.UPDATE_ASSET_LINK_URL, () => {
it('updates an asset link with a new URL', () => {
state.release = release;
const newUrl = 'https://example.com/updated/url';
mutations[types.UPDATE_ASSET_LINK_URL](state, {
linkIdToUpdate: state.release.assets.links[0].id,
newUrl,
});
expect(state.release.assets.links[0].url).toEqual(newUrl);
});
});
describe(types.UPDATE_ASSET_LINK_NAME, () => {
it('updates an asset link with a new name', () => {
state.release = release;
const newName = 'Updated Link';
mutations[types.UPDATE_ASSET_LINK_NAME](state, {
linkIdToUpdate: state.release.assets.links[0].id,
newName,
});
expect(state.release.assets.links[0].name).toEqual(newName);
});
});
describe(types.REMOVE_ASSET_LINK, () => {
it('removes an asset link from the release', () => {
state.release = release;
const linkToRemove = state.release.assets.links[0];
mutations[types.REMOVE_ASSET_LINK](state, linkToRemove.id);
expect(state.release.assets.links).not.toContainEqual(linkToRemove);
});
});
});
......@@ -53,7 +53,8 @@ describe ReleasesHelper do
markdown_preview_path
markdown_docs_path
releases_page_path
update_release_api_docs_path)
update_release_api_docs_path
release_assets_docs_path)
expect(helper.data_for_edit_release_page.keys).to eq(keys)
end
end
......
......@@ -335,6 +335,54 @@ describe Gitlab::Auth::AuthFinders do
end
end
describe '#find_personal_access_token_from_http_basic_auth' do
def auth_header_with(token)
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials('username', token)
end
context 'access token is valid' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'finds the token from basic auth' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to eq personal_access_token
end
end
context 'access token is not valid' do
let(:route_authentication_setting) { { basic_auth_personal_access_token: true } }
it 'returns nil' do
auth_header_with('failing_token')
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
context 'route_setting is not set' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
it 'returns nil' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
context 'route_setting is not correct' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
let(:route_authentication_setting) { { basic_auth_personal_access_token: false } }
it 'returns nil' do
auth_header_with(personal_access_token.token)
expect(find_personal_access_token_from_http_basic_auth).to be_nil
end
end
end
describe '#find_user_from_basic_auth_job' do
def basic_http_auth(username, password)
ActionController::HttpAuthentication::Basic.encode_credentials(username, password)
......
......@@ -3681,15 +3681,15 @@ describe User, :do_not_mock_admin_mode do
end
it 'returns false if email can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
stub_omniauth_setting(sync_profile_attributes: %w(location name))
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:email)).to be_falsey
end
it 'returns false if location can not be synced' do
stub_omniauth_setting(sync_profile_attributes: %w(location email))
stub_omniauth_setting(sync_profile_attributes: %w(name email))
expect(user.sync_attribute?(:name)).to be_falsey
expect(user.sync_attribute?(:location)).to be_falsey
end
it 'returns true for all syncable attributes if all syncable attributes can be synced' do
......
......@@ -53,6 +53,7 @@ describe ProjectPolicy do
admin_commit_status admin_build admin_container_image
admin_pipeline admin_environment admin_deployment destroy_release add_cluster
daily_statistics read_deploy_token create_deploy_token destroy_deploy_token
admin_terraform_state
]
end
......
......@@ -526,13 +526,49 @@ describe API::MergeRequests do
expect_response_contain_exactly(merge_request3.id)
end
context 'filter by author' do
let(:user3) { create(:user) }
let(:project) { create(:project, :public, :repository, creator: user3, namespace: user3.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let!(:merge_request3) do
create(:merge_request, :simple, author: user3, assignees: [user3], source_project: project, target_project: project, source_branch: 'other-branch')
end
context 'when only `author_id` is passed' do
it 'returns an array of merge requests authored by the given user' do
merge_request3 = create(:merge_request, :simple, author: user2, assignees: [user], source_project: project2, target_project: project2, source_branch: 'other-branch')
get api('/merge_requests', user), params: {
author_id: user3.id,
scope: :all
}
expect_response_contain_exactly(merge_request3.id)
end
end
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
context 'when only `author_username` is passed' do
it 'returns an array of merge requests authored by the given user(by `author_username`)' do
get api('/merge_requests', user), params: {
author_username: user3.username,
scope: :all
}
expect_response_contain_exactly(merge_request3.id)
end
end
context 'when both `author_id` and `author_username` are passed' do
it 'returns a 400' do
get api('/merge_requests', user), params: {
author_id: user.id,
author_username: user2.username,
scope: :all
}
expect(response).to have_gitlab_http_status(:bad_request)
expect(json_response['error']).to eq(
'author_id, author_username are mutually exclusive')
end
end
end
it 'returns an array of merge requests assigned to the given user' do
merge_request3 = create(:merge_request, :simple, author: user, assignees: [user2], source_project: project2, target_project: project2, source_branch: 'other-branch')
......@@ -1525,7 +1561,7 @@ describe API::MergeRequests do
it "returns 400 when target_branch is missing" do
post api("/projects/#{forked_project.id}/merge_requests", user2),
params: { title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id }
params: { title: 'Test merge_request', source_branch: "master", author: user2, target_project_id: project.id }
expect(response).to have_gitlab_http_status(:bad_request)
end
......
......@@ -1349,19 +1349,6 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
expect(response.header['X-GitLab-Trace-Update-Interval']).to eq('30')
end
end
context 'when feature flag runner_job_trace_update_interval_header is disabled' do
before do
stub_feature_flags(runner_job_trace_update_interval_header: { enabled: false })
end
it 'does not return X-GitLab-Trace-Update-Interval header' do
patch_the_trace
expect(response).to have_gitlab_http_status(:accepted)
expect(response.header).not_to have_key 'X-GitLab-Trace-Update-Interval'
end
end
end
context 'when Runner makes a force-patch' do
......
# frozen_string_literal: true
require 'spec_helper'
describe API::Terraform::State do
def auth_header_for(user)
auth_header = ActionController::HttpAuthentication::Basic.encode_credentials(
user.username,
create(:personal_access_token, user: user).token
)
{ 'HTTP_AUTHORIZATION' => auth_header }
end
let!(:project) { create(:project) }
let(:developer) { create(:user) }
let(:maintainer) { create(:user) }
let(:state_name) { 'state' }
before do
project.add_maintainer(maintainer)
end
describe 'GET /projects/:id/terraform/state/:name' do
it 'returns 401 if user is not authenticated' do
headers = { 'HTTP_AUTHORIZATION' => 'failing_token' }
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: headers
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'returns terraform state belonging to a project of given state name' do
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
it 'returns not found if the project does not exists' do
get api("/projects/0000/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_found)
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'POST /projects/:id/terraform/state/:name' do
context 'when terraform state with a given name is already present' do
it 'updates the state' do
post api("/projects/#{project.id}/terraform/state/#{state_name}"),
params: '{ "instance": "example-instance" }',
headers: { 'Content-Type' => 'text/plain' }.merge(auth_header_for(maintainer))
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'when there is no terraform state of a given name' do
it 'creates a new state' do
post api("/projects/#{project.id}/terraform/state/example2"),
headers: auth_header_for(maintainer),
params: '{ "database": "example-database" }'
expect(response).to have_gitlab_http_status(:not_implemented)
expect(response.body).to eq('not implemented')
end
end
end
describe 'DELETE /projects/:id/terraform/state/:name' do
it 'deletes the state' do
delete api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(maintainer)
expect(response).to have_gitlab_http_status(:not_implemented)
end
it 'returns forbidden if the user cannot access the state' do
project.add_developer(developer)
get api("/projects/#{project.id}/terraform/state/#{state_name}"), headers: auth_header_for(developer)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
end
......@@ -214,15 +214,62 @@ RSpec.configure do |config|
# modifying a significant number of specs to test both states for admin
# mode enabled / disabled.
#
# See https://gitlab.com/gitlab-org/gitlab/issues/31511
# See gitlab/spec/support/helpers/admin_mode_helpers.rb
# This will only be applied to specs below dirs in `admin_mode_mock_dirs`
#
# If it is required to have the real behaviour that an admin is signed in
# See ongoing migration: https://gitlab.com/gitlab-org/gitlab/-/issues/31511
#
# Until the migration is finished, if it is required to have the real
# behaviour in any of the mocked dirs specs that an admin is signed in
# with normal user mode and needs to switch to admin mode, it is possible to
# mark such tests with the `do_not_mock_admin_mode` metadata tag, e.g:
#
# context 'some test with normal user mode', :do_not_mock_admin_mode do ... end
unless example.metadata[:do_not_mock_admin_mode]
# context 'some test in mocked dir', :do_not_mock_admin_mode do ... end
admin_mode_mock_dirs = %w(
./ee/spec/controllers
./ee/spec/elastic_integration
./ee/spec/features
./ee/spec/finders
./ee/spec/lib
./ee/spec/models
./ee/spec/policies
./ee/spec/requests/admin
./ee/spec/serializers
./ee/spec/services
./ee/spec/support/protected_tags
./ee/spec/support/shared_examples
./spec/controllers
./spec/features
./spec/finders
./spec/frontend
./spec/helpers
./spec/lib
./spec/models
./spec/policies
./spec/requests
./spec/serializers
./spec/services
./spec/support/cycle_analytics_helpers
./spec/support/protected_tags
./spec/support/shared_examples
./spec/views
./spec/workers
)
if !example.metadata[:do_not_mock_admin_mode] && example.metadata[:file_path].start_with?(*admin_mode_mock_dirs)
allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
current_user_mode.send(:user)&.admin?
end
end
# Administrators have to re-authenticate in order to access administrative
# functionality when feature flag :user_mode_in_session is active. Any spec
# that requires administrative access can use the tag :enable_admin_mode
# to avoid the second auth step (provided the user is already an admin):
#
# context 'some test that requires admin mode', :enable_admin_mode do ... end
#
# See also spec/support/helpers/admin_mode_helpers.rb
if example.metadata[:enable_admin_mode]
allow_any_instance_of(Gitlab::Auth::CurrentUserMode).to receive(:admin_mode?) do |current_user_mode|
current_user_mode.send(:user)&.admin?
end
......
......@@ -17,7 +17,7 @@ RSpec.shared_examples 'a deploy token creation service' do
end
it 'returns a DeployToken' do
expect(subject).to be_an_instance_of DeployToken
expect(subject[:deploy_token]).to be_an_instance_of DeployToken
end
end
......@@ -25,7 +25,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, expires_at: '') }
it 'sets Forever.date' do
expect(subject.read_attribute(:expires_at)).to eq(Forever.date)
expect(subject[:deploy_token].read_attribute(:expires_at)).to eq(Forever.date)
end
end
......@@ -33,7 +33,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: '') }
it 'converts it to nil' do
expect(subject.read_attribute(:username)).to be_nil
expect(subject[:deploy_token].read_attribute(:username)).to be_nil
end
end
......@@ -41,7 +41,7 @@ RSpec.shared_examples 'a deploy token creation service' do
let(:deploy_token_params) { attributes_for(:deploy_token, username: 'deployer') }
it 'keeps the provided username' do
expect(subject.read_attribute(:username)).to eq('deployer')
expect(subject[:deploy_token].read_attribute(:username)).to eq('deployer')
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