Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-02-13 21:08:59 +00:00
parent 6a9d7c009e
commit d466ee5042
91 changed files with 1487 additions and 732 deletions

View File

@ -106,6 +106,10 @@ Please view this file on the master branch, on stable branches it's out of date.
- Remove "creations" in gitlab_subscription_histories on gitlab.com. !22278
## 12.6.7
- No changes.
## 12.6.6
- No changes.

View File

@ -455,7 +455,7 @@ group :ed25519 do
end
# Gitaly GRPC protocol definitions
gem 'gitaly', '~> 1.85.0'
gem 'gitaly', '~> 1.86.0'
gem 'grpc', '~> 1.24.0'

View File

@ -375,7 +375,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
git (1.5.0)
gitaly (1.85.0)
gitaly (1.86.0)
grpc (~> 1.0)
github-markup (1.7.0)
gitlab-chronic (0.10.5)
@ -1230,7 +1230,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly (~> 1.85.0)
gitaly (~> 1.86.0)
github-markup (~> 1.7.0)
gitlab-chronic (~> 0.10.5)
gitlab-labkit (= 0.9.1)

View File

@ -1,9 +1,10 @@
<script>
import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
export default {
components: {
GlAlert,
GlLoadingIcon,
},
props: {
@ -17,9 +18,14 @@ export default {
isLoading: false,
};
},
computed: {
canDismiss() {
return !this.message.action;
},
},
methods: {
...mapActions(['setErrorMessage']),
clickAction() {
doAction() {
if (this.isLoading) return;
this.isLoading = true;
@ -33,28 +39,23 @@ export default {
this.isLoading = false;
});
},
clickFlash() {
if (!this.message.action) {
this.setErrorMessage(null);
}
dismiss() {
this.setErrorMessage(null);
},
},
};
</script>
<template>
<div class="flash-container flash-container-page" @click="clickFlash">
<div class="flash-alert" data-qa-selector="flash_alert">
<span v-html="message.text"> </span>
<button
v-if="message.action"
type="button"
class="flash-action text-white p-0 border-top-0 border-right-0 border-left-0 bg-transparent"
@click.stop.prevent="clickAction"
>
{{ message.actionText }}
<gl-loading-icon v-show="isLoading" inline />
</button>
</div>
</div>
<gl-alert
data-qa-selector="flash_alert"
variant="danger"
:dismissible="canDismiss"
:primary-button-text="message.actionText"
@dismiss="dismiss"
@primaryAction="doAction"
>
<span v-html="message.text"></span>
<gl-loading-icon v-show="isLoading" inline class="vertical-align-middle ml-1" />
</gl-alert>
</template>

View File

@ -1,5 +1,4 @@
<script>
import _ from 'underscore';
import { GlLoadingIcon } from '@gitlab/ui';
import StageColumnComponent from './stage_column_component.vue';
import GraphMixin from '../../mixins/graph_component_mixin';
@ -70,7 +69,7 @@ export default {
expandedTriggeredBy() {
return (
this.pipeline.triggered_by &&
_.isArray(this.pipeline.triggered_by) &&
Array.isArray(this.pipeline.triggered_by) &&
this.pipeline.triggered_by.find(el => el.isExpanded)
);
},

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isEmpty, escape as esc } from 'lodash';
import stageColumnMixin from '../../mixins/stage_column_mixin';
import JobItem from './job_item.vue';
import JobGroupDropdown from './job_group_dropdown.vue';
@ -39,12 +39,12 @@ export default {
},
computed: {
hasAction() {
return !_.isEmpty(this.action);
return !isEmpty(this.action);
},
},
methods: {
groupId(group) {
return `ci-badge-${_.escape(group.name)}`;
return `ci-badge-${esc(group.name)}`;
},
pipelineActionRequestComplete() {
this.$emit('refreshPipelineGraph');

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isEmpty } from 'lodash';
import { GlLink } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
@ -43,7 +43,7 @@ export default {
);
},
hasRef() {
return !_.isEmpty(this.pipeline.ref);
return !isEmpty(this.pipeline.ref);
},
},
methods: {

View File

@ -1,11 +1,11 @@
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { escape } from 'lodash';
import { __, sprintf } from '~/locale';
import popover from '~/vue_shared/directives/popover';
const popoverTitle = sprintf(
_.escape(
escape(
__(
`This pipeline makes use of a predefined CI/CD configuration enabled by %{strongStart}Auto DevOps.%{strongEnd}`,
),
@ -49,7 +49,7 @@ export default {
href="${this.autoDevopsHelpPath}"
target="_blank"
rel="noopener noreferrer nofollow">
${_.escape(__('Learn more about Auto DevOps'))}
${escape(__('Learn more about Auto DevOps'))}
</a>`,
};
},

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isEqual } from 'lodash';
import { __, sprintf, s__ } from '../../locale';
import createFlash from '../../flash';
import PipelinesService from '../services/pipelines_service';
@ -218,7 +218,7 @@ export default {
successCallback(resp) {
// Because we are polling & the user is interacting verify if the response received
// matches the last request made
if (_.isEqual(resp.config.params, this.requestData)) {
if (isEqual(resp.config.params, this.requestData)) {
this.store.storeCount(resp.data.count);
this.store.storePagination(resp.headers);
this.setCommonData(resp.data.pipelines);

View File

@ -1,4 +1,4 @@
import _ from 'underscore';
import { escape } from 'lodash';
export default {
props: {
@ -18,7 +18,7 @@ export default {
},
methods: {
capitalizeStageName(name) {
const escapedName = _.escape(name);
const escapedName = escape(name);
return escapedName.charAt(0).toUpperCase() + escapedName.slice(1);
},
isFirstColumn(index) {

View File

@ -1,5 +1,4 @@
import Vue from 'vue';
import _ from 'underscore';
export default class PipelineStore {
constructor() {
@ -61,7 +60,7 @@ export default class PipelineStore {
Vue.set(newPipeline, 'isLoading', false);
if (newPipeline.triggered_by) {
if (!_.isArray(newPipeline.triggered_by)) {
if (!Array.isArray(newPipeline.triggered_by)) {
Object.assign(newPipeline, { triggered_by: [newPipeline.triggered_by] });
}
this.parseTriggeredByPipelines(oldPipeline, newPipeline.triggered_by[0]);

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isString } from 'lodash';
import { mapState, mapActions, mapGetters } from 'vuex';
import PodBox from './pod_box.vue';
import Url from './url.vue';
@ -42,7 +42,7 @@ export default {
return this.func.name;
},
description() {
return _.isString(this.func.description) ? this.func.description : '';
return isString(this.func.description) ? this.func.description : '';
},
funcUrl() {
return this.func.url;

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isString } from 'lodash';
import Timeago from '~/vue_shared/components/time_ago_tooltip.vue';
import Url from './url.vue';
import { visitUrl } from '~/lib/utils/url_utility';
@ -20,7 +20,7 @@ export default {
return this.func.name;
},
description() {
if (!_.isString(this.func.description)) {
if (!isString(this.func.description)) {
return '';
}

View File

@ -5,6 +5,7 @@ import { __, sprintf } from '~/locale';
import { convertToFixedRange, isEqualTimeRanges, findTimeRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import DateTimePickerInput from './date_time_picker_input.vue';
import {
defaultTimeRanges,
@ -24,6 +25,7 @@ const events = {
export default {
components: {
Icon,
TooltipOnTruncate,
DateTimePickerInput,
GlFormGroup,
GlButton,
@ -149,61 +151,68 @@ export default {
};
</script>
<template>
<gl-dropdown
:text="timeWindowText"
class="date-time-picker"
menu-class="date-time-picker-menu"
v-bind="$attrs"
toggle-class="w-100 text-truncate"
<tooltip-on-truncate
:title="timeWindowText"
:truncate-target="elem => elem.querySelector('.date-time-picker-toggle')"
placement="top"
class="d-inline-block"
>
<div class="d-flex justify-content-between gl-p-2">
<gl-form-group
:label="__('Custom range')"
label-for="custom-from-time"
label-class="gl-pb-1"
class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
>
<div class="gl-pt-2">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
:label="__('From')"
:state="startInputValid"
/>
<date-time-picker-input
id="custom-time-to"
v-model="endInput"
:label="__('To')"
:state="endInputValid"
/>
</div>
<gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button variant="success" :disabled="!isValid" @click="setFixedRange()">
{{ __('Apply') }}
</gl-button>
</gl-form-group>
</gl-form-group>
<gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
<template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span>
</template>
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
<gl-dropdown
:text="timeWindowText"
v-bind="$attrs"
class="date-time-picker w-100"
menu-class="date-time-picker-menu"
toggle-class="date-time-picker-toggle text-truncate"
>
<div class="d-flex justify-content-between gl-p-2">
<gl-form-group
:label="__('Custom range')"
label-for="custom-from-time"
label-class="gl-pb-1"
class="custom-time-range-form-group col-md-7 gl-pl-1 gl-pr-0 m-0"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
</gl-dropdown-item>
</gl-form-group>
</div>
</gl-dropdown>
<div class="gl-pt-2">
<date-time-picker-input
id="custom-time-from"
v-model="startInput"
:label="__('From')"
:state="startInputValid"
/>
<date-time-picker-input
id="custom-time-to"
v-model="endInput"
:label="__('To')"
:state="endInputValid"
/>
</div>
<gl-form-group>
<gl-button @click="closeDropdown">{{ __('Cancel') }}</gl-button>
<gl-button variant="success" :disabled="!isValid" @click="setFixedRange()">
{{ __('Apply') }}
</gl-button>
</gl-form-group>
</gl-form-group>
<gl-form-group label-for="group-id-dropdown" class="col-md-5 gl-pl-1 gl-pr-1 m-0">
<template #label>
<span class="gl-pl-5">{{ __('Quick range') }}</span>
</template>
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
>
<icon
name="mobile-issue-close"
class="align-bottom"
:class="{ invisible: !isOptionActive(option) }"
/>
{{ option.label }}
</gl-dropdown-item>
</gl-form-group>
</div>
</gl-dropdown>
</tooltip-on-truncate>
</template>

View File

@ -1,5 +1,5 @@
<script>
import _ from 'underscore';
import { isFunction } from 'lodash';
import tooltip from '../directives/tooltip';
export default {
@ -28,16 +28,18 @@ export default {
showTooltip: false,
};
},
watch: {
title() {
// Wait on $nextTick in case of slot width changes
this.$nextTick(this.updateTooltip);
},
},
mounted() {
const target = this.selectTarget();
if (target && target.scrollWidth > target.offsetWidth) {
this.showTooltip = true;
}
this.updateTooltip();
},
methods: {
selectTarget() {
if (_.isFunction(this.truncateTarget)) {
if (isFunction(this.truncateTarget)) {
return this.truncateTarget(this.$el);
} else if (this.truncateTarget === 'child') {
return this.$el.childNodes[0];
@ -45,6 +47,10 @@ export default {
return this.$el;
},
updateTooltip() {
const target = this.selectTarget();
this.showTooltip = Boolean(target && target.scrollWidth > target.offsetWidth);
},
},
};
</script>

View File

@ -901,7 +901,9 @@ class Project < ApplicationRecord
if Gitlab::UrlSanitizer.valid?(value)
import_url = Gitlab::UrlSanitizer.new(value)
super(import_url.sanitized_url)
create_or_update_import_data(credentials: import_url.credentials)
credentials = import_url.credentials.to_h.transform_values { |value| CGI.unescape(value.to_s) }
create_or_update_import_data(credentials: credentials)
else
super(value)
end

View File

@ -134,15 +134,6 @@ class Repository
end
end
# the opts are:
# - :path
# - :limit
# - :offset
# - :skip_merges
# - :after
# - :before
# - :all
# - :first_parent
def commits(ref = nil, opts = {})
options = {
repo: raw_repository,
@ -155,7 +146,8 @@ class Repository
after: opts[:after],
before: opts[:before],
all: !!opts[:all],
first_parent: !!opts[:first_parent]
first_parent: !!opts[:first_parent],
order: opts[:order]
}
commits = Gitlab::Git::Commit.where(options)

View File

@ -19,8 +19,10 @@ module Users
LEASE_TIMEOUT = 1.minute.to_i
# user - The User for which to refresh the authorized projects.
def initialize(user)
def initialize(user, incorrect_auth_found_callback: nil, missing_auth_found_callback: nil)
@user = user
@incorrect_auth_found_callback = incorrect_auth_found_callback
@missing_auth_found_callback = missing_auth_found_callback
# We need an up to date User object that has access to all relations that
# may have been created earlier. The only way to ensure this is to reload
@ -55,6 +57,10 @@ module Users
# rows not in the new list or with a different access level should be
# removed.
if !fresh[project_id] || fresh[project_id] != row.access_level
if incorrect_auth_found_callback
incorrect_auth_found_callback.call(project_id, row.access_level)
end
array << row.project_id
end
end
@ -63,6 +69,10 @@ module Users
# rows not in the old list or with a different access level should be
# added.
if !current[project_id] || current[project_id].access_level != level
if missing_auth_found_callback
missing_auth_found_callback.call(project_id, level)
end
array << [user.id, project_id, level]
end
end
@ -104,5 +114,9 @@ module Users
def fresh_authorizations
Gitlab::ProjectAuthorizations.new(user).calculate
end
private
attr_reader :incorrect_auth_found_callback, :missing_auth_found_callback
end
end

View File

@ -0,0 +1,5 @@
---
title: Add tooltip when dates in date picker are too long
merge_request: 24664
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: 'API: Ability to list commits in order (--topo-order)'
merge_request: 24702
author:
type: added

View File

@ -0,0 +1,5 @@
---
title: Fix Web IDE alert message look and feel
merge_request: 23300
author: Sean Nichols
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Avoid double encoding of credential while importing a Project by URL
merge_request: 24514
author:
type: fixed

View File

@ -1,5 +0,0 @@
---
title: Fix autocomplete limitation bug
merge_request: 25127
author:
type: fixed

View File

@ -0,0 +1,5 @@
---
title: Separate access entities into own class files
merge_request: 24845
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Separate environment entities into own class files
merge_request: 24951
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Separate JobRequest entities into own class files
merge_request: 24977
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Separate page domain entities into own class files
merge_request: 24987
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Separate badge entities into own class files
merge_request: 25116
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Separate cluster entities into own class files
merge_request: 25121
author: Rajendra Kadam
type: added

View File

@ -0,0 +1,5 @@
---
title: Replace underscore with lodash for ./app/assets/javascripts/serverless
merge_request: 25011
author: Tobias Spagert
type: other

View File

@ -0,0 +1,43 @@
# frozen_string_literal: true
class ScheduleRecalculateProjectAuthorizations < ActiveRecord::Migration[5.1]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
MIGRATION = 'RecalculateProjectAuthorizations'
BATCH_SIZE = 2_500
DELAY_INTERVAL = 2.minutes.to_i
disable_ddl_transaction!
class Namespace < ActiveRecord::Base
include ::EachBatch
self.table_name = 'namespaces'
end
class ProjectAuthorization < ActiveRecord::Base
include ::EachBatch
self.table_name = 'project_authorizations'
end
def up
say "Scheduling #{MIGRATION} jobs"
max_group_id = Namespace.where(type: 'Group').maximum(:id)
project_authorizations = ProjectAuthorization.where('project_id <= ?', max_group_id)
.select(:user_id)
.distinct
project_authorizations.each_batch(of: BATCH_SIZE, column: :user_id) do |authorizations, index|
delay = index * DELAY_INTERVAL
user_ids = authorizations.map(&:user_id)
BackgroundMigrationWorker.perform_in(delay, MIGRATION, [user_ids])
end
end
def down
end
end

View File

@ -18,6 +18,7 @@ GET /projects/:id/repository/commits
| `all` | boolean | no | Retrieve every commit from the repository |
| `with_stats` | boolean | no | Stats about each commit will be added to the response |
| `first_parent` | boolean | no | Follow only the first parent commit upon seeing a merge commit |
| `order` | string | no | List commits in order. Possible value: [`topo`](https://git-scm.com/docs/git-log#Documentation/git-log.txt---topo-order). |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/repository/commits"

View File

@ -84,5 +84,6 @@ Ignoring an error will prevent it from appearing in the [Error Tracking List](#e
From within the [Error Details](#error-details) page you can resolve a Sentry error by
clicking the **Resolve** button near the top of the page.
Marking an error as resolved indicates that the error has stopped firing events. If another event
occurs, the error reverts to unresolved.
Marking an error as resolved indicates that the error has stopped firing events. If a GitLab issue is linked to the error, then the issue will be closed.
If another event occurs, the error reverts to unresolved.

View File

@ -38,6 +38,7 @@ module API
optional :all, type: Boolean, desc: 'Every commit will be returned'
optional :with_stats, type: Boolean, desc: 'Stats about each commit will be added to the response'
optional :first_parent, type: Boolean, desc: 'Only include the first parent of merges'
optional :order, type: String, desc: 'List commits in order', values: %w[topo]
use :pagination
end
get ':id/repository/commits' do
@ -49,6 +50,7 @@ module API
all = params[:all]
with_stats = params[:with_stats]
first_parent = params[:first_parent]
order = params[:order]
commits = user_project.repository.commits(ref,
path: path,
@ -57,7 +59,8 @@ module API
before: before,
after: after,
all: all,
first_parent: first_parent)
first_parent: first_parent,
order: order)
commit_count =
if all || path || before || after || first_parent

View File

@ -129,40 +129,6 @@ module API
end
end
class Namespace < NamespaceBasic
expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _|
namespace.users_with_descendants.count
end
def expose_members_count_with_descendants?(namespace, opts)
namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
end
end
class MemberAccess < Grape::Entity
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
::NotificationSetting.levels[member.notification_setting.level]
end
end
end
class ProjectAccess < MemberAccess
end
class GroupAccess < MemberAccess
end
class NotificationSetting < Grape::Entity
expose :level
expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
::NotificationSetting.email_events.each do |event|
expose event
end
end
end
class Trigger < Grape::Entity
include ::API::Helpers::Presentable
@ -204,39 +170,6 @@ module API
expose :variables, using: Entities::Variable
end
class EnvironmentBasic < Grape::Entity
expose :id, :name, :slug, :external_url
end
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at, :updated_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
expose :deployable, using: Entities::Job
expose :status
end
class Environment < EnvironmentBasic
expose :project, using: Entities::BasicProjectDetails
expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
expose :state
end
class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
end
class License < LicenseBasic
expose :popular?, as: :popular
expose(:description) { |license| license.meta['description'] }
expose(:conditions) { |license| license.meta['conditions'] }
expose(:permissions) { |license| license.meta['permissions'] }
expose(:limitations) { |license| license.meta['limitations'] }
expose :content
end
class ImpersonationToken < PersonalAccessToken
expose :impersonation
end
@ -267,93 +200,6 @@ module API
end
end
module JobRequest
class JobInfo < Grape::Entity
expose :name, :stage
expose :project_id, :project_name
end
class GitInfo < Grape::Entity
expose :repo_url, :ref, :sha, :before_sha
expose :ref_type
expose :refspecs
expose :git_depth, as: :depth
end
class RunnerInfo < Grape::Entity
expose :metadata_timeout, as: :timeout
expose :runner_session_url
end
class Step < Grape::Entity
expose :name, :script, :timeout, :when, :allow_failure
end
class Port < Grape::Entity
expose :number, :protocol, :name
end
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: JobRequest::Port
end
class Service < Image
expose :alias, :command
end
class Artifacts < Grape::Entity
expose :name
expose :untracked
expose :paths
expose :when
expose :expire_in
expose :artifact_type
expose :artifact_format
end
class Cache < Grape::Entity
expose :key, :untracked, :paths, :policy
end
class Credentials < Grape::Entity
expose :type, :url, :username, :password
end
class Dependency < Grape::Entity
expose :id, :name, :token
expose :artifacts_file, using: JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
class Response < Grape::Entity
expose :id
expose :token
expose :allow_git_fetch
expose :job_info, using: JobInfo do |model|
model
end
expose :git_info, using: GitInfo do |model|
model
end
expose :runner_info, using: RunnerInfo do |model|
model
end
expose :variables
expose :steps, using: Step
expose :image, using: Image
expose :services, using: Service
expose :artifacts, using: Artifacts
expose :cache, using: Cache
expose :credentials, using: Credentials
expose :all_dependencies, as: :dependencies, using: Dependency
expose :features
end
end
class UserAgentDetail < Grape::Entity
expose :user_agent
expose :ip_address
@ -370,45 +216,6 @@ module API
expose :expiration
end
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
expose :certificate
expose :certificate_text
end
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
expose :project_id
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
expose :auto_ssl_enabled
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificateExpiration do |pages_domain|
pages_domain
end
end
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
expose :auto_ssl_enabled
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: PagesDomainCertificate do |pages_domain|
pages_domain
end
end
class Application < Grape::Entity
expose :id
expose :uid, as: :application_id
@ -437,49 +244,6 @@ module API
expose :project_id
end
class BasicBadgeDetails < Grape::Entity
expose :name
expose :link_url
expose :image_url
expose :rendered_link_url do |badge, options|
badge.rendered_link_url(options.fetch(:project, nil))
end
expose :rendered_image_url do |badge, options|
badge.rendered_image_url(options.fetch(:project, nil))
end
end
class Badge < BasicBadgeDetails
expose :id
expose :kind do |badge|
badge.type == 'ProjectBadge' ? 'project' : 'group'
end
end
class ResourceLabelEvent < Grape::Entity
expose :id
expose :user, using: Entities::UserBasic
expose :created_at
expose :resource_type do |event, options|
event.issuable.class.name
end
expose :resource_id do |event, options|
event.issuable.id
end
expose :label, using: Entities::LabelBasic
expose :action
end
class Suggestion < Grape::Entity
expose :id
expose :from_line
expose :to_line
expose :appliable?, as: :appliable
expose :applied
expose :from_content
expose :to_content
end
module Platform
class Kubernetes < Grape::Entity
expose :api_url
@ -501,23 +265,6 @@ module API
end
end
class Cluster < Grape::Entity
expose :id, :name, :created_at, :domain
expose :provider_type, :platform_type, :environment_scope, :cluster_type
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
expose :provider_gcp, using: Entities::Provider::Gcp
expose :management_project, using: Entities::ProjectIdentity
end
class ClusterProject < Cluster
expose :project, using: Entities::BasicProjectDetails
end
class ClusterGroup < Cluster
expose :group, using: Entities::BasicGroupDetails
end
module InternalPostReceive
class Message < Grape::Entity
expose :message

12
lib/api/entities/badge.rb Normal file
View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
class Badge < Entities::BasicBadgeDetails
expose :id
expose :kind do |badge|
badge.type == 'ProjectBadge' ? 'project' : 'group'
end
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module API
module Entities
class BasicBadgeDetails < Grape::Entity
expose :name
expose :link_url
expose :image_url
expose :rendered_link_url do |badge, options|
badge.rendered_link_url(options.fetch(:project, nil))
end
expose :rendered_image_url do |badge, options|
badge.rendered_image_url(options.fetch(:project, nil))
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
class Cluster < Grape::Entity
expose :id, :name, :created_at, :domain
expose :provider_type, :platform_type, :environment_scope, :cluster_type
expose :user, using: Entities::UserBasic
expose :platform_kubernetes, using: Entities::Platform::Kubernetes
expose :provider_gcp, using: Entities::Provider::Gcp
expose :management_project, using: Entities::ProjectIdentity
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class ClusterGroup < Entities::Cluster
expose :group, using: Entities::BasicGroupDetails
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class ClusterProject < Entities::Cluster
expose :project, using: Entities::BasicProjectDetails
end
end
end

View File

@ -0,0 +1,13 @@
# frozen_string_literal: true
module API
module Entities
class Deployment < Grape::Entity
expose :id, :iid, :ref, :sha, :created_at, :updated_at
expose :user, using: Entities::UserBasic
expose :environment, using: Entities::EnvironmentBasic
expose :deployable, using: Entities::Job
expose :status
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
class Environment < Entities::EnvironmentBasic
expose :project, using: Entities::BasicProjectDetails
expose :last_deployment, using: Entities::Deployment, if: { last_deployment: true }
expose :state
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
module API
module Entities
class EnvironmentBasic < Grape::Entity
expose :id, :name, :slug, :external_url
end
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module API
module Entities
class GroupAccess < MemberAccess
end
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Artifacts < Grape::Entity
expose :name
expose :untracked
expose :paths
expose :when
expose :expire_in
expose :artifact_type
expose :artifact_format
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Cache < Grape::Entity
expose :key, :untracked, :paths, :policy
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Credentials < Grape::Entity
expose :type, :url, :username, :password
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Dependency < Grape::Entity
expose :id, :name, :token
expose :artifacts_file, using: Entities::JobArtifactFile, if: ->(job, _) { job.artifacts? }
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class GitInfo < Grape::Entity
expose :repo_url, :ref, :sha, :before_sha
expose :ref_type
expose :refspecs
expose :git_depth, as: :depth
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Image < Grape::Entity
expose :name, :entrypoint
expose :ports, using: Entities::JobRequest::Port
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class JobInfo < Grape::Entity
expose :name, :stage
expose :project_id, :project_name
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Port < Grape::Entity
expose :number, :protocol, :name
end
end
end
end

View File

@ -0,0 +1,35 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Response < Grape::Entity
expose :id
expose :token
expose :allow_git_fetch
expose :job_info, using: Entities::JobRequest::JobInfo do |model|
model
end
expose :git_info, using: Entities::JobRequest::GitInfo do |model|
model
end
expose :runner_info, using: Entities::JobRequest::RunnerInfo do |model|
model
end
expose :variables
expose :steps, using: Entities::JobRequest::Step
expose :image, using: Entities::JobRequest::Image
expose :services, using: Entities::JobRequest::Service
expose :artifacts, using: Entities::JobRequest::Artifacts
expose :cache, using: Entities::JobRequest::Cache
expose :credentials, using: Entities::JobRequest::Credentials
expose :all_dependencies, as: :dependencies, using: Entities::JobRequest::Dependency
expose :features
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class RunnerInfo < Grape::Entity
expose :metadata_timeout, as: :timeout
expose :runner_session_url
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Service < Entities::JobRequest::Image
expose :alias, :command
end
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
module JobRequest
class Step < Grape::Entity
expose :name, :script, :timeout, :when, :allow_failure
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
class License < Entities::LicenseBasic
expose :popular?, as: :popular
expose(:description) { |license| license.meta['description'] }
expose(:conditions) { |license| license.meta['conditions'] }
expose(:permissions) { |license| license.meta['permissions'] }
expose(:limitations) { |license| license.meta['limitations'] }
expose :content
end
end
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
module API
module Entities
class LicenseBasic < Grape::Entity
expose :key, :name, :nickname
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
class MemberAccess < Grape::Entity
expose :access_level
expose :notification_level do |member, options|
if member.notification_setting
::NotificationSetting.levels[member.notification_setting.level]
end
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
class Namespace < Entities::NamespaceBasic
expose :members_count_with_descendants, if: -> (namespace, opts) { expose_members_count_with_descendants?(namespace, opts) } do |namespace, _|
namespace.users_with_descendants.count
end
def expose_members_count_with_descendants?(namespace, opts)
namespace.kind == 'group' && Ability.allowed?(opts[:current_user], :admin_group, namespace)
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module API
module Entities
class NotificationSetting < Grape::Entity
expose :level
expose :events, if: ->(notification_setting, _) { notification_setting.custom? } do
::NotificationSetting.email_events.each do |event|
expose event
end
end
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
module API
module Entities
class PagesDomain < Grape::Entity
expose :domain
expose :url
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
expose :auto_ssl_enabled
expose :certificate,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: Entities::PagesDomainCertificate do |pages_domain|
pages_domain
end
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module API
module Entities
class PagesDomainBasic < Grape::Entity
expose :domain
expose :url
expose :project_id
expose :verified?, as: :verified
expose :verification_code, as: :verification_code
expose :enabled_until
expose :auto_ssl_enabled
expose :certificate,
as: :certificate_expiration,
if: ->(pages_domain, _) { pages_domain.certificate? },
using: Entities::PagesDomainCertificateExpiration do |pages_domain|
pages_domain
end
end
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
module API
module Entities
class PagesDomainCertificate < Grape::Entity
expose :subject
expose :expired?, as: :expired
expose :certificate
expose :certificate_text
end
end
end

View File

@ -0,0 +1,8 @@
# frozen_string_literal: true
module API
module Entities
class ProjectAccess < Entities::MemberAccess
end
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
module API
module Entities
class ResourceLabelEvent < Grape::Entity
expose :id
expose :user, using: Entities::UserBasic
expose :created_at
expose :resource_type do |event, options|
event.issuable.class.name
end
expose :resource_id do |event, options|
event.issuable.id
end
expose :label, using: Entities::LabelBasic
expose :action
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module API
module Entities
class Suggestion < Grape::Entity
expose :id
expose :from_line
expose :to_line
expose :appliable?, as: :appliable
expose :applied
expose :from_content
expose :to_content
end
end
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
module Gitlab
module BackgroundMigration
# rubocop:disable Style/Documentation
class RecalculateProjectAuthorizations
def perform(user_ids)
user_ids.each do |user_id|
user = User.find_by(id: user_id)
next unless user
service = Users::RefreshAuthorizedProjectsService.new(
user,
incorrect_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Removing ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end,
missing_auth_found_callback:
->(project_id, access_level) do
logger.info(message: 'Creating ProjectAuthorizations',
user_id: user.id,
project_id: project_id,
access_level: access_level)
end
)
service.execute
end
end
private
def logger
@logger ||= Gitlab::BackgroundMigration::Logger.build
end
end
end
end

View File

@ -353,11 +353,7 @@ module Gitlab
def fetch_blob(sha, path)
return unless sha
# Load only patch_hard_limit_bytes number of bytes for the blob
# Because otherwise, it is too large to be displayed
Blob.lazy(
repository.project, sha, path,
blob_size_limit: Gitlab::Git::Diff.patch_hard_limit_bytes)
Blob.lazy(repository.project, sha, path)
end
def total_blob_lines(blob)

View File

@ -130,8 +130,7 @@ module Gitlab
# :skip is the number of commits to skip
# :order is the commits order and allowed value is :none (default), :date,
# :topo, or any combination of them (in an array). Commit ordering types
# are documented here:
# http://www.rubydoc.info/github/libgit2/rugged/Rugged#SORT_NONE-constant)
# are documented here: https://git-scm.com/docs/git-log#_commit_ordering
def find_all(repo, options = {})
wrapped_gitaly_errors do
Gitlab::GitalyClient::CommitService.new(repo).find_all_commits(options)

View File

@ -324,6 +324,7 @@ module Gitlab
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
request.before = GitalyClient.timestamp(options[:before]) if options[:before]
request.revision = encode_binary(options[:ref]) if options[:ref]
request.order = options[:order].upcase if options[:order].present?
request.paths = encode_repeated(Array(options[:path])) if options[:path].present?

View File

@ -68,12 +68,10 @@ module Gitlab
.select([namespaces[:id], members[:access_level]])
.except(:order)
if Feature.enabled?(:share_group_with_group, default_enabled: true)
# Namespaces shared with any of the group
cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level'])
.joins(join_group_group_links)
.joins(join_members_on_group_group_links)
end
# Namespaces shared with any of the group
cte << Group.select([namespaces[:id], 'group_group_links.group_access AS access_level'])
.joins(join_group_group_links)
.joins(join_members_on_group_group_links)
# Sub groups of any groups the user is a member of.
cte << Group.select([
@ -114,6 +112,8 @@ module Gitlab
members = Member.arel_table
cond = group_group_links[:shared_with_group_id].eq(members[:source_id])
.and(members[:source_type].eq('Namespace'))
.and(members[:requested_at].eq(nil))
.and(members[:user_id].eq(user.id))
Arel::Nodes::InnerJoin.new(members, Arel::Nodes::On.new(cond))
end

View File

@ -201,7 +201,6 @@
"yarn-deduplicate": "^1.1.1"
},
"resolutions": {
"at.js": "https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe",
"vue-jest/ts-jest": "24.0.0",
"monaco-editor": "0.18.1"
},

View File

@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
context 'Create', :smoke do
context 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/205511', type: :bug } do
describe 'Snippet creation' do
it 'User creates a snippet' do
Flow::Login.sign_in

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
FactoryBot.define do
factory :project_authorization do
user
project
access_level { Gitlab::Access::REPORTER }
end
end

View File

@ -1,4 +1,4 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import ErrorMessage from '~/ide/components/error_message.vue';
@ -15,7 +15,7 @@ describe('IDE error message component', () => {
actions: { setErrorMessage: setErrorMessageMock },
});
wrapper = shallowMount(ErrorMessage, {
wrapper = mount(ErrorMessage, {
propsData: {
message: {
text: 'some text',
@ -38,15 +38,18 @@ describe('IDE error message component', () => {
wrapper = null;
});
const findDismissButton = () => wrapper.find('button[aria-label=Dismiss]');
const findActionButton = () => wrapper.find('button.gl-alert-action');
it('renders error message', () => {
const text = 'error message';
createComponent({ text });
expect(wrapper.text()).toContain(text);
});
it('clears error message on click', () => {
it('clears error message on dismiss click', () => {
createComponent();
wrapper.trigger('click');
findDismissButton().trigger('click');
expect(setErrorMessageMock).toHaveBeenCalledWith(expect.any(Object), null, undefined);
});
@ -68,29 +71,27 @@ describe('IDE error message component', () => {
});
it('renders action button', () => {
const button = wrapper.find('button');
const button = findActionButton();
expect(button.exists()).toBe(true);
expect(button.text()).toContain(message.actionText);
});
it('does not clear error message on click', () => {
wrapper.trigger('click');
expect(setErrorMessageMock).not.toHaveBeenCalled();
it('does not show dismiss button', () => {
expect(findDismissButton().exists()).toBe(false);
});
it('dispatches action', () => {
wrapper.find('button').trigger('click');
findActionButton().trigger('click');
expect(actionMock).toHaveBeenCalledWith(message.actionPayload);
});
it('does not dispatch action when already loading', () => {
wrapper.find('button').trigger('click');
findActionButton().trigger('click');
actionMock.mockReset();
return wrapper.vm.$nextTick(() => {
wrapper.find('button').trigger('click');
findActionButton().trigger('click');
return wrapper.vm.$nextTick().then(() => {
expect(actionMock).not.toHaveBeenCalled();
@ -106,7 +107,7 @@ describe('IDE error message component', () => {
resolveAction = resolve;
}),
);
wrapper.find('button').trigger('click');
findActionButton().trigger('click');
return wrapper.vm.$nextTick(() => {
expect(wrapper.find(GlLoadingIcon).isVisible()).toBe(true);
@ -115,7 +116,7 @@ describe('IDE error message component', () => {
});
it('hides loading icon when operation finishes', () => {
wrapper.find('button').trigger('click');
findActionButton().trigger('click');
return actionMock()
.then(() => wrapper.vm.$nextTick())
.then(() => {

View File

@ -61,14 +61,14 @@ describe('ide component, non-empty repo', () => {
});
it('shows error message when set', done => {
expect(vm.$el.querySelector('.flash-container')).toBe(null);
expect(vm.$el.querySelector('.gl-alert')).toBe(null);
vm.$store.state.errorMessage = {
text: 'error',
};
vm.$nextTick(() => {
expect(vm.$el.querySelector('.flash-container')).not.toBe(null);
expect(vm.$el.querySelector('.gl-alert')).not.toBe(null);
done();
});

View File

@ -1,149 +1,160 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, shallowMount } from '@vue/test-utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
const TEST_TITLE = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
const STYLE_TRUNCATED = 'display: inline-block; max-width: 20px;';
const STYLE_NORMAL = 'display: inline-block; max-width: 1000px;';
const TEXT_SHORT = 'lorem';
const TEXT_LONG = 'lorem-ipsum-dolar-sit-amit-consectur-adipiscing-elit-sed-do';
const localVue = createLocalVue();
const TEXT_TRUNCATE = 'white-space: nowrap; overflow:hidden;';
const STYLE_NORMAL = `${TEXT_TRUNCATE} display: inline-block; max-width: 1000px;`; // does not overflows
const STYLE_OVERFLOWED = `${TEXT_TRUNCATE} display: inline-block; max-width: 50px;`; // overflowed when text is long
const createElementWithStyle = (style, content) => `<a href="#" style="${style}">${content}</a>`;
describe('TooltipOnTruncate component', () => {
let wrapper;
let parent;
const createComponent = ({ propsData, ...options } = {}) => {
wrapper = shallowMount(localVue.extend(TooltipOnTruncate), {
localVue,
wrapper = shallowMount(TooltipOnTruncate, {
attachToDocument: true,
propsData: {
title: TEST_TITLE,
...propsData,
},
attrs: {
style: STYLE_OVERFLOWED,
},
...options,
});
};
const createWrappedComponent = ({ propsData, ...options }) => {
// set a parent around the tested component
parent = mount(
{
props: {
title: { default: '' },
},
template: `
<TooltipOnTruncate :title="title" truncate-target="child" style="${STYLE_OVERFLOWED}">
<div>{{title}}</div>
</TooltipOnTruncate>
`,
components: {
TooltipOnTruncate,
},
},
{
propsData: { ...propsData },
attachToDocument: true,
...options,
},
);
wrapper = parent.find(TooltipOnTruncate);
};
const hasTooltip = () => wrapper.classes('js-show-tooltip');
afterEach(() => {
wrapper.destroy();
});
const hasTooltip = () => wrapper.classes('js-show-tooltip');
describe('with default target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_TRUNCATED,
propsData: {
title: TEXT_LONG,
},
slots: {
default: [TEST_TITLE],
default: [TEXT_LONG],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEST_TITLE);
expect(wrapper.attributes('data-placement')).toEqual('top');
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top');
});
});
it('does not render tooltip if normal', done => {
it('does not render tooltip if normal', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
propsData: {
title: TEXT_SHORT,
},
slots: {
default: [TEST_TITLE],
default: [TEXT_SHORT],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('with child target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
},
propsData: {
title: TEXT_LONG,
truncateTarget: 'child',
},
slots: {
default: createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE),
default: createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
});
});
it('does not render tooltip if normal', done => {
it('does not render tooltip if normal', () => {
createComponent({
propsData: {
truncateTarget: 'child',
},
slots: {
default: createElementWithStyle(STYLE_NORMAL, TEST_TITLE),
default: createElementWithStyle(STYLE_NORMAL, TEXT_LONG),
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(false);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('with fn target', () => {
it('renders tooltip if truncated', done => {
it('renders tooltip if truncated', () => {
createComponent({
attrs: {
style: STYLE_NORMAL,
},
propsData: {
title: TEXT_LONG,
truncateTarget: el => el.childNodes[1],
},
slots: {
default: [
createElementWithStyle('', TEST_TITLE),
createElementWithStyle(STYLE_TRUNCATED, TEST_TITLE),
createElementWithStyle('', TEXT_LONG),
createElementWithStyle(STYLE_OVERFLOWED, TEXT_LONG),
],
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
});
});
});
describe('placement', () => {
it('sets data-placement when tooltip is rendered', done => {
it('sets data-placement when tooltip is rendered', () => {
const placement = 'bottom';
createComponent({
@ -151,21 +162,75 @@ describe('TooltipOnTruncate component', () => {
placement,
},
attrs: {
style: STYLE_TRUNCATED,
style: STYLE_OVERFLOWED,
},
slots: {
default: TEST_TITLE,
default: TEXT_LONG,
},
});
wrapper.vm
.$nextTick()
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-placement')).toEqual(placement);
})
.then(done)
.catch(done.fail);
return wrapper.vm.$nextTick().then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-placement')).toEqual(placement);
});
});
});
describe('updates when title and slot content changes', () => {
describe('is initialized with a long text', () => {
beforeEach(() => {
createWrappedComponent({
propsData: { title: TEXT_LONG },
});
return parent.vm.$nextTick();
});
it('renders tooltip', () => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top');
});
it('does not render tooltip after updated to a short text', () => {
parent.setProps({
title: TEXT_SHORT,
});
return wrapper.vm
.$nextTick()
.then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
.then(() => {
expect(hasTooltip()).toBe(false);
});
});
});
describe('is initialized with a short text', () => {
beforeEach(() => {
createWrappedComponent({
propsData: { title: TEXT_SHORT },
});
return wrapper.vm.$nextTick();
});
it('does not render tooltip', () => {
expect(hasTooltip()).toBe(false);
});
it('renders tooltip after updated to a long text', () => {
parent.setProps({
title: TEXT_LONG,
});
return wrapper.vm
.$nextTick()
.then(() => wrapper.vm.$nextTick()) // wait 2 times to get an updated slot
.then(() => {
expect(hasTooltip()).toBe(true);
expect(wrapper.attributes('data-original-title')).toEqual(TEXT_LONG);
expect(wrapper.attributes('data-placement')).toEqual('top');
});
});
});
});
});

View File

@ -0,0 +1,243 @@
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::BackgroundMigration::RecalculateProjectAuthorizations, :migration, schema: 20200204113223 do
let(:users_table) { table(:users) }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_authorizations_table) { table(:project_authorizations) }
let(:members_table) { table(:members) }
let(:group_group_links) { table(:group_group_links) }
let(:project_group_links) { table(:project_group_links) }
let(:user) { users_table.create!(id: 1, email: 'user@example.com', projects_limit: 10) }
let(:group) { namespaces_table.create!(type: 'Group', name: 'group', path: 'group') }
subject { described_class.new.perform([user.id]) }
context 'missing authorization' do
context 'personal project' do
before do
user_namespace = namespaces_table.create!(owner_id: user.id, name: 'User', path: 'user')
projects_table.create!(id: 1,
name: 'personal-project',
path: 'personal-project',
visibility_level: 0,
namespace_id: user_namespace.id)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 40)]))
end
end
context 'group membership' do
before do
projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)]))
end
end
context 'inherited group membership' do
before do
sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup',
path: 'subgroup', parent_id: group.id)
projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: sub_group.id)
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, notification_level: 3)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)]))
end
end
context 'project membership' do
before do
project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project',
type: 'ProjectMember', access_level: 20, notification_level: 3)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)]))
end
end
context 'shared group' do
before do
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 30, notification_level: 3)
shared_group = namespaces_table.create!(type: 'Group', name: 'shared group',
path: 'shared-group')
projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0,
namespace_id: shared_group.id)
group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id,
group_access: 20)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)]))
end
end
context 'shared project' do
before do
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 30, notification_level: 3)
another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group')
shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project',
visibility_level: 0, namespace_id: another_group.id)
project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20)
end
it 'creates correct authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(0).to(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 20)]))
end
end
end
context 'unapproved access requests' do
context 'group membership' do
before do
projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3)
end
it 'does not create authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(0)
end
end
context 'inherited group membership' do
before do
sub_group = namespaces_table.create!(type: 'Group', name: 'subgroup', path: 'subgroup',
parent_id: group.id)
projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: sub_group.id)
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 20, requested_at: Time.now, notification_level: 3)
end
it 'does not create authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(0)
end
end
context 'project membership' do
before do
project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
members_table.create!(user_id: user.id, source_id: project.id, source_type: 'Project',
type: 'ProjectMember', access_level: 20, requested_at: Time.now, notification_level: 3)
end
it 'does not create authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(0)
end
end
context 'shared group' do
before do
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3)
shared_group = namespaces_table.create!(type: 'Group', name: 'shared group',
path: 'shared-group')
projects_table.create!(id: 1, name: 'project', path: 'project', visibility_level: 0,
namespace_id: shared_group.id)
group_group_links.create(shared_group_id: shared_group.id, shared_with_group_id: group.id,
group_access: 20)
end
it 'does not create authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(0)
end
end
context 'shared project' do
before do
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 30, requested_at: Time.now, notification_level: 3)
another_group = namespaces_table.create!(type: 'Group', name: 'another group', path: 'another-group')
shared_project = projects_table.create!(id: 1, name: 'shared project', path: 'shared-project',
visibility_level: 0, namespace_id: another_group.id)
project_group_links.create(project_id: shared_project.id, group_id: group.id, group_access: 20)
end
it 'does not create authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(0)
end
end
end
context 'incorrect authorization' do
before do
project = projects_table.create!(id: 1, name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
members_table.create!(user_id: user.id, source_id: group.id, source_type: 'Namespace',
type: 'GroupMember', access_level: 30, notification_level: 3)
project_authorizations_table.create!(user_id: user.id, project_id: project.id,
access_level: 10)
end
it 'fixes authorization' do
expect { subject }.not_to change { project_authorizations_table.count }.from(1)
expect(project_authorizations_table.all).to(
match_array([have_attributes(user_id: 1, project_id: 1, access_level: 30)]))
end
end
context 'unwanted authorization' do
before do
project = projects_table.create!(name: 'group-project', path: 'group-project',
visibility_level: 0, namespace_id: group.id)
project_authorizations_table.create!(user_id: user.id, project_id: project.id,
access_level: 10)
end
it 'deletes authorization' do
expect { subject }.to change { project_authorizations_table.count }.from(1).to(0)
end
end
context 'deleted user' do
let(:nonexistent_user_id) { User.maximum(:id).to_i + 999 }
it 'does not fail' do
expect { described_class.new.perform([nonexistent_user_id]) }.not_to raise_error
end
end
end

View File

@ -175,7 +175,7 @@ describe Gitlab::Diff::File do
[diff_file.new_content_sha, diff_file.new_path], [diff_file.old_content_sha, diff_file.old_path]
]
expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: 100 * 1024).and_call_original
expect(project.repository).to receive(:blobs_at).with(items, blob_size_limit: 10.megabytes).and_call_original
old_data = diff_file.old_blob.data
data = diff_file.new_blob.data

View File

@ -279,4 +279,19 @@ describe Gitlab::GitalyClient::CommitService do
expect(subject.deletions).to eq(15)
end
end
describe '#find_commits' do
it 'sends an RPC request' do
request = Gitaly::FindCommitsRequest.new(
repository: repository_message,
disable_walk: true,
order: 'TOPO'
)
expect_any_instance_of(Gitaly::CommitService::Stub).to receive(:find_commits)
.with(request, kind_of(Hash)).and_return([])
client.find_commits(order: 'topo')
end
end
end

View File

@ -97,87 +97,68 @@ describe Gitlab::ProjectAuthorizations do
create(:group_group_link, shared_group: shared_group, shared_with_group: group)
end
context 'when feature flag share_group_with_group is enabled' do
before do
stub_feature_flags(share_group_with_group: true)
end
context 'group user' do
let(:user) { group_user }
context 'group user' do
let(:user) { group_user }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER)
expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER)
end
end
context 'parent group user' do
let(:user) { parent_group_user }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
end
context 'child group user' do
let(:user) { child_group_user }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to eq(Gitlab::Access::DEVELOPER)
expect(mapping[project_child.id]).to eq(Gitlab::Access::DEVELOPER)
end
end
context 'when feature flag share_group_with_group is disabled' do
before do
stub_feature_flags(share_group_with_group: false)
context 'parent group user' do
let(:user) { parent_group_user }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
end
context 'group user' do
let(:user) { group_user }
context 'child group user' do
let(:user) { child_group_user }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
end
context 'parent group user' do
let(:user) { parent_group_user }
context 'user without accepted access request' do
let!(:user) { create(:user) }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
it 'does not have access to group and its projects' do
create(:group_member, :developer, :access_request, user: user, group: group)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
end
context 'child group user' do
let(:user) { child_group_user }
context 'unrelated project owner' do
let(:common_id) { [Project.maximum(:id).to_i, Namespace.maximum(:id).to_i].max + 999 }
let!(:group) { create(:group, id: common_id) }
let!(:unrelated_project) { create(:project, id: common_id) }
let(:user) { unrelated_project.owner }
it 'creates proper authorizations' do
mapping = map_access_levels(authorizations)
it 'does not have access to group and its projects' do
mapping = map_access_levels(authorizations)
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
expect(mapping[project_parent.id]).to be_nil
expect(mapping[project.id]).to be_nil
expect(mapping[project_child.id]).to be_nil
end
end
end

View File

@ -0,0 +1,57 @@
# frozen_string_literal: true
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20200204113223_schedule_recalculate_project_authorizations.rb')
describe ScheduleRecalculateProjectAuthorizations, :migration do
let(:users_table) { table(:users) }
let(:namespaces_table) { table(:namespaces) }
let(:projects_table) { table(:projects) }
let(:project_authorizations_table) { table(:project_authorizations) }
let(:user1) { users_table.create!(name: 'user1', email: 'user1@example.com', projects_limit: 1) }
let(:user2) { users_table.create!(name: 'user2', email: 'user2@example.com', projects_limit: 1) }
let(:group) { namespaces_table.create!(id: 1, type: 'Group', name: 'group', path: 'group') }
let(:project) do
projects_table.create!(id: 1, name: 'project', path: 'project',
visibility_level: 0, namespace_id: group.id)
end
before do
stub_const("#{described_class}::BATCH_SIZE", 1)
project_authorizations_table.create!(user_id: user1.id, project_id: project.id, access_level: 30)
project_authorizations_table.create!(user_id: user2.id, project_id: project.id, access_level: 30)
end
it 'schedules background migration' do
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION).to be_scheduled_migration([user1.id])
expect(described_class::MIGRATION).to be_scheduled_migration([user2.id])
end
end
end
it 'ignores projects with higher id than maximum group id' do
another_user = users_table.create!(name: 'another user', email: 'another-user@example.com',
projects_limit: 1)
ignored_project = projects_table.create!(id: 2, name: 'ignored-project', path: 'ignored-project',
visibility_level: 0, namespace_id: group.id)
project_authorizations_table.create!(user_id: another_user.id, project_id: ignored_project.id,
access_level: 30)
Sidekiq::Testing.fake! do
Timecop.freeze do
migrate!
expect(BackgroundMigrationWorker.jobs.size).to eq(2)
expect(described_class::MIGRATION).to be_scheduled_migration([user1.id])
expect(described_class::MIGRATION).to be_scheduled_migration([user2.id])
end
end
end
end

View File

@ -1980,6 +1980,23 @@ describe Project do
expect(project.reload.import_url).to eq('http://test.com')
end
it 'saves the url credentials percent decoded' do
url = 'http://user:pass%21%3F%40@github.com/t.git'
project = build(:project, import_url: url)
# When the credentials are not decoded this expectation fails
expect(project.import_url).to eq(url)
expect(project.import_data.credentials).to eq(user: 'user', password: 'pass!?@')
end
it 'saves url with no credentials' do
url = 'http://github.com/t.git'
project = build(:project, import_url: url)
expect(project.import_url).to eq(url)
expect(project.import_data.credentials).to eq(user: nil, password: nil)
end
end
describe '#container_registry_url' do

View File

@ -325,6 +325,14 @@ describe Repository do
expect(repository.commits(nil, all: true, limit: 60).size).to eq(60)
end
end
context "when 'order' flag is set" do
it 'passes order option to perform the query' do
expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(order: 'topo')).and_call_original
repository.commits('master', limit: 1, order: 'topo')
end
end
end
describe '#new_commits' do

View File

@ -12,7 +12,6 @@ describe API::Commits do
let(:project) { create(:project, :repository, creator: user, path: 'my.project') }
let(:branch_with_dot) { project.repository.find_branch('ends-with.json') }
let(:branch_with_slash) { project.repository.find_branch('improve/awesome') }
let(:project_id) { project.id }
let(:current_user) { nil }
@ -241,6 +240,40 @@ describe API::Commits do
end
end
end
context 'with order parameter' do
let(:route) { "/projects/#{project_id}/repository/commits?ref_name=0031876&per_page=6&order=#{order}" }
context 'set to topo' do
let(:order) { 'topo' }
# git log --graph -n 6 --pretty=format:"%h" --topo-order 0031876
# * 0031876
# |\
# | * 48ca272
# | * 335bc94
# * | bf6e164
# * | 9d526f8
# |/
# * 1039376
it 'returns project commits ordered by topo order' do
commits = project.repository.commits("0031876", limit: 6, order: 'topo')
get api(route, current_user)
expect(json_response.size).to eq(6)
expect(json_response.map { |entry| entry["id"] }).to eq(commits.map(&:id))
end
end
context 'set to blank' do
let(:order) { '' }
it_behaves_like '400 response' do
let(:request) { get api(route, current_user) }
end
end
end
end
end

View File

@ -795,13 +795,13 @@ describe API::Issues do
it 'returns issues from non archived projects only by default' do
get api("/groups/#{group1.id}/issues", user), params: { scope: 'all' }
expect_response_contain_exactly(issue2, issue1)
expect_paginated_array_response([issue2.id, issue1.id])
end
it 'returns issues from archived and non archived projects when non_archived is false' do
get api("/groups/#{group1.id}/issues", user), params: { non_archived: false, scope: 'all' }
expect_response_contain_exactly(issue1, issue2, issue3)
expect_paginated_array_response([issue3.id, issue2.id, issue1.id])
end
end
end
@ -888,9 +888,4 @@ describe API::Issues do
include_examples 'time tracking endpoints', 'issue'
end
def expect_response_contain_exactly(*items)
expect(json_response.length).to eq(items.size)
expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id))
end
end

View File

@ -41,8 +41,7 @@ describe API::MergeRequests do
it 'returns merge requests for public projects' do
get api(endpoint_path)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
end
end
@ -87,10 +86,11 @@ describe API::MergeRequests do
it 'returns an array of all merge_requests' do
get api(endpoint_path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
expect(json_response.last['sha']).to eq(merge_request.diff_head_sha)
@ -111,7 +111,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect_successful_response_with_paginated_array
expect(json_response.last['labels'].pluck('name')).to eq([label2.title, label.title])
expect(json_response.last['labels'].first).to match_schema('/public_api/v4/label_basic')
end
@ -139,11 +139,11 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
expect(json_response.last.keys).to match_array(%w(id iid title web_url created_at description project_id state updated_at))
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect(json_response.last['iid']).to eq(merge_request.iid)
expect(json_response.last['title']).to eq(merge_request.title)
expect(json_response.last).to have_key('web_url')
@ -157,10 +157,10 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
expect(json_response.last['title']).to eq(merge_request.title)
end
@ -169,10 +169,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect_paginated_array_response([merge_request.id])
expect(json_response.last['title']).to eq(merge_request.title)
end
@ -181,10 +178,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect_paginated_array_response([merge_request_closed.id])
expect(json_response.first['title']).to eq(merge_request_closed.title)
end
@ -193,10 +187,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect_paginated_array_response([merge_request_merged.id])
expect(json_response.first['title']).to eq(merge_request_merged.title)
end
@ -210,17 +201,13 @@ describe API::MergeRequests do
it 'returns an empty array if no issue matches milestone' do
get api(endpoint_path, user), params: { milestone: '1.0.0' }
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'returns an empty array if milestone does not exist' do
get api(endpoint_path, user), params: { milestone: 'foo' }
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'returns an array of merge requests in given milestone' do
@ -234,9 +221,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests matching state in milestone' do
get api(endpoint_path, user), params: { milestone: '0.9', state: 'closed' }
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect_paginated_array_response([merge_request_closed.id])
expect(json_response.first['id']).to eq(merge_request_closed.id)
end
@ -248,8 +233,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
end
@ -259,9 +243,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'returns an empty array if no merge request matches labels' do
@ -269,9 +251,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'returns an array of labeled merge requests where all labels match' do
@ -279,8 +259,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
end
@ -288,8 +267,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: [" #{label.title} ", " #{label2.title} "] }
expect_paginated_array_response
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
expect(json_response.first['id']).to eq(merge_request.id)
@ -298,8 +276,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: ["#{label.title} , #{label2.title}"] }
expect_paginated_array_response
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['labels']).to eq([label2.title, label.title])
expect(json_response.first['id']).to eq(merge_request.id)
@ -308,7 +285,7 @@ describe API::MergeRequests do
it 'returns an array of merge requests with any label when filtering by any label' do
get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_ANY }
expect_paginated_array_response
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request.id)
end
@ -316,10 +293,9 @@ describe API::MergeRequests do
it 'returns an array of merge requests without a label when filtering by no label' do
get api(endpoint_path, user), params: { labels: IssuesFinder::FILTER_NONE }
response_ids = json_response.map { |merge_request| merge_request['id'] }
expect_paginated_array_response
expect(response_ids).to contain_exactly(merge_request_closed.id, merge_request_merged.id, merge_request_locked.id)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
])
end
end
@ -339,10 +315,7 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(mr2.id)
expect_paginated_array_response([mr2.id])
end
context 'with ordering' do
@ -356,10 +329,10 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request_closed.id, merge_request_locked.id,
merge_request_merged.id, merge_request.id
])
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@ -369,10 +342,10 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request.id, merge_request_merged.id,
merge_request_locked.id, merge_request_closed.id
])
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@ -414,10 +387,10 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request.id, merge_request_locked.id,
merge_request_merged.id, merge_request_closed.id
])
response_dates = json_response.map { |merge_request| merge_request['updated_at'] }
expect(response_dates).to eq(response_dates.sort.reverse)
end
@ -427,10 +400,10 @@ describe API::MergeRequests do
get api(path, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(4)
expect_paginated_array_response([
merge_request_closed.id, merge_request_locked.id,
merge_request_merged.id, merge_request.id
])
response_dates = json_response.map { |merge_request| merge_request['created_at'] }
expect(response_dates).to eq(response_dates.sort)
end
@ -440,7 +413,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given source branch' do
get api(endpoint_path, user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
])
end
end
@ -448,7 +423,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api(endpoint_path, user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
])
end
end
end
@ -471,7 +448,10 @@ describe API::MergeRequests do
it 'returns an array of all merge requests' do
get api('/merge_requests', user), params: { scope: 'all' }
expect_paginated_array_response
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
end
it "returns authentication error without any scope" do
@ -507,30 +487,23 @@ describe API::MergeRequests do
it 'returns an array of all merge requests except unauthorized ones' do
get api('/merge_requests', user), params: { scope: :all }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request_locked.id, merge_request2.id)
expect_paginated_array_response([
merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
])
end
it "returns an array of no merge_requests when wip=yes" do
get api("/merge_requests", user), params: { wip: 'yes' }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it "returns an array of no merge_requests when wip=no" do
get api("/merge_requests", user), params: { wip: 'no' }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.map { |mr| mr['id'] })
.to contain_exactly(merge_request.id, merge_request_closed.id, merge_request_merged.id, merge_request_locked.id, merge_request2.id)
expect_paginated_array_response([
merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
])
end
it 'does not return unauthorized merge requests' do
@ -539,7 +512,9 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { scope: :all }
expect_response_contain_exactly(merge_request2, merge_request_merged, merge_request_closed, merge_request, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request2.id, merge_request_locked.id, merge_request_closed.id, merge_request.id
])
expect(json_response.map { |mr| mr['id'] }).not_to include(merge_request3.id)
end
@ -548,7 +523,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2)
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests authored by the given user' do
@ -556,7 +531,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { author_id: user2.id, scope: :all }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests assigned to the given user' do
@ -564,7 +539,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: user2.id, scope: :all }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests with no assignee' do
@ -572,7 +547,7 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: 'None', scope: :all }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests with any assignee' do
@ -581,7 +556,10 @@ describe API::MergeRequests do
get api('/merge_requests', user), params: { assignee_id: 'Any', scope: :all }
expect_response_contain_exactly(merge_request, merge_request2, merge_request_closed, merge_request_merged, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request2.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
end
it 'returns an array of merge requests assigned to me' do
@ -589,7 +567,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'assigned_to_me' }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests assigned to me (kebab-case)' do
@ -597,7 +575,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'assigned-to-me' }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests created by me' do
@ -605,7 +583,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'created_by_me' }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns an array of merge requests created by me (kebab-case)' do
@ -613,7 +591,7 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { scope: 'created-by-me' }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
it 'returns merge requests reacted by the authenticated user by the given emoji' do
@ -622,14 +600,16 @@ describe API::MergeRequests do
get api('/merge_requests', user2), params: { my_reaction_emoji: award_emoji.name, scope: 'all' }
expect_response_ordered_exactly(merge_request3)
expect_paginated_array_response([merge_request3.id])
end
context 'source_branch param' do
it 'returns merge requests with the given source branch' do
get api('/merge_requests', user), params: { source_branch: merge_request_closed.source_branch, state: 'all' }
expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
])
end
end
@ -637,7 +617,9 @@ describe API::MergeRequests do
it 'returns merge requests with the given target branch' do
get api('/merge_requests', user), params: { target_branch: merge_request_closed.target_branch, state: 'all' }
expect_response_contain_exactly(merge_request_closed, merge_request_merged, merge_request_locked)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id, merge_request_closed.id
])
end
end
@ -646,7 +628,7 @@ describe API::MergeRequests do
get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user)
expect_response_ordered_exactly(merge_request2)
expect_paginated_array_response([merge_request2.id])
end
it 'returns merge requests created after a specific date' do
@ -654,7 +636,7 @@ describe API::MergeRequests do
get api("/merge_requests?created_after=#{merge_request2.created_at}", user)
expect_response_ordered_exactly(merge_request2)
expect_paginated_array_response([merge_request2.id])
end
it 'returns merge requests updated before a specific date' do
@ -662,7 +644,7 @@ describe API::MergeRequests do
get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user)
expect_response_ordered_exactly(merge_request2)
expect_paginated_array_response([merge_request2.id])
end
it 'returns merge requests updated after a specific date' do
@ -670,7 +652,7 @@ describe API::MergeRequests do
get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user)
expect_response_ordered_exactly(merge_request2)
expect_paginated_array_response([merge_request2.id])
end
context 'search params' do
@ -681,25 +663,25 @@ describe API::MergeRequests do
it 'returns merge requests matching given search string for title' do
get api("/merge_requests", user), params: { search: merge_request.title }
expect_response_ordered_exactly(merge_request)
expect_paginated_array_response([merge_request.id])
end
it 'returns merge requests matching given search string for title and scoped in title' do
get api("/merge_requests", user), params: { search: merge_request.title, in: 'title' }
expect_response_ordered_exactly(merge_request)
expect_paginated_array_response([merge_request.id])
end
it 'returns an empty array if no merge reques matches given search string for description and scoped in title' do
it 'returns an empty array if no merge request matches given search string for description and scoped in title' do
get api("/merge_requests", user), params: { search: merge_request.description, in: 'title' }
expect_response_contain_exactly
expect_empty_array_response
end
it 'returns merge requests for project matching given search string for description' do
get api("/merge_requests", user), params: { project_id: project.id, search: merge_request.description }
expect_response_ordered_exactly(merge_request)
expect_paginated_array_response([merge_request.id])
end
end
@ -707,7 +689,7 @@ describe API::MergeRequests do
it 'returns merge requests with the given state' do
get api('/merge_requests', user), params: { state: 'locked' }
expect_response_contain_exactly(merge_request_locked)
expect_paginated_array_response([merge_request_locked.id])
end
end
end
@ -729,18 +711,13 @@ describe API::MergeRequests do
it "returns an array of no merge_requests when wip=yes" do
get api("/projects/#{project.id}/merge_requests", user), params: { wip: 'yes' }
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'returns merge_request by "iids" array' do
get api(endpoint_path, user), params: { iids: [merge_request.iid, merge_request_closed.iid] }
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(2)
expect_paginated_array_response([merge_request_closed.id, merge_request.id])
expect(json_response.first['title']).to eq merge_request_closed.title
expect(json_response.first['id']).to eq merge_request_closed.id
end
@ -815,12 +792,10 @@ describe API::MergeRequests do
it 'returns an array excluding merge_requests from archived projects' do
get api(endpoint_path, user)
expect_response_contain_exactly(
merge_request_merged,
merge_request_locked,
merge_request_closed,
merge_request
)
expect_paginated_array_response([
merge_request_merged.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
end
context 'with non_archived param set as false' do
@ -829,13 +804,10 @@ describe API::MergeRequests do
get api(path, user)
expect_response_contain_exactly(
merge_request_merged,
merge_request_locked,
merge_request_closed,
merge_request,
merge_request_archived
)
expect_paginated_array_response([
merge_request_merged.id, merge_request_archived.id, merge_request_locked.id,
merge_request_closed.id, merge_request.id
])
end
end
end
@ -1079,9 +1051,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/commits", user)
commit = merge_request.commits.first
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.size).to eq(merge_request.commits.size)
expect(json_response.first['id']).to eq(commit.id)
expect(json_response.first['title']).to eq(commit.title)
@ -1105,9 +1075,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/context_commits", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.size).to eq(merge_request.context_commits.size)
expect(json_response.first['id']).to eq(context_commit.id)
expect(json_response.first['title']).to eq(context_commit.title)
@ -1147,9 +1115,7 @@ describe API::MergeRequests do
it 'returns a paginated array of corresponding pipelines' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/pipelines")
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.count).to eq(1)
expect(json_response.first['id']).to eq(pipeline.id)
end
@ -1395,7 +1361,7 @@ describe API::MergeRequests do
expect(json_response['labels']).to eq([])
end
xit 'empty label param as array, does not add any labels' do
it 'empty label param as array, does not add any labels' do
params[:labels] = []
post api("/projects/#{project.id}/merge_requests", user), params: params
@ -2232,7 +2198,7 @@ describe API::MergeRequests do
expect(json_response['labels']).to eq []
end
xit 'empty label as array, removes labels' do
it 'empty label as array, removes labels' do
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user),
params: {
title: 'new issue',
@ -2240,7 +2206,6 @@ describe API::MergeRequests do
}
expect(response.status).to eq(200)
# fails, as grape ommits for some reason empty array as optional param value, so nothing it passed along
expect(json_response['labels']).to eq []
end
@ -2306,9 +2271,7 @@ describe API::MergeRequests do
get api("/projects/#{project.id}/merge_requests/#{mr.iid}/closes_issues", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(issue.id)
end
@ -2316,10 +2279,7 @@ describe API::MergeRequests do
it 'returns an empty array when there are no issues to be closed' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
expect_empty_array_response
end
it 'handles external issues' do
@ -2332,9 +2292,7 @@ describe API::MergeRequests do
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(2)
expect(json_response.second['title']).to eq(ext_issue.title)
expect(json_response.second['id']).to eq(ext_issue.id)
@ -2546,22 +2504,4 @@ describe API::MergeRequests do
merge_request_closed.save
merge_request_closed
end
def expect_response_contain_exactly(*items)
expect_paginated_array_response
expect(json_response.length).to eq(items.size)
expect(json_response.map { |element| element['id'] }).to contain_exactly(*items.map(&:id))
end
def expect_response_ordered_exactly(*items)
expect_paginated_array_response
expect(json_response.length).to eq(items.size)
expect(json_response.map { |element| element['id'] }).to eq(items.map(&:id))
end
def expect_paginated_array_response
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
end

View File

@ -22,6 +22,42 @@ describe Users::RefreshAuthorizedProjectsService do
service.execute
end
context 'callbacks' do
let(:callback) { double('callback') }
context 'incorrect_auth_found_callback callback' do
let(:user) { create(:user) }
let(:service) do
described_class.new(user,
incorrect_auth_found_callback: callback)
end
it 'is called' do
access_level = Gitlab::Access::DEVELOPER
create(:project_authorization, user: user, project: project, access_level: access_level)
expect(callback).to receive(:call).with(project.id, access_level).once
service.execute
end
end
context 'missing_auth_found_callback callback' do
let(:service) do
described_class.new(user,
missing_auth_found_callback: callback)
end
it 'is called' do
ProjectAuthorization.delete_all
expect(callback).to receive(:call).with(project.id, Gitlab::Access::MAINTAINER).once
service.execute
end
end
end
end
describe '#execute_without_lease' do

View File

@ -40,6 +40,17 @@ module ApiHelpers
end
end
def expect_empty_array_response
expect_successful_response_with_paginated_array
expect(json_response.length).to eq(0)
end
def expect_successful_response_with_paginated_array
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
end
def expect_paginated_array_response(items)
expect(response).to have_gitlab_http_status(:ok)
expect(response).to include_pagination_headers

View File

@ -1774,9 +1774,10 @@ asynckit@^0.4.0:
resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha1-x57Zf380y48robyXkLzDZkdLS3k=
at.js@^1.5.4, "at.js@https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe":
at.js@^1.5.4:
version "1.5.4"
resolved "https://gitlab.com/gitlab-org/frontend/At.js.git#121ce9a557b51c33f5693ac8df52d2bda1e53cbe"
resolved "https://registry.yarnpkg.com/at.js/-/at.js-1.5.4.tgz#8fc60cc80eadbe4874449b166a818e7ae1d784c1"
integrity sha512-G8mgUb/PqShPoH8AyjuxsTGvIr1o716BtQUKDM44C8qN2W615y7KGJ68MlTGamd0J0D/m28emUkzagaHTdrGZw==
atob@^2.1.1:
version "2.1.2"