Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
4944e9e5a9
commit
63324f9cfd
|
@ -1125,33 +1125,6 @@ Rails/SaveBang:
|
|||
- 'spec/requests/api/labels_spec.rb'
|
||||
- 'spec/requests/api/project_import_spec.rb'
|
||||
- 'spec/requests/projects/cycle_analytics_events_spec.rb'
|
||||
- 'spec/services/auth/container_registry_authentication_service_spec.rb'
|
||||
- 'spec/services/auto_merge/base_service_spec.rb'
|
||||
- 'spec/services/auto_merge_service_spec.rb'
|
||||
- 'spec/services/clusters/update_service_spec.rb'
|
||||
- 'spec/services/deployments/after_create_service_spec.rb'
|
||||
- 'spec/services/design_management/generate_image_versions_service_spec.rb'
|
||||
- 'spec/services/discussions/resolve_service_spec.rb'
|
||||
- 'spec/services/draft_notes/destroy_service_spec.rb'
|
||||
- 'spec/services/emails/confirm_service_spec.rb'
|
||||
- 'spec/services/groups/destroy_service_spec.rb'
|
||||
- 'spec/services/groups/import_export/import_service_spec.rb'
|
||||
- 'spec/services/labels/promote_service_spec.rb'
|
||||
- 'spec/services/notes/create_service_spec.rb'
|
||||
- 'spec/services/notification_recipients/build_service_spec.rb'
|
||||
- 'spec/services/notification_service_spec.rb'
|
||||
- 'spec/services/packages/conan/create_package_file_service_spec.rb'
|
||||
- 'spec/services/reset_project_cache_service_spec.rb'
|
||||
- 'spec/services/resource_events/change_milestone_service_spec.rb'
|
||||
- 'spec/services/system_hooks_service_spec.rb'
|
||||
- 'spec/services/system_note_service_spec.rb'
|
||||
- 'spec/services/system_notes/issuables_service_spec.rb'
|
||||
- 'spec/services/todo_service_spec.rb'
|
||||
- 'spec/services/todos/destroy/confidential_issue_service_spec.rb'
|
||||
- 'spec/services/users/destroy_service_spec.rb'
|
||||
- 'spec/services/users/repair_ldap_blocked_service_spec.rb'
|
||||
- 'spec/services/verify_pages_domain_service_spec.rb'
|
||||
- 'spec/sidekiq/cron/job_gem_dependency_spec.rb'
|
||||
|
||||
# Offense count: 187
|
||||
# Cop supports --auto-correct.
|
||||
|
@ -1269,11 +1242,10 @@ RSpec/TimecopTravel:
|
|||
- 'spec/workers/concerns/reenqueuer_spec.rb'
|
||||
- 'spec/lib/gitlab/analytics/cycle_analytics/median_spec.rb'
|
||||
|
||||
# Offense count: 43
|
||||
# Offense count: 21
|
||||
Graphql/IDType:
|
||||
Exclude:
|
||||
- 'ee/app/graphql/ee/mutations/issues/update.rb'
|
||||
- 'ee/app/graphql/ee/types/boards/board_issue_input_base_type.rb'
|
||||
- 'ee/app/graphql/mutations/iterations/update.rb'
|
||||
- 'ee/app/graphql/resolvers/iterations_resolver.rb'
|
||||
- 'app/graphql/mutations/boards/issues/issue_move_list.rb'
|
||||
|
|
|
@ -12,6 +12,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
|
|||
tag: __('Yes or No'),
|
||||
lowercaseValueOnSubmit: true,
|
||||
capitalizeTokenValue: true,
|
||||
hideNotEqual: true,
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
|
@ -30,20 +31,6 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
|
|||
value: __('No'),
|
||||
operator: '=',
|
||||
},
|
||||
{
|
||||
url: 'not[wip]=yes',
|
||||
replacementUrl: 'not[draft]=yes',
|
||||
tokenKey: 'draft',
|
||||
value: __('Yes'),
|
||||
operator: '!=',
|
||||
},
|
||||
{
|
||||
url: 'not[wip]=no',
|
||||
replacementUrl: 'not[draft]=no',
|
||||
tokenKey: 'draft',
|
||||
value: __('No'),
|
||||
operator: '!=',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
|
|
@ -39,7 +39,7 @@ export default class DropdownOperator extends FilteredSearchDropdown {
|
|||
this.dispatchInputEvent();
|
||||
}
|
||||
|
||||
renderContent(forceShowList = false) {
|
||||
renderContent(forceShowList = false, dropdownName = '') {
|
||||
const dropdownData = [
|
||||
{
|
||||
tag: 'equal',
|
||||
|
@ -48,8 +48,9 @@ export default class DropdownOperator extends FilteredSearchDropdown {
|
|||
help: __('is'),
|
||||
},
|
||||
];
|
||||
const dropdownToken = this.tokenKeys.searchByKey(dropdownName.toLowerCase());
|
||||
|
||||
if (gon.features?.notIssuableQueries) {
|
||||
if (gon.features?.notIssuableQueries && !dropdownToken?.hideNotEqual) {
|
||||
dropdownData.push({
|
||||
tag: 'not-equal',
|
||||
type: 'string',
|
||||
|
|
|
@ -83,16 +83,16 @@ export default class FilteredSearchDropdown {
|
|||
}
|
||||
}
|
||||
|
||||
render(forceRenderContent = false, forceShowList = false) {
|
||||
render(forceRenderContent = false, forceShowList = false, hideNotEqual = false) {
|
||||
this.setAsDropdown();
|
||||
|
||||
const currentHook = this.getCurrentHook();
|
||||
const firstTimeInitialized = currentHook === null;
|
||||
|
||||
if (firstTimeInitialized || forceRenderContent) {
|
||||
this.renderContent(forceShowList);
|
||||
this.renderContent(forceShowList, hideNotEqual);
|
||||
} else if (currentHook.list.list.id !== this.dropdown.id) {
|
||||
this.renderContent(forceShowList);
|
||||
this.renderContent(forceShowList, hideNotEqual);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -107,7 +107,7 @@ export default class FilteredSearchDropdownManager {
|
|||
this.mapping[key].reference.setOffset(offset);
|
||||
}
|
||||
|
||||
load(key, firstLoad = false) {
|
||||
load(key, firstLoad = false, dropdownKey = '') {
|
||||
const mappingKey = this.mapping[key];
|
||||
const glClass = mappingKey.gl;
|
||||
const { element } = mappingKey;
|
||||
|
@ -141,12 +141,12 @@ export default class FilteredSearchDropdownManager {
|
|||
}
|
||||
|
||||
this.updateDropdownOffset(key);
|
||||
mappingKey.reference.render(firstLoad, forceShowList);
|
||||
mappingKey.reference.render(firstLoad, forceShowList, dropdownKey);
|
||||
|
||||
this.currentDropdown = key;
|
||||
}
|
||||
|
||||
loadDropdown(dropdownName = '') {
|
||||
loadDropdown(dropdownName = '', dropdownKey = '') {
|
||||
let firstLoad = false;
|
||||
|
||||
if (!this.droplab) {
|
||||
|
@ -155,7 +155,7 @@ export default class FilteredSearchDropdownManager {
|
|||
}
|
||||
|
||||
if (dropdownName === DROPDOWN_TYPE.operator) {
|
||||
this.load(dropdownName, firstLoad);
|
||||
this.load(dropdownName, firstLoad, dropdownKey);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,7 +167,7 @@ export default class FilteredSearchDropdownManager {
|
|||
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
|
||||
const key = match && match.key ? match.key : DROPDOWN_TYPE.hint;
|
||||
|
||||
this.load(key, firstLoad);
|
||||
this.load(key, firstLoad, dropdownKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,11 +200,11 @@ export default class FilteredSearchDropdownManager {
|
|||
dropdownToOpen = hasOperator && lastOperatorToken ? dropdownName : DROPDOWN_TYPE.operator;
|
||||
}
|
||||
|
||||
this.loadDropdown(dropdownToOpen);
|
||||
this.loadDropdown(dropdownToOpen, dropdownName);
|
||||
} else if (lastToken) {
|
||||
const lastOperator = FilteredSearchVisualTokens.getLastTokenOperator();
|
||||
// Token has been initialized into an object because it has a value
|
||||
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator);
|
||||
this.loadDropdown(lastOperator ? lastToken.key : DROPDOWN_TYPE.operator, lastToken.key);
|
||||
} else {
|
||||
this.loadDropdown(DROPDOWN_TYPE.hint);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
<script>
|
||||
/* eslint-disable vue/no-v-html */
|
||||
import { escape } from 'lodash';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
|
||||
import { s__, sprintf } from '~/locale';
|
||||
import { deprecatedCreateFlash as Flash } from '~/flash';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlModal: DeprecatedModal2,
|
||||
GlModal,
|
||||
GlButton,
|
||||
},
|
||||
directives: {
|
||||
GlModalDirective,
|
||||
SafeHtml,
|
||||
},
|
||||
props: {
|
||||
actionUrl: {
|
||||
type: String,
|
||||
|
@ -54,6 +56,21 @@ Please update your Git repository remotes as soon as possible.`),
|
|||
false,
|
||||
);
|
||||
},
|
||||
primaryProps() {
|
||||
return {
|
||||
text: s__('Update username'),
|
||||
attributes: [
|
||||
{ variant: 'warning' },
|
||||
{ category: 'primary' },
|
||||
{ disabled: this.isRequestPending },
|
||||
],
|
||||
};
|
||||
},
|
||||
cancelProps() {
|
||||
return {
|
||||
text: s__('Cancel'),
|
||||
};
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onConfirm() {
|
||||
|
@ -103,22 +120,21 @@ Please update your Git repository remotes as soon as possible.`),
|
|||
<p class="form-text text-muted">{{ path }}</p>
|
||||
</div>
|
||||
<gl-button
|
||||
:data-target="`#${$options.modalId}`"
|
||||
v-gl-modal-directive="$options.modalId"
|
||||
:disabled="isRequestPending || newUsername === username"
|
||||
category="primary"
|
||||
variant="warning"
|
||||
data-toggle="modal"
|
||||
data-testid="username-change-confirmation-modal"
|
||||
>{{ $options.buttonText }}</gl-button
|
||||
>
|
||||
{{ $options.buttonText }}
|
||||
</gl-button>
|
||||
<gl-modal
|
||||
:id="$options.modalId"
|
||||
:header-title-text="s__('Profiles|Change username') + '?'"
|
||||
:footer-primary-button-text="$options.buttonText"
|
||||
footer-primary-button-variant="warning"
|
||||
@submit="onConfirm"
|
||||
:modal-id="$options.modalId"
|
||||
:title="s__('Profiles|Change username') + '?'"
|
||||
:action-primary="primaryProps"
|
||||
:action-cancel="cancelProps"
|
||||
@primary="onConfirm"
|
||||
>
|
||||
<span v-html="modalText"></span>
|
||||
<span v-safe-html="modalText"></span>
|
||||
</gl-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Resolvers
|
||||
module Ci
|
||||
class RunnerSetupResolver < BaseResolver
|
||||
type Types::Ci::RunnerSetupType, null: true
|
||||
|
||||
argument :platform, GraphQL::STRING_TYPE,
|
||||
required: true,
|
||||
description: 'Platform to generate the instructions for'
|
||||
|
||||
argument :architecture, GraphQL::STRING_TYPE,
|
||||
required: true,
|
||||
description: 'Architecture to generate the instructions for'
|
||||
|
||||
argument :project_id, ::Types::GlobalIDType[::Project],
|
||||
required: false,
|
||||
description: 'Project to register the runner for'
|
||||
|
||||
argument :group_id, ::Types::GlobalIDType[::Group],
|
||||
required: false,
|
||||
description: 'Group to register the runner for'
|
||||
|
||||
def resolve(platform:, architecture:, **args)
|
||||
instructions = Gitlab::Ci::RunnerInstructions.new(
|
||||
{ current_user: current_user, os: platform, arch: architecture }.merge(target_param(args))
|
||||
)
|
||||
|
||||
{
|
||||
install_instructions: instructions.install_script,
|
||||
register_instructions: instructions.register_command
|
||||
}
|
||||
ensure
|
||||
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'User is not authorized to register a runner for the specified resource!' if instructions.errors.include?('Gitlab::Access::AccessDeniedError')
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def target_param(args)
|
||||
project_param(args[:project_id]) || group_param(args[:group_id]) || {}
|
||||
end
|
||||
|
||||
def project_param(project_id)
|
||||
return unless project_id
|
||||
|
||||
{ project: find_object(project_id) }
|
||||
end
|
||||
|
||||
def group_param(group_id)
|
||||
return unless group_id
|
||||
|
||||
{ group: find_object(group_id) }
|
||||
end
|
||||
|
||||
def find_object(gid)
|
||||
GlobalID::Locator.locate(gid)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Types
|
||||
module Ci
|
||||
# rubocop: disable Graphql/AuthorizeTypes
|
||||
class RunnerSetupType < BaseObject
|
||||
graphql_name 'RunnerSetup'
|
||||
|
||||
field :install_instructions, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Instructions for installing the runner on the specified architecture'
|
||||
field :register_instructions, GraphQL::STRING_TYPE, null: false,
|
||||
description: 'Instructions for registering the runner'
|
||||
end
|
||||
end
|
||||
end
|
|
@ -84,6 +84,10 @@ module Types
|
|||
null: true, description: 'Supported runner platforms',
|
||||
resolver: Resolvers::Ci::RunnerPlatformsResolver
|
||||
|
||||
field :runner_setup, Types::Ci::RunnerSetupType, null: true,
|
||||
description: 'Get runner setup instructions',
|
||||
resolver: Resolvers::Ci::RunnerSetupResolver
|
||||
|
||||
def design_management
|
||||
DesignManagementObject.new(nil)
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Update change username modal
|
||||
merge_request: 44325
|
||||
author:
|
||||
type: changed
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Fix Rails/SaveBang offenses for spec/services/* and spec/sidekiq/*
|
||||
merge_request: 45391
|
||||
author: matthewbried
|
||||
type: other
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: Removed not equal filter option for drafts on merge requests
|
||||
merge_request: 45649
|
||||
author:
|
||||
type: fixed
|
|
@ -1695,7 +1695,7 @@ input BoardIssueInput {
|
|||
"""
|
||||
Filter by epic ID. Incompatible with epicWildcardId
|
||||
"""
|
||||
epicId: ID
|
||||
epicId: EpicID
|
||||
|
||||
"""
|
||||
Filter by epic ID wildcard. Incompatible with epicId
|
||||
|
@ -8571,6 +8571,11 @@ type Group {
|
|||
webUrl: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Identifier of Group
|
||||
"""
|
||||
scalar GroupID
|
||||
|
||||
"""
|
||||
Represents a Group Membership
|
||||
"""
|
||||
|
@ -12568,7 +12573,7 @@ input NegatedBoardIssueInput {
|
|||
"""
|
||||
Filter by epic ID. Incompatible with epicWildcardId
|
||||
"""
|
||||
epicId: ID
|
||||
epicId: EpicID
|
||||
|
||||
"""
|
||||
Filter by label name
|
||||
|
@ -15629,6 +15634,31 @@ type Query {
|
|||
last: Int
|
||||
): RunnerPlatformConnection
|
||||
|
||||
"""
|
||||
Get runner setup instructions
|
||||
"""
|
||||
runnerSetup(
|
||||
"""
|
||||
Architecture to generate the instructions for
|
||||
"""
|
||||
architecture: String!
|
||||
|
||||
"""
|
||||
Group to register the runner for
|
||||
"""
|
||||
groupId: GroupID
|
||||
|
||||
"""
|
||||
Platform to generate the instructions for
|
||||
"""
|
||||
platform: String!
|
||||
|
||||
"""
|
||||
Project to register the runner for
|
||||
"""
|
||||
projectId: ProjectID
|
||||
): RunnerSetup
|
||||
|
||||
"""
|
||||
Find Snippets visible to the current user
|
||||
"""
|
||||
|
@ -16922,6 +16952,18 @@ type RunnerPlatformEdge {
|
|||
node: RunnerPlatform
|
||||
}
|
||||
|
||||
type RunnerSetup {
|
||||
"""
|
||||
Instructions for installing the runner on the specified architecture
|
||||
"""
|
||||
installInstructions: String!
|
||||
|
||||
"""
|
||||
Instructions for registering the runner
|
||||
"""
|
||||
registerInstructions: String!
|
||||
}
|
||||
|
||||
"""
|
||||
Represents a CI configuration of SAST
|
||||
"""
|
||||
|
|
|
@ -4518,7 +4518,7 @@
|
|||
"description": "Filter by epic ID. Incompatible with epicWildcardId",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"name": "EpicID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
|
@ -23240,6 +23240,16 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "GroupID",
|
||||
"description": "Identifier of Group",
|
||||
"fields": null,
|
||||
"inputFields": null,
|
||||
"interfaces": null,
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "GroupMember",
|
||||
|
@ -36941,7 +36951,7 @@
|
|||
"description": "Filter by epic ID. Incompatible with epicWildcardId",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ID",
|
||||
"name": "EpicID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
|
@ -45234,6 +45244,67 @@
|
|||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "runnerSetup",
|
||||
"description": "Get runner setup instructions",
|
||||
"args": [
|
||||
{
|
||||
"name": "platform",
|
||||
"description": "Platform to generate the instructions for",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "architecture",
|
||||
"description": "Architecture to generate the instructions for",
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "projectId",
|
||||
"description": "Project to register the runner for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "ProjectID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
},
|
||||
{
|
||||
"name": "groupId",
|
||||
"description": "Group to register the runner for",
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "GroupID",
|
||||
"ofType": null
|
||||
},
|
||||
"defaultValue": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "OBJECT",
|
||||
"name": "RunnerSetup",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "snippets",
|
||||
"description": "Find Snippets visible to the current user",
|
||||
|
@ -48854,6 +48925,55 @@
|
|||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "RunnerSetup",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "installInstructions",
|
||||
"description": "Instructions for installing the runner on the specified architecture",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "registerInstructions",
|
||||
"description": "Instructions for registering the runner",
|
||||
"args": [
|
||||
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [
|
||||
|
||||
],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SastCiConfiguration",
|
||||
|
|
|
@ -2276,6 +2276,13 @@ Autogenerated return type of RunDASTScan.
|
|||
| `humanReadableName` | String! | Human readable name of the runner platform |
|
||||
| `name` | String! | Name slug of the runner platform |
|
||||
|
||||
### RunnerSetup
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `installInstructions` | String! | Instructions for installing the runner on the specified architecture |
|
||||
| `registerInstructions` | String! | Instructions for registering the runner |
|
||||
|
||||
### SastCiConfigurationAnalyzersEntity
|
||||
|
||||
Represents an analyzer entity in SAST CI configuration.
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
---
|
||||
comments: false
|
||||
description: 'Image Resizing'
|
||||
---
|
||||
|
||||
# Image resizing for avatars and content images
|
||||
|
||||
Currently, we are showing all uploaded images 1:1, which is of course not ideal. To improve performance greatly we will add image resizing to the backend. There are two main areas of image resizing to consider; avatars and content images. The MVC for this implementation will focus on Avatars. Avatars requests consist of approximately 70% of total image requests. There is an identified set of sizes we intend to support which makes the scope of this first MVC very narrow. Content image resizing has many more considerations for size and features. It is entirely possible that we have two separate development efforts with the same goal of increasing performance via image resizing.
|
||||
|
||||
## MVC Avatar Resizing
|
||||
|
||||
We will implement a dynamic image resizing solution. This means image should be resized and optimized on the fly so that if we define new targeted sizes later we can add them dynamically. This would mean a huge improvement in performance as some of the measurements suggest that we can save up to 95% of our current load size. Our initial investigations indicate that we have uploaded approximately 1.65 million avatars totaling approximately 80GB in size and averaging approximately 48kb each. Early measurements indicate we can reduce the most common avatar dimensions to between 1-3kb in size, netting us a greater than 90% size reduction. For the MVC we will not consider application level caching and rely purely on HTTP based caches as implemented in CDNs and browsers, but might revisit this decision later on. To easily mitigate performance issues with avatar resizing, especially in the case of self managed, an operations feature flag will be implemented to disable dynamic image resizing.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
Requester->>Workhorse: Incoming request
|
||||
Workhorse->>RailsApp: Incoming request
|
||||
alt All is true: 1.Avatar is requested, 2.Requested Width is allowed, 3.Feature is enabled
|
||||
Note right of RailsApp: Width Allowlist: https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/models/concerns/avatarable.rb#L10
|
||||
RailsApp->>Workhorse: `send-scaled-img:` request
|
||||
Note right of RailsApp: Set `send-scaled-img:` Header
|
||||
Workhorse->>Workhorse: Image resizing using Go lib
|
||||
Workhorse->>Requester: Serve the resized image
|
||||
else All other cases
|
||||
RailsApp->>Workhorse: Usual request scenario
|
||||
Workhorse->>Requester: Usual request scenario
|
||||
end
|
||||
```
|
||||
|
||||
## Content Image Resizing
|
||||
|
||||
Content image resizing is a more complex problem to tackle. There are no set size restrictions and there are additional features or requirements to consider.
|
||||
|
||||
- Dynamic WebP support - the WebP format typically achieves an average of 30% more compression than JPEG without the loss of image quality. More details [here](https://developers.google.com/speed/webp/docs/c_study)
|
||||
- Extract first image of GIF's so we can prevent from loading 10MB pixels
|
||||
- Check Device Pixel Ratio to deliver nice images on High DPI screens
|
||||
- Progressive image loading, similar to what is described [here](https://www.sitepoint.com/how-to-build-your-own-progressive-image-loader/)
|
||||
- Resizing recommendations (size, clarity, etc.)
|
||||
- Storage
|
||||
|
||||
The MVC Avatar resizing implementation is integrated into Workhorse. With the extra requirements for content image resizing, this may require further use of GraphicsMagik (GM) or a similar library and breaking it out of Workhorse.
|
||||
|
||||
## Iterations
|
||||
|
||||
1. [x] POC on different image resizing solutions
|
||||
1. [x] Review solutions with security team
|
||||
1. [x] Implement avatar resizing MVC
|
||||
1. [ ] Deploy, measure, monitor
|
||||
1. [ ] Clarify features for content image resizing
|
||||
1. [ ] Weigh options between using current implementation of image resizing vs new solution
|
||||
1. [ ] Implement content image resizing MVC
|
||||
1. [ ] Deploy, measure, monitor
|
||||
|
||||
## Who
|
||||
|
||||
Proposal:
|
||||
|
||||
| Role | Who
|
||||
|------------------------------|-------------------------|
|
||||
| Author | Craig Gomes |
|
||||
| Architecture Evolution Coach | Kamil Trzciński |
|
||||
| Engineering Leader | Tim Zallmann |
|
||||
| Domain Expert | Matthias Kaeppler |
|
||||
| Domain Expert | Aleksei Lipniagov |
|
||||
|
||||
DRIs:
|
||||
|
||||
| Role | Who
|
||||
|------------------------------|------------------------|
|
||||
| Product | Josh Lambert |
|
||||
| Leadership | Craig Gomes |
|
||||
| Engineering | Matthias Kaeppler |
|
|
@ -219,10 +219,17 @@ You can also specify a static part of the URL at `environment:url:`, such as
|
|||
|
||||
The assigned URL for the `review/your-branch-name` environment is [visible in the UI](#using-the-environment-url).
|
||||
|
||||
> **Notes:**
|
||||
>
|
||||
> - `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the `DYNAMIC_ENVIRONMENT_URL` variable. Therefore you should not set `environment:url:` in the `stop_review` job.
|
||||
> - If the environment URL is not valid (for example, the URL is malformed), the system doesn't update the environment URL.
|
||||
Note the following:
|
||||
|
||||
- `stop_review` doesn't generate a dotenv report artifact, so it won't recognize the
|
||||
`DYNAMIC_ENVIRONMENT_URL` variable. Therefore you shouldn't set `environment:url:` in the
|
||||
`stop_review` job.
|
||||
- If the environment URL isn't valid (for example, the URL is malformed), the system doesn't update
|
||||
the environment URL.
|
||||
- If the script that runs in `stop_review` exists only in your repository and therefore can't use
|
||||
`GIT_STRATEGY: none`, configure [pipelines for merge requests](../../ci/merge_request_pipelines/index.md)
|
||||
for these jobs. This ensures that runners can fetch the repository even after a feature branch is
|
||||
deleted. For more information, see [Ref Specs for Runners](../pipelines/index.md#ref-specs-for-runners).
|
||||
|
||||
### Configuring manual deployments
|
||||
|
||||
|
@ -675,24 +682,23 @@ deploy_review:
|
|||
name: review/$CI_COMMIT_REF_NAME
|
||||
url: https://$CI_ENVIRONMENT_SLUG.example.com
|
||||
on_stop: stop_review
|
||||
only:
|
||||
- branches
|
||||
except:
|
||||
- master
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
|
||||
stop_review:
|
||||
stage: deploy
|
||||
variables:
|
||||
GIT_STRATEGY: none
|
||||
script:
|
||||
- echo "Remove review app"
|
||||
when: manual
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
action: stop
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
when: manual
|
||||
```
|
||||
|
||||
Setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the
|
||||
If you can't use [Pipelines for merge requests](../merge_request_pipelines/index.md),
|
||||
setting the [`GIT_STRATEGY`](../yaml/README.md#git-strategy) to `none` is necessary in the
|
||||
`stop_review` job so that the [runner](https://docs.gitlab.com/runner/) won't
|
||||
try to check out the code after the branch is deleted.
|
||||
|
||||
|
@ -748,13 +754,17 @@ review_app:
|
|||
name: review/$CI_COMMIT_REF_NAME
|
||||
on_stop: stop_review_app
|
||||
auto_stop_in: 1 week
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
|
||||
stop_review_app:
|
||||
script: stop-review-app
|
||||
environment:
|
||||
name: review/$CI_COMMIT_REF_NAME
|
||||
action: stop
|
||||
when: manual
|
||||
rules:
|
||||
- if: $CI_MERGE_REQUEST_ID
|
||||
when: manual
|
||||
```
|
||||
|
||||
As long as a merge request is active and keeps getting new commits,
|
||||
|
|
|
@ -79,6 +79,27 @@ You can also configure specific aspects of your pipelines through the GitLab UI.
|
|||
- [Pipeline schedules](schedules.md).
|
||||
- [Custom CI/CD variables](../variables/README.md#custom-environment-variables).
|
||||
|
||||
### Ref Specs for Runners
|
||||
|
||||
When a runner picks a pipeline job, GitLab provides that job's metadata. This includes the [Git refspecs](https://git-scm.com/book/en/v2/Git-Internals-The-Refspec),
|
||||
which indicate which ref (branch, tag, and so on) and commit (SHA1) are checked out from your
|
||||
project repository.
|
||||
|
||||
This table lists the refspecs injected for each pipeline type:
|
||||
|
||||
| Pipeline type | Refspecs |
|
||||
|--------------- |---------------------------------------- |
|
||||
| Pipeline for Branches | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/heads/<name>:refs/remotes/origin/<name>` |
|
||||
| pipeline for Tags | `+refs/pipelines/<id>:refs/pipelines/<id>` and `+refs/tags/<name>:refs/tags/<name>` |
|
||||
| [Pipeline for Merge Requests](../merge_request_pipelines/index.md) | `+refs/pipelines/<id>:refs/pipelines/<id>` |
|
||||
|
||||
The refs `refs/heads/<name>` and `refs/tags/<name>` exist in your
|
||||
project repository. GitLab generates the special ref `refs/pipelines/<id>` during a
|
||||
running pipeline job. This ref can be created even after the associated branch or tag has been
|
||||
deleted. It's therefore useful in some features such as [automatically stopping an environment](../environments/index.md#automatically-stopping-an-environment),
|
||||
and [merge trains](../merge_request_pipelines/pipelines_for_merged_results/merge_trains/index.md)
|
||||
that might run pipelines after branch deletion.
|
||||
|
||||
### View pipelines
|
||||
|
||||
You can find the current and historical pipeline runs under your project's
|
||||
|
|
|
@ -489,15 +489,15 @@ DAST can be [configured](#customizing-the-dast-settings) using environment varia
|
|||
| `DAST_API_HOST_OVERRIDE` | string | Used to override domains defined in API specification files. Only supported when importing the API specification from a URL. Example: `example.com:8080` |
|
||||
| `DAST_EXCLUDE_RULES` | string | Set to a comma-separated list of Vulnerability Rule IDs to exclude them from running during the scan. Rule IDs are numbers and can be found from the DAST log or on the [ZAP project](https://github.com/zaproxy/zaproxy/blob/develop/docs/scanners.md). For example, `HTTP Parameter Override` has a rule ID of `10026`. **Note:** In earlier versions of GitLab the excluded rules were executed but alerts they generated were suppressed. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/118641) in GitLab 12.10. |
|
||||
| `DAST_REQUEST_HEADERS` | string | Set to a comma-separated list of request header names and values. Headers are added to every request made by DAST. For example, `Cache-control: no-cache,User-Agent: DAST/1.0` |
|
||||
| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false` |
|
||||
| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. |
|
||||
| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. |
|
||||
| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. |
|
||||
| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. |
|
||||
| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false` |
|
||||
| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false` |
|
||||
| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html` |
|
||||
| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. |
|
||||
| `DAST_DEBUG` | boolean | Enable debug message output. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_SPIDER_MINS` | number | The maximum duration of the spider scan in minutes. Set to `0` for unlimited. Default: One minute, or unlimited when the scan is a full scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_HTML_REPORT` | string | The filename of the HTML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_MARKDOWN_REPORT` | string | The filename of the Markdown report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1.|
|
||||
| `DAST_XML_REPORT` | string | The filename of the XML report written at the end of a scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_INCLUDE_ALPHA_VULNERABILITIES` | boolean | Set to `true` to include alpha passive and active scan rules. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_USE_AJAX_SPIDER` | boolean | Set to `true` to use the AJAX spider in addition to the traditional spider, useful for crawling sites that require JavaScript. Default: `false`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_PATHS` | string | Set to a comma-separated list of URLs for DAST to scan. For example, `/page1.html,/category1/page3.html,/page2.html`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214120) in GitLab 13.4. |
|
||||
| `DAST_ZAP_CLI_OPTIONS` | string | ZAP server command-line options. For example, `-Xmx3072m` would set the Java maximum memory allocation pool size. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/12652) in GitLab 13.1. |
|
||||
| `DAST_ZAP_LOG_CONFIGURATION` | string | Set to a semicolon-separated list of additional log4j properties for the ZAP Server. For example, `log4j.logger.org.parosproxy.paros.network.HttpSender=DEBUG;log4j.logger.com.crawljax=DEBUG` |
|
||||
|
||||
### DAST command-line options
|
||||
|
|
|
@ -33,7 +33,7 @@ SAST supports the following official analyzers:
|
|||
- [`sobelow`](https://gitlab.com/gitlab-org/security-products/analyzers/sobelow) (Sobelow (Elixir Phoenix))
|
||||
- [`spotbugs`](https://gitlab.com/gitlab-org/security-products/analyzers/spotbugs) (SpotBugs with the Find Sec Bugs plugin (Ant, Gradle and wrapper, Grails, Maven and wrapper, SBT))
|
||||
|
||||
The analyzers are published as Docker images that SAST will use to launch
|
||||
The analyzers are published as Docker images that SAST uses to launch
|
||||
dedicated containers for each analysis.
|
||||
|
||||
SAST is pre-configured with a set of **default images** that are maintained by
|
||||
|
@ -77,12 +77,12 @@ variables:
|
|||
SAST_DEFAULT_ANALYZERS: "bandit,flawfinder"
|
||||
```
|
||||
|
||||
`bandit` runs first. When merging the reports, SAST will
|
||||
remove the duplicates and will keep the `bandit` entries.
|
||||
`bandit` runs first. When merging the reports, SAST
|
||||
removes the duplicates and keeps the `bandit` entries.
|
||||
|
||||
### Disabling default analyzers
|
||||
|
||||
Setting `SAST_DEFAULT_ANALYZERS` to an empty string will disable all the official
|
||||
Setting `SAST_DEFAULT_ANALYZERS` to an empty string disables all the official
|
||||
default analyzers. In `.gitlab-ci.yml` define:
|
||||
|
||||
```yaml
|
||||
|
|
|
@ -189,7 +189,7 @@ requires [GraphQL](../../../api/graphql/index.md) to be enabled.
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/36427) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.10.
|
||||
> - Health status of closed issues [can't be edited](https://gitlab.com/gitlab-org/gitlab/-/issues/220867) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.4 and later.
|
||||
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.5.
|
||||
> - Issue health status visible in issue lists [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/45141) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.6.
|
||||
|
||||
To help you track the status of your issues, you can assign a status to each issue to flag work
|
||||
that's progressing as planned or needs attention to keep on schedule:
|
||||
|
|
|
@ -28386,6 +28386,9 @@ msgstr ""
|
|||
msgid "Update now"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update username"
|
||||
msgstr ""
|
||||
|
||||
msgid "Update variable"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'Merge Requests > User filters by draft', :js do
|
||||
include FilteredSearchHelpers
|
||||
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
let(:user) { project.creator }
|
||||
|
||||
before do
|
||||
create(:merge_request, title: 'Draft: Bugfix', source_project: project, target_project: project, source_branch: 'bugfix2')
|
||||
|
||||
sign_in(user)
|
||||
visit project_merge_requests_path(project)
|
||||
end
|
||||
|
||||
it 'filters results' do
|
||||
input_filtered_search_keys('draft:=yes')
|
||||
|
||||
expect(page).to have_content('Draft: Bugfix')
|
||||
end
|
||||
|
||||
it 'does not allow filtering by is not equal' do
|
||||
find('#filtered-search-merge_requests').click
|
||||
|
||||
click_button 'Draft'
|
||||
|
||||
expect(page).not_to have_content('!=')
|
||||
end
|
||||
end
|
|
@ -101,10 +101,10 @@ RSpec.describe 'Profile account page', :js do
|
|||
it 'changes my username' do
|
||||
fill_in 'username-change-input', with: 'new-username'
|
||||
|
||||
page.find('[data-target="#username-change-confirmation-modal"]').click
|
||||
page.find('[data-testid="username-change-confirmation-modal"]').click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.js-modal-primary-action').click
|
||||
find('.js-modal-action-primary').click
|
||||
end
|
||||
|
||||
expect(page).to have_content('new-username')
|
||||
|
|
|
@ -128,10 +128,10 @@ def update_username(new_username)
|
|||
|
||||
fill_in 'username-change-input', with: new_username
|
||||
|
||||
page.find('[data-target="#username-change-confirmation-modal"]').click
|
||||
page.find('[data-testid="username-change-confirmation-modal"]').click
|
||||
|
||||
page.within('.modal') do
|
||||
find('.js-modal-primary-action').click
|
||||
find('.js-modal-action-primary').click
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
|
|
@ -1,173 +1,135 @@
|
|||
import Vue from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import mountComponent from 'helpers/vue_mount_component_helper';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
|
||||
import updateUsername from '~/profile/account/components/update_username.vue';
|
||||
import UpdateUsername from '~/profile/account/components/update_username.vue';
|
||||
|
||||
describe('UpdateUsername component', () => {
|
||||
const rootUrl = TEST_HOST;
|
||||
const actionUrl = `${TEST_HOST}/update/username`;
|
||||
const username = 'hasnoname';
|
||||
const newUsername = 'new_username';
|
||||
let Component;
|
||||
let vm;
|
||||
const defaultProps = {
|
||||
actionUrl,
|
||||
rootUrl,
|
||||
initialUsername: 'hasnoname',
|
||||
};
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
|
||||
const createComponent = (props = {}) => {
|
||||
wrapper = shallowMount(UpdateUsername, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlModal,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = new MockAdapter(axios);
|
||||
Component = Vue.extend(updateUsername);
|
||||
vm = mountComponent(Component, {
|
||||
actionUrl,
|
||||
rootUrl,
|
||||
initialUsername: username,
|
||||
});
|
||||
createComponent();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
wrapper.destroy();
|
||||
axiosMock.restore();
|
||||
});
|
||||
|
||||
const findElements = () => {
|
||||
const modalSelector = `#${vm.$options.modalId}`;
|
||||
const modal = wrapper.find(GlModal);
|
||||
|
||||
return {
|
||||
input: vm.$el.querySelector(`#${vm.$options.inputId}`),
|
||||
openModalBtn: vm.$el.querySelector(`[data-target="${modalSelector}"]`),
|
||||
modal: vm.$el.querySelector(modalSelector),
|
||||
modalBody: vm.$el.querySelector(`${modalSelector} .modal-body`),
|
||||
modalHeader: vm.$el.querySelector(`${modalSelector} .modal-title`),
|
||||
confirmModalBtn: vm.$el.querySelector(`${modalSelector} .btn-warning`),
|
||||
modal,
|
||||
input: wrapper.find(`#${wrapper.vm.$options.inputId}`),
|
||||
openModalBtn: wrapper.find('[data-testid="username-change-confirmation-modal"]'),
|
||||
modalBody: modal.find('.modal-body'),
|
||||
modalHeader: modal.find('.modal-title'),
|
||||
confirmModalBtn: wrapper.find('.btn-warning'),
|
||||
};
|
||||
};
|
||||
|
||||
it('has a disabled button if the username was not changed', done => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
input.dispatchEvent(new Event('input'));
|
||||
it('has a disabled button if the username was not changed', async () => {
|
||||
const { openModalBtn } = findElements();
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.username).toBe(username);
|
||||
expect(vm.newUsername).toBe(username);
|
||||
expect(openModalBtn).toBeDisabled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(openModalBtn.props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('has an enabled button which if the username was changed', done => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
input.value = newUsername;
|
||||
input.dispatchEvent(new Event('input'));
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.username).toBe(username);
|
||||
expect(vm.newUsername).toBe(newUsername);
|
||||
expect(openModalBtn).not.toBeDisabled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('confirmation modal contains proper header and body', done => {
|
||||
const { modalBody, modalHeader } = findElements();
|
||||
|
||||
vm.newUsername = newUsername;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(modalHeader.textContent).toContain('Change username?');
|
||||
expect(modalBody.textContent).toContain(
|
||||
`You are going to change the username ${username} to ${newUsername}`,
|
||||
);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('confirmation modal should escape usernames properly', done => {
|
||||
const { modalBody } = findElements();
|
||||
|
||||
vm.username = '<i>Italic</i>';
|
||||
vm.newUsername = vm.username;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(modalBody.innerHTML).toContain('<i>Italic</i>');
|
||||
expect(modalBody.innerHTML).not.toContain(vm.username);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('executes API call on confirmation button click', done => {
|
||||
const { confirmModalBtn } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
|
||||
jest.spyOn(axios, 'put');
|
||||
|
||||
vm.newUsername = newUsername;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
confirmModalBtn.click();
|
||||
|
||||
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('sets the username after a successful update', done => {
|
||||
it('has an enabled button which if the username was changed', async () => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => {
|
||||
expect(input).toBeDisabled();
|
||||
expect(openModalBtn).toBeDisabled();
|
||||
input.element.value = 'newUsername';
|
||||
input.trigger('input');
|
||||
|
||||
return [200, { message: 'Username changed' }];
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(openModalBtn.props('disabled')).toBe(false);
|
||||
});
|
||||
|
||||
describe('changing username', () => {
|
||||
const newUsername = 'new_username';
|
||||
|
||||
beforeEach(async () => {
|
||||
createComponent();
|
||||
wrapper.setData({ newUsername });
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
});
|
||||
|
||||
vm.newUsername = newUsername;
|
||||
it('confirmation modal contains proper header and body', async () => {
|
||||
const { modal } = findElements();
|
||||
|
||||
vm.onConfirm()
|
||||
.then(() => {
|
||||
expect(vm.username).toBe(newUsername);
|
||||
expect(vm.newUsername).toBe(newUsername);
|
||||
expect(input).not.toBeDisabled();
|
||||
expect(input.value).toBe(newUsername);
|
||||
expect(openModalBtn).toBeDisabled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('does not set the username after a erroneous update', done => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => {
|
||||
expect(input).toBeDisabled();
|
||||
expect(openModalBtn).toBeDisabled();
|
||||
|
||||
return [400, { message: 'Invalid username' }];
|
||||
expect(modal.attributes('title')).toBe('Change username?');
|
||||
expect(modal.text()).toContain(
|
||||
`You are going to change the username ${defaultProps.initialUsername} to ${newUsername}`,
|
||||
);
|
||||
});
|
||||
|
||||
const invalidUsername = 'anything.git';
|
||||
vm.newUsername = invalidUsername;
|
||||
it('executes API call on confirmation button click', async () => {
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
|
||||
jest.spyOn(axios, 'put');
|
||||
|
||||
vm.onConfirm()
|
||||
.then(() => done.fail('Expected onConfirm to throw!'))
|
||||
.catch(() => {
|
||||
expect(vm.username).toBe(username);
|
||||
expect(vm.newUsername).toBe(invalidUsername);
|
||||
expect(input).not.toBeDisabled();
|
||||
expect(input.value).toBe(invalidUsername);
|
||||
expect(openModalBtn).not.toBeDisabled();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
await wrapper.vm.onConfirm();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(axios.put).toHaveBeenCalledWith(actionUrl, { user: { username: newUsername } });
|
||||
});
|
||||
|
||||
it('sets the username after a successful update', async () => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => {
|
||||
expect(input.attributes('disabled')).toBe('disabled');
|
||||
expect(openModalBtn.props('disabled')).toBe(true);
|
||||
|
||||
return [200, { message: 'Username changed' }];
|
||||
});
|
||||
|
||||
await wrapper.vm.onConfirm();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(input.attributes('disabled')).toBe(undefined);
|
||||
expect(openModalBtn.props('disabled')).toBe(true);
|
||||
});
|
||||
|
||||
it('does not set the username after a erroneous update', async () => {
|
||||
const { input, openModalBtn } = findElements();
|
||||
|
||||
axiosMock.onPut(actionUrl).replyOnce(() => {
|
||||
expect(input.attributes('disabled')).toBe('disabled');
|
||||
expect(openModalBtn.props('disabled')).toBe(true);
|
||||
|
||||
return [400, { message: 'Invalid username' }];
|
||||
});
|
||||
|
||||
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
|
||||
expect(input.attributes('disabled')).toBe(undefined);
|
||||
expect(openModalBtn.props('disabled')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Resolvers::Ci::RunnerSetupResolver do
|
||||
include GraphqlHelpers
|
||||
|
||||
describe '#resolve' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
subject(:resolve_subject) { resolve(described_class, ctx: { current_user: user }, args: { platform: 'linux', architecture: 'amd64' }.merge(target_param)) }
|
||||
|
||||
context 'without target parameter' do
|
||||
let(:target_param) { {} }
|
||||
|
||||
context 'when user is not admin' do
|
||||
it 'returns access error' do
|
||||
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is admin' do
|
||||
before do
|
||||
user.update!(admin: true)
|
||||
end
|
||||
|
||||
it 'returns install and register instructions' do
|
||||
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
|
||||
expect(resolve_subject.values).not_to include(nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with project target parameter' do
|
||||
let(:project) { create(:project) }
|
||||
let(:target_param) { { project_id: project.to_global_id } }
|
||||
|
||||
context 'when user has access to admin builds on project' do
|
||||
before do
|
||||
project.add_maintainer(user)
|
||||
end
|
||||
|
||||
it 'returns install and register instructions' do
|
||||
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
|
||||
expect(resolve_subject.values).not_to include(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to admin builds on project' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns access error' do
|
||||
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'with group target parameter' do
|
||||
let(:group) { create(:group) }
|
||||
let(:target_param) { { group_id: group.to_global_id } }
|
||||
|
||||
context 'when user has access to admin builds on group' do
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
it 'returns install and register instructions' do
|
||||
expect(resolve_subject.keys).to contain_exactly(:install_instructions, :register_instructions)
|
||||
expect(resolve_subject.values).not_to include(nil)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not have access to admin builds on group' do
|
||||
before do
|
||||
group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns access error' do
|
||||
expect { resolve_subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Types::Ci::RunnerSetupType do
|
||||
specify { expect(described_class.graphql_name).to eq('RunnerSetup') }
|
||||
|
||||
it 'exposes the expected fields' do
|
||||
expected_fields = %i[
|
||||
install_instructions
|
||||
register_instructions
|
||||
]
|
||||
|
||||
expect(described_class).to have_graphql_fields(*expected_fields)
|
||||
end
|
||||
end
|
|
@ -80,4 +80,12 @@ RSpec.describe GitlabSchema.types['Query'] do
|
|||
is_expected.to have_graphql_type(Types::Ci::RunnerPlatformType.connection_type)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'runner_setup field' do
|
||||
subject { described_class.fields['runnerSetup'] }
|
||||
|
||||
it 'returns runner setup instructions' do
|
||||
is_expected.to have_graphql_type(Types::Ci::RunnerSetupType)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -642,7 +642,7 @@ RSpec.describe Auth::ContainerRegistryAuthenticationService do
|
|||
let_it_be(:project) { create(:project, :public, container_registry_enabled: false) }
|
||||
|
||||
before do
|
||||
project.update(container_registry_enabled: false)
|
||||
project.update!(container_registry_enabled: false)
|
||||
end
|
||||
|
||||
context 'disallow when pulling' do
|
||||
|
|
|
@ -131,7 +131,7 @@ RSpec.describe AutoMerge::BaseService do
|
|||
end
|
||||
|
||||
describe '#update' do
|
||||
subject { service.update(merge_request) }
|
||||
subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang
|
||||
|
||||
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
|
||||
|
||||
|
|
|
@ -148,7 +148,7 @@ RSpec.describe AutoMergeService do
|
|||
end
|
||||
|
||||
describe '#update' do
|
||||
subject { service.update(merge_request) }
|
||||
subject { service.update(merge_request) } # rubocop:disable Rails/SaveBang
|
||||
|
||||
context 'when auto merge is enabled' do
|
||||
let(:merge_request) { create(:merge_request, :merge_when_pipeline_succeeds) }
|
||||
|
|
|
@ -197,7 +197,7 @@ RSpec.describe Clusters::UpdateService do
|
|||
|
||||
context 'manangement_project is outside of the namespace scope' do
|
||||
before do
|
||||
management_project.update(group: create(:group))
|
||||
management_project.update!(group: create(:group))
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
|
@ -224,7 +224,7 @@ RSpec.describe Clusters::UpdateService do
|
|||
|
||||
context 'manangement_project is outside of the namespace scope' do
|
||||
before do
|
||||
management_project.update(group: create(:group))
|
||||
management_project.update!(group: create(:group))
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
|
|
|
@ -44,7 +44,7 @@ RSpec.describe DesignManagement::GenerateImageVersionsService do
|
|||
end
|
||||
|
||||
it 'logs if the raw image cannot be found' do
|
||||
version.designs.first.update(filename: 'foo.png')
|
||||
version.designs.first.update!(filename: 'foo.png')
|
||||
|
||||
expect(Gitlab::AppLogger).to receive(:error).with("No design file found for Action: #{action.id}")
|
||||
|
||||
|
|
|
@ -40,11 +40,11 @@ RSpec.describe Discussions::ResolveService do
|
|||
|
||||
context 'with a project that requires all discussion to be resolved' do
|
||||
before do
|
||||
project.update(only_allow_merge_if_all_discussions_are_resolved: true)
|
||||
project.update!(only_allow_merge_if_all_discussions_are_resolved: true)
|
||||
end
|
||||
|
||||
after do
|
||||
project.update(only_allow_merge_if_all_discussions_are_resolved: false)
|
||||
project.update!(only_allow_merge_if_all_discussions_are_resolved: false)
|
||||
end
|
||||
|
||||
let_it_be(:other_discussion) { create(:diff_note_on_merge_request, noteable: merge_request, project: project).to_discussion }
|
||||
|
|
|
@ -22,7 +22,7 @@ RSpec.describe DraftNotes::DestroyService do
|
|||
it 'destroys all draft notes for a user in a merge request' do
|
||||
create_list(:draft_note, 2, merge_request: merge_request, author: user)
|
||||
|
||||
expect { destroy }.to change { DraftNote.count }.by(-2)
|
||||
expect { destroy }.to change { DraftNote.count }.by(-2) # rubocop:disable Rails/SaveBang
|
||||
expect(DraftNote.count).to eq(0)
|
||||
end
|
||||
|
||||
|
@ -45,7 +45,7 @@ RSpec.describe DraftNotes::DestroyService do
|
|||
allow_any_instance_of(DraftNote).to receive_message_chain(:diff_file, :unfolded?) { true }
|
||||
expect(merge_request).to receive_message_chain(:diffs, :clear_cache)
|
||||
|
||||
destroy
|
||||
destroy # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,7 +9,7 @@ RSpec.describe Emails::ConfirmService do
|
|||
|
||||
describe '#execute' do
|
||||
it 'enqueues a background job to send confirmation email again' do
|
||||
email = user.emails.create(email: 'new@email.com')
|
||||
email = user.emails.create!(email: 'new@email.com')
|
||||
|
||||
expect { service.execute(email) }.to have_enqueued_job.on_queue('mailers')
|
||||
end
|
||||
|
|
|
@ -95,7 +95,7 @@ RSpec.describe Groups::DestroyService do
|
|||
context 'projects in pending_delete' do
|
||||
before do
|
||||
project.pending_delete = true
|
||||
project.save
|
||||
project.save!
|
||||
end
|
||||
|
||||
it_behaves_like 'group destruction', false
|
||||
|
|
|
@ -63,7 +63,7 @@ RSpec.describe Groups::ImportExport::ImportService do
|
|||
before do
|
||||
stub_feature_flags(group_import_ndjson: false)
|
||||
|
||||
ImportExportUpload.create(group: group, import_file: import_file)
|
||||
ImportExportUpload.create!(group: group, import_file: import_file)
|
||||
|
||||
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
|
||||
allow(import_logger).to receive(:error)
|
||||
|
@ -105,7 +105,7 @@ RSpec.describe Groups::ImportExport::ImportService do
|
|||
subject { service.execute }
|
||||
|
||||
before do
|
||||
ImportExportUpload.create(group: group, import_file: import_file)
|
||||
ImportExportUpload.create!(group: group, import_file: import_file)
|
||||
|
||||
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
|
||||
allow(import_logger).to receive(:error)
|
||||
|
@ -216,7 +216,7 @@ RSpec.describe Groups::ImportExport::ImportService do
|
|||
subject { service.execute }
|
||||
|
||||
before do
|
||||
ImportExportUpload.create(group: group, import_file: import_file)
|
||||
ImportExportUpload.create!(group: group, import_file: import_file)
|
||||
|
||||
allow(Gitlab::Import::Logger).to receive(:build).and_return(import_logger)
|
||||
allow(import_logger).to receive(:error)
|
||||
|
|
|
@ -89,10 +89,10 @@ RSpec.describe Labels::PromoteService do
|
|||
|
||||
it 'keeps users\' subscriptions' do
|
||||
user2 = create(:user)
|
||||
project_label_1_1.subscriptions.create(user: user, subscribed: true)
|
||||
project_label_2_1.subscriptions.create(user: user, subscribed: true)
|
||||
project_label_3_2.subscriptions.create(user: user, subscribed: true)
|
||||
project_label_2_1.subscriptions.create(user: user2, subscribed: true)
|
||||
project_label_1_1.subscriptions.create!(user: user, subscribed: true)
|
||||
project_label_2_1.subscriptions.create!(user: user, subscribed: true)
|
||||
project_label_3_2.subscriptions.create!(user: user, subscribed: true)
|
||||
project_label_2_1.subscriptions.create!(user: user2, subscribed: true)
|
||||
|
||||
expect { service.execute(project_label_1_1) }.to change { Subscription.count }.from(4).to(3)
|
||||
|
||||
|
|
|
@ -286,7 +286,7 @@ RSpec.describe Notes::CreateService do
|
|||
QuickAction.new(
|
||||
action_text: "/wip",
|
||||
before_action: -> {
|
||||
issuable.reload.update(title: "title")
|
||||
issuable.reload.update!(title: "title")
|
||||
},
|
||||
expectation: ->(issuable, can_use_quick_action) {
|
||||
expect(issuable.work_in_progress?).to eq(can_use_quick_action)
|
||||
|
@ -296,7 +296,7 @@ RSpec.describe Notes::CreateService do
|
|||
QuickAction.new(
|
||||
action_text: "/wip",
|
||||
before_action: -> {
|
||||
issuable.reload.update(title: "WIP: title")
|
||||
issuable.reload.update!(title: "WIP: title")
|
||||
},
|
||||
expectation: ->(noteable, can_use_quick_action) {
|
||||
expect(noteable.work_in_progress?).not_to eq(can_use_quick_action)
|
||||
|
|
|
@ -44,7 +44,7 @@ RSpec.describe NotificationRecipients::BuildService do
|
|||
context 'when there are multiple subscribers' do
|
||||
def create_user
|
||||
subscriber = create(:user)
|
||||
issue.subscriptions.create(user: subscriber, project: project, subscribed: true)
|
||||
issue.subscriptions.create!(user: subscriber, project: project, subscribed: true)
|
||||
end
|
||||
|
||||
include_examples 'no N+1 queries'
|
||||
|
@ -96,7 +96,7 @@ RSpec.describe NotificationRecipients::BuildService do
|
|||
context 'when there are multiple subscribers' do
|
||||
def create_user
|
||||
subscriber = create(:user)
|
||||
merge_request.subscriptions.create(user: subscriber, project: project, subscribed: true)
|
||||
merge_request.subscriptions.create!(user: subscriber, project: project, subscribed: true)
|
||||
end
|
||||
|
||||
include_examples 'no N+1 queries'
|
||||
|
|
|
@ -361,7 +361,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
|
||||
context 'where the project has disabled the feature' do
|
||||
before do
|
||||
project.update(service_desk_enabled: false)
|
||||
project.update!(service_desk_enabled: false)
|
||||
end
|
||||
|
||||
it_should_not_email!
|
||||
|
@ -453,7 +453,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
context 'by note' do
|
||||
before do
|
||||
note.author = @u_lazy_participant
|
||||
note.save
|
||||
note.save!
|
||||
end
|
||||
|
||||
it { expect { subject }.not_to have_enqueued_email(@u_lazy_participant.id, note.id, mail: "note_issue_email") }
|
||||
|
@ -467,7 +467,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
note.project.namespace_id = group.id
|
||||
group.add_user(@u_watcher, GroupMember::MAINTAINER)
|
||||
group.add_user(@u_custom_global, GroupMember::MAINTAINER)
|
||||
note.project.save
|
||||
note.project.save!
|
||||
|
||||
@u_watcher.notification_settings_for(note.project).participating!
|
||||
@u_watcher.notification_settings_for(group).global!
|
||||
|
@ -1719,7 +1719,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
|
||||
before do
|
||||
merge_request.author = participant
|
||||
merge_request.save
|
||||
merge_request.save!
|
||||
notification.new_merge_request(merge_request, @u_disabled)
|
||||
end
|
||||
|
||||
|
@ -1962,7 +1962,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
|
||||
describe 'when merge_when_pipeline_succeeds is true' do
|
||||
before do
|
||||
merge_request.update(
|
||||
merge_request.update!(
|
||||
merge_when_pipeline_succeeds: true,
|
||||
merge_user: create(:user)
|
||||
)
|
||||
|
@ -2725,7 +2725,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
before do
|
||||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
project.update!(group: group)
|
||||
|
||||
create(:email, :confirmed, user: u_custom_notification_enabled, email: group_notification_email)
|
||||
create(:notification_setting, user: u_custom_notification_enabled, source: group, notification_email: group_notification_email)
|
||||
|
@ -2761,7 +2761,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
before do
|
||||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
project.update!(group: group)
|
||||
create(:email, :confirmed, user: u_member, email: group_notification_email)
|
||||
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
|
||||
end
|
||||
|
@ -2821,7 +2821,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
context 'when the creator has no read_build access' do
|
||||
before do
|
||||
pipeline = create_pipeline(u_member, :failed)
|
||||
project.update(public_builds: false)
|
||||
project.update!(public_builds: false)
|
||||
project.team.truncate
|
||||
notification.pipeline_finished(pipeline)
|
||||
end
|
||||
|
@ -2855,7 +2855,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
before do
|
||||
group = create(:group)
|
||||
|
||||
project.update(group: group)
|
||||
project.update!(group: group)
|
||||
create(:email, :confirmed, user: u_member, email: group_notification_email)
|
||||
create(:notification_setting, user: u_member, source: group, notification_email: group_notification_email)
|
||||
end
|
||||
|
@ -3212,7 +3212,7 @@ RSpec.describe NotificationService, :mailer do
|
|||
# with different notification settings
|
||||
def build_group(project, visibility: :public)
|
||||
group = create_nested_group(visibility)
|
||||
project.update(namespace_id: group.id)
|
||||
project.update!(namespace_id: group.id)
|
||||
|
||||
# Group member: global=disabled, group=watch
|
||||
@g_watcher ||= create_user_with_notification(:watch, 'group_watcher', project.group)
|
||||
|
@ -3277,11 +3277,11 @@ RSpec.describe NotificationService, :mailer do
|
|||
end
|
||||
|
||||
def add_user_subscriptions(issuable)
|
||||
issuable.subscriptions.create(user: @unsubscribed_mentioned, project: project, subscribed: false)
|
||||
issuable.subscriptions.create(user: @subscriber, project: project, subscribed: true)
|
||||
issuable.subscriptions.create(user: @subscribed_participant, project: project, subscribed: true)
|
||||
issuable.subscriptions.create(user: @unsubscriber, project: project, subscribed: false)
|
||||
issuable.subscriptions.create!(user: @unsubscribed_mentioned, project: project, subscribed: false)
|
||||
issuable.subscriptions.create!(user: @subscriber, project: project, subscribed: true)
|
||||
issuable.subscriptions.create!(user: @subscribed_participant, project: project, subscribed: true)
|
||||
issuable.subscriptions.create!(user: @unsubscriber, project: project, subscribed: false)
|
||||
# Make the watcher a subscriber to detect dupes
|
||||
issuable.subscriptions.create(user: @watcher_and_subscriber, project: project, subscribed: true)
|
||||
issuable.subscriptions.create!(user: @watcher_and_subscriber, project: project, subscribed: true)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -100,7 +100,7 @@ RSpec.describe Packages::Conan::CreatePackageFileService do
|
|||
end
|
||||
|
||||
let(:tmp_object) do
|
||||
fog_connection.directories.new(key: 'packages').files.create(
|
||||
fog_connection.directories.new(key: 'packages').files.create( # rubocop:disable Rails/SaveBang
|
||||
key: "tmp/uploads/#{file_name}",
|
||||
body: 'content'
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ RSpec.describe ResetProjectCacheService do
|
|||
|
||||
context 'when project cache_index is a numeric value' do
|
||||
before do
|
||||
project.update(jobs_cache_index: 1)
|
||||
project.update!(jobs_cache_index: 1)
|
||||
end
|
||||
|
||||
it 'increments project cache index' do
|
||||
|
|
|
@ -11,7 +11,7 @@ RSpec.describe ResourceEvents::ChangeMilestoneService do
|
|||
|
||||
[:issue, :merge_request].each do |issuable|
|
||||
it_behaves_like 'timebox(milestone or iteration) resource events creator', ResourceMilestoneEvent do
|
||||
let_it_be(:resource) { create(issuable) }
|
||||
let_it_be(:resource) { create(issuable) } # rubocop:disable Rails/SaveBang
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -85,7 +85,7 @@ RSpec.describe SystemHooksService do
|
|||
end
|
||||
|
||||
it 'handles nil datetime columns' do
|
||||
user.update(created_at: nil, updated_at: nil)
|
||||
user.update!(created_at: nil, updated_at: nil)
|
||||
data = event_data(user, :destroy)
|
||||
|
||||
expect(data[:created_at]).to be(nil)
|
||||
|
|
|
@ -378,13 +378,13 @@ RSpec.describe SystemNoteService do
|
|||
noteable_types.each do |type|
|
||||
context "when noteable is a #{type}" do
|
||||
it "blocks cross reference when #{type.underscore}_events is false" do
|
||||
jira_tracker.update("#{type}_events" => false)
|
||||
jira_tracker.update!("#{type}_events" => false)
|
||||
|
||||
expect(cross_reference(type)).to eq(s_('JiraService|Events for %{noteable_model_name} are disabled.') % { noteable_model_name: type.pluralize.humanize.downcase })
|
||||
end
|
||||
|
||||
it "creates cross reference when #{type.underscore}_events is true" do
|
||||
jira_tracker.update("#{type}_events" => true)
|
||||
jira_tracker.update!("#{type}_events" => true)
|
||||
|
||||
expect(cross_reference(type)).to eq(success_message)
|
||||
end
|
||||
|
|
|
@ -373,7 +373,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
|
|||
before do
|
||||
# Mention issue (noteable) from commit0
|
||||
system_note = service.cross_reference(commit0)
|
||||
system_note.update(note: system_note.note.capitalize)
|
||||
system_note.update!(note: system_note.note.capitalize)
|
||||
end
|
||||
|
||||
it 'is truthy when already mentioned' do
|
||||
|
@ -407,7 +407,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
|
|||
before do
|
||||
# Mention commit1 from commit0
|
||||
system_note = service.cross_reference(commit1)
|
||||
system_note.update(note: system_note.note.capitalize)
|
||||
system_note.update!(note: system_note.note.capitalize)
|
||||
end
|
||||
|
||||
it 'is truthy when already mentioned' do
|
||||
|
@ -436,7 +436,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
|
|||
context 'legacy capitalized cross reference' do
|
||||
before do
|
||||
system_note = service.cross_reference(commit0)
|
||||
system_note.update(note: system_note.note.capitalize)
|
||||
system_note.update!(note: system_note.note.capitalize)
|
||||
end
|
||||
|
||||
it 'is true when a fork mentions an external issue' do
|
||||
|
@ -582,7 +582,7 @@ RSpec.describe ::SystemNotes::IssuablesService do
|
|||
|
||||
it 'creates the note text correctly' do
|
||||
[:issue, :merge_request].each do |type|
|
||||
issuable = create(type)
|
||||
issuable = create(type) # rubocop:disable Rails/SaveBang
|
||||
|
||||
service = described_class.new(noteable: issuable, author: author)
|
||||
expect(service.discussion_lock.note)
|
||||
|
|
|
@ -145,7 +145,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates correct todos for each valid user based on the type of mention' do
|
||||
issue.update(description: directly_addressed_and_mentioned)
|
||||
issue.update!(description: directly_addressed_and_mentioned)
|
||||
|
||||
service.new_issue(issue, author)
|
||||
|
||||
|
@ -222,7 +222,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
|
||||
issue.update(description: directly_addressed_and_mentioned)
|
||||
issue.update!(description: directly_addressed_and_mentioned)
|
||||
|
||||
service.update_issue(issue, author, skip_users)
|
||||
|
||||
|
@ -291,7 +291,7 @@ RSpec.describe TodoService do
|
|||
|
||||
context 'issues with a task list' do
|
||||
it 'does not create todo when tasks are marked as completed' do
|
||||
issue.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
|
||||
issue.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
|
||||
|
||||
service.update_issue(issue, author)
|
||||
|
||||
|
@ -304,7 +304,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'does not create directly addressed todo when tasks are marked as completed' do
|
||||
addressed_issue.update(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
|
||||
addressed_issue.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [x] Task 2\n")
|
||||
|
||||
service.update_issue(addressed_issue, author)
|
||||
|
||||
|
@ -317,7 +317,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'does not raise an error when description not change' do
|
||||
issue.update(title: 'Sample')
|
||||
issue.update!(title: 'Sample')
|
||||
|
||||
expect { service.update_issue(issue, author) }.not_to raise_error
|
||||
end
|
||||
|
@ -427,7 +427,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a todo for each valid user based on the type of mention' do
|
||||
note.update(note: directly_addressed_and_mentioned)
|
||||
note.update!(note: directly_addressed_and_mentioned)
|
||||
|
||||
service.new_note(note, john_doe)
|
||||
|
||||
|
@ -694,7 +694,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a todo for each valid user based on the type of mention' do
|
||||
mr_assigned.update(description: directly_addressed_and_mentioned)
|
||||
mr_assigned.update!(description: directly_addressed_and_mentioned)
|
||||
|
||||
service.new_merge_request(mr_assigned, author)
|
||||
|
||||
|
@ -726,7 +726,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
|
||||
mr_assigned.update(description: directly_addressed_and_mentioned)
|
||||
mr_assigned.update!(description: directly_addressed_and_mentioned)
|
||||
|
||||
service.update_merge_request(mr_assigned, author, skip_users)
|
||||
|
||||
|
@ -772,7 +772,7 @@ RSpec.describe TodoService do
|
|||
|
||||
context 'with a task list' do
|
||||
it 'does not create todo when tasks are marked as completed' do
|
||||
mr_assigned.update(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
|
||||
mr_assigned.update!(description: "- [x] Task 1\n- [X] Task 2 #{mentions}")
|
||||
|
||||
service.update_merge_request(mr_assigned, author)
|
||||
|
||||
|
@ -786,7 +786,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'does not create directly addressed todo when tasks are marked as completed' do
|
||||
addressed_mr_assigned.update(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
|
||||
addressed_mr_assigned.update!(description: "#{directly_addressed}\n- [x] Task 1\n- [X] Task 2")
|
||||
|
||||
service.update_merge_request(addressed_mr_assigned, author)
|
||||
|
||||
|
@ -800,7 +800,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'does not raise an error when description not change' do
|
||||
mr_assigned.update(title: 'Sample')
|
||||
mr_assigned.update!(title: 'Sample')
|
||||
|
||||
expect { service.update_merge_request(mr_assigned, author) }.not_to raise_error
|
||||
end
|
||||
|
@ -883,7 +883,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a pending todo for each merge_participant' do
|
||||
mr_unassigned.update(merge_when_pipeline_succeeds: true, merge_user: admin)
|
||||
mr_unassigned.update!(merge_when_pipeline_succeeds: true, merge_user: admin)
|
||||
service.merge_request_became_unmergeable(mr_unassigned)
|
||||
|
||||
merge_participants.each do |participant|
|
||||
|
@ -991,7 +991,7 @@ RSpec.describe TodoService do
|
|||
end
|
||||
|
||||
it 'creates a todo for each valid user not included in skip_users based on the type of mention' do
|
||||
note.update(note: directly_addressed_and_mentioned)
|
||||
note.update!(note: directly_addressed_and_mentioned)
|
||||
|
||||
service.update_note(note, author, skip_users)
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ RSpec.describe Todos::Destroy::ConfidentialIssueService do
|
|||
|
||||
context 'when provided issue is not confidential' do
|
||||
it 'does not remove any todos' do
|
||||
issue_1.update(confidential: false)
|
||||
issue_1.update!(confidential: false)
|
||||
|
||||
expect { subject }.not_to change { Todo.count }
|
||||
end
|
||||
|
|
|
@ -108,7 +108,7 @@ RSpec.describe Users::DestroyService do
|
|||
context 'projects in pending_delete' do
|
||||
before do
|
||||
project.pending_delete = true
|
||||
project.save
|
||||
project.save!
|
||||
end
|
||||
|
||||
it 'destroys a project in pending_delete' do
|
||||
|
@ -310,7 +310,7 @@ RSpec.describe Users::DestroyService do
|
|||
|
||||
it 'of group_members' do
|
||||
group_member = create(:group_member)
|
||||
group_member.group.group_members.create(user: user, access_level: 40)
|
||||
group_member.group.group_members.create!(user: user, access_level: 40)
|
||||
|
||||
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:find).once
|
||||
expect_any_instance_of(GroupMember).to receive(:run_callbacks).with(:initialize).once
|
||||
|
|
|
@ -10,7 +10,7 @@ RSpec.describe Users::RepairLdapBlockedService do
|
|||
|
||||
describe '#execute' do
|
||||
it 'changes to normal block after destroying last ldap identity' do
|
||||
identity.destroy
|
||||
identity.destroy!
|
||||
service.execute
|
||||
|
||||
expect(user.reload).not_to be_ldap_blocked
|
||||
|
|
|
@ -189,7 +189,7 @@ RSpec.describe VerifyPagesDomainService do
|
|||
let(:domain) { build(:pages_domain, :expired, :with_missing_chain) }
|
||||
|
||||
before do
|
||||
domain.save(validate: false)
|
||||
domain.save!(validate: false)
|
||||
end
|
||||
|
||||
it 'can be disabled' do
|
||||
|
|
|
@ -7,7 +7,7 @@ RSpec.describe Sidekiq::Cron::Job do
|
|||
context 'when Fugit depends on ZoTime or EoTime' do
|
||||
before do
|
||||
described_class
|
||||
.create(name: 'TestCronWorker',
|
||||
.create(name: 'TestCronWorker', # rubocop:disable Rails/SaveBang
|
||||
cron: Settings.cron_jobs[:pipeline_schedule_worker]['cron'],
|
||||
class: Settings.cron_jobs[:pipeline_schedule_worker]['job_class'])
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue