Commit 51cc01b6 authored by Dylan Griffith's avatar Dylan Griffith

Merge branch 'master' into siemens-runner-per-group

parents 9447e5c2 2e00c1a7
...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective: ...@@ -143,7 +143,7 @@ Lint/MissingCopEnableDirective:
Lint/NestedPercentLiteral: Lint/NestedPercentLiteral:
Exclude: Exclude:
- 'lib/gitlab/git/repository.rb' - 'lib/gitlab/git/repository.rb'
- 'spec/support/email_format_shared_examples.rb' - 'spec/support/shared_examples/email_format_shared_examples.rb'
# Offense count: 1 # Offense count: 1
Lint/ReturnInVoidContext: Lint/ReturnInVoidContext:
...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase: ...@@ -195,8 +195,8 @@ Naming/HeredocDelimiterCase:
- 'spec/lib/gitlab/diff/parser_spec.rb' - 'spec/lib/gitlab/diff/parser_spec.rb'
- 'spec/lib/json_web_token/rsa_token_spec.rb' - 'spec/lib/json_web_token/rsa_token_spec.rb'
- 'spec/models/commit_spec.rb' - 'spec/models/commit_spec.rb'
- 'spec/support/repo_helpers.rb' - 'spec/support/helpers/repo_helpers.rb'
- 'spec/support/seed_repo.rb' - 'spec/support/helpers/seed_repo.rb'
# Offense count: 112 # Offense count: 112
# Configuration parameters: Blacklist. # Configuration parameters: Blacklist.
...@@ -496,7 +496,7 @@ Style/EmptyLiteral: ...@@ -496,7 +496,7 @@ Style/EmptyLiteral:
- 'spec/lib/gitlab/request_context_spec.rb' - 'spec/lib/gitlab/request_context_spec.rb'
- 'spec/lib/gitlab/workhorse_spec.rb' - 'spec/lib/gitlab/workhorse_spec.rb'
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/support/chat_slash_commands_shared_examples.rb' - 'spec/support/shared_examples/chat_slash_commands_shared_examples.rb'
# Offense count: 102 # Offense count: 102
# Cop supports --auto-correct. # Cop supports --auto-correct.
......
...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._ ...@@ -26,7 +26,9 @@ _This notice should stay as the first item in the CONTRIBUTING.md file._
- [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc) - [Type labels (~"feature proposal", ~bug, ~customer, etc.)](#type-labels-feature-proposal-bug-customer-etc)
- [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc) - [Subject labels (~wiki, ~"container registry", ~ldap, ~api, etc.)](#subject-labels-wiki-container-registry-ldap-api-etc)
- [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc) - [Team labels (~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.)](#team-labels-cicd-discussion-edge-platform-etc)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#priority-labels-deliverable-stretch-next-patch-release) - [Milestone labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#milestone-labels-deliverable-stretch-next-patch-release)
- [Priority labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-priority-labels-p1-p2-p3-etc)
- [Severity labels (~Deliverable, ~Stretch, ~"Next Patch Release")](#bug-severity-labels-s1-s2-s3-etc)
- [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests) - [Label for community contributors (~"Accepting Merge Requests")](#label-for-community-contributors-accepting-merge-requests)
- [Implement design & UI elements](#implement-design-ui-elements) - [Implement design & UI elements](#implement-design-ui-elements)
- [Issue tracker](#issue-tracker) - [Issue tracker](#issue-tracker)
...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following: ...@@ -127,6 +129,8 @@ Most issues will have labels for at least one of the following:
- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc. - Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
- Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc. - Team: ~"CI/CD", ~Discussion, ~Edge, ~Platform, etc.
- Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release" - Milestone: ~Deliverable, ~Stretch, ~"Next Patch Release"
- Priority: ~P1, ~P2, ~P3, ~P4
- Severity: ~S1, ~S2, ~S3, ~S4
All labels, their meaning and priority are defined on the All labels, their meaning and priority are defined on the
[labels page][labels-page]. [labels page][labels-page].
...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga ...@@ -210,7 +214,7 @@ This label documents the planned timeline & urgency which is used to measure aga
| Label | Meaning | Estimate time to fix | Guidance | | Label | Meaning | Estimate time to fix | Guidance |
|-------|-----------------|------------------------------------------------------------------|----------| |-------|-----------------|------------------------------------------------------------------|----------|
| ~P1 | Urgent Priority | The current release | | | ~P1 | Urgent Priority | The current release + potentially immediate hotfix to GitLab.com | |
| ~P2 | High Priority | The next release | | | ~P2 | High Priority | The next release | |
| ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | | | ~P3 | Medium Priority | Within the next 3 releases (approx one quarter) | |
| ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented | | ~P4 | Low Priority | Anything outside the next 3 releases (approx beyond one quarter) | The issue is prominent but does not impact user workflow and a workaround is documented |
......
...@@ -32,26 +32,38 @@ export default { ...@@ -32,26 +32,38 @@ export default {
required: true, required: true,
}, },
buttonDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
data() {
return {
isDisabled: false,
linkRequested: '',
};
},
computed: { computed: {
cssClass() { cssClass() {
const actionIconDash = dasherize(this.actionIcon); const actionIconDash = dasherize(this.actionIcon);
return `${actionIconDash} js-icon-${actionIconDash}`; return `${actionIconDash} js-icon-${actionIconDash}`;
}, },
isDisabled() { },
return this.buttonDisabled === this.link; watch: {
requestFinishedFor() {
if (this.requestFinishedFor === this.linkRequested) {
this.isDisabled = false;
}
}, },
}, },
methods: { methods: {
onClickAction() { onClickAction() {
$(this.$el).tooltip('hide'); $(this.$el).tooltip('hide');
eventHub.$emit('graphAction', this.link); eventHub.$emit('graphAction', this.link);
this.linkRequested = this.link;
this.isDisabled = true;
}, },
}, },
}; };
...@@ -62,7 +74,8 @@ export default { ...@@ -62,7 +74,8 @@ export default {
@click="onClickAction" @click="onClickAction"
v-tooltip v-tooltip
:title="tooltipText" :title="tooltipText"
class="btn btn-blank btn-transparent ci-action-icon-container ci-action-icon-wrapper" class="js-ci-action btn btn-blank
btn-transparent ci-action-icon-container ci-action-icon-wrapper"
:class="cssClass" :class="cssClass"
data-container="body" data-container="body"
:disabled="isDisabled" :disabled="isDisabled"
......
<script>
import icon from '../../../vue_shared/components/icon.vue';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
components: {
icon,
},
directives: {
tooltip,
},
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
};
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-container="body"
aria-label="Job's action"
>
<icon :name="actionIcon" />
</a>
</template>
<script> <script>
import $ from 'jquery'; import $ from 'jquery';
import jobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue'; import JobComponent from './job_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
/** /**
* Renders the dropdown for the pipeline graph. * Renders the dropdown for the pipeline graph.
* *
* The following object should be provided as `job`: * The following object should be provided as `job`:
...@@ -27,14 +27,14 @@ ...@@ -27,14 +27,14 @@
* } * }
* } * }
*/ */
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
jobComponent, JobComponent,
jobNameComponent, JobNameComponent,
}, },
props: { props: {
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
type: Object, type: Object,
required: true, required: true,
}, },
requestFinishedFor: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
...@@ -56,22 +61,23 @@ ...@@ -56,22 +61,23 @@
methods: { methods: {
/** /**
* When the user right clicks or cmd/ctrl + click in the job name * When the user right clicks or cmd/ctrl + click in the job name or the action icon
* the dropdown should not be closed and the link should open in another tab, * the dropdown should not be closed so we stop propagation
* so we stop propagation of the click event inside the dropdown. * of the click event inside the dropdown.
* *
* Since this component is rendered multiple times per page we need to guarantee we only * Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component. * target the click event of this component.
*/ */
stopDropdownClickPropagation() { stopDropdownClickPropagation() {
$(this.$el $(
.querySelectorAll('.js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item')) '.js-grouped-pipeline-dropdown button, .js-grouped-pipeline-dropdown a.mini-pipeline-graph-dropdown-item',
.on('click', (e) => { this.$el,
).on('click', e => {
e.stopPropagation(); e.stopPropagation();
}); });
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container"> <div class="ci-job-dropdown-container">
...@@ -101,8 +107,8 @@ ...@@ -101,8 +107,8 @@
:key="i"> :key="i">
<job-component <job-component
:job="item" :job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item" css-class-job-name="mini-pipeline-graph-dropdown-item"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
</ul> </ul>
......
...@@ -7,7 +7,6 @@ export default { ...@@ -7,7 +7,6 @@ export default {
StageColumnComponent, StageColumnComponent,
LoadingIcon, LoadingIcon,
}, },
props: { props: {
isLoading: { isLoading: {
type: Boolean, type: Boolean,
...@@ -17,10 +16,10 @@ export default { ...@@ -17,10 +16,10 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
actionDisabled: { requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -75,7 +74,7 @@ export default { ...@@ -75,7 +74,7 @@ export default {
:key="stage.name" :key="stage.name"
:stage-connector-class="stageConnectorClass(index, stage)" :stage-connector-class="stageConnectorClass(index, stage)"
:is-first-column="isFirstColumn(index)" :is-first-column="isFirstColumn(index)"
:action-disabled="actionDisabled" :request-finished-for="requestFinishedFor"
/> />
</ul> </ul>
</div> </div>
......
<script> <script>
import ActionComponent from './action_component.vue'; import ActionComponent from './action_component.vue';
import DropdownActionComponent from './dropdown_action_component.vue';
import JobNameComponent from './job_name_component.vue'; import JobNameComponent from './job_name_component.vue';
import tooltip from '../../../vue_shared/directives/tooltip'; import tooltip from '../../../vue_shared/directives/tooltip';
...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip'; ...@@ -32,10 +31,8 @@ import tooltip from '../../../vue_shared/directives/tooltip';
export default { export default {
components: { components: {
ActionComponent, ActionComponent,
DropdownActionComponent,
JobNameComponent, JobNameComponent,
}, },
directives: { directives: {
tooltip, tooltip,
}, },
...@@ -44,26 +41,17 @@ export default { ...@@ -44,26 +41,17 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
cssClassJobName: { cssClassJobName: {
type: String, type: String,
required: false, required: false,
default: '', default: '',
}, },
requestFinishedFor: {
isDropdown: {
type: Boolean,
required: false,
default: false,
},
actionDisabled: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
computed: { computed: {
status() { status() {
return this.job && this.job.status ? this.job.status : {}; return this.job && this.job.status ? this.job.status : {};
...@@ -134,19 +122,11 @@ export default { ...@@ -134,19 +122,11 @@ export default {
</div> </div>
<action-component <action-component
v-if="hasAction && !isDropdown" v-if="hasAction"
:tooltip-text="status.action.title"
:link="status.action.path"
:action-icon="status.action.icon"
:button-disabled="actionDisabled"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="status.action.title" :tooltip-text="status.action.title"
:link="status.action.path" :link="status.action.path"
:action-icon="status.action.icon" :action-icon="status.action.icon"
:action-method="status.action.method" :request-finished-for="requestFinishedFor"
/> />
</div> </div>
</template> </template>
...@@ -29,10 +29,11 @@ export default { ...@@ -29,10 +29,11 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
actionDisabled: {
requestFinishedFor: {
type: String, type: String,
required: false, required: false,
default: null, default: '',
}, },
}, },
...@@ -74,12 +75,12 @@ export default { ...@@ -74,12 +75,12 @@ export default {
v-if="job.size === 1" v-if="job.size === 1"
:job="job" :job="job"
css-class-job-name="build-content" css-class-job-name="build-content"
:action-disabled="actionDisabled"
/> />
<dropdown-job-component <dropdown-job-component
v-if="job.size > 1" v-if="job.size > 1"
:job="job" :job="job"
:request-finished-for="requestFinishedFor"
/> />
</li> </li>
......
...@@ -25,7 +25,7 @@ export default () => { ...@@ -25,7 +25,7 @@ export default () => {
data() { data() {
return { return {
mediator, mediator,
actionDisabled: null, requestFinishedFor: null,
}; };
}, },
created() { created() {
...@@ -36,15 +36,17 @@ export default () => { ...@@ -36,15 +36,17 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.actionDisabled = action; // Click was made, reset this variable
this.requestFinishedFor = null;
this.mediator.service.postAction(action) this.mediator.service
.postAction(action)
.then(() => { .then(() => {
this.mediator.refreshPipeline(); this.mediator.refreshPipeline();
this.actionDisabled = null; this.requestFinishedFor = action;
}) })
.catch(() => { .catch(() => {
this.actionDisabled = null; this.requestFinishedFor = action;
Flash(__('An error occurred while making the request.')); Flash(__('An error occurred while making the request.'));
}); });
}, },
...@@ -54,7 +56,7 @@ export default () => { ...@@ -54,7 +56,7 @@ export default () => {
props: { props: {
isLoading: this.mediator.state.isLoading, isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline, pipeline: this.mediator.store.state.pipeline,
actionDisabled: this.actionDisabled, requestFinishedFor: this.requestFinishedFor,
}, },
}); });
}, },
...@@ -79,7 +81,8 @@ export default () => { ...@@ -79,7 +81,8 @@ export default () => {
}, },
methods: { methods: {
postAction(action) { postAction(action) {
this.mediator.service.postAction(action.path) this.mediator.service
.postAction(action.path)
.then(() => this.mediator.refreshPipeline()) .then(() => this.mediator.refreshPipeline())
.catch(() => Flash(__('An error occurred while making the request.'))); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
......
<script> <script>
import ciIcon from './ci_icon.vue'; import CiIcon from './ci_icon.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
/** /**
* Renders CI Badge link with CI icon and status text based on * Renders CI Badge link with CI icon and status text based on
* API response shared between all places where it is used. * API response shared between all places where it is used.
* *
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - MR widget * - MR widget
*/ */
export default { export default {
components: { components: {
ciIcon, CiIcon,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
return className ? `ci-status ci-${className}` : 'ci-status'; return className ? `ci-status ci-${className}` : 'ci-status';
}, },
}, },
}; };
</script> </script>
<template> <template>
<a <a
......
<script> <script>
import icon from '../../vue_shared/components/icon.vue'; import Icon from '../../vue_shared/components/icon.vue';
/** /**
* Renders CI icon based on API response shared between all places where it is used. * Renders CI icon based on API response shared between all places where it is used.
* *
* Receives status object containing: * Receives status object containing:
...@@ -22,9 +22,9 @@ ...@@ -22,9 +22,9 @@
* - Jobs show view header * - Jobs show view header
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
export default { export default {
components: { components: {
icon, Icon,
}, },
props: { props: {
status: { status: {
...@@ -32,14 +32,13 @@ ...@@ -32,14 +32,13 @@
required: true, required: true,
}, },
}, },
computed: { computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span :class="cssClass"> <span :class="cssClass">
......
<script> <script>
/** /**
* Falls back to the code used in `copy_to_clipboard.js` * Falls back to the code used in `copy_to_clipboard.js`
*
* Renders a button with a clipboard icon that copies the content of `data-clipboard-text`
* when clicked.
*
* @example
* <clipboard-button
* title="Copy to clipbard"
* text="Content to be copied"
* css-class="btn-transparent"
* />
*/ */
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
export default { export default {
name: 'ClipboardButton', name: 'ClipboardButton',
directives: { directives: {
tooltip, tooltip,
...@@ -34,7 +44,7 @@ ...@@ -34,7 +44,7 @@
default: 'btn-default', default: 'btn-default',
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
import commitIconSvg from 'icons/_icon_commit.svg'; import UserAvatarLink from './user_avatar/user_avatar_link.vue';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import tooltip from '../directives/tooltip';
import tooltip from '../directives/tooltip'; import Icon from '../../vue_shared/components/icon.vue';
import icon from '../../vue_shared/components/icon.vue';
export default { export default {
directives: { directives: {
tooltip, tooltip,
}, },
components: { components: {
userAvatarLink, UserAvatarLink,
icon, Icon,
}, },
props: { props: {
/** /**
...@@ -94,10 +93,7 @@ ...@@ -94,10 +93,7 @@
* @returns {Boolean} * @returns {Boolean}
*/ */
hasAuthor() { hasAuthor() {
return this.author && return this.author && this.author.avatar_url && this.author.path && this.author.username;
this.author.avatar_url &&
this.author.path &&
this.author.username;
}, },
/** /**
* If information about the author is provided will return a string * If information about the author is provided will return a string
...@@ -106,14 +102,10 @@ ...@@ -106,14 +102,10 @@
* @returns {String} * @returns {String}
*/ */
userImageAltDescription() { userImageAltDescription() {
return this.author && return this.author && this.author.username ? `${this.author.username}'s avatar` : null;
this.author.username ? `${this.author.username}'s avatar` : null;
}, },
}, },
created() { };
this.commitIconSvg = commitIconSvg;
},
};
</script> </script>
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
...@@ -141,11 +133,10 @@ ...@@ -141,11 +133,10 @@
{{ commitRef.name }} {{ commitRef.name }}
</a> </a>
</template> </template>
<div <icon
v-html="commitIconSvg" name="commit"
class="commit-icon js-commit-icon" class="commit-icon js-commit-icon"
> />
</div>
<a <a
class="commit-sha" class="commit-sha"
......
<script> <script>
import { __ } from '~/locale'; import { __ } from '~/locale';
/** /**
* Port of detail_behavior expand button. * Port of detail_behavior expand button.
* *
* @example * @example
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
* </template> * </template>
* </expand-button> * </expand-button>
*/ */
export default { export default {
name: 'ExpandButton', name: 'ExpandButton',
data() { data() {
return { return {
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
this.isCollapsed = !this.isCollapsed; this.isCollapsed = !this.isCollapsed;
}, },
}, },
}; };
</script> </script>
<template> <template>
<span> <span>
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import CiIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue'; import LoadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import TimeagoTooltip from './time_ago_tooltip.vue';
import tooltip from '../directives/tooltip'; import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue'; import UserAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
* *
* Used in: * Used in:
* - job show page * - job show page
* - pipeline show page * - pipeline show page
*/ */
export default { export default {
components: { components: {
ciIconBadge, CiIconBadge,
loadingIcon, LoadingIcon,
timeagoTooltip, TimeagoTooltip,
userAvatarImage, UserAvatarImage,
}, },
directives: { directives: {
tooltip, tooltip,
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
this.$emit('actionClicked', action); this.$emit('actionClicked', action);
}, },
}, },
}; };
</script> </script>
<template> <template>
......
<script> <script>
/* This is a re-usable vue component for rendering a svg sprite
/* This is a re-usable vue component for rendering a svg sprite
icon icon
Sample configuration: Sample configuration:
...@@ -11,11 +10,11 @@ ...@@ -11,11 +10,11 @@
css-classes="top" css-classes="top"
/> />
*/ */
// only allow classes in images.scss e.g. s12 // only allow classes in images.scss e.g. s12
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72]; const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
props: { props: {
name: { name: {
type: String, type: String,
...@@ -70,7 +69,7 @@ ...@@ -70,7 +69,7 @@
return this.size ? `s${this.size}` : ''; return this.size ? `s${this.size}` : '';
}, },
}, },
}; };
</script> </script>
<template> <template>
...@@ -79,7 +78,8 @@ ...@@ -79,7 +78,8 @@
:width="width" :width="width"
:height="height" :height="height"
:x="x" :x="x"
:y="y"> :y="y"
>
<use v-bind="{ 'xlink:href':spriteHref }" /> <use v-bind="{ 'xlink:href':spriteHref }" />
</svg> </svg>
</template> </template>
<script> <script>
import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
...@@ -98,11 +99,18 @@ export default { ...@@ -98,11 +99,18 @@ export default {
this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, { this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
handleClick: this.handleClick, handleClick: this.handleClick,
}); });
$(this.$refs.dropdown).on('hidden.gl.dropdown', this.handleDropdownHidden);
}, },
methods: { methods: {
handleClick(label) { handleClick(label) {
this.$emit('onLabelClick', label); this.$emit('onLabelClick', label);
}, },
handleCollapsedValueClick() {
this.$emit('toggleCollapse');
},
handleDropdownHidden() {
this.$emit('onDropdownClose');
},
}, },
}; };
</script> </script>
...@@ -112,6 +120,7 @@ export default { ...@@ -112,6 +120,7 @@ export default {
<dropdown-value-collapsed <dropdown-value-collapsed
v-if="showCreate" v-if="showCreate"
:labels="context.labels" :labels="context.labels"
@onValueClick="handleCollapsedValueClick"
/> />
<dropdown-title <dropdown-title
:can-edit="canEdit" :can-edit="canEdit"
...@@ -133,7 +142,10 @@ export default { ...@@ -133,7 +142,10 @@ export default {
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :label="label"
/> />
<div class="dropdown"> <div
class="dropdown"
ref="dropdown"
>
<dropdown-button <dropdown-button
:ability-name="abilityName" :ability-name="abilityName"
:field-name="hiddenInputName" :field-name="hiddenInputName"
......
...@@ -26,6 +26,11 @@ export default { ...@@ -26,6 +26,11 @@ export default {
return labelsString; return labelsString;
}, },
}, },
methods: {
handleClick() {
this.$emit('onValueClick');
},
},
}; };
</script> </script>
...@@ -36,6 +41,7 @@ export default { ...@@ -36,6 +41,7 @@ export default {
data-placement="left" data-placement="left"
data-container="body" data-container="body"
:title="labelsList" :title="labelsList"
@click="handleClick"
> >
<i <i
aria-hidden="true" aria-hidden="true"
......
...@@ -468,6 +468,14 @@ ...@@ -468,6 +468,14 @@
margin-bottom: 10px; margin-bottom: 10px;
white-space: normal; white-space: normal;
.ci-job-dropdown-container {
// override dropdown.scss
.dropdown-menu li button {
padding: 0;
text-align: center;
}
}
// ensure .build-content has hover style when action-icon is hovered // ensure .build-content has hover style when action-icon is hovered
.ci-job-dropdown-container:hover .build-content { .ci-job-dropdown-container:hover .build-content {
@extend .build-content:hover; @extend .build-content:hover;
......
...@@ -32,6 +32,7 @@ class UsersFinder ...@@ -32,6 +32,7 @@ class UsersFinder
users = by_active(users) users = by_active(users)
users = by_external_identity(users) users = by_external_identity(users)
users = by_external(users) users = by_external(users)
users = by_2fa(users)
users = by_created_at(users) users = by_created_at(users)
users = by_custom_attributes(users) users = by_custom_attributes(users)
...@@ -76,4 +77,15 @@ class UsersFinder ...@@ -76,4 +77,15 @@ class UsersFinder
users.external users.external
end end
def by_2fa(users)
case params[:two_factor]
when 'enabled'
users.with_two_factor
when 'disabled'
users.without_two_factor
else
users
end
end
end end
...@@ -102,7 +102,7 @@ module Routable ...@@ -102,7 +102,7 @@ module Routable
# the route. Caching this per request ensures that even if we have multiple instances, # the route. Caching this per request ensures that even if we have multiple instances,
# we will not have to duplicate work, avoiding N+1 queries in some cases. # we will not have to duplicate work, avoiding N+1 queries in some cases.
def full_path def full_path
return uncached_full_path unless RequestStore.active? return uncached_full_path unless RequestStore.active? && persisted?
RequestStore[full_path_key] ||= uncached_full_path RequestStore[full_path_key] ||= uncached_full_path
end end
...@@ -124,6 +124,11 @@ module Routable ...@@ -124,6 +124,11 @@ module Routable
end end
end end
# Group would override this to check from association
def owned_by?(user)
owner == user
end
private private
def set_path_errors def set_path_errors
......
...@@ -130,6 +130,10 @@ class Group < Namespace ...@@ -130,6 +130,10 @@ class Group < Namespace
self[:lfs_enabled] self[:lfs_enabled]
end end
def owned_by?(user)
owners.include?(user)
end
def add_users(users, access_level, current_user: nil, expires_at: nil) def add_users(users, access_level, current_user: nil, expires_at: nil)
GroupMember.add_users( GroupMember.add_users(
self, self,
......
require "flowdock-git-hook" require "flowdock-git-hook"
# Flow dock depends on Grit to compute the number of commits between two given
# commits. To make this depend on Gitaly, a monkey patch is applied
module Flowdock
class Git
# pass down a Repository all the way down
def repo
@options[:repo]
end
def config
{}
end
def messages
Git::Builder.new(repo: repo,
ref: @ref,
before: @from,
after: @to,
commit_url: @commit_url,
branch_url: @branch_url,
diff_url: @diff_url,
repo_url: @repo_url,
repo_name: @repo_name,
permanent_refs: @permanent_refs,
tags: tags
).to_hashes
end
class Builder
def commits
@repo.commits_between(@before, @after).map do |commit|
{
url: @opts[:commit_url] ? @opts[:commit_url] % [commit.sha] : nil,
id: commit.sha,
message: commit.message,
author: {
name: commit.author_name,
email: commit.author_email
}
}
end
end
end
end
end
class FlowdockService < Service class FlowdockService < Service
prop_accessor :token prop_accessor :token
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
...@@ -34,7 +80,7 @@ class FlowdockService < Service ...@@ -34,7 +80,7 @@ class FlowdockService < Service
data[:before], data[:before],
data[:after], data[:after],
token: token, token: token,
repo: project.repository.path_to_repo, repo: project.repository,
repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}", repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s", commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/%s",
diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s" diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
......
...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated ...@@ -260,7 +260,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout if current_user && can?(current_user, :admin_pipeline, project) && repository.gitlab_ci_yml.blank? && !show_auto_devops_callout
OpenStruct.new(enabled: auto_devops_enabled?, OpenStruct.new(enabled: auto_devops_enabled?,
label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'), label: auto_devops_enabled? ? _('Auto DevOps enabled') : _('Enable Auto DevOps'),
link: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) link: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
elsif auto_devops_enabled? elsif auto_devops_enabled?
OpenStruct.new(enabled: true, OpenStruct.new(enabled: true,
label: _('Auto DevOps enabled'), label: _('Auto DevOps enabled'),
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.help-block .help-block
Manage repository storage paths. Learn more in the Manage repository storage paths. Learn more in the
= succeed "." do = succeed "." do
= link_to "repository storages documentation", help_page_path("administration/repository_storages") = link_to "repository storages documentation", help_page_path("administration/repository_storage_paths")
.sub-section .sub-section
%h4 Circuit breaker %h4 Circuit breaker
.form-group .form-group
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%hr %hr
%p %p
- link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings')) - link_to_auto_devops_settings = link_to(s_('AutoDevOps|enable Auto DevOps (Beta)'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'))
- link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project)) - link_to_add_kubernetes_cluster = link_to(s_('AutoDevOps|add a Kubernetes cluster'), new_project_cluster_path(@project))
= s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster } = s_('AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}.').html_safe % { link_to_auto_devops_settings: link_to_auto_devops_settings, link_to_add_kubernetes_cluster: link_to_add_kubernetes_cluster }
......
.row.prepend-top-default
.col-lg-12
= form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project)
%fieldset.builds-feature
.form-group
- message = auto_devops_warning_message(@project)
- ci_file_formatted = '<code>.gitlab-ci.yml</code>'.html_safe
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong= s_('CICD|Enable Auto DevOps')
%br
= s_('CICD|The Auto DevOps pipeline configuration will be used when there is no %{ci_file} in the project.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong= s_('CICD|Disable Auto DevOps')
%br
= s_('CICD|An explicit %{ci_file} needs to be specified before you can begin using Continuous Integration and Delivery.').html_safe % { ci_file: ci_file_formatted }
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong= s_('CICD|Instance default (%{state})') % { state: "#{Gitlab::CurrentSettings.auto_devops_enabled? ? _('enabled') : _('disabled')}" }
%br
= s_('CICD|Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific %{ci_file}.').html_safe % { ci_file: ci_file_formatted }
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
.help-block
= s_('CICD|You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.')
= f.submit 'Save changes', class: "btn btn-success prepend-top-15"
...@@ -3,44 +3,6 @@ ...@@ -3,44 +3,6 @@
= form_for @project, url: project_settings_ci_cd_path(@project) do |f| = form_for @project, url: project_settings_ci_cd_path(@project) do |f|
= form_errors(@project) = form_errors(@project)
%fieldset.builds-feature %fieldset.builds-feature
.form-group
%h5 Auto DevOps (Beta)
%p
Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.
= link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
- message = auto_devops_warning_message(@project)
- if message
%p.settings-message.text-center
= message.html_safe
= f.fields_for :auto_devops_attributes, @auto_devops do |form|
.radio
= form.label :enabled_true do
= form.radio_button :enabled, 'true'
%strong Enable Auto DevOps
%br
%span.descr
The Auto DevOps pipeline configuration will be used when there is no <code>.gitlab-ci.yml</code> in the project.
.radio
= form.label :enabled_false do
= form.radio_button :enabled, 'false'
%strong Disable Auto DevOps
%br
%span.descr
An explicit <code>.gitlab-ci.yml</code> needs to be specified before you can begin using Continuous Integration and Delivery.
.radio
= form.label :enabled_ do
= form.radio_button :enabled, ''
%strong Instance default (#{Gitlab::CurrentSettings.auto_devops_enabled? ? 'enabled' : 'disabled'})
%br
%span.descr
Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific <code>.gitlab-ci.yml</code>.
%p
You need to specify a domain if you want to use Auto Review Apps and Auto Deploy stages.
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
%hr
.form-group.append-bottom-default.js-secret-runner-token .form-group.append-bottom-default.js-secret-runner-token
= f.label :runners_token, "Runner token", class: 'label-light' = f.label :runners_token, "Runner token", class: 'label-light'
.form-control.js-secret-value-placeholder .form-control.js-secret-value-placeholder
......
...@@ -12,10 +12,22 @@ ...@@ -12,10 +12,22 @@
%button.btn.js-settings-toggle{ type: 'button' } %button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p %p
Update your CI/CD configuration, like job timeout or Auto DevOps. Access your runner token, customize your pipeline configuration, and view your pipeline status and coverage report.
.settings-content .settings-content
= render 'form' = render 'form'
%section.settings#autodevops-settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= s_('CICD|Auto DevOps (Beta)')
%button.btn.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= s_('CICD|Auto DevOps will automatically build, test, and deploy your application based on a predefined Continuous Integration and Delivery configuration.')
= link_to s_('CICD|Learn more about Auto DevOps'), help_page_path('topics/autodevops/index.md')
.settings-content
= render 'autodevops_form'
%section.settings.no-animate{ class: ('expanded' if expanded) } %section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
- link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer') - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
= s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link } = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
.banner-buttons .banner-buttons
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn js-close-callout' = link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn js-close-callout'
%button.btn-transparent.banner-close.close.js-close-callout{ type: 'button', %button.btn-transparent.banner-close.close.js-close-callout{ type: 'button',
'aria-label' => 'Dismiss Auto DevOps box' } 'aria-label' => 'Dismiss Auto DevOps box' }
......
---
title: Prevent pipeline actions in dropdown to redirct to a new page
merge_request:
author:
type: fixed
---
title: Fix discussions API setting created_at for notable in a group or notable in
a project in a group with owners
merge_request: 18464
author:
type: fixed
---
title: Create settings section for autodevops
merge_request: 18321
author:
type: changed
---
title: Fix project creation for user endpoint when jobs_enabled parameter supplied
merge_request:
author:
type: fixed
---
title: Add missing changelog type to docs
merge_request: 18526
author: "@blackst0ne"
type: other
---
title: Add 2FA filter to users API for admins only
merge_request: 18503
author:
type: changed
---
title: Fix missing namespace for some internal users
merge_request: 18357
author:
type: fixed
# rubocop:disable GitlabSecurity/PublicSend require_dependency File.expand_path('../../lib/gitlab', __dir__) # Load Gitlab as soon as possible
require_dependency Rails.root.join('lib/gitlab') # Load Gitlab as soon as possible
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { "#{Rails.root}/config/gitlab.yml" }
namespace Rails.env
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
# Default settings # Default settings
Settings['ldap'] ||= Settingslogic.new({}) Settings['ldap'] ||= Settingslogic.new({})
......
module Gitlab
def self.config
Settings
end
VERSION = File.read(Rails.root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
end
require './spec/support/sidekiq' require './spec/support/sidekiq'
require './spec/support/test_env' require './spec/support/helpers/test_env'
class Gitlab::Seeder::CycleAnalytics class Gitlab::Seeder::CycleAnalytics
def initialize(project, perf: false) def initialize(project, perf: false)
......
class CreateMissingNamespaceForInternalUsers < ActiveRecord::Migration
DOWNTIME = false
def up
connection.exec_query(users_query.to_sql).rows.each do |id, username|
create_namespace(id, username)
# When testing locally I've noticed that these internal users are missing
# the notification email, for more details visit the below link:
# https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/18357#note_68327560
set_notification_email(id)
end
end
def down
# no-op
end
private
def users
@users ||= Arel::Table.new(:users)
end
def namespaces
@namespaces ||= Arel::Table.new(:namespaces)
end
def users_query
condition = users[:ghost].eq(true)
if column_exists?(:users, :support_bot)
condition = condition.or(users[:support_bot].eq(true))
end
users.join(namespaces, Arel::Nodes::OuterJoin)
.on(namespaces[:type].eq(nil).and(namespaces[:owner_id].eq(users[:id])))
.where(namespaces[:owner_id].eq(nil))
.where(condition)
.project(users[:id], users[:username])
end
def create_namespace(user_id, username)
path = Uniquify.new.string(username) do |str|
query = "SELECT id FROM namespaces WHERE parent_id IS NULL AND path='#{str}' LIMIT 1"
connection.exec_query(query).present?
end
insert_query = "INSERT INTO namespaces(owner_id, path, name) VALUES(#{user_id}, '#{path}', '#{path}')"
namespace_id = connection.insert_sql(insert_query)
create_route(namespace_id)
end
def create_route(namespace_id)
return unless namespace_id
row = connection.exec_query("SELECT id, path FROM namespaces WHERE id=#{namespace_id}").first
id, path = row.values_at('id', 'path')
execute("INSERT INTO routes(source_id, source_type, path, name) VALUES(#{id}, 'Namespace', '#{path}', '#{path}')")
end
def set_notification_email(user_id)
execute "UPDATE users SET notification_email = email WHERE notification_email IS NULL AND id = #{user_id}"
end
end
...@@ -112,13 +112,13 @@ POST /projects/import ...@@ -112,13 +112,13 @@ POST /projects/import
| `file` | string | yes | The file to be uploaded | | `file` | string | yes | The file to be uploaded |
| `path` | string | yes | Name and path for new project | | `path` | string | yes | Name and path for new project |
| `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false | | `overwrite` | boolean | no | If there is a project with the same path the import will overwrite it. Default to false |
| `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md)] | | `override_params` | Hash | no | Supports all fields defined in the [Project API](projects.md) |
The override params passed will take precendence over all values defined inside the export file. The override params passed will take precedence over all values defined inside the export file.
To upload a file from your filesystem, use the `--form` argument. This causes To upload a file from your file system, use the `--form` argument. This causes
cURL to post data using the header `Content-Type: multipart/form-data`. cURL to post data using the header `Content-Type: multipart/form-data`.
The `file=` parameter must point to a file on your filesystem and be preceded The `file=` parameter must point to a file on your file system and be preceded
by `@`. For example: by `@`. For example:
```console ```console
......
...@@ -55,6 +55,7 @@ GET /users ...@@ -55,6 +55,7 @@ GET /users
| --------- | ---- | -------- | ----------- | | --------- | ---- | -------- | ----------- |
| `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` | | `order_by` | string | no | Return projects ordered by `id`, `name`, `username`, `created_at`, or `updated_at` fields. Default is `id` |
| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` | | `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
| `two_factor` | string | no | Filter users by Two-factor authentication. Filter values are `enabled` or `disabled`. By default it returns all users |
```json ```json
[ [
......
...@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this ...@@ -22,7 +22,7 @@ The `merge_request` value is a reference to a merge request that adds this
entry, and the `author` key is used to give attribution to community entry, and the `author` key is used to give attribution to community
contributors. **Both are optional**. contributors. **Both are optional**.
The `type` field maps the category of the change, The `type` field maps the category of the change,
valid options are: added, fixed, changed, deprecated, removed, security, other. **Type field is mandatory**. valid options are: added, fixed, changed, deprecated, removed, security, performance, other. **Type field is mandatory**.
Community contributors and core team members are encouraged to add their name to Community contributors and core team members are encouraged to add their name to
the `author` field. GitLab team members **should not**. the `author` field. GitLab team members **should not**.
......
...@@ -90,6 +90,25 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load) ...@@ -90,6 +90,25 @@ Finished in 34.51 seconds (files took 0.76702 seconds to load)
Note: `live_debug` only works on javascript enabled specs. Note: `live_debug` only works on javascript enabled specs.
### Fast unit tests
Some classes are well-isolated from Rails and you should be able to test them
without the overhead added by the Rails environment and Bundler's `:default`
group's gem loading. In these cases, you can `require 'fast_spec_helper'`
instead of `require 'spec_helper'` in your test file, and your test should run
really fast since:
- Gems loading is skipped
- Rails app boot is skipped
- gitlab-shell and Gitaly setup are skipped
- Test repositories setup are skipped
Note that in some cases, you might have to add some `require_dependency 'foo'`
in your file under test since Rails autoloading is not available in these cases.
This shouldn't be a problem since explicitely listing dependencies should be
considered a good practice anyway.
### `let` variables ### `let` variables
GitLab's RSpec suite has made extensive use of `let` variables to reduce GitLab's RSpec suite has made extensive use of `let` variables to reduce
...@@ -281,14 +300,13 @@ All fixtures should be be placed under `spec/fixtures/`. ...@@ -281,14 +300,13 @@ All fixtures should be be placed under `spec/fixtures/`.
RSpec config files are files that change the RSpec config (i.e. RSpec config files are files that change the RSpec config (i.e.
`RSpec.configure do |config|` blocks). They should be placed under `RSpec.configure do |config|` blocks). They should be placed under
`spec/support/config/`. `spec/support/`.
Each file should be related to a specific domain, e.g. Each file should be related to a specific domain, e.g.
`spec/support/config/capybara.rb`, `spec/support/config/carrierwave.rb`, etc. `spec/support/capybara.rb`, `spec/support/carrierwave.rb`, etc.
Helpers can be included in the `spec/support/config/rspec.rb` file. If a If a helpers module applies only to a certain kind of specs, it should add
helpers module applies only to a certain kind of specs, it should add modifiers modifiers to the `config.include` call. For instance if
to the `config.include` call. For instance if
`spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and `spec/support/helpers/cycle_analytics_helpers.rb` applies to `:lib` and
`type: :model` specs only, you would write the following: `type: :model` specs only, you would write the following:
...@@ -299,6 +317,14 @@ RSpec.configure do |config| ...@@ -299,6 +317,14 @@ RSpec.configure do |config|
end end
``` ```
If a config file only consists of `config.include`, you can add these
`config.include` directly in `spec/spec_helper.rb`.
For very generic helpers, consider including them in the `spec/support/rspec.rb`
file which is used by the `spec/fast_spec_helper.rb` file. See
[Fast unit tests](#fast-unit-tests) for more details about the
`spec/fast_spec_helper.rb` file.
--- ---
[Return to Testing documentation](index.md) [Return to Testing documentation](index.md)
...@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the ...@@ -9,7 +9,7 @@ At a minimum, requiring the Rake helper will redirect `stdout`, include the
runtime task helpers, and include the `RakeHelpers` Spec support module. runtime task helpers, and include the `RakeHelpers` Spec support module.
The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make The `RakeHelpers` module exposes a `run_rake_task(<task>)` method to make
executing tasks simple. See `spec/support/rake_helpers.rb` for all available executing tasks simple. See `spec/support/helpers/rake_helpers.rb` for all available
methods. methods.
Example: Example:
......
...@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje ...@@ -23,7 +23,7 @@ You can create as many deploy tokens as you like from the settings of your proje
![Personal access tokens page](img/deploy_tokens.png) ![Personal access tokens page](img/deploy_tokens.png)
## Revoking a personal access token ## Revoking a deploy token
At any time, you can revoke any deploy token by just clicking the At any time, you can revoke any deploy token by just clicking the
respective **Revoke** button under the 'Active deploy tokens' area. respective **Revoke** button under the 'Active deploy tokens' area.
......
...@@ -12,7 +12,11 @@ end ...@@ -12,7 +12,11 @@ end
WebMock.enable! WebMock.enable!
%w(select2_helper test_env repo_helpers wait_for_requests sidekiq project_forks_helper webmock).each do |f| %w(select2_helper test_env repo_helpers wait_for_requests project_forks_helper).each do |f|
require Rails.root.join('spec', 'support', 'helpers', f)
end
%w(sidekiq webmock).each do |f|
require Rails.root.join('spec', 'support', f) require Rails.root.join('spec', 'support', f)
end end
......
...@@ -64,8 +64,10 @@ module API ...@@ -64,8 +64,10 @@ module API
authorize! :create_note, noteable authorize! :create_note, noteable
parent = noteable_parent(noteable) parent = noteable_parent(noteable)
if opts[:created_at] if opts[:created_at]
opts.delete(:created_at) unless current_user.admin? || parent.owner == current_user opts.delete(:created_at) unless
current_user.admin? || parent.owned_by?(current_user)
end end
project = parent if parent.is_a?(Project) project = parent if parent.is_a?(Project)
......
...@@ -74,6 +74,11 @@ module API ...@@ -74,6 +74,11 @@ module API
present options[:with].prepare_relation(projects, options), options present options[:with].prepare_relation(projects, options), options
end end
def translate_params_for_compatibility(params)
params[:builds_enabled] = params.delete(:jobs_enabled) if params.key?(:jobs_enabled)
params
end
end end
resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do resource :users, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
...@@ -123,7 +128,7 @@ module API ...@@ -123,7 +128,7 @@ module API
end end
post do post do
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(current_user, attrs).execute project = ::Projects::CreateService.new(current_user, attrs).execute
if project.saved? if project.saved?
...@@ -155,6 +160,7 @@ module API ...@@ -155,6 +160,7 @@ module API
not_found!('User') unless user not_found!('User') unless user
attrs = declared_params(include_missing: false) attrs = declared_params(include_missing: false)
attrs = translate_params_for_compatibility(attrs)
project = ::Projects::CreateService.new(user, attrs).execute project = ::Projects::CreateService.new(user, attrs).execute
if project.saved? if project.saved?
...@@ -276,7 +282,7 @@ module API ...@@ -276,7 +282,7 @@ module API
authorize! :rename_project, user_project if attrs[:name].present? authorize! :rename_project, user_project if attrs[:name].present?
authorize! :change_visibility_level, user_project if attrs[:visibility].present? authorize! :change_visibility_level, user_project if attrs[:visibility].present?
attrs[:builds_enabled] = attrs.delete(:jobs_enabled) if attrs.key?(:jobs_enabled) attrs = translate_params_for_compatibility(attrs)
result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute result = ::Projects::UpdateService.new(user_project, current_user, attrs).execute
......
...@@ -77,7 +77,7 @@ module API ...@@ -77,7 +77,7 @@ module API
authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?) authenticated_as_admin! if params[:external].present? || (params[:extern_uid].present? && params[:provider].present?)
unless current_user&.admin? unless current_user&.admin?
params.except!(:created_after, :created_before, :order_by, :sort) params.except!(:created_after, :created_before, :order_by, :sort, :two_factor)
end end
users = UsersFinder.new(current_user, params).execute users = UsersFinder.new(current_user, params).execute
......
require_dependency 'gitlab/git' require_dependency 'settings'
require_dependency 'gitlab/popen'
module Gitlab module Gitlab
def self.root
Pathname.new(File.expand_path('..', __dir__))
end
def self.config
Settings
end
COM_URL = 'https://gitlab.com'.freeze COM_URL = 'https://gitlab.com'.freeze
APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))} APP_DIRS_PATTERN = %r{^/?(app|config|ee|lib|spec|\(\w*\))}
SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z} SUBDOMAIN_REGEX = %r{\Ahttps://[a-z0-9]+\.gitlab\.com\z}
VERSION = File.read(root.join("VERSION")).strip.freeze
REVISION = Gitlab::Popen.popen(%W(#{config.git.bin_path} log --pretty=format:%h -n 1)).first.chomp.freeze
def self.com? def self.com?
# Check `gl_subdomain?` as well to keep parity with gitlab.com # Check `gl_subdomain?` as well to keep parity with gitlab.com
......
require_dependency 'gitlab/encoding_helper'
module Gitlab module Gitlab
module Git module Git
# The ID of empty tree. # The ID of empty tree.
......
module Gitlab module Gitlab
module Wiki module Git
class CommitterWithHooks < Gollum::Committer class CommitterWithHooks < Gollum::Committer
attr_reader :gl_wiki attr_reader :gl_wiki
...@@ -9,6 +9,9 @@ module Gitlab ...@@ -9,6 +9,9 @@ module Gitlab
end end
def commit def commit
# TODO: Remove after 10.8
return super unless allowed_to_run_hooks?
result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch( result = Gitlab::Git::OperationService.new(git_user, gl_wiki.repository).with_branch(
@wiki.ref, @wiki.ref,
start_branch_name: @wiki.ref start_branch_name: @wiki.ref
...@@ -24,6 +27,11 @@ module Gitlab ...@@ -24,6 +27,11 @@ module Gitlab
private private
# TODO: Remove after 10.8
def allowed_to_run_hooks?
@options[:user_id] != 0 && @options[:username].present?
end
def git_user def git_user
@git_user ||= Gitlab::Git::User.new(@options[:username], @git_user ||= Gitlab::Git::User.new(@options[:username],
@options[:name], @options[:name],
......
...@@ -290,7 +290,7 @@ module Gitlab ...@@ -290,7 +290,7 @@ module Gitlab
end end
def committer_with_hooks(commit_details) def committer_with_hooks(commit_details)
Gitlab::Wiki::CommitterWithHooks.new(self, commit_details.to_h) Gitlab::Git::CommitterWithHooks.new(self, commit_details.to_h)
end end
def with_committer_with_hooks(commit_details, &block) def with_committer_with_hooks(commit_details, &block)
......
require 'settingslogic'
class Settings < Settingslogic
source ENV.fetch('GITLAB_CONFIG') { Pathname.new(File.expand_path('..', __dir__)).join('config/gitlab.yml') }
namespace ENV.fetch('GITLAB_ENV') { Rails.env }
class << self
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
def host_without_www(url)
host(url).sub('www.', '')
end
def build_gitlab_ci_url
custom_port =
if on_standard_port?(gitlab)
nil
else
":#{gitlab.port}"
end
[
gitlab.protocol,
"://",
gitlab.host,
custom_port,
gitlab.relative_url_root
].join('')
end
def build_pages_url
base_url(pages).join('')
end
def build_gitlab_shell_ssh_path_prefix
user_host = "#{gitlab_shell.ssh_user}@#{gitlab_shell.ssh_host}"
if gitlab_shell.ssh_port != 22
"ssh://#{user_host}:#{gitlab_shell.ssh_port}/"
else
if gitlab_shell.ssh_host.include? ':'
"[#{user_host}]:"
else
"#{user_host}:"
end
end
end
def build_base_gitlab_url
base_url(gitlab).join('')
end
def build_gitlab_url
(base_url(gitlab) + [gitlab.relative_url_root]).join('')
end
# check that values in `current` (string or integer) is a contant in `modul`.
def verify_constant_array(modul, current, default)
values = default || []
unless current.nil?
values = []
current.each do |constant|
values.push(verify_constant(modul, constant, nil))
end
values.delete_if { |value| value.nil? }
end
values
end
# check that `current` (string or integer) is a contant in `modul`.
def verify_constant(modul, current, default)
constant = modul.constants.find { |name| modul.const_get(name) == current }
value = constant.nil? ? default : modul.const_get(constant)
if current.is_a? String
value = modul.const_get(current.upcase) rescue default
end
value
end
def absolute(path)
File.expand_path(path, Rails.root)
end
private
def base_url(config)
custom_port = on_standard_port?(config) ? nil : ":#{config.port}"
[
config.protocol,
"://",
config.host,
custom_port
]
end
def on_standard_port?(config)
config.port.to_i == (config.https ? 443 : 80)
end
# Extract the host part of the given +url+.
def host(url)
url = url.downcase
url = "http://#{url}" unless url.start_with?('http')
# Get rid of the path so that we don't even have to encode it
url_without_path = url.sub(%r{(https?://[^/]+)/?.*}, '\1')
URI.parse(url_without_path).host
end
# Runs every minute in a random ten-minute period on Sundays, to balance the
# load on the server receiving these pings. The usage ping is safe to run
# multiple times because of a 24 hour exclusive lock.
def cron_for_usage_ping
hour = rand(24)
minute = rand(6)
"#{minute}0-#{minute}9 #{hour} * * 0"
end
end
end
module RuboCop module RuboCop
module SpecHelpers module SpecHelpers
SPEC_HELPERS = %w[spec_helper.rb rails_helper.rb].freeze SPEC_HELPERS = %w[fast_spec_helper.rb rails_helper.rb spec_helper.rb].freeze
# Returns true if the given node originated from the spec directory. # Returns true if the given node originated from the spec directory.
def in_spec?(node) def in_spec?(node)
......
...@@ -223,11 +223,12 @@ describe Import::BitbucketController do ...@@ -223,11 +223,12 @@ describe Import::BitbucketController do
end end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do context 'user has chosen an existing nested namespace and name for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
before do before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user) nested_namespace.add_owner(user)
end end
...@@ -273,7 +274,7 @@ describe Import::BitbucketController do ...@@ -273,7 +274,7 @@ describe Import::BitbucketController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } let!(:parent_namespace) { create(:group, name: 'foo') }
before do before do
parent_namespace.add_owner(user) parent_namespace.add_owner(user)
......
...@@ -196,10 +196,11 @@ describe Import::GitlabController do ...@@ -196,10 +196,11 @@ describe Import::GitlabController do
end end
context 'user has chosen an existing nested namespace for the project', :postgresql do context 'user has chosen an existing nested namespace for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
before do before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user) nested_namespace.add_owner(user)
end end
...@@ -245,7 +246,7 @@ describe Import::GitlabController do ...@@ -245,7 +246,7 @@ describe Import::GitlabController do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } let!(:parent_namespace) { create(:group, name: 'foo') }
before do before do
parent_namespace.add_owner(user) parent_namespace.add_owner(user)
......
...@@ -4,7 +4,11 @@ describe Projects::ForksController do ...@@ -4,7 +4,11 @@ describe Projects::ForksController do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
let(:forked_project) { Projects::ForkService.new(project, user).execute } let(:forked_project) { Projects::ForkService.new(project, user).execute }
let(:group) { create(:group, owner: forked_project.creator) } let(:group) { create(:group) }
before do
group.add_owner(user)
end
describe 'GET index' do describe 'GET index' do
def get_forks def get_forks
......
require_relative '../support/repo_helpers' require_relative '../support/helpers/repo_helpers'
FactoryBot.define do FactoryBot.define do
factory :commit do factory :commit do
......
require_relative '../support/gpg_helpers'
FactoryBot.define do FactoryBot.define do
factory :gpg_key_subkey do factory :gpg_key_subkey do
gpg_key gpg_key
......
require_relative '../support/gpg_helpers' require_relative '../support/helpers/gpg_helpers'
FactoryBot.define do FactoryBot.define do
factory :gpg_key do factory :gpg_key do
......
require_relative '../support/gpg_helpers'
FactoryBot.define do FactoryBot.define do
factory :gpg_signature do factory :gpg_signature do
commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) } commit_sha { Digest::SHA1.hexdigest(SecureRandom.hex) }
......
...@@ -5,6 +5,14 @@ FactoryBot.define do ...@@ -5,6 +5,14 @@ FactoryBot.define do
type 'Group' type 'Group'
owner nil owner nil
after(:create) do |group|
if group.owner
# We could remove this after we have proper constraint:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/43292
raise "Don't set owner for groups, use `group.add_owner(user)` instead"
end
end
trait :public do trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC visibility_level Gitlab::VisibilityLevel::PUBLIC
end end
......
...@@ -2,6 +2,22 @@ FactoryBot.define do ...@@ -2,6 +2,22 @@ FactoryBot.define do
factory :namespace do factory :namespace do
sequence(:name) { |n| "namespace#{n}" } sequence(:name) { |n| "namespace#{n}" }
path { name.downcase.gsub(/\s/, '_') } path { name.downcase.gsub(/\s/, '_') }
owner
# This is a workaround to avoid the user creating another namespace via
# User#ensure_namespace_correct. We should try to remove it and then
# we could remove this workaround
association :owner, factory: :user, strategy: :build
before(:create) do |namespace|
owner = namespace.owner
if owner
# We're changing the username here because we want to keep our path,
# and User#ensure_namespace_correct would change the path based on
# username, so we're forced to do this otherwise we'll need to change
# a lot of existing tests.
owner.username = namespace.path
owner.namespace = namespace
end
end
end end
end end
require_relative '../support/repo_helpers' require_relative '../support/helpers/repo_helpers'
include ActionDispatch::TestProcess include ActionDispatch::TestProcess
......
require_relative '../support/test_env' require_relative '../support/helpers/test_env'
FactoryBot.define do FactoryBot.define do
# Project without repository # Project without repository
......
require 'bundler/setup'
ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
unless Object.respond_to?(:require_dependency)
class Object
alias_method :require_dependency, :require
end
end
# Defines Gitlab and Gitlab.config which are at the center of the app
require_relative '../lib/gitlab' unless defined?(Gitlab.config)
require_relative 'support/rspec'
...@@ -64,7 +64,7 @@ feature 'New project' do ...@@ -64,7 +64,7 @@ feature 'New project' do
end end
context 'with group namespace' do context 'with group namespace' do
let(:group) { create(:group, :private, owner: user) } let(:group) { create(:group, :private) }
before do before do
group.add_owner(user) group.add_owner(user)
...@@ -81,7 +81,7 @@ feature 'New project' do ...@@ -81,7 +81,7 @@ feature 'New project' do
end end
context 'with subgroup namespace' do context 'with subgroup namespace' do
let(:group) { create(:group, owner: user) } let(:group) { create(:group) }
let(:subgroup) { create(:group, parent: group) } let(:subgroup) { create(:group, parent: group) }
before do before do
......
...@@ -8,6 +8,7 @@ describe "Projects > Settings > Pipelines settings" do ...@@ -8,6 +8,7 @@ describe "Projects > Settings > Pipelines settings" do
before do before do
sign_in(user) sign_in(user)
project.add_role(user, role) project.add_role(user, role)
create(:project_auto_devops, project: project)
end end
context 'for developer' do context 'for developer' do
...@@ -27,10 +28,17 @@ describe "Projects > Settings > Pipelines settings" do ...@@ -27,10 +28,17 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project) visit project_settings_ci_cd_path(project)
fill_in('Test coverage parsing', with: 'coverage_regex') fill_in('Test coverage parsing', with: 'coverage_regex')
page.within '#js-general-pipeline-settings' do
click_on 'Save changes' click_on 'Save changes'
end
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
page.within '#js-general-pipeline-settings' do
expect(page).to have_button('Save changes', disabled: false) expect(page).to have_button('Save changes', disabled: false)
end
expect(page).to have_field('Test coverage parsing', with: 'coverage_regex') expect(page).to have_field('Test coverage parsing', with: 'coverage_regex')
end end
...@@ -38,10 +46,15 @@ describe "Projects > Settings > Pipelines settings" do ...@@ -38,10 +46,15 @@ describe "Projects > Settings > Pipelines settings" do
visit project_settings_ci_cd_path(project) visit project_settings_ci_cd_path(project)
page.check('Auto-cancel redundant, pending pipelines') page.check('Auto-cancel redundant, pending pipelines')
page.within '#js-general-pipeline-settings' do
click_on 'Save changes' click_on 'Save changes'
end
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
page.within '#js-general-pipeline-settings' do
expect(page).to have_button('Save changes', disabled: false) expect(page).to have_button('Save changes', disabled: false)
end
checkbox = find_field('project_auto_cancel_pending_pipelines') checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked expect(checkbox).to be_checked
...@@ -51,13 +64,16 @@ describe "Projects > Settings > Pipelines settings" do ...@@ -51,13 +64,16 @@ describe "Projects > Settings > Pipelines settings" do
it 'update auto devops settings' do it 'update auto devops settings' do
visit project_settings_ci_cd_path(project) visit project_settings_ci_cd_path(project)
page.within '#autodevops-settings' do
fill_in('project_auto_devops_attributes_domain', with: 'test.com') fill_in('project_auto_devops_attributes_domain', with: 'test.com')
page.choose('project_auto_devops_attributes_enabled_false') page.choose('project_auto_devops_attributes_enabled_false')
click_on 'Save changes' click_on 'Save changes'
end
expect(page.status_code).to eq(200) expect(page.status_code).to eq(200)
expect(project.auto_devops).to be_present expect(project.auto_devops).to be_present
expect(project.auto_devops).not_to be_enabled expect(project.auto_devops).not_to be_enabled
expect(project.auto_devops.domain).to eq('test.com')
end end
end end
end end
......
...@@ -65,7 +65,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do ...@@ -65,7 +65,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do page.within('.project-stats') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end end
end end
...@@ -75,7 +75,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do ...@@ -75,7 +75,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project) visit project_path(project)
page.within('.project-stats') do page.within('.project-stats') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end end
end end
end end
...@@ -212,7 +212,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do ...@@ -212,7 +212,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
describe 'Auto DevOps button' do describe 'Auto DevOps button' do
it '"Enable Auto DevOps" button linked to settings page' do it '"Enable Auto DevOps" button linked to settings page' do
page.within('.project-stats') do page.within('.project-stats') do
expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end end
end end
...@@ -222,7 +222,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do ...@@ -222,7 +222,7 @@ describe 'Projects > Show > User sees setup shortcut buttons' do
visit project_path(project) visit project_path(project)
page.within('.project-stats') do page.within('.project-stats') do
expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings'))
end end
end end
......
...@@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper'; ...@@ -6,7 +6,7 @@ import mountComponent from '../../helpers/vue_mount_component_helper';
describe('pipeline graph action component', () => { describe('pipeline graph action component', () => {
let component; let component;
beforeEach((done) => { beforeEach(done => {
const ActionComponent = Vue.extend(actionComponent); const ActionComponent = Vue.extend(actionComponent);
component = mountComponent(ActionComponent, { component = mountComponent(ActionComponent, {
tooltipText: 'bar', tooltipText: 'bar',
...@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => { ...@@ -22,7 +22,7 @@ describe('pipeline graph action component', () => {
}); });
it('should emit an event with the provided link', () => { it('should emit an event with the provided link', () => {
eventHub.$on('graphAction', (link) => { eventHub.$on('graphAction', link => {
expect(link).toEqual('foo'); expect(link).toEqual('foo');
}); });
}); });
...@@ -31,7 +31,7 @@ describe('pipeline graph action component', () => { ...@@ -31,7 +31,7 @@ describe('pipeline graph action component', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar'); expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
}); });
it('should update bootstrap tooltip when title changes', (done) => { it('should update bootstrap tooltip when title changes', done => {
component.tooltipText = 'changed'; component.tooltipText = 'changed';
setTimeout(() => { setTimeout(() => {
...@@ -44,4 +44,45 @@ describe('pipeline graph action component', () => { ...@@ -44,4 +44,45 @@ describe('pipeline graph action component', () => {
expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined(); expect(component.$el.querySelector('.ci-action-icon-wrapper')).toBeDefined();
expect(component.$el.querySelector('svg')).toBeDefined(); expect(component.$el.querySelector('svg')).toBeDefined();
}); });
it('disables the button when clicked', done => {
component.$el.click();
component.$nextTick(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
done();
});
});
it('re-enabled the button when `requestFinishedFor` matches `linkRequested`', done => {
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'foo';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toBeNull();
})
.then(done)
.catch(done.fail);
});
it('does not re-enable the button when `requestFinishedFor` does not matches `linkRequested`', done => {
component.$el.click();
component
.$nextTick()
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
component.requestFinishedFor = 'bar';
})
.then(() => {
expect(component.$el.getAttribute('disabled')).toEqual('disabled');
})
.then(done)
.catch(done.fail);
});
}); });
import Vue from 'vue';
import dropdownActionComponent from '~/pipelines/components/graph/dropdown_action_component.vue';
describe('action component', () => {
let component;
beforeEach((done) => {
const DropdownActionComponent = Vue.extend(dropdownActionComponent);
component = new DropdownActionComponent({
propsData: {
tooltipText: 'bar',
link: 'foo',
actionMethod: 'post',
actionIcon: 'cancel',
},
}).$mount();
Vue.nextTick(done);
});
it('should render a link', () => {
expect(component.$el.getAttribute('href')).toEqual('foo');
});
it('should render the provided title as a bootstrap tooltip', () => {
expect(component.$el.getAttribute('data-original-title')).toEqual('bar');
});
it('should render an svg', () => {
expect(component.$el.querySelector('svg')).toBeDefined();
});
});
...@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => { ...@@ -93,17 +93,6 @@ describe('pipeline graph job component', () => {
}); });
}); });
describe('dropdown', () => {
it('should render the dropdown action icon', () => {
component = mountComponent(JobComponent, {
job: mockJob,
isDropdown: true,
});
expect(component.$el.querySelector('a.ci-action-icon-wrapper')).toBeDefined();
});
});
it('should render provided class name', () => { it('should render provided class name', () => {
component = mountComponent(JobComponent, { component = mountComponent(JobComponent, {
job: mockJob, job: mockJob,
......
...@@ -13,9 +13,9 @@ describe('Settings Panels', () => { ...@@ -13,9 +13,9 @@ describe('Settings Panels', () => {
}); });
it('should expand linked hash fragment panel', () => { it('should expand linked hash fragment panel', () => {
location.hash = '#js-general-pipeline-settings'; location.hash = '#autodevops-settings';
const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings'); const pipelineSettingsPanel = document.querySelector('#autodevops-settings');
// Our test environment automatically expands everything so we need to clear that out first // Our test environment automatically expands everything so we need to clear that out first
pipelineSettingsPanel.classList.remove('expanded'); pipelineSettingsPanel.classList.remove('expanded');
......
import Vue from 'vue'; import Vue from 'vue';
import ciIcon from '~/vue_shared/components/ci_icon.vue'; import ciIcon from '~/vue_shared/components/ci_icon.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('CI Icon component', () => { describe('CI Icon component', () => {
let CiIcon; const Component = Vue.extend(ciIcon);
beforeEach(() => { let vm;
CiIcon = Vue.extend(ciIcon);
afterEach(() => {
vm.$destroy();
}); });
it('should render a span element with an svg', () => { it('should render a span element with an svg', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_success', icon: 'icon_status_success',
}, },
}, });
}).$mount();
expect(component.$el.tagName).toEqual('SPAN'); expect(vm.$el.tagName).toEqual('SPAN');
expect(component.$el.querySelector('span > svg')).toBeDefined(); expect(vm.$el.querySelector('span > svg')).toBeDefined();
}); });
it('should render a success status', () => { it('should render a success status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_success', icon: 'icon_status_success',
group: 'success', group: 'success',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-success')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-success')).toEqual(true);
}); });
it('should render a failed status', () => { it('should render a failed status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_failed', icon: 'icon_status_failed',
group: 'failed', group: 'failed',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-failed')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-failed')).toEqual(true);
}); });
it('should render success with warnings status', () => { it('should render success with warnings status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_warning', icon: 'icon_status_warning',
group: 'warning', group: 'warning',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-warning')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-warning')).toEqual(true);
}); });
it('should render pending status', () => { it('should render pending status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_pending', icon: 'icon_status_pending',
group: 'pending', group: 'pending',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-pending')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-pending')).toEqual(true);
}); });
it('should render running status', () => { it('should render running status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_running', icon: 'icon_status_running',
group: 'running', group: 'running',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-running')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-running')).toEqual(true);
}); });
it('should render created status', () => { it('should render created status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_created', icon: 'icon_status_created',
group: 'created', group: 'created',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-created')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-created')).toEqual(true);
}); });
it('should render skipped status', () => { it('should render skipped status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_skipped', icon: 'icon_status_skipped',
group: 'skipped', group: 'skipped',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-skipped')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-skipped')).toEqual(true);
}); });
it('should render canceled status', () => { it('should render canceled status', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_canceled', icon: 'icon_status_canceled',
group: 'canceled', group: 'canceled',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-canceled')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-canceled')).toEqual(true);
}); });
it('should render status for manual action', () => { it('should render status for manual action', () => {
const component = new CiIcon({ vm = mountComponent(Component, {
propsData: {
status: { status: {
icon: 'icon_status_manual', icon: 'icon_status_manual',
group: 'manual', group: 'manual',
}, },
}, });
}).$mount();
expect(component.$el.classList.contains('ci-status-icon-manual')).toEqual(true); expect(vm.$el.classList.contains('ci-status-icon-manual')).toEqual(true);
}); });
}); });
...@@ -3,10 +3,10 @@ import clipboardButton from '~/vue_shared/components/clipboard_button.vue'; ...@@ -3,10 +3,10 @@ import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('clipboard button', () => { describe('clipboard button', () => {
const Component = Vue.extend(clipboardButton);
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(clipboardButton);
vm = mountComponent(Component, { vm = mountComponent(Component, {
text: 'copy me', text: 'copy me',
title: 'Copy this value into Clipboard!', title: 'Copy this value into Clipboard!',
......
...@@ -55,7 +55,6 @@ describe('Commit component', () => { ...@@ -55,7 +55,6 @@ describe('Commit component', () => {
path: '/jschatz1', path: '/jschatz1',
username: 'jschatz1', username: 'jschatz1',
}, },
commitIconSvg: '<svg></svg>',
}; };
component = mountComponent(CommitComponent, props); component = mountComponent(CommitComponent, props);
...@@ -82,8 +81,10 @@ describe('Commit component', () => { ...@@ -82,8 +81,10 @@ describe('Commit component', () => {
expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha); expect(component.$el.querySelector('.commit-sha').textContent).toContain(props.shortSha);
}); });
it('should render the given commitIconSvg', () => { it('should render icon for commit', () => {
expect(component.$el.querySelector('.js-commit-icon').children).toContain('svg'); expect(
component.$el.querySelector('.js-commit-icon use').getAttribute('xlink:href'),
).toContain('commit');
}); });
describe('Given commit title and author props', () => { describe('Given commit title and author props', () => {
......
...@@ -3,10 +3,10 @@ import expandButton from '~/vue_shared/components/expand_button.vue'; ...@@ -3,10 +3,10 @@ import expandButton from '~/vue_shared/components/expand_button.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('expand button', () => { describe('expand button', () => {
const Component = Vue.extend(expandButton);
let vm; let vm;
beforeEach(() => { beforeEach(() => {
const Component = Vue.extend(expandButton);
vm = mountComponent(Component, { vm = mountComponent(Component, {
slots: { slots: {
expanded: '<p>Expanded!</p>', expanded: '<p>Expanded!</p>',
...@@ -22,7 +22,7 @@ describe('expand button', () => { ...@@ -22,7 +22,7 @@ describe('expand button', () => {
expect(vm.$el.textContent.trim()).toEqual('...'); expect(vm.$el.textContent.trim()).toEqual('...');
}); });
it('hides expander on click', (done) => { it('hides expander on click', done => {
vm.$el.querySelector('button').click(); vm.$el.querySelector('button').click();
vm.$nextTick(() => { vm.$nextTick(() => {
expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;'); expect(vm.$el.querySelector('button').getAttribute('style')).toEqual('display: none;');
......
...@@ -73,6 +73,22 @@ describe('BaseComponent', () => { ...@@ -73,6 +73,22 @@ describe('BaseComponent', () => {
expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]); expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
}); });
}); });
describe('handleCollapsedValueClick', () => {
it('emits toggleCollapse event on component', () => {
spyOn(vm, '$emit');
vm.handleCollapsedValueClick();
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapse');
});
});
describe('handleDropdownHidden', () => {
it('emits onDropdownClose event on component', () => {
spyOn(vm, '$emit');
vm.handleDropdownHidden();
expect(vm.$emit).toHaveBeenCalledWith('onDropdownClose');
});
});
}); });
describe('mounted', () => { describe('mounted', () => {
......
...@@ -56,6 +56,16 @@ describe('DropdownValueCollapsedComponent', () => { ...@@ -56,6 +56,16 @@ describe('DropdownValueCollapsedComponent', () => {
}); });
}); });
describe('methods', () => {
describe('handleClick', () => {
it('emits onValueClick event on component', () => {
spyOn(vm, '$emit');
vm.handleClick();
expect(vm.$emit).toHaveBeenCalledWith('onValueClick');
});
});
});
describe('template', () => { describe('template', () => {
it('renders component container element with tooltip`', () => { it('renders component container element with tooltip`', () => {
expect(vm.$el.dataset.placement).toBe('left'); expect(vm.$el.dataset.placement).toBe('left');
......
...@@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do ...@@ -15,7 +15,7 @@ describe Gitlab::BitbucketImport::ProjectCreator do
has_wiki?: false) has_wiki?: false)
end end
let(:namespace) { create(:group, owner: user) } let(:namespace) { create(:group) }
let(:token) { "asdasd12345" } let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" } let(:secret) { "sekrettt" }
let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } } let(:access_params) { { bitbucket_access_token: token, bitbucket_access_token_secret: secret } }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::Wiki::CommitterWithHooks, seed_helper: true do describe Gitlab::Git::CommitterWithHooks, seed_helper: true do
shared_examples 'calling wiki hooks' do shared_examples 'calling wiki hooks' do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { project.owner } let(:user) { project.owner }
......
...@@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do ...@@ -12,7 +12,7 @@ describe Gitlab::GitlabImport::ProjectCreator do
owner: { name: "john" } owner: { name: "john" }
}.with_indifferent_access }.with_indifferent_access
end end
let(:namespace) { create(:group, owner: user) } let(:namespace) { create(:group) }
let(:token) { "asdffg" } let(:token) { "asdffg" }
let(:access_params) { { gitlab_access_token: token } } let(:access_params) { { gitlab_access_token: token } }
......
...@@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do ...@@ -9,7 +9,7 @@ describe Gitlab::GoogleCodeImport::ProjectCreator do
"repositoryUrls" => ["https://vim.googlecode.com/git/"] "repositoryUrls" => ["https://vim.googlecode.com/git/"]
) )
end end
let(:namespace) { create(:group, owner: user) } let(:namespace) { create(:group) }
before do before do
namespace.add_owner(user) namespace.add_owner(user)
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::LegacyGithubImport::ProjectCreator do describe Gitlab::LegacyGithubImport::ProjectCreator do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:namespace) { create(:group, owner: user) } let(:namespace) { create(:group) }
let(:repo) do let(:repo) do
OpenStruct.new( OpenStruct.new(
......
require 'rails_helper' require 'fast_spec_helper'
require_dependency 'gitlab'
describe Gitlab do describe Gitlab do
describe '.root' do
it 'returns the root path of the app' do
expect(described_class.root).to eq(Pathname.new(File.expand_path('../..', __dir__)))
end
end
describe '.com?' do describe '.com?' do
it 'is true when on GitLab.com' do it 'is true when on GitLab.com' do
stub_config_setting(url: 'https://gitlab.com') stub_config_setting(url: 'https://gitlab.com')
......
require 'spec_helper'
require Rails.root.join('db', 'migrate', '20180413022611_create_missing_namespace_for_internal_users.rb')
describe CreateMissingNamespaceForInternalUsers, :migration do
let(:users) { table(:users) }
let(:namespaces) { table(:namespaces) }
let(:routes) { table(:routes) }
internal_user_types = [:ghost]
internal_user_types << :support_bot if ActiveRecord::Base.connection.column_exists?(:users, :support_bot)
internal_user_types.each do |attr|
context "for #{attr} user" do
let(:internal_user) do
users.create!(email: 'test@example.com', projects_limit: 100, username: 'test', attr => true)
end
it 'creates the missing namespace' do
expect(namespaces.find_by(owner_id: internal_user.id)).to be_nil
migrate!
namespace = Namespace.find_by(type: nil, owner_id: internal_user.id)
route = namespace.route
expect(namespace.path).to eq(route.path)
expect(namespace.name).to eq(route.name)
end
it 'sets notification email' do
users.update(internal_user.id, notification_email: nil)
expect(users.find(internal_user.id).notification_email).to be_nil
migrate!
user = users.find(internal_user.id)
expect(user.notification_email).to eq(user.email)
end
end
end
end
...@@ -344,7 +344,7 @@ describe Project do ...@@ -344,7 +344,7 @@ describe Project do
let(:owner) { create(:user, name: 'Gitlab') } let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) }
let(:project) { create(:project, path: 'sample-project', namespace: namespace) } let(:project) { create(:project, path: 'sample-project', namespace: namespace) }
let(:group) { create(:group, name: 'Group', path: 'sample-group', owner: owner) } let(:group) { create(:group, name: 'Group', path: 'sample-group') }
context 'when nil argument' do context 'when nil argument' do
it 'returns nil' do it 'returns nil' do
......
...@@ -1164,8 +1164,12 @@ describe User do ...@@ -1164,8 +1164,12 @@ describe User do
end end
context 'with a group route matching the given path' do context 'with a group route matching the given path' do
let!(:group) { create(:group, path: 'group_path') }
context 'when the group namespace has an owner_id (legacy data)' do context 'when the group namespace has an owner_id (legacy data)' do
let!(:group) { create(:group, path: 'group_path', owner: user) } before do
group.update!(owner_id: user.id)
end
it 'returns nil' do it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil) expect(described_class.find_by_full_path('group_path')).to eq(nil)
...@@ -1173,8 +1177,6 @@ describe User do ...@@ -1173,8 +1177,6 @@ describe User do
end end
context 'when the group namespace does not have an owner_id' do context 'when the group namespace does not have an owner_id' do
let!(:group) { create(:group, path: 'group_path') }
it 'returns nil' do it 'returns nil' do
expect(described_class.find_by_full_path('group_path')).to eq(nil) expect(described_class.find_by_full_path('group_path')).to eq(nil)
end end
......
...@@ -321,7 +321,7 @@ describe ProjectPresenter do ...@@ -321,7 +321,7 @@ describe ProjectPresenter do
expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false, expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false,
label: 'Enable Auto DevOps', label: 'Enable Auto DevOps',
link: presenter.project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))) link: presenter.project_settings_ci_cd_path(project, anchor: 'autodevops-settings')))
end end
end end
end end
......
...@@ -685,7 +685,8 @@ describe API::Projects do ...@@ -685,7 +685,8 @@ describe API::Projects do
issues_enabled: false, issues_enabled: false,
merge_requests_enabled: false, merge_requests_enabled: false,
wiki_enabled: false, wiki_enabled: false,
request_access_enabled: true request_access_enabled: true,
jobs_enabled: true
}) })
post api("/projects/user/#{user.id}", admin), project post api("/projects/user/#{user.id}", admin), project
......
...@@ -212,6 +212,18 @@ describe API::Users do ...@@ -212,6 +212,18 @@ describe API::Users do
expect(json_response.last['id']).to eq(user.id) expect(json_response.last['id']).to eq(user.id)
end end
it 'returns users with 2fa enabled' do
admin
user
user_with_2fa = create(:user, :two_factor_via_otp)
get api('/users', admin), { two_factor: 'enabled' }
expect(response).to match_response_schema('public_api/v4/user/admins')
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq(user_with_2fa.id)
end
it 'returns 400 when provided incorrect sort params' do it 'returns 400 when provided incorrect sort params' do
get api('/users', admin), { order_by: 'magic', sort: 'asc' } get api('/users', admin), { order_by: 'magic', sort: 'asc' }
......
...@@ -59,8 +59,11 @@ describe Groups::NestedCreateService do ...@@ -59,8 +59,11 @@ describe Groups::NestedCreateService do
describe "#execute" do describe "#execute" do
it 'returns the group if it already existed' do it 'returns the group if it already existed' do
parent = create(:group, path: 'a-group', owner: user) parent = create(:group, path: 'a-group')
child = create(:group, path: 'a-sub-group', parent: parent, owner: user) child = create(:group, path: 'a-sub-group', parent: parent)
parent.add_owner(user)
child.add_owner(user)
expect(service.execute).to eq(child) expect(service.execute).to eq(child)
end end
......
...@@ -32,42 +32,19 @@ require 'rainbow/ext/string' ...@@ -32,42 +32,19 @@ require 'rainbow/ext/string'
# Requires supporting ruby files with custom matchers and macros, etc, # Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories. # in spec/support/ and its subdirectories.
# Requires helpers, and shared contexts/examples first since they're used in other support files
Dir[Rails.root.join("spec/support/helpers/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/shared_contexts/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/shared_examples/*.rb")].each { |f| require f }
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f } Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
RSpec.configure do |config| RSpec.configure do |config|
config.use_transactional_fixtures = false config.use_transactional_fixtures = false
config.use_instantiated_fixtures = false config.use_instantiated_fixtures = false
config.mock_with :rspec
config.verbose_retry = true config.verbose_retry = true
config.display_try_failure_messages = true config.display_try_failure_messages = true
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::ControllerHelpers, type: :view
config.include Devise::Test::IntegrationHelpers, type: :feature
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
config.include CookieHelper, :js
config.include InputHelper, :js
config.include SelectionHelper, :js
config.include InspectRequests, :js
config.include WaitForRequests, :js
config.include LiveDebugger, :js
config.include StubConfiguration
config.include EmailHelpers, :mailer, type: :mailer
config.include TestEnv
config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
config.include ApiHelpers, :api
config.include Gitlab::Routing, type: :routing
config.include MigrationsHelpers, :migration
config.include StubFeatureFlags
config.include StubENV
config.include ExpectOffense
config.infer_spec_type_from_file_location! config.infer_spec_type_from_file_location!
config.define_derived_metadata(file_path: %r{/spec/}) do |metadata| config.define_derived_metadata(file_path: %r{/spec/}) do |metadata|
...@@ -82,7 +59,33 @@ RSpec.configure do |config| ...@@ -82,7 +59,33 @@ RSpec.configure do |config|
metadata[:type] = match[1].singularize.to_sym if match metadata[:type] = match[1].singularize.to_sym if match
end end
config.raise_errors_for_deprecations! config.include ActiveJob::TestHelper
config.include ActiveSupport::Testing::TimeHelpers
config.include CycleAnalyticsHelpers
config.include ExpectOffense
config.include FactoryBot::Syntax::Methods
config.include FixtureHelpers
config.include GitlabRoutingHelper
config.include StubFeatureFlags
config.include StubGitlabCalls
config.include StubGitlabData
config.include TestEnv
config.include Devise::Test::ControllerHelpers, type: :controller
config.include Devise::Test::IntegrationHelpers, type: :feature
config.include LoginHelpers, type: :feature
config.include SearchHelpers, type: :feature
config.include EmailHelpers, :mailer, type: :mailer
config.include Warden::Test::Helpers, type: :request
config.include Gitlab::Routing, type: :routing
config.include Devise::Test::ControllerHelpers, type: :view
config.include ApiHelpers, :api
config.include CookieHelper, :js
config.include InputHelper, :js
config.include SelectionHelper, :js
config.include InspectRequests, :js
config.include WaitForRequests, :js
config.include LiveDebugger, :js
config.include MigrationsHelpers, :migration
if ENV['CI'] if ENV['CI']
# This includes the first try, i.e. tests will be run 4 times before failing. # This includes the first try, i.e. tests will be run 4 times before failing.
......
...@@ -60,6 +60,8 @@ Capybara::Screenshot.register_driver(:chrome) do |driver, path| ...@@ -60,6 +60,8 @@ Capybara::Screenshot.register_driver(:chrome) do |driver, path|
end end
RSpec.configure do |config| RSpec.configure do |config|
config.include CapybaraHelpers, type: :feature
config.before(:context, :js) do config.before(:context, :js) do
next if $capybara_server_already_started next if $capybara_server_already_started
......
...@@ -56,7 +56,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do ...@@ -56,7 +56,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end end
it "assigns variables" do it "assigns variables" do
project = create(:project, import_type: provider, creator_id: user.id) project = create(:project, import_type: provider, namespace: user.namespace)
stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo]) stub_client(repos: [repo, org_repo], orgs: [org], org_repos: [org_repo])
get :status get :status
...@@ -69,7 +69,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do ...@@ -69,7 +69,7 @@ shared_examples 'a GitHub-ish import controller: GET status' do
end end
it "does not show already added project" do it "does not show already added project" do
project = create(:project, import_type: provider, creator_id: user.id, import_source: 'asd/vim') project = create(:project, import_type: provider, namespace: user.namespace, import_source: 'asd/vim')
stub_client(repos: [repo], orgs: []) stub_client(repos: [repo], orgs: [])
get :status get :status
...@@ -257,11 +257,12 @@ shared_examples 'a GitHub-ish import controller: POST create' do ...@@ -257,11 +257,12 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end end
context 'user has chosen an existing nested namespace and name for the project', :postgresql do context 'user has chosen an existing nested namespace and name for the project', :postgresql do
let(:parent_namespace) { create(:group, name: 'foo', owner: user) } let(:parent_namespace) { create(:group, name: 'foo') }
let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) } let(:nested_namespace) { create(:group, name: 'bar', parent: parent_namespace) }
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
before do before do
parent_namespace.add_owner(user)
nested_namespace.add_owner(user) nested_namespace.add_owner(user)
end end
...@@ -307,7 +308,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do ...@@ -307,7 +308,7 @@ shared_examples 'a GitHub-ish import controller: POST create' do
context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do context 'user has chosen existent and non-existent nested namespaces and name for the project', :postgresql do
let(:test_name) { 'test_name' } let(:test_name) { 'test_name' }
let!(:parent_namespace) { create(:group, name: 'foo', owner: user) } let!(:parent_namespace) { create(:group, name: 'foo') }
before do before do
parent_namespace.add_owner(user) parent_namespace.add_owner(user)
......
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
# #
# Usage: # Usage:
# #
# ./spec/support/generate-seed-repo-rb > spec/support/seed_repo.rb # ./spec/support/generate-seed-repo-rb > spec/support/helpers/seed_repo.rb
# #
# #
......
...@@ -12,5 +12,5 @@ inflate the size of the gitlab-ce repository. ...@@ -12,5 +12,5 @@ inflate the size of the gitlab-ce repository.
- make changes in your local clone of gitlab-git-test - make changes in your local clone of gitlab-git-test
- run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git` - run `git push` which will push to your local source `gitlab-ce/spec/support/gitlab-git-test.git`
- in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit` - in gitlab-ce: run `spec/support/prepare-gitlab-git-test-for-commit`
- in gitlab-ce: `git add spec/support/seed_repo.rb spec/support/gitlab-git-test.git` - in gitlab-ce: `git add spec/support/helpers/seed_repo.rb spec/support/gitlab-git-test.git`
- commit your changes in gitlab-ce - commit your changes in gitlab-ce
...@@ -41,7 +41,3 @@ module CapybaraHelpers ...@@ -41,7 +41,3 @@ module CapybaraHelpers
page.driver.browser.manage.delete_cookie('_gitlab_session') page.driver.browser.manage.delete_cookie('_gitlab_session')
end end
end end
RSpec.configure do |config|
config.include CapybaraHelpers, type: :feature
end
...@@ -135,7 +135,3 @@ module CycleAnalyticsHelpers ...@@ -135,7 +135,3 @@ module CycleAnalyticsHelpers
end end
end end
end end
RSpec.configure do |config|
config.include CycleAnalyticsHelpers
end
...@@ -9,7 +9,3 @@ module FixtureHelpers ...@@ -9,7 +9,3 @@ module FixtureHelpers
File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename)) File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end end
end end
RSpec.configure do |config|
config.include FixtureHelpers
end
module GitlabVerifyHelpers
def collect_ranges(args = {})
verifier = described_class.new(args.merge(batch_size: 1))
collect_results(verifier).map { |range, _| range }
end
def collect_failures
verifier = described_class.new(batch_size: 1)
out = {}
collect_results(verifier).map { |_, failures| out.merge!(failures) }
out
end
def collect_results(verifier)
out = []
verifier.run_batches { |*args| out << args }
out
end
end
module ActiveRecord
class QueryRecorder
attr_reader :log, :cached
def initialize(&block)
@log = []
@cached = []
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end
def show_backtrace(values)
Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
caller.each { |line| Rails.logger.debug(" --> #{line}") }
end
def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
if values[:name]&.include?("CACHE")
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
end
end
def count
@log.count
end
def cached_count
@cached.count
end
def log_message
@log.join("\n\n")
end
end
end
module QuickActionsHelpers
def write_note(text)
Sidekiq::Testing.fake! do
page.within('.js-main-target-form') do
fill_in 'note[note]', with: text
find('.js-comment-submit-button').click
end
end
end
end
...@@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze ...@@ -9,7 +9,7 @@ TEST_MUTABLE_REPO_PATH = 'mutable-repo.git'.freeze
TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze TEST_BROKEN_REPO_PATH = 'broken-repo.git'.freeze
module SeedHelper module SeedHelper
GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __FILE__).freeze GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__).freeze
def ensure_seeds def ensure_seeds
if File.exist?(SEED_STORAGE_PATH) if File.exist?(SEED_STORAGE_PATH)
...@@ -108,11 +108,3 @@ bla/bla.txt ...@@ -108,11 +108,3 @@ bla/bla.txt
{ 'GIT_TEMPLATE_DIR' => '' } { 'GIT_TEMPLATE_DIR' => '' }
end end
end end
RSpec.configure do |config|
config.include SeedHelper, :seed_helper
config.before(:all, :seed_helper) do
ensure_seeds
end
end
# Helper allows you to sort items
#
# Params
# value - value for sorting
#
# Usage:
# include SortingHelper
#
# sorting_by('Oldest updated')
#
module SortingHelper
def sorting_by(value)
find('button.dropdown-toggle').click
page.within('.content ul.dropdown-menu.dropdown-menu-align-right li') do
click_link value
end
end
end
require 'active_support/core_ext/hash/transform_values'
require 'active_support/hash_with_indifferent_access'
module StubConfiguration module StubConfiguration
def stub_application_setting(messages) def stub_application_setting(messages)
add_predicates(messages) add_predicates(messages)
......
module StubConfiguration module StubObjectStorage
def stub_object_storage_uploader( def stub_object_storage_uploader(
config:, config:,
uploader:, uploader:,
......
shared_context 'JSON response' do
let(:json_response) { JSON.parse(response.body) }
end
RSpec.configure do |config| RSpec.configure do |config|
config.include_context 'JSON response' config.include_context 'JSON response'
config.include_context 'JSON response', type: :request config.include_context 'JSON response', type: :request
......
module ActiveRecord
class QueryRecorder
attr_reader :log, :cached
def initialize(&block)
@log = []
@cached = []
ActiveSupport::Notifications.subscribed(method(:callback), 'sql.active_record', &block)
end
def show_backtrace(values)
Rails.logger.debug("QueryRecorder SQL: #{values[:sql]}")
caller.each { |line| Rails.logger.debug(" --> #{line}") }
end
def callback(name, start, finish, message_id, values)
show_backtrace(values) if ENV['QUERY_RECORDER_DEBUG']
if values[:name]&.include?("CACHE")
@cached << values[:sql]
elsif !values[:name]&.include?("SCHEMA")
@log << values[:sql]
end
end
def count
@log.count
end
def cached_count
@cached.count
end
def log_message
@log.join("\n\n")
end
end
end
RSpec::Matchers.define :exceed_query_limit do |expected| RSpec::Matchers.define :exceed_query_limit do |expected|
supports_block_expectations supports_block_expectations
......
#!/usr/bin/env ruby #!/usr/bin/env ruby
abort unless [ abort unless [
system('spec/support/generate-seed-repo-rb', out: 'spec/support/seed_repo.rb'), system('spec/support/generate-seed-repo-rb', out: 'spec/support/helpers/seed_repo.rb'),
system('spec/support/unpack-gitlab-git-test') system('spec/support/unpack-gitlab-git-test')
].all? ].all?
......
RSpec.configure do |config|
config.include GitlabRoutingHelper
end
require_relative "helpers/stub_configuration"
require_relative "helpers/stub_object_storage"
require_relative "helpers/stub_env"
RSpec.configure do |config|
config.mock_with :rspec
config.raise_errors_for_deprecations!
config.include StubConfiguration
config.include StubObjectStorage
config.include StubENV
end
RSpec.configure do |config|
config.include SeedHelper, :seed_helper
config.before(:all, :seed_helper) do
ensure_seeds
end
end
shared_context 'JSON response' do
let(:json_response) { JSON.parse(response.body) }
end
...@@ -17,29 +17,3 @@ RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do ...@@ -17,29 +17,3 @@ RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do
end end
end end
end end
module GitlabVerifyHelpers
def collect_ranges(args = {})
verifier = described_class.new(args.merge(batch_size: 1))
collect_results(verifier).map { |range, _| range }
end
def collect_failures
verifier = described_class.new(batch_size: 1)
out = {}
collect_results(verifier).map { |_, failures| out.merge!(failures) }
out
end
def collect_results(verifier)
out = []
verifier.run_batches { |*args| out << args }
out
end
end
require 'spec_helper' require 'spec_helper'
describe 'projects/settings/ci_cd/_form' do describe 'projects/settings/ci_cd/_autodevops_form' do
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
before do before do
......
...@@ -14,7 +14,9 @@ describe IssueDueSchedulerWorker do ...@@ -14,7 +14,9 @@ describe IssueDueSchedulerWorker do
create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow) create(:issue, :closed, project: project_closed_issue, due_date: Date.tomorrow)
create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today) create(:issue, :opened, project: project_issue_due_another_day, due_date: Date.today)
expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async).with([[project1.id], [project2.id]]) expect(MailScheduler::IssueDueWorker).to receive(:bulk_perform_async) do |args|
expect(args).to match_array([[project1.id], [project2.id]])
end
described_class.new.perform described_class.new.perform
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