Commit 75206afb authored by Matija Čupić's avatar Matija Čupić

Merge branch 'master' into mc/fix/project-variables-scope-ee

parents c5727d53 02c15350
......@@ -146,7 +146,7 @@ stages:
- export KNAPSACK_TEST_FILE_PATTERN="ee/spec/**{,/*/**}/*_spec.rb" KNAPSACK_GENERATE_REPORT=true CACHE_CLASSES=true
- JOB_NAME=( $CI_JOB_NAME )
- export CI_NODE_INDEX=${JOB_NAME[-2]} CI_NODE_TOTAL=${JOB_NAME[-1]}
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report-ee.json
- export KNAPSACK_REPORT_PATH=knapsack/${CI_PROJECT_NAME}/${JOB_NAME[0]}_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- cp ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH} ${KNAPSACK_REPORT_PATH}
- scripts/gitaly-test-spawn
- knapsack rspec "-Ispec --color --format documentation --tag ~geo"
......@@ -303,7 +303,7 @@ update-tests-metadata:
- retry gem install fog-aws mime-types
- scripts/merge-reports ${KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*.json
- scripts/merge-reports ${KNAPSACK_SPINACH_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/spinach-pg_node_*.json
- scripts/merge-reports ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg_node_*-ee.json
- scripts/merge-reports ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH} knapsack/${CI_PROJECT_NAME}/rspec-pg-ee_node_*.json
- scripts/merge-reports ${FLAKY_RSPEC_SUITE_REPORT_PATH} rspec_flaky/all_*_*.json
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $KNAPSACK_RSPEC_SUITE_REPORT_PATH $KNAPSACK_SPINACH_SUITE_REPORT_PATH'
- '[[ -z ${TESTS_METADATA_S3_BUCKET} ]] || scripts/sync-reports put $TESTS_METADATA_S3_BUCKET $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH'
......
......@@ -10,7 +10,6 @@ import SearchAutocomplete from './search_autocomplete';
import UsersSelect from './users_select';
import UserCallout from './user_callout';
import ZenMode from './zen_mode';
import initCompareAutocomplete from './compare_autocomplete';
import initGeoInfoModal from 'ee/init_geo_info_modal'; // eslint-disable-line import/first
import initGroupAnalytics from 'ee/init_group_analytics'; // eslint-disable-line import/first
import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first
......@@ -53,19 +52,6 @@ var Dispatcher;
});
});
function initBlobEE() {
const dataEl = document.getElementById('js-file-lock');
if (dataEl) {
const {
toggle_path,
path,
} = JSON.parse(dataEl.innerHTML);
initPathLocks(toggle_path, path);
}
}
switch (page) {
case 'projects:environments:metrics':
import('./pages/projects/environments/metrics')
......@@ -86,7 +72,6 @@ var Dispatcher;
import('./pages/projects/milestones/show')
.then(callDefault)
.catch(fail);
new UserCallout();
break;
case 'groups:milestones:show':
import('./pages/groups/milestones/show')
......@@ -185,14 +170,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'groups:epics:show':
new ZenMode();
break;
case 'groups:epics:index':
import(/* webpackChunkName: "ee_epics_show" */ 'ee/pages/epics')
.then(callDefault)
.catch(fail);
break;
case 'projects:compare:show':
import('./pages/projects/compare/show')
.then(callDefault)
......@@ -229,24 +206,17 @@ var Dispatcher;
import('./pages/projects/merge_requests/creations/new')
.then(callDefault)
.catch(fail);
new UserCallout();
case 'projects:merge_requests:creations:diffs':
import('./pages/projects/merge_requests/creations/diffs')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
// ee-start
initApprovals();
// ee-end
break;
case 'projects:merge_requests:edit':
import('./pages/projects/merge_requests/edit')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
// ee-start
initApprovals();
// ee-end
break;
case 'projects:tags:new':
import('./pages/projects/tags/new')
......@@ -331,9 +301,6 @@ var Dispatcher;
break;
case 'projects:show':
shortcut_handler = true;
// ee-start
initGeoInfoModal();
// ee-end
break;
case 'projects:edit':
import(/* webpackChunkName: "ee_projects_edit" */ 'ee/pages/projects/edit')
......@@ -415,14 +382,12 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
shortcut_handler = true;
initBlobEE();
break;
case 'projects:blame:show':
import('./pages/projects/blame/show')
.then(callDefault)
.catch(fail);
shortcut_handler = true;
initBlobEE();
break;
case 'groups:labels:new':
import('./pages/groups/labels/new')
......@@ -485,26 +450,11 @@ var Dispatcher;
import('./pages/search/show')
.then(callDefault)
.catch(fail);
new UserCallout();
break;
case 'projects:mirrors:show':
case 'projects:mirrors:update':
new UsersSelect();
break;
case 'admin:emails:show':
import(/* webpackChunkName: "ee_admin_emails_show" */ 'ee/pages/admin/emails/show').then(m => m.default()).catch(fail);
break;
case 'admin:audit_logs:index':
import(/* webpackChunkName: "ee_audit_logs" */ 'ee/pages/admin/audit_logs').then(m => m.default()).catch(fail);
break;
case 'projects:settings:repository:show':
import('./pages/projects/settings/repository/show')
.then(callDefault)
.catch(fail);
// ee-start
new UsersSelect();
new UserCallout();
// ee-end
break;
case 'projects:settings:ci_cd:show':
import('./pages/projects/settings/ci_cd/show')
......@@ -552,11 +502,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'profiles:personal_access_tokens:index':
import('./pages/profiles/personal_access_tokens')
.then(callDefault)
.catch(fail);
break;
case 'projects:clusters:show':
case 'projects:clusters:update':
case 'projects:clusters:destroy':
......@@ -573,14 +518,6 @@ var Dispatcher;
import('./pages/dashboard/groups/index')
.then(callDefault)
.catch(fail);
case 'admin:licenses:new':
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
case 'groups:analytics:show':
initGroupAnalytics();
break;
case 'groups:ldap_group_links:index':
initLDAPGroupsSelect();
break;
}
switch (path[0]) {
......@@ -616,11 +553,7 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
break;
}
break;
case 'projects':
import('./pages/admin/projects')
......@@ -645,11 +578,6 @@ var Dispatcher;
.then(callDefault)
.catch(fail);
break;
case 'geo_nodes':
import(/* webpackChunkName: 'geo_node_form' */ './geo/geo_node_form')
.then(geoNodeForm => geoNodeForm.default($('.js-geo-node-form')))
.catch(() => {});
break;
}
break;
case 'profiles':
......@@ -664,7 +592,9 @@ var Dispatcher;
shortcut_handler = true;
switch (path[1]) {
case 'compare':
initCompareAutocomplete();
import('./pages/projects/compare')
.then(callDefault)
.catch(fail);
break;
case 'create':
case 'new':
......@@ -686,6 +616,81 @@ var Dispatcher;
new Shortcuts();
}
// EE-only route-based code
function initBlobEE() {
const dataEl = document.getElementById('js-file-lock');
if (dataEl) {
const {
toggle_path,
path,
} = JSON.parse(dataEl.innerHTML);
initPathLocks(toggle_path, path);
}
}
switch (page) {
case 'groups:epics:show':
new ZenMode();
break;
case 'groups:epics:index':
import(/* webpackChunkName: "ee_epics_index" */ 'ee/pages/epics')
.then(callDefault)
.catch(fail);
break;
case 'projects:milestones:show':
case 'search:show':
new UserCallout();
break;
case 'projects:merge_requests:creations:new':
new UserCallout();
initApprovals();
break;
case 'projects:merge_requests:creations:diffs':
case 'projects:merge_requests:edit':
initApprovals();
break;
case 'projects:show':
initGeoInfoModal();
break;
case 'projects:blob:show':
case 'projects:blame:show':
initBlobEE();
break;
case 'projects:mirrors:show':
case 'projects:mirrors:update':
new UsersSelect();
break;
case 'admin:emails:show':
import(/* webpackChunkName: "ee_admin_emails_show" */ 'ee/pages/admin/emails/show').then(m => m.default()).catch(fail);
break;
case 'admin:audit_logs:index':
import(/* webpackChunkName: "ee_audit_logs" */ 'ee/pages/admin/audit_logs').then(m => m.default()).catch(fail);
break;
case 'projects:settings:repository:show':
new UsersSelect();
new UserCallout();
break;
case 'admin:licenses:new':
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break;
case 'groups:analytics:show':
initGroupAnalytics();
break;
case 'groups:ldap_group_links:index':
initLDAPGroupsSelect();
break;
case 'admin:groups:edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
break;
case 'admin:geo_nodes':
import(/* webpackChunkName: 'geo_node_form' */ './geo/geo_node_form')
.then(geoNodeForm => geoNodeForm.default($('.js-geo-node-form')))
.catch(() => {});
break;
}
if (document.querySelector('#peek')) {
import('./performance_bar')
.then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
......
......@@ -5,13 +5,10 @@ import UsersSelect from './users_select';
import issueStatusSelect from './issue_status_select';
import MilestoneSelect from './milestone_select';
import WeightSelect from 'ee/weight_select'; // eslint-disable-line import/first
export default () => {
new UsersSelect();
new LabelsSelect();
new MilestoneSelect();
issueStatusSelect();
subscriptionSelect();
new WeightSelect();
};
import _ from 'underscore';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import Flash from '../flash';
import AUTH_METHOD from './constants';
import { backOff } from '../lib/utils/common_utils';
......@@ -145,22 +147,24 @@ export default class MirrorPull {
if (selectedAuthType === AUTH_METHOD.SSH &&
!$sshPublicKey.text().trim()) {
this.$dropdownAuthType.disable();
$.ajax({
type: 'PUT',
url: projectMirrorAuthTypeEndpoint,
contentType: 'application/json; charset=utf-8',
data: JSON.stringify(authTypeData),
axios.put(projectMirrorAuthTypeEndpoint, JSON.stringify(authTypeData), {
headers: {
'Content-Type': 'application/json; charset=utf-8',
},
})
.done((res) => {
.then(({ data }) => {
// Show SSH public key container and fill in public key
this.toggleAuthWell(selectedAuthType);
this.toggleSSHAuthWellMessage(true);
this.setSSHPublicKey(res.import_data_attributes.ssh_public_key);
})
.fail(() => {
Flash('Something went wrong on our end.');
this.setSSHPublicKey(data.import_data_attributes.ssh_public_key);
this.$wellAuthTypeChanging.addClass('hidden');
this.$dropdownAuthType.enable();
})
.always(() => {
.catch(() => {
Flash(__('Something went wrong on our end.'));
this.$wellAuthTypeChanging.addClass('hidden');
this.$dropdownAuthType.enable();
});
......
......@@ -33,9 +33,6 @@ import flash from '../flash';
$('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
this.form.on('submit', this.onSubmitForm);
}
......@@ -48,21 +45,6 @@ import flash from '../flash';
return this.saveForm();
}
beforeUpdateUsername() {
$('.loading-username', this).removeClass('hidden');
}
afterUpdateUsername() {
$('.loading-username', this).addClass('hidden');
$('button[type=submit]', this).enable();
}
onUpdateNotifs(e, data) {
return data.saved ?
flash(__('Notification settings saved'), 'notice') :
flash(__('Failed to save new settings'));
}
saveForm() {
const self = this;
const formData = new FormData(this.form[0]);
......
......@@ -42,7 +42,7 @@ export default function initSettingsPanels() {
if (location.hash) {
const $target = $(location.hash);
if ($target.length && $target.hasClass('.settings')) {
if ($target.length && $target.hasClass('settings')) {
expandSection($target);
}
}
......
......@@ -72,7 +72,7 @@
{{ sprintf(__('Lock %{issuableDisplayName}'), { issuableDisplayName: issuableDisplayName }) }}
<button
v-if="isEditable"
class="pull-right lock-edit btn btn-blank"
class="pull-right lock-edit"
type="button"
@click.prevent="toggleForm"
>
......
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="success" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Ready to be merged automatically.
Ask someone with write access to this repository to merge this request
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNotAllowed',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="success"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Ready to be merged automatically.
Ask someone with write access to this repository to merge this request`) }}
</span>
</div>
</div>
</template>
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
statusIcon,
},
template: `
<div class="mr-widget-body media">
<status-icon status="warning" :show-disabled-button="true" />
<div class="media-body space-children">
<span class="bold">
Pipeline blocked. The pipeline for this merge request requires a manual action to proceed
</span>
</div>
</div>
`,
};
<script>
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetPipelineBlocked',
components: {
StatusIcon,
},
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon
status="warning"
:show-disabled-button="true"
/>
<div class="media-body space-children">
<span class="bold">
{{ s__(`mrWidget|Pipeline blocked.
The pipeline for this merge request requires a manual action to proceed`) }}
</span>
</div>
</div>
</template>
......@@ -25,11 +25,11 @@ export { default as ArchivedState } from './components/states/mr_widget_archived
export { default as ConflictsState } from './components/states/mr_widget_conflicts.vue';
export { default as NothingToMergeState } from './components/states/mr_widget_nothing_to_merge';
export { default as MissingBranchState } from './components/states/mr_widget_missing_branch.vue';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed';
export { default as NotAllowedState } from './components/states/mr_widget_not_allowed.vue';
export { default as ReadyToMergeState } from 'ee/vue_merge_request_widget/components/states/mr_widget_ready_to_merge';
export { default as SHAMismatchState } from './components/states/mr_widget_sha_mismatch';
export { default as UnresolvedDiscussionsState } from './components/states/mr_widget_unresolved_discussions';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked';
export { default as PipelineBlockedState } from './components/states/mr_widget_pipeline_blocked.vue';
export { default as PipelineFailedState } from './components/states/mr_widget_pipeline_failed';
export { default as MergeWhenPipelineSucceedsState } from './components/states/mr_widget_merge_when_pipeline_succeeds.vue';
export { default as RebaseState } from './components/states/mr_widget_rebase.vue';
......
......@@ -197,11 +197,18 @@
margin-left: 0;
}
a.edit-link:not([href]):hover {
color: rgba($avatar-border, .2);
}
.lock-edit, // uses same style, different js behaviour
.edit-link {
@extend .btn-blank;
color: $gl-text-color;
&:not([href]):hover {
color: rgba($avatar-border, .2);
&:hover {
text-decoration: underline;
color: $md-link-color;
}
}
}
......
......@@ -126,8 +126,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def referenced_merge_requests
@merge_requests = @issue.referenced_merge_requests(current_user)
@closed_by_merge_requests = @issue.closed_by_merge_requests(current_user)
@merge_requests, @closed_by_merge_requests = ::Issues::FetchReferencedMergeRequestsService.new(project, current_user).execute(issue)
respond_to do |format|
format.json do
......
......@@ -160,7 +160,7 @@ module DiffHelper
end
def diff_file_changed_icon(diff_file)
if diff_file.deleted_file? || diff_file.renamed_file?
if diff_file.deleted_file?
"file-deletion"
elsif diff_file.new_file?
"file-addition"
......
......@@ -298,6 +298,10 @@ module ProjectsHelper
nav_tabs << :pipelines
end
if project.external_issue_tracker
nav_tabs << :external_issue_tracker
end
tab_ability_map.each do |tab, ability|
if can?(current_user, ability, project)
nav_tabs << tab
......
......@@ -116,6 +116,10 @@ class Commit
raw.id
end
def project_id
project.id
end
def ==(other)
other.is_a?(self.class) && raw == other.raw
end
......
......@@ -317,7 +317,7 @@ class Member < ActiveRecord::Base
end
def notification_setting
@notification_setting ||= user.notification_settings_for(source)
@notification_setting ||= user&.notification_settings_for(source)
end
def notifiable?(type, opts = {})
......
......@@ -61,12 +61,9 @@ module Network
@reserved[i] = []
end
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37436
Gitlab::GitalyClient.allow_n_plus_1_calls do
commits_sort_by_ref.each do |commit|
place_chain(commit)
end
end
# find parent spaces for not overlap lines
@commits.each do |c|
......
......@@ -10,6 +10,8 @@ class JiraService < IssueTrackerService
before_update :reset_password
alias_method :project_url, :url
# This is confusing, but JiraService does not really support these events.
# The values here are required to display correct options in the service
# configuration screen.
......
......@@ -6,10 +6,7 @@ class DeleteMergedBranchesService < BaseService
def execute
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :push_code, project)
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37438
Gitlab::GitalyClient.allow_n_plus_1_calls do
branches = project.repository.branch_names
branches = branches.select { |branch| project.repository.merged_to_root_ref?(branch) }
branches = project.repository.merged_branch_names
# Prevent deletion of branches relevant to open merge requests
branches -= merge_request_branch_names
# Prevent deletion of protected branches
......@@ -19,7 +16,6 @@ class DeleteMergedBranchesService < BaseService
DeleteBranchService.new(project, current_user).execute(branch)
end
end
end
private
......
module Issues
class FetchReferencedMergeRequestsService < Issues::BaseService
def execute(issue)
referenced_merge_requests = issue.referenced_merge_requests(current_user)
referenced_merge_requests = Gitlab::IssuableSorter.sort(project, referenced_merge_requests) { |i| i.iid.to_s }
closed_by_merge_requests = issue.closed_by_merge_requests(current_user)
closed_by_merge_requests = Gitlab::IssuableSorter.sort(project, closed_by_merge_requests) { |i| i.iid.to_s }
[referenced_merge_requests, closed_by_merge_requests]
end
end
end
......@@ -11,6 +11,7 @@ module Members
Member.transaction do
unassign_issues_and_merge_requests(member) unless member.invite?
member.notification_setting&.destroy
member.destroy
end
......
......@@ -141,6 +141,19 @@
= link_to project_milestones_path(@project), title: 'Milestones' do
%span
Milestones
- if project_nav_tab? :external_issue_tracker
= nav_link do
- issue_tracker = @project.external_issue_tracker
= link_to issue_tracker.issue_tracker_path, class: 'shortcuts-external_tracker' do
.nav-icon-container
= sprite_icon('issue-external')
%span.nav-item-name
= issue_tracker.title
%ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(html_options: { class: "fly-out-top-item" } ) do
= link_to issue_tracker.issue_tracker_path do
%strong.fly-out-top-item-name
= issue_tracker.title
- if project_nav_tab? :merge_requests
= nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
......
---
title: 'Geo: Reset force_redownload flag after successful sync'
merge_request:
author:
type: fixed
---
title: "[Geo] Fix redownload repository recovery when there is not local repo at all"
merge_request:
author:
type: fixed
---
title: Fix the background_upload configuration being ignored.
merge_request: 4507
author:
type: fixed
---
title: Bump Geo JWT timeout from 1 minute to 10 minutes
merge_request:
author:
type: performance
---
title: Group MRs on issue page by project and namespace.
merge_request: 8494
author: Jeff Stubler
---
title: Display a link to external issue tracker when enabled
merge_request:
author:
type: changed
---
title: Render modified icon for moved file in changes dropdown
merge_request:
author:
type: fixed
---
title: Remove user notification settings for groups and projects when user leaves
merge_request: 16906
author: Jacopo Beschi @jacopo-beschi
type: fixed
---
title: Fix settings panels not expanding when fragment hash linked
merge_request: 17074
author:
type: fixed
---
title: 'API: Get references a commit is pushed to'
merge_request: 15026
author: Robert Schilling
type: added
---
title: Allow including custom attributes in API responses
merge_request: 16526
author: Markus Koller
type: changed
---
title: Resolve PrepareUntrackedUploads PostgreSQL syntax error
merge_request: 17019
author:
type: fixed
---
title: LDAP Person no longer throws exception on invalid entry
merge_request:
author:
type: fixed
......@@ -348,9 +348,9 @@ Settings.artifacts['storage_path'] = Settings.absolute(Settings.artifacts.values
Settings.artifacts['path'] = Settings.artifacts['storage_path']
Settings.artifacts['max_size'] ||= 100 # in megabytes
Settings.artifacts['object_store'] ||= Settingslogic.new({})
Settings.artifacts['object_store']['enabled'] ||= false
Settings.artifacts['object_store']['enabled'] = false if Settings.artifacts['object_store']['enabled'].nil?
Settings.artifacts['object_store']['remote_directory'] ||= nil
Settings.artifacts['object_store']['background_upload'] ||= true
Settings.artifacts['object_store']['background_upload'] = true if Settings.artifacts['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.artifacts['object_store']['connection']&.deep_stringify_keys!
......@@ -394,9 +394,9 @@ Settings['lfs'] ||= Settingslogic.new({})
Settings.lfs['enabled'] = true if Settings.lfs['enabled'].nil?
Settings.lfs['storage_path'] = Settings.absolute(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"))
Settings.lfs['object_store'] ||= Settingslogic.new({})
Settings.lfs['object_store']['enabled'] ||= false
Settings.lfs['object_store']['enabled'] = false if Settings.lfs['object_store']['enabled'].nil?
Settings.lfs['object_store']['remote_directory'] ||= nil
Settings.lfs['object_store']['background_upload'] ||= true
Settings.lfs['object_store']['background_upload'] = true if Settings.lfs['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.lfs['object_store']['connection']&.deep_stringify_keys!
......@@ -407,19 +407,12 @@ Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
Settings.uploads['object_store'] ||= Settingslogic.new({})
Settings.uploads['object_store']['enabled'] ||= false
Settings.uploads['object_store']['enabled'] = false if Settings.uploads['object_store']['enabled'].nil?
Settings.uploads['object_store']['remote_directory'] ||= 'uploads'
Settings.uploads['object_store']['background_upload'] ||= true
Settings.uploads['object_store']['background_upload'] = true if Settings.uploads['object_store']['background_upload'].nil?
# Convert upload connection settings to use string keys, to make Fog happy
Settings.uploads['object_store']['connection']&.deep_stringify_keys!
#
# Uploads
#
Settings['uploads'] ||= Settingslogic.new({})
Settings.uploads['storage_path'] = Settings.absolute(Settings.uploads['storage_path'] || 'public')
Settings.uploads['base_dir'] = Settings.uploads['base_dir'] || 'uploads/-/system'
#
# Mattermost
#
......
#
# Monkey patching the https support for private urls
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/4879
#
module Fog
module Storage
class GoogleXML
class File < Fog::Model
module MonkeyPatch
def url(expires)
requires :key
collection.get_https_url(key, expires)
end
end
prepend MonkeyPatch
end
end
end
end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class SchedulePopulateUntrackedUploadsIfNeeded < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
FOLLOW_UP_MIGRATION = 'PopulateUntrackedUploads'.freeze
class UntrackedFile < ActiveRecord::Base
include EachBatch
self.table_name = 'untracked_files_for_uploads'
end
def up
if table_exists?(:untracked_files_for_uploads)
process_or_remove_table
end
end
def down
# nothing
end
private
def process_or_remove_table
if UntrackedFile.all.empty?
drop_temp_table
else
schedule_populate_untracked_uploads_jobs
end
end
def drop_temp_table
drop_table(:untracked_files_for_uploads, if_exists: true)
end
def schedule_populate_untracked_uploads_jobs
say "Scheduling #{FOLLOW_UP_MIGRATION} background migration jobs since there are rows in untracked_files_for_uploads."
bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION)
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20180206200543) do
ActiveRecord::Schema.define(version: 20180208183958) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......
......@@ -54,6 +54,7 @@ following locations:
- [Repositories](repositories.md)
- [Repository Files](repository_files.md)
- [Runners](runners.md)
- [Search](search.md)
- [Services](services.md)
- [Settings](settings.md)
- [Sidekiq metrics](sidekiq_metrics.md)
......
......@@ -198,6 +198,41 @@ Example response:
}
```
## Get references a commit is pushed to
> [Introduced][ce-15026] in GitLab 10.6
Get all references (from branches or tags) a commit is pushed to.
The pagination parameters `page` and `per_page` can be used to restrict the list of references.
```
GET /projects/:id/repository/commits/:sha/refs
```
Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
| `sha` | string | yes | The commit hash |
| `type` | string | no | The scope of commits. Possible values `branch`, `tag`, `all`. Default is `all`. |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits/5937ac0a7beb003549fc5fd26fc247adbce4a52e/refs?type=all"
```
Example response:
```json
[
{"type": "branch", "name": "'test'"},
{"type": "branch", "name": "add-balsamiq-file"},
{"type": "branch", "name": "wip"},
{"type": "tag", "name": "v1.1.0"}
]
```
## Cherry pick a commit
> [Introduced][ce-8047] in GitLab 8.15.
......@@ -500,3 +535,4 @@ Example response:
[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
[ce-8047]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8047
[ce-15026]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15026
......@@ -15,6 +15,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
......@@ -98,6 +99,7 @@ Parameters:
| `order_by` | string | no | Order groups by `name` or `path`. Default is `name` |
| `sort` | string | no | Order groups in `asc` or `desc` order. Default is `asc` |
| `statistics` | boolean | no | Include group statistics (admins only) |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `owned` | boolean | no | Limit to groups owned by the current user |
```
......@@ -145,6 +147,7 @@ Parameters:
| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
| `owned` | boolean | no | Limit by projects owned by the current user |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
Example response:
......@@ -204,6 +207,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/4
......
......@@ -39,7 +39,7 @@ Example response:
"path": "group1",
"kind": "group"
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2,
"plan": "bronze"
},
......@@ -49,7 +49,7 @@ Example response:
"path": "bar",
"kind": "group",
"full_path": "foo/bar",
"parent_id": "9",
"parent_id": 9,
"members_count_with_descendants": 5
}
]
......@@ -85,7 +85,7 @@ Example response:
"path": "twitter",
"kind": "group",
"full_path": "twitter",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
]
......@@ -118,7 +118,7 @@ Example response:
"path": "group1",
"kind": "group",
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
```
......@@ -138,7 +138,7 @@ Example response:
"path": "group1",
"kind": "group",
"full_path": "group1",
"parent_id": "null",
"parent_id": null,
"members_count_with_descendants": 2
}
```
......@@ -37,6 +37,7 @@ GET /projects
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......@@ -222,6 +223,7 @@ GET /users/:user_id/projects
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......@@ -390,6 +392,7 @@ GET /projects/:id
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
```json
{
......@@ -674,6 +677,7 @@ GET /projects/:id/forks
| `membership` | boolean | no | Limit by projects that the current user is a member of |
| `starred` | boolean | no | Limit by projects starred by the current user |
| `statistics` | boolean | no | Include project statistics |
| `with_custom_attributes` | boolean | no | Include [custom attributes](custom_attributes.md) in response (admins only) |
| `with_issues_enabled` | boolean | no | Limit by enabled issues feature |
| `with_merge_requests_enabled` | boolean | no | Limit by enabled merge requests feature |
......
......@@ -302,7 +302,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -334,7 +335,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -358,7 +360,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......@@ -603,7 +606,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -635,7 +639,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -659,7 +664,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......@@ -901,7 +907,8 @@ Example response:
"filename": "home.md",
"id": null,
"ref": "master",
"startline": 5
"startline": 5,
"project_id": 6
}
]
```
......@@ -931,7 +938,8 @@ Example response:
"authored_date": "2013-02-18T22:02:54.000Z",
"committer_name": "angus croll",
"committer_email": "anguscroll@gmail.com",
"committed_date": "2013-02-18T22:02:54.000Z"
"committed_date": "2013-02-18T22:02:54.000Z",
"project_id": 6
}
]
```
......@@ -953,7 +961,8 @@ Example response:
"filename": "README.md",
"id": null,
"ref": "master",
"startline": 46
"startline": 46,
"project_id": 6
}
]
```
......
......@@ -167,6 +167,12 @@ You can filter by [custom attributes](custom_attributes.md) with:
GET /users?custom_attributes[key]=value&custom_attributes[other_key]=other_value
```
You can include the users' [custom attributes](custom_attributes.md) in the response with:
```
GET /users?with_custom_attributes=true
```
## Single user
Get a single user.
......@@ -248,6 +254,12 @@ Parameters:
}
```
You can include the user's [custom attributes](custom_attributes.md) in the response with:
```
GET /users/:id?with_custom_attributes=true
```
## User creation
Creates a new user. Note only administrators can create new users. Either `password` or `reset_password` should be specified (`reset_password` takes priority).
......
# Query Count Limits
Each controller or API endpoint is allowed to execute up to 100 SQL queries. In
a production environment we'll only log an error in case this threshold is
exceeded, but in a test environment we'll raise an error instead.
Each controller or API endpoint is allowed to execute up to 100 SQL queries and
in test environments we'll raise an error when this threshold is exceeded.
## Solving Failing Tests
......
......@@ -17,6 +17,9 @@ would be `process_something`. If you're not sure what queue a worker uses,
you can find it using `SomeWorker.queue`. There is almost never a reason to
manually override the queue name using `sidekiq_options queue: :some_queue`.
You must always add any new queues to `app/workers/all_queues.yml` otherwise
your worker will not run.
## Queue Namespaces
While different workers cannot share a queue, they can share a queue namespace.
......
......@@ -395,7 +395,7 @@ data before running `pg_basebackup`.
gitlab-ctl replicate-geo-database --slot-name=secondary_example --host=1.2.3.4
```
When prompted, enter the password you set up for the `gitlab_replicator`
When prompted, enter the _plaintext_ password you set up for the `gitlab_replicator`
user in the first step.
This command also takes a number of additional options. You can use `--help`
......@@ -416,6 +416,8 @@ data before running `pg_basebackup`.
replication slot automatically if it does not exist.
- If you're repurposing an old server into a Geo secondary, you'll need to
add `--force` to the command line.
- When not in a production machine you can disable backup step if you
really sure this is what you want by adding `--skip-backup`
1. Verify that the secondary is configured correctly and that the primary is
reachable:
......
......@@ -6,7 +6,6 @@
and [problems](https://bugs.mysql.com/bug.php?id=65830) that
[suggested](https://bugs.mysql.com/bug.php?id=50909)
[fixes](https://bugs.mysql.com/bug.php?id=65830) [have](https://bugs.mysql.com/bug.php?id=63164).
- We recommend using MySQL version 5.6 or later. Please see the following [issue][ce-38152].
## Initial database setup
......
......@@ -11,11 +11,7 @@ in the table below.
| `issues_url` | The URL to the issue in Bugzilla project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
| `new_issue_url` | This is the URL to create a new issue in Bugzilla for the project linked to this GitLab project. Note that the `new_issue_url` requires PRODUCT_NAME to be updated with the product/project name in Bugzilla. |
Once you have configured and enabled Bugzilla:
- the **Issues** link on the GitLab project pages takes you to the appropriate
Bugzilla product page
- clicking **New issue** on the project dashboard takes you to Bugzilla for entering a new issue
Once you have configured and enabled Bugzilla you'll see the Bugzilla link on the GitLab project pages that takes you to the appropriate Bugzilla project.
## Referencing issues in Bugzilla
......
......@@ -118,7 +118,7 @@ in the table below.
| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
After saving the configuration, your GitLab project will be able to interact
with all JIRA projects in your JIRA instance.
with all JIRA projects in your JIRA instance and you'll see the JIRA link on the GitLab project pages that takes you to the appropriate JIRA project.
![JIRA service page](img/jira_service_page.png)
......
......@@ -12,6 +12,8 @@ in the table below.
| `issues_url` | The URL to the issue in Redmine project that is linked to this GitLab project. Note that the `issues_url` requires `:id` in the URL. This ID is used by GitLab as a placeholder to replace the issue number. |
| `new_issue_url` | This is the URL to create a new issue in Redmine for the project linked to this GitLab project. **This is currently not being used and will be removed in a future release.** |
Once you have configured and enabled Redmine you'll see the Redmine link on the GitLab project pages that takes you to the appropriate Redmine project.
As an example, below is a configuration for a project named gitlab-ci.
![Redmine configuration](img/redmine_configuration.png)
......
......@@ -169,9 +169,6 @@ from the UI.
If you want approvals to persist, independent of changes to the merge request,
turn this setting to off by unchecking the box and saving the changes.
If one of the approvers pushes a commit to the branch that is tied to the merge
request, they automatically get excluded from the approvers list.
## Merge requests with different source branch and target branch projects
If the merge request source branch and target branch belong to different
......
import Api from '~/api';
import { __ } from '~/locale';
import Flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
export default class ApproversSelect {
constructor() {
......@@ -147,7 +150,7 @@ export default class ApproversSelect {
}
static saveApprovers(fieldName) {
const $input = window.$(`[name="${fieldName}"]`);
const $input = $(`[name="${fieldName}"]`);
const newValue = $input.val();
const $loadWrapper = $('.load-wrapper');
const $approverSelect = $('.js-select-user-and-group');
......@@ -158,23 +161,24 @@ export default class ApproversSelect {
const $form = $('.js-add-approvers').closest('form');
$loadWrapper.removeClass('hidden');
window.$.ajax({
url: $form.attr('action'),
type: 'POST',
data: {
_method: 'PATCH',
[fieldName]: newValue,
axios.post($form.attr('action'), `_method=PATCH&${[encodeURIComponent(fieldName)]}=${newValue}`, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
success: ApproversSelect.updateApproverList,
complete() {
}).then(({ data }) => {
ApproversSelect.updateApproverList(data);
ApproversSelect.saveApproversComplete($input, $approverSelect, $loadWrapper);
}).catch(() => {
Flash(__('An error occurred while adding approver'));
ApproversSelect.saveApproversComplete($input, $approverSelect, $loadWrapper);
});
}
static saveApproversComplete($input, $approverSelect, $loadWrapper) {
$input.val('');
$approverSelect.select2('val', '');
$loadWrapper.addClass('hidden');
},
error() {
window.Flash('Failed to add Approver', 'alert');
},
});
}
static removeApprover(e) {
......@@ -182,17 +186,17 @@ export default class ApproversSelect {
const target = e.currentTarget;
const $loadWrapper = $('.load-wrapper');
$loadWrapper.removeClass('hidden');
$.ajax({
url: target.getAttribute('href'),
type: 'POST',
data: {
_method: 'DELETE',
},
success: ApproversSelect.updateApproverList,
complete: () => $loadWrapper.addClass('hidden'),
error() {
window.Flash('Failed to remove Approver', 'alert');
axios.post(target.getAttribute('href'), '_method=DELETE', {
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
},
}).then(({ data }) => {
ApproversSelect.updateApproverList(data);
$loadWrapper.addClass('hidden');
}).catch(() => {
Flash(__('An error occurred while removing approver'));
$loadWrapper.addClass('hidden');
});
}
......
import flash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
export default function initPathLocks(url, path) {
$('a.path-lock').on('click', (e) => {
e.preventDefault();
$.post(url, {
axios.post(url, {
path,
}, () => {
}).then(() => {
location.reload();
});
}).catch(() => flash(__('An error occurred while initializing path locks')));
});
}
......@@ -20,30 +20,6 @@ function WeightSelect(els, options = {}) {
inputField.val(options.selected);
}
updateWeight = function(selected) {
var data;
data = {};
data[abilityName] = {};
data[abilityName].weight = selected != null ? selected : null;
$loading.fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
dataType: 'json',
url: updateUrl,
data: data
}).done(function(data) {
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
$selectbox.hide();
if (data.weight != null) {
$value.html(`<strong>${data.weight}</strong>`);
} else {
$value.html('<span class="no-value">None</span>');
}
return $sidebarCollapsedValue.html(data.weight);
});
};
return $dropdown.glDropdown({
selectable: true,
fieldName,
......@@ -70,13 +46,8 @@ function WeightSelect(els, options = {}) {
e.preventDefault();
selected = inputField.val();
options.handleClick(selected);
} else if ($(dropdown).is(".js-filter-submit")) {
return $(dropdown).parents('form').submit();
} else if ($dropdown.is('.js-issuable-form-weight')) {
e.preventDefault();
} else {
selected = inputField.val();
return updateWeight(selected);
}
}
});
......
......@@ -80,7 +80,7 @@ module Geo
url = Gitlab::Geo.primary_node.url + repository.full_path + '.git'
# Fetch the repository, using a JWT header for authentication
authorization = ::Gitlab::Geo::BaseRequest.new.authorization
authorization = ::Gitlab::Geo::RepoSyncRequest.new.authorization
header = { "http.#{url}.extraHeader" => "Authorization: #{authorization}" }
repository.with_config(header) do
......@@ -108,6 +108,7 @@ module Geo
attrs["resync_#{type}"] = false
attrs["#{type}_retry_count"] = nil
attrs["#{type}_retry_at"] = nil
attrs["force_to_redownload_#{type}"] = false
end
registry.update!(attrs)
......@@ -183,8 +184,7 @@ module Geo
# Remove the deleted path in case it exists, but it may not be there
gitlab_shell.remove_repository(project.repository_storage_path, deleted_disk_path_temp)
# Move the original repository out of the way
unless gitlab_shell.mv_repository(project.repository_storage_path, repository.disk_path, deleted_disk_path_temp)
if project.repository_exists? && !gitlab_shell.mv_repository(project.repository_storage_path, repository.disk_path, deleted_disk_path_temp)
raise Gitlab::Shell::Error, 'Can not move original repository out of the way'
end
......
module Geo
class FileUploadService < FileService
IAT_LEEWAY = 60.seconds.to_i
attr_reader :auth_header
def initialize(params, auth_header)
......
module Gitlab
module Ci
module External
module File
class Base
YAML_WHITELIST_EXTENSION = /(yml|yaml)$/i.freeze
def initialize(location, opts = {})
@location = location
end
def valid?
location.match(YAML_WHITELIST_EXTENSION) && content
end
def content
raise NotImplementedError, 'content must be implemented and return a string or nil'
end
def error_message
raise NotImplementedError, 'error_message must be implemented and return a string'
end
end
end
end
end
end
......@@ -2,27 +2,28 @@ module Gitlab
module Ci
module External
module File
class Local
class Local < Base
attr_reader :location, :project, :sha
def initialize(location, opts = {})
@location = location
super
@project = opts.fetch(:project)
@sha = opts.fetch(:sha)
end
def valid?
local_file_content
def content
@content ||= fetch_local_content
end
def content
local_file_content
def error_message
"Local file '#{location}' is not valid."
end
private
def local_file_content
@local_file_content ||= project.repository.blob_data_at(sha, location)
def fetch_local_content
project.repository.blob_data_at(sha, location)
end
end
end
......
......@@ -2,28 +2,24 @@ module Gitlab
module Ci
module External
module File
class Remote
class Remote < Base
include Gitlab::Utils::StrongMemoize
attr_reader :location
def initialize(location, opts = {})
@location = location
end
def valid?
::Gitlab::UrlSanitizer.valid?(location) && content
end
def content
return @content if defined?(@content)
@content = strong_memoize(:content) do
begin
HTTParty.get(location)
rescue HTTParty::Error, Timeout::Error
false
rescue HTTParty::Error, Timeout::Error, SocketError
nil
end
end
end
def error_message
"Remote file '#{location}' is not valid."
end
end
end
......
......@@ -17,10 +17,8 @@ module Gitlab
attr_reader :locations, :project, :sha
def build_external_file(location)
remote_file = Gitlab::Ci::External::File::Remote.new(location)
if remote_file.valid?
remote_file
if ::Gitlab::UrlSanitizer.valid?(location)
Gitlab::Ci::External::File::Remote.new(location)
else
options = { project: project, sha: sha }
Gitlab::Ci::External::File::Local.new(location, options)
......
......@@ -28,7 +28,7 @@ module Gitlab
def validate_external_file(external_file)
unless external_file.valid?
raise FileError, "External file: '#{external_file.location}' should be a valid local or remote file"
raise FileError, external_file.error_message
end
end
......
......@@ -77,6 +77,7 @@ module Gitlab
extname = File.extname(filename)
basename = filename.sub(/#{extname}$/, '')
content = result["_source"]["blob"]["content"]
project_id = result["_parent"].to_i
total_lines = content.lines.size
term =
......@@ -113,7 +114,8 @@ module Gitlab
basename: basename,
ref: ref,
startline: from + 1,
data: data.join
data: data.join,
project_id: project_id
)
end
......
......@@ -20,6 +20,10 @@ module Gitlab
geo_auth_token(request_data)
end
def expiration_time
1.minute
end
private
def geo_auth_token(message)
......@@ -27,6 +31,7 @@ module Gitlab
raise GeoNodeNotFoundError unless geo_node
token = JSONWebToken::HMACToken.new(geo_node.secret_access_key)
token.expire_time = Time.now + expiration_time
token[:data] = message.to_json
"#{GITLAB_GEO_AUTH_TOKEN_TYPE} #{geo_node.access_key}:#{token.encoded}"
......
module Gitlab
module Geo
class RepoSyncRequest < BaseRequest
def expiration_time
10.minutes
end
end
end
end
......@@ -141,7 +141,7 @@ describe Groups::EpicsController do
show_epic(:json)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('entities/epic')
expect(response).to match_response_schema('entities/epic', dir: 'ee')
end
context 'with unauthorized user' do
......
require_relative '../support/test_env'
FactoryBot.define do
factory :remote_mirror, class: 'RemoteMirror' do
association :project, :repository
......
require 'spec_helper'
describe 'Fog::Storage::GoogleXML::File' do
let(:storage) do
Fog.mock!
Fog::Storage.new({
google_storage_access_key_id: "asdf",
google_storage_secret_access_key: "asdf",
provider: "Google"
})
end
let(:file) do
directory = storage.directories.create(key: 'data')
directory.files.create(
body: 'Hello World!',
key: 'hello_world.txt'
)
end
it 'delegates to #get_https_url' do
expect(file.url(Time.now)).to start_with("https://")
end
end
......@@ -6,7 +6,7 @@ describe EE::Gitlab::Ci::Config do
let(:gitlab_ci_yml) do
<<~HEREDOC
include:
- /spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
- /ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml
- #{remote_location}
image: ruby:2.2
......@@ -38,7 +38,7 @@ describe EE::Gitlab::Ci::Config do
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
HEREDOC
end
let(:local_file_content) { File.read("#{Rails.root}/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml") }
let(:local_file_content) { File.read(Rails.root.join('ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) }
before do
allow(project).to receive(:feature_available?).with(:external_files_in_gitlab_ci).and_return(true)
......@@ -46,7 +46,7 @@ describe EE::Gitlab::Ci::Config do
context "when gitlab_ci_yml has valid 'include' defined" do
before do
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:local_file_content).and_return(local_file_content)
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content)
WebMock.stub_request(:get, remote_location).to_return(body: remote_file_content)
end
......@@ -85,7 +85,7 @@ describe EE::Gitlab::Ci::Config do
it 'raises error YamlProcessor validationError' do
expect { config }.to raise_error(
::Gitlab::Ci::YamlProcessor::ValidationError,
"External file: 'invalid' should be a valid local or remote file"
"Local file 'invalid' is not valid."
)
end
end
......
......@@ -4,12 +4,12 @@ describe Gitlab::Ci::External::File::Local do
let(:project) { create(:project, :repository) }
let(:local_file) { described_class.new(location, { project: project, sha: '12345' }) }
describe "#valid?" do
describe '#valid?' do
context 'when is a valid local path' do
let(:location) { '/vendor/gitlab-ci-yml/existent-file.yml' }
before do
allow_any_instance_of(described_class).to receive(:local_file_content).and_return("image: 'ruby2:2'")
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'")
end
it 'should return true' do
......@@ -25,7 +25,17 @@ describe Gitlab::Ci::External::File::Local do
end
end
describe "#content" do
context 'when is not a yaml file' do
let(:location) { '/config/application.rb' }
it 'should return false' do
expect(local_file.valid?).to be_falsy
end
end
end
describe '#content' do
context 'with a a valid file' do
let(:local_file_content) do
<<~HEREDOC
before_script:
......@@ -36,18 +46,31 @@ describe Gitlab::Ci::External::File::Local do
- bundle install --jobs $(nproc) "${FLAGS[@]}"
HEREDOC
end
context 'with a local file' do
let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
let(:location) { '/vendor/gitlab-ci-yml/existent-file.yml' }
before do
allow_any_instance_of(described_class).to receive(:local_file_content).and_return(local_file_content)
allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content)
end
it 'should return the content of the file' do
expect(local_file.content).to eq(local_file_content)
end
end
context 'with an invalid file' do
let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
it 'should be nil' do
expect(local_file.content).to be_nil
end
end
end
describe '#error_message' do
let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' }
it 'should return an error message' do
expect(local_file.error_message).to eq("Local file '#{location}' is not valid.")
end
end
end
......@@ -25,7 +25,7 @@ describe Gitlab::Ci::External::File::Remote do
end
end
context 'when is not a valid remote url' do
context 'with an irregular url' do
let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
it 'should return false' do
......@@ -42,6 +42,14 @@ describe Gitlab::Ci::External::File::Remote do
expect(remote_file.valid?).to be_falsy
end
end
context 'when is not a yaml file' do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
it 'should be falsy' do
expect(remote_file.valid?).to be_falsy
end
end
end
describe "#content" do
......@@ -64,5 +72,25 @@ describe Gitlab::Ci::External::File::Remote do
expect(remote_file.content).to be_falsy
end
end
context 'with an invalid remote url' do
let(:location) { 'https://asdasdasdaj48ggerexample.com' }
before do
WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error'))
end
it 'should be nil' do
expect(remote_file.content).to be_nil
end
end
end
describe "#error_message" do
let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
it 'should return an error message' do
expect(remote_file.error_message).to eq("Remote file '#{location}' is not valid.")
end
end
end
......@@ -19,18 +19,23 @@ describe Gitlab::Ci::External::Processor do
it 'should raise an error' do
expect { processor.perform }.to raise_error(
described_class::FileError,
"External file: '/vendor/gitlab-ci-yml/non-existent-file.yml' should be a valid local or remote file"
"Local file '/vendor/gitlab-ci-yml/non-existent-file.yml' is not valid."
)
end
end
context 'when an invalid remote file is defined' do
let(:values) { { include: 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml', image: 'ruby:2.2' } }
let(:remote_file) { 'http://doesntexist.com/.gitlab-ci-1.yml' }
let(:values) { { include: remote_file, image: 'ruby:2.2' } }
before do
WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error'))
end
it 'should raise an error' do
expect { processor.perform }.to raise_error(
described_class::FileError,
"External file: 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' should be a valid local or remote file"
"Remote file '#{remote_file}' is not valid."
)
end
end
......@@ -85,7 +90,7 @@ describe Gitlab::Ci::External::Processor do
end
before do
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:local_file_content).and_return(local_file_content)
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content)
end
it 'should append the file to the values' do
......@@ -102,7 +107,7 @@ describe Gitlab::Ci::External::Processor do
let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' }
let(:external_files) do
[
'/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml',
'/ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml',
remote_file
]
end
......@@ -123,8 +128,8 @@ describe Gitlab::Ci::External::Processor do
end
before do
local_file_content = File.read("#{Rails.root}/spec/ee/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml")
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:local_file_content).and_return(local_file_content)
local_file_content = File.read(Rails.root.join('ee/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml'))
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content)
WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content)
end
......@@ -143,7 +148,7 @@ describe Gitlab::Ci::External::Processor do
let(:local_file_content) { 'invalid content file ////' }
before do
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:local_file_content).and_return(local_file_content)
allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content)
end
it 'should raise an error' do
......
......@@ -8,7 +8,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
stub_config_setting(host: 'localhost')
end
let(:email_raw) { fixture_file('emails/service_desk.eml') }
let(:email_raw) { fixture_file('emails/service_desk.eml', dir: 'ee') }
let(:namespace) { create(:namespace, name: "email") }
context 'service desk is enabled for the project' do
......@@ -64,7 +64,7 @@ describe Gitlab::Email::Handler::EE::ServiceDeskHandler do
end
context 'when the email is forwarded through an alias' do
let(:email_raw) { fixture_file('emails/service_desk_forwarded.eml') }
let(:email_raw) { fixture_file('emails/service_desk_forwarded.eml', dir: 'ee') }
it 'sends thank you the email and creates issue' do
setup_attachment
......
require 'spec_helper'
describe Gitlab::Email::Handler do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
describe '.for' do
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
def ee_handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture, dir: 'ee'), mail_key)
end
context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(ee_handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end
it 'uses no handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(ee_handler_for('emails/service_desk.eml', 'some/project')).to be_nil
end
end
context 'a new issue email' do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
end
......@@ -33,7 +33,7 @@ describe SystemCheck::Geo::AuthorizedKeysCheck do
it 'skips when config file is not readable' do
override_sshd_config('system_check/sshd_config')
allow(File).to receive(:readable?).with(expand_fixture_ee_path('system_check/sshd_config')) { false }
allow(File).to receive(:readable?).with(expand_fixture_path('system_check/sshd_config', dir: 'ee')) { false }
expect_skipped('Cannot access OpenSSH configuration file')
......@@ -167,6 +167,6 @@ describe SystemCheck::Geo::AuthorizedKeysCheck do
end
def override_sshd_config(relative_path)
allow(subject).to receive(:openssh_config_path) { expand_fixture_ee_path(relative_path) }
allow(subject).to receive(:openssh_config_path) { expand_fixture_path(relative_path, dir: 'ee') }
end
end
......@@ -56,7 +56,7 @@ describe API::V3::Github do
expect(json_response).to be_an(Array)
expect(json_response.size).to eq(2)
expect(response).to match_response_schema('entities/github/repositories')
expect(response).to match_response_schema('entities/github/repositories', dir: 'ee')
end
it 'returns valid project path as name' do
......@@ -104,7 +104,7 @@ describe API::V3::Github do
expect(response).to include_pagination_headers
expect(json_response).to be_an(Array)
expect(response).to match_response_schema('entities/github/branches')
expect(response).to match_response_schema('entities/github/branches', dir: 'ee')
end
end
......@@ -142,7 +142,7 @@ describe API::V3::Github do
get v3_api("/repos/#{project.namespace.path}/#{project.path}/commits/#{commit_id}", user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('entities/github/commit')
expect(response).to match_response_schema('entities/github/commit', dir: 'ee')
end
end
......
......@@ -38,13 +38,13 @@ describe "Git HTTP requests (Geo)" do
context 'post-dated Geo JWT token' do
let(:env) { valid_geo_env }
it { travel_to(2.minutes.ago) { is_expected.to have_gitlab_http_status(:unauthorized) } }
it { travel_to(11.minutes.ago) { is_expected.to have_gitlab_http_status(:unauthorized) } }
end
context 'expired Geo JWT token' do
let(:env) { valid_geo_env }
it { travel_to(Time.now + 2.minutes) { is_expected.to have_gitlab_http_status(:unauthorized) } }
it { travel_to(Time.now + 11.minutes) { is_expected.to have_gitlab_http_status(:unauthorized) } }
end
context 'invalid Geo JWT token' do
......
......@@ -116,4 +116,103 @@ describe AuditEventService do
end
end
end
describe '#for_failed_login' do
let(:author_name) { 'testuser' }
let(:ip_address) { '127.0.0.1' }
let(:service) { described_class.new(author_name, nil, ip_address: ip_address) }
let(:event) { service.for_failed_login.unauth_security_event }
before do
stub_licensed_features(extended_audit_events: true)
end
it 'has the right type' do
expect(event.entity_type).to eq('User')
end
it 'has the right author' do
expect(event.details[:author_name]).to eq(author_name)
end
it 'has the right IP address' do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
expect(event.details[:ip_address]).to eq(ip_address)
end
it 'has the right auth method for OAUTH' do
oauth_service = described_class.new(author_name, nil, ip_address: ip_address, with: 'ldap')
event = oauth_service.for_failed_login.unauth_security_event
expect(event.details[:failed_login]).to eq('LDAP')
end
end
describe 'license' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:service) { described_class.new(user, project, action: :create) }
let(:event) { service.for_project.security_event }
before do
disable_license_audit_features(service)
end
describe 'has the audit_admin feature' do
before do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has the entity_path' do
expect(event.details[:entity_path]).to eq(project.full_path)
end
end
describe 'has the extended_audit_events feature' do
before do
allow(service).to receive(:entity_audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'entity has the audit_events feature' do
before do
allow(service).to receive(:audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'has not any audit event feature' do
it 'does not log the audit event' do
expect { event }.not_to change(AuditEvent, :count)
end
end
def disable_license_audit_features(service)
[:entity_audit_events_enabled?,
:admin_audit_log_enabled?,
:audit_events_enabled?].each do |f|
allow(service).to receive(f).and_return(false)
end
end
end
end
require 'spec_helper'
describe Ci::RegisterJobService do
let!(:project) { create :project, shared_runners_enabled: false }
let!(:pipeline) { create :ci_empty_pipeline, project: project }
let!(:pending_build) { create :ci_build, pipeline: pipeline }
let(:shared_runner) { create(:ci_runner, :shared) }
describe '#execute' do
context 'for project with shared runners when global minutes limit is set' do
before do
project.update(shared_runners_enabled: true)
stub_application_setting(shared_runners_minutes: 100)
end
context 'allow to pick builds' do
let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Ci::Build) }
end
context 'when over the global quota' do
before do
project.namespace.create_namespace_statistics(
shared_runners_seconds: 6001)
end
let(:build) { execute(shared_runner) }
it "does not return a build" do
expect(build).to be_nil
end
context 'when project is public' do
before do
project.update(visibility_level: Project::PUBLIC)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
context 'when namespace limit is set to unlimited' do
before do
project.namespace.update(shared_runners_minutes_limit: 0)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
context 'when namespace quota is bigger than a global one' do
before do
project.namespace.update(shared_runners_minutes_limit: 101)
end
it "does return the build" do
expect(build).to be_kind_of(Ci::Build)
end
end
end
context 'when group is subgroup' do
let!(:root_ancestor) { create(:group) }
let!(:group) { create(:group, parent: root_ancestor) }
let!(:project) { create :project, shared_runners_enabled: true, group: group }
let(:build) { execute(shared_runner) }
context 'when shared_runner_minutes_on_root_namespace is disabled' do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: false)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
context 'when shared_runner_minutes_on_root_namespace is enabled', :nested_groups do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: true)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "limit is ignored and build is returned" do
expect(build).not_to be_nil
end
end
context 'when we are over limit on root namespace' do
before do
root_ancestor.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
end
end
def execute(runner)
described_class.new(runner).execute.build
end
end
end
require 'spec_helper'
describe AuditEventService do
describe '#for_failed_login' do
let(:author_name) { 'testuser' }
let(:ip_address) { '127.0.0.1' }
let(:service) { described_class.new(author_name, nil, ip_address: ip_address) }
let(:event) { service.for_failed_login.unauth_security_event }
before do
stub_licensed_features(extended_audit_events: true)
end
it 'has the right type' do
expect(event.entity_type).to eq('User')
end
it 'has the right author' do
expect(event.details[:author_name]).to eq(author_name)
end
it 'has the right IP address' do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
expect(event.details[:ip_address]).to eq(ip_address)
end
it 'has the right auth method for OAUTH' do
oauth_service = described_class.new(author_name, nil, ip_address: ip_address, with: 'ldap')
event = oauth_service.for_failed_login.unauth_security_event
expect(event.details[:failed_login]).to eq('LDAP')
end
end
describe 'license' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:service) { described_class.new(user, project, action: :create) }
let(:event) { service.for_project.security_event }
before do
disable_license_audit_features(service)
end
describe 'has the audit_admin feature' do
before do
allow(service).to receive(:admin_audit_log_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has the entity_path' do
expect(event.details[:entity_path]).to eq(project.full_path)
end
end
describe 'has the extended_audit_events feature' do
before do
allow(service).to receive(:entity_audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'entity has the audit_events feature' do
before do
allow(service).to receive(:audit_events_enabled?).and_return(true)
end
it 'logs an audit event' do
expect { event }.to change(AuditEvent, :count).by(1)
end
it 'has not the entity_path' do
expect(event.details[:entity_path]).not_to eq(project.full_path)
end
end
describe 'has not any audit event feature' do
it 'does not log the audit event' do
expect { event }.not_to change(AuditEvent, :count)
end
end
def disable_license_audit_features(service)
[:entity_audit_events_enabled?,
:admin_audit_log_enabled?,
:audit_events_enabled?].each do |f|
allow(service).to receive(f).and_return(false)
end
end
end
end
require 'spec_helper'
module Ci
describe RegisterJobService do
let!(:project) { create :project, shared_runners_enabled: false }
let!(:pipeline) { create :ci_empty_pipeline, project: project }
let!(:pending_build) { create :ci_build, pipeline: pipeline }
let(:shared_runner) { create(:ci_runner, :shared) }
describe '#execute' do
context 'for project with shared runners when global minutes limit is set' do
before do
project.update(shared_runners_enabled: true)
stub_application_setting(shared_runners_minutes: 100)
end
context 'allow to pick builds' do
let(:build) { execute(shared_runner) }
it { expect(build).to be_kind_of(Build) }
end
context 'when over the global quota' do
before do
project.namespace.create_namespace_statistics(
shared_runners_seconds: 6001)
end
let(:build) { execute(shared_runner) }
it "does not return a build" do
expect(build).to be_nil
end
context 'when project is public' do
before do
project.update(visibility_level: Project::PUBLIC)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace limit is set to unlimited' do
before do
project.namespace.update(shared_runners_minutes_limit: 0)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
context 'when namespace quota is bigger than a global one' do
before do
project.namespace.update(shared_runners_minutes_limit: 101)
end
it "does return the build" do
expect(build).to be_kind_of(Build)
end
end
end
context 'when group is subgroup' do
let!(:root_ancestor) { create(:group) }
let!(:group) { create(:group, parent: root_ancestor) }
let!(:project) { create :project, shared_runners_enabled: true, group: group }
let(:build) { execute(shared_runner) }
context 'when shared_runner_minutes_on_root_namespace is disabled' do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: false)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
context 'when shared_runner_minutes_on_root_namespace is enabled', :nested_groups do
before do
stub_feature_flags(shared_runner_minutes_on_root_namespace: true)
end
it "does return a build" do
expect(build).not_to be_nil
end
context 'when we are over limit on subnamespace' do
before do
group.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "limit is ignored and build is returned" do
expect(build).not_to be_nil
end
end
context 'when we are over limit on root namespace' do
before do
root_ancestor.create_namespace_statistics(
shared_runners_seconds: 6001)
end
it "does not return a build" do
expect(build).to be_nil
end
end
end
end
end
def execute(runner)
described_class.new(runner).execute.build
end
end
end
end
......@@ -210,6 +210,14 @@ describe Geo::RepositorySyncService do
subject.execute
end
it 'sets the redownload flag to false after success' do
registry = create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD + 1, force_to_redownload_repository: true)
subject.execute
expect(registry.reload.force_to_redownload_repository).to be false
end
it 'tries to redownload repo' do
create(:geo_project_registry, project: project, repository_retry_count: Geo::BaseSyncService::RETRY_BEFORE_REDOWNLOAD + 1)
......@@ -239,7 +247,7 @@ describe Geo::RepositorySyncService do
subject.execute
end
it 'successfully redownloads the file even if the retry time exceeds max value' do
it 'successfully redownloads the repository even if the retry time exceeds max value' do
timestamp = Time.now.utc
registry = create(
:geo_project_registry,
......@@ -257,6 +265,20 @@ describe Geo::RepositorySyncService do
registry.reload
expect(registry.repository_retry_at).to be_nil
end
context 'no repository' do
let(:project) { create(:project) }
it 'does not raise an error' do
create(
:geo_project_registry,
project: project,
force_to_redownload_repository: true
)
subject.execute
end
end
end
end
end
RSpec.configure do |config|
config.before(:suite) do
FactoryBot.definition_file_paths = [
Rails.root.join('ee', 'spec', 'factories')
]
FactoryBot.find_definitions
end
end
......@@ -26,17 +26,24 @@ Dir["#{Rails.root}/features/steps/shared/*.rb"].each { |file| require file }
Spinach.hooks.before_run do
include RSpec::Mocks::ExampleMethods
include ActiveJob::TestHelper
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
RSpec::Mocks.setup
TestEnv.init(mailer: false)
# EE-specific START
FactoryBot.definition_file_paths = [
Rails.root.join('ee', 'spec', 'factories')
]
FactoryBot.find_definitions
License.destroy_all
TestLicense.init
# EE-specific END
# skip pre-receive hook check so we can use
# web editor and merge
TestEnv.disable_pre_receive
include FactoryBot::Syntax::Methods
include GitlabRoutingHelper
end
Spinach.hooks.after_scenario do |scenario_data, step_definitions|
......
......@@ -161,6 +161,27 @@ module API
end
end
desc 'Get all references a commit is pushed to' do
detail 'This feature was introduced in GitLab 10.6'
success Entities::BasicRef
end
params do
requires :sha, type: String, desc: 'A commit sha'
optional :type, type: String, values: %w[branch tag all], default: 'all', desc: 'Scope'
use :pagination
end
get ':id/repository/commits/:sha/refs', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
commit = user_project.commit(params[:sha])
not_found!('Commit') unless commit
refs = []
refs.concat(user_project.repository.branch_names_contains(commit.id).map {|name| { type: 'branch', name: name }}) unless params[:type] == 'tag'
refs.concat(user_project.repository.tag_names_contains(commit.id).map {|name| { type: 'tag', name: name }}) unless params[:type] == 'branch'
refs = Kaminari.paginate_array(refs)
present paginate(refs), with: Entities::BasicRef
end
desc 'Post comment to commit' do
success Entities::CommitNote
end
......@@ -170,7 +191,7 @@ module API
optional :path, type: String, desc: 'The file path'
given :path do
requires :line, type: Integer, desc: 'The line number'
requires :line_type, type: String, values: %w(new old), default: 'new', desc: 'The type of the line'
requires :line_type, type: String, values: %w[new old], default: 'new', desc: 'The type of the line'
end
end
post ':id/repository/commits/:sha/comments', requirements: API::COMMIT_ENDPOINT_REQUIREMENTS do
......
......@@ -22,6 +22,7 @@ module API
end
expose :avatar_path, if: ->(user, options) { options.fetch(:only_path, false) && user.avatar_path }
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
expose :web_url do |user, options|
Gitlab::Routing.url_helpers.user_url(user)
......@@ -119,6 +120,8 @@ module API
expose :star_count, :forks_count
expose :last_activity_at
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
def self.preload_relation(projects_relation, options = {})
projects_relation.preload(:project_feature, :route)
.preload(namespace: [:route, :owner],
......@@ -256,6 +259,8 @@ module API
expose :parent_id
end
expose :custom_attributes, using: 'API::Entities::CustomAttribute', if: :with_custom_attributes
expose :statistics, if: :statistics do
with_options format_with: -> (value) { value.to_i } do
expose :storage_size
......@@ -303,6 +308,11 @@ module API
expose :stats, using: Entities::CommitStats, if: :stats
expose :status
expose :last_pipeline, using: 'API::Entities::PipelineBasic'
expose :project_id
end
class BasicRef < Grape::Entity
expose :type, :name
end
class Branch < Grape::Entity
......@@ -1450,6 +1460,7 @@ module API
expose :id
expose :ref
expose :startline
expose :project_id
end
end
end
module API
class Groups < Grape::API
include PaginationParams
include Helpers::CustomAttributes
before { authenticate_non_get! }
......@@ -78,6 +79,8 @@ module API
}
groups = groups.with_statistics if options[:statistics]
groups, options = with_custom_attributes(groups, options)
present paginate(groups), options
end
end
......@@ -90,6 +93,7 @@ module API
end
params do
use :group_list_params
use :with_custom_attributes
end
get do
groups = find_groups(params)
......@@ -176,9 +180,20 @@ module API
desc 'Get a single group, with containing projects.' do
success Entities::GroupDetail
end
params do
use :with_custom_attributes
end
get ":id" do
group = find_group!(params[:id])
present group, with: Entities::GroupDetail, current_user: current_user
options = {
with: Entities::GroupDetail,
current_user: current_user
}
group, options = with_custom_attributes(group, options)
present group, options
end
desc 'Remove a group.'
......@@ -212,12 +227,19 @@ module API
optional :starred, type: Boolean, default: false, desc: 'Limit by starred status'
use :pagination
use :with_custom_attributes
end
get ":id/projects" do
projects = find_group_projects(params)
entity = params[:simple] ? Entities::BasicProjectDetails : Entities::Project
present entity.prepare_relation(projects), with: entity, current_user: current_user
options = {
with: params[:simple] ? Entities::BasicProjectDetails : Entities::Project,
current_user: current_user
}
projects, options = with_custom_attributes(projects, options)
present options[:with].prepare_relation(projects), options
end
desc 'Get a list of subgroups in this group.' do
......@@ -225,6 +247,7 @@ module API
end
params do
use :group_list_params
use :with_custom_attributes
end
get ":id/subgroups" do
groups = find_groups(params)
......
module API
module Helpers
module CustomAttributes
extend ActiveSupport::Concern
included do
helpers do
params :with_custom_attributes do
optional :with_custom_attributes, type: Boolean, default: false, desc: 'Include custom attributes in the response'
end
def with_custom_attributes(collection_or_resource, options = {})
options = options.merge(
with_custom_attributes: params[:with_custom_attributes] &&
can?(current_user, :read_custom_attribute)
)
if options[:with_custom_attributes] && collection_or_resource.is_a?(ActiveRecord::Relation)
collection_or_resource = collection_or_resource.includes(:custom_attributes)
end
[collection_or_resource, options]
end
end
end
end
end
end
......@@ -3,6 +3,7 @@ require_dependency 'declarative_policy'
module API
class Projects < Grape::API
include PaginationParams
include Helpers::CustomAttributes
before { authenticate_non_get! }
......@@ -86,6 +87,7 @@ module API
projects = projects.with_merge_requests_enabled if params[:with_merge_requests_enabled]
projects = projects.with_statistics if params[:statistics]
projects = paginate(projects)
projects, options = with_custom_attributes(projects, options)
if current_user
project_members = current_user.project_members.preload(:source, user: [notification_settings: :source])
......@@ -113,6 +115,7 @@ module API
requires :user_id, type: String, desc: 'The ID or username of the user'
use :collection_params
use :statistics_params
use :with_custom_attributes
end
get ":user_id/projects" do
user = find_user(params[:user_id])
......@@ -133,6 +136,7 @@ module API
params do
use :collection_params
use :statistics_params
use :with_custom_attributes
end
get do
present_projects load_projects
......@@ -202,11 +206,19 @@ module API
end
params do
use :statistics_params
use :with_custom_attributes
end
get ":id" do
entity = current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails
present user_project, with: entity, current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project), statistics: params[:statistics]
options = {
with: current_user ? Entities::ProjectWithAccess : Entities::BasicProjectDetails,
current_user: current_user,
user_can_admin_project: can?(current_user, :admin_project, user_project),
statistics: params[:statistics]
}
project, options = with_custom_attributes(user_project, options)
present project, options
end
desc 'Fork new project for the current user or provided namespace.' do
......@@ -248,6 +260,7 @@ module API
end
params do
use :collection_params
use :with_custom_attributes
end
get ':id/forks' do
forks = ForkProjectsFinder.new(user_project, params: project_finder_params, current_user: current_user).execute
......
......@@ -11,7 +11,7 @@ module API
projects: Entities::BasicProjectDetails,
milestones: Entities::Milestone,
notes: Entities::Note,
commits: Entities::Commit,
commits: Entities::CommitDetail,
blobs: Entities::Blob,
wiki_blobs: Entities::Blob,
snippet_titles: Entities::Snippet,
......@@ -43,7 +43,7 @@ module API
case params[:scope]
when 'wiki_blobs'
paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob) }
paginate(results).map { |blob| Gitlab::ProjectSearchResults.parse_search_result(blob, user_project) }
when 'blobs'
paginate(results).map { |blob| blob[1] }
else
......@@ -110,9 +110,7 @@ module API
get ':id/-/search' do
check_elasticsearch_scope!
group = find_group!(params[:id])
present search(group_id: group.id), with: entity
present search(group_id: user_group.id), with: entity
end
end
......@@ -131,9 +129,7 @@ module API
use :pagination
end
get ':id/-/search' do
project = find_project!(params[:id])
present search(project_id: project.id), with: entity
present search(project_id: user_project.id), with: entity
end
end
end
......
......@@ -2,6 +2,7 @@ module API
class Users < Grape::API
include PaginationParams
include APIGuard
include Helpers::CustomAttributes
allow_access_with_scope :read_user, if: -> (request) { request.get? }
......@@ -73,6 +74,7 @@ module API
use :sort_params
use :pagination
use :with_custom_attributes
# EE
optional :skip_ldap, type: Boolean, default: false, desc: 'Skip LDAP users'
......@@ -100,8 +102,9 @@ module API
entity = current_user&.admin? ? Entities::UserWithAdmin : Entities::UserBasic
users = users.preload(:identities, :u2f_registrations) if entity == Entities::UserWithAdmin
users, options = with_custom_attributes(users, with: entity)
present paginate(users), with: entity
present paginate(users), options
end
desc 'Get a single user' do
......@@ -109,12 +112,16 @@ module API
end
params do
requires :id, type: Integer, desc: 'The ID of the user'
use :with_custom_attributes
end
get ":id" do
user = User.find_by(id: params[:id])
not_found!('User') unless user && can?(current_user, :read_user, user)
opts = current_user&.admin? ? { with: Entities::UserWithAdmin } : { with: Entities::User }
user, opts = with_custom_attributes(user, opts)
present user, opts
end
......
......@@ -249,7 +249,7 @@ module Gitlab
end
def drop_temp_table_if_finished
if UntrackedFile.all.empty?
if UntrackedFile.all.empty? && !Rails.env.test? # Dropping a table intermittently breaks test cleanup
UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
if_exists: true)
end
......
......@@ -43,8 +43,12 @@ module Gitlab
store_untracked_file_paths
if UntrackedFile.all.empty?
drop_temp_table
else
schedule_populate_untracked_uploads_jobs
end
end
private
......@@ -92,7 +96,7 @@ module Gitlab
end
end
yield(paths)
yield(paths) if paths.any?
end
def build_find_command(search_dir)
......@@ -165,6 +169,13 @@ module Gitlab
bulk_queue_background_migration_jobs_by_range(
UntrackedFile, FOLLOW_UP_MIGRATION)
end
def drop_temp_table
unless Rails.env.test? # Dropping a table intermittently breaks test cleanup
UntrackedFile.connection.drop_table(:untracked_files_for_uploads,
if_exists: true)
end
end
end
end
end
......@@ -28,7 +28,7 @@ module Gitlab
def find_by_content(query)
results = repository.search_files_by_content(query, ref).first(BATCH_SIZE)
results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result) }
results.map { |result| Gitlab::ProjectSearchResults.parse_search_result(result, project) }
end
def find_by_filename(query, except: [])
......@@ -45,7 +45,8 @@ module Gitlab
basename: File.basename(blob.path),
ref: ref,
startline: 1,
data: blob.data
data: blob.data,
project: project
)
end
end
......
......@@ -67,8 +67,6 @@ module Gitlab
Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
@entry = entry
@provider = provider
validate_entry
end
def name
......@@ -121,19 +119,6 @@ module Gitlab
entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
end
def validate_entry
allowed_attrs = self.class.ldap_attributes(config).map(&:downcase)
# Net::LDAP::Entry transforms keys to symbols. Change to strings to compare.
entry_attrs = entry.attribute_names.map { |n| n.to_s.downcase }
invalid_attrs = entry_attrs - allowed_attrs
if invalid_attrs.any?
raise InvalidEntryError,
"#{self.class.name} initialized with Net::LDAP::Entry containing invalid attributes(s): #{invalid_attrs}"
end
end
end
end
end
......@@ -41,7 +41,7 @@ module Gitlab
@commits_count ||= commits.count
end
def self.parse_search_result(result)
def self.parse_search_result(result, project = nil)
ref = nil
filename = nil
basename = nil
......@@ -66,7 +66,8 @@ module Gitlab
basename: basename,
ref: ref,
startline: startline,
data: data
data: data,
project_id: project ? project.id : nil
)
end
......
......@@ -51,13 +51,7 @@ module Gitlab
error = ThresholdExceededError.new(error_message)
if raise_error?
raise(error)
else
# Raven automatically logs to the Rails log if disabled, thus we don't
# need to manually log anything in case Sentry support is not enabled.
Raven.capture_exception(error)
end
raise(error) if raise_error?
end
def increment
......
module Gitlab
class SearchResults
class FoundBlob
attr_reader :id, :filename, :basename, :ref, :startline, :data
attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
def initialize(opts = {})
@id = opts.fetch(:id, nil)
......@@ -11,6 +11,7 @@ module Gitlab
@startline = opts.fetch(:startline, nil)
@data = opts.fetch(:data, nil)
@per_page = opts.fetch(:per_page, 20)
@project_id = opts.fetch(:project_id, nil)
end
def path
......
......@@ -117,9 +117,8 @@ module QA
autoload :Show, 'qa/page/project/pipeline/show'
end
module Pipeline
autoload :Index, 'qa/page/project/pipeline/index'
autoload :Show, 'qa/page/project/pipeline/show'
module Job
autoload :Show, 'qa/page/project/job/show'
end
module Settings
......@@ -170,6 +169,7 @@ module QA
#
module Git
autoload :Repository, 'qa/git/repository'
autoload :Location, 'qa/git/location'
end
##
......
......@@ -4,7 +4,7 @@ module QA
module Factory
module Resource
class Runner < Factory::Base
attr_writer :name, :tags
attr_writer :name, :tags, :image
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-ci-cd'
......@@ -19,6 +19,10 @@ module QA
@tags || %w[qa e2e]
end
def image
@image || 'gitlab/gitlab-runner:alpine'
end
def fabricate!
project.visit!
......@@ -31,6 +35,7 @@ module QA
runner.token = runners.registration_token
runner.address = runners.coordinator_address
runner.tags = tags
runner.image = image
runner.register!
end
end
......
require 'uri'
require 'forwardable'
module QA
module Git
class Location
extend Forwardable
attr_reader :git_uri, :uri
def_delegators :@uri, :user, :host, :path
# See: config/initializers/1_settings.rb
# Settings#build_gitlab_shell_ssh_path_prefix
def initialize(git_uri)
@git_uri = git_uri
@uri =
if git_uri.start_with?('ssh://')
URI.parse(git_uri)
else
*rest, path = git_uri.split(':')
# Host cannot have : so we'll need to escape it
user_host = rest.join('%3A').sub(/\A\[(.+)\]\z/, '\1')
URI.parse("ssh://#{user_host}/#{path}")
end
end
def port
uri.port || 22
end
end
end
end
require 'cgi'
require 'uri'
module QA
......
......@@ -17,7 +17,8 @@ module QA
start = Time.now
while Time.now - start < max
return true if yield
result = yield
return result if result
sleep(time)
......
module QA::Page
module Project::Job
class Show < QA::Page::Base
view 'app/views/projects/jobs/show.html.haml' do
element :build_output, '.js-build-output'
end
def output
css = '.js-build-output'
wait(reload: false) do
has_css?(css)
end
find(css).text
end
end
end
end
......@@ -6,7 +6,13 @@ module QA::Page
end
def go_to_latest_pipeline
first('.js-pipeline-url-link').click
css = '.js-pipeline-url-link'
link = wait(reload: false) do
first(css)
end
link.click
end
end
end
......
......@@ -11,6 +11,7 @@ module QA::Page
view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do
element :job_component, /class.*ci-job-component.*/
element :job_link, /class.*js-pipeline-graph-job-link.*/
end
view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do
......@@ -30,6 +31,16 @@ module QA::Page
end
end
end
def go_to_first_job
css = '.js-pipeline-graph-job-link'
wait(reload: false) do
has_css?(css)
end
first(css).click
end
end
end
end
......@@ -22,22 +22,24 @@ module QA
end
def choose_repository_clone_http
wait(reload: false) do
click_element :clone_dropdown
page.within('.clone-options-dropdown') do
click_link('HTTP')
choose_repository_clone('HTTP', 'http')
end
# Ensure git clone textbox was updated to http URI
repository_location.include?('http')
end
def choose_repository_clone_ssh
# It's not always beginning with ssh:// so detecting with @
# would be more reliable because ssh would always contain it.
# We can't use .git because HTTP also contain that part.
choose_repository_clone('SSH', '@')
end
def repository_location
find('#project_clone').value
end
def repository_location_uri
Git::Location.new(repository_location)
end
def project_name
find('.qa-project-name').text
end
......@@ -56,6 +58,21 @@ module QA
click_link 'New issue'
end
private
def choose_repository_clone(kind, detect_text)
wait(reload: false) do
click_element :clone_dropdown
page.within('.clone-options-dropdown') do
click_link(kind)
end
# Ensure git clone textbox was updated
repository_location.include?(detect_text)
end
end
end
end
end
......
......@@ -7,7 +7,7 @@ module QA
extend Forwardable
attr_reader :key
def_delegators :@key, :fingerprint
def_delegators :@key, :fingerprint, :to_pem
def initialize(bits = 4096)
@key = OpenSSL::PKey::RSA.new(bits)
......
require 'digest/sha1'
module QA
feature 'cloning code using a deploy key', :core, :docker do
let(:runner_name) { "qa-runner-#{Time.now.to_i}" }
let(:key) { Runtime::RSAKey.new }
given(:project) do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'deploy-key-clone-project'
end
end
after do
Service::Runner.new(runner_name).remove!
end
scenario 'user sets up a deploy key to clone code using pipelines' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Runner.fabricate! do |resource|
resource.project = project
resource.name = runner_name
resource.tags = %w[qa docker]
resource.image = 'gitlab/gitlab-runner:ubuntu'
end
Factory::Resource::DeployKey.fabricate! do |resource|
resource.project = project
resource.title = 'deploy key title'
resource.key = key.public_key
end
Factory::Resource::SecretVariable.fabricate! do |resource|
resource.project = project
resource.key = 'DEPLOY_KEY'
resource.value = key.to_pem
end
project.visit!
repository_uri = Page::Project::Show.act do
choose_repository_clone_ssh
repository_location_uri
end
gitlab_ci = <<~YAML
cat-config:
script:
- mkdir -p ~/.ssh
- ssh-keyscan -p #{repository_uri.port} #{repository_uri.host} >> ~/.ssh/known_hosts
- eval $(ssh-agent -s)
- echo "$DEPLOY_KEY" | ssh-add -
- git clone #{repository_uri.git_uri}
- sha1sum #{project.name}/.gitlab-ci.yml
tags:
- qa
- docker
YAML
Factory::Repository::Push.fabricate! do |resource|
resource.project = project
resource.file_name = '.gitlab-ci.yml'
resource.commit_message = 'Add .gitlab-ci.yml'
resource.file_content = gitlab_ci
end
sha1sum = Digest::SHA1.hexdigest(gitlab_ci)
Page::Project::Show.act { wait_for_push }
Page::Menu::Side.act { click_ci_cd_pipelines }
Page::Project::Pipeline::Index.act { go_to_latest_pipeline }
Page::Project::Pipeline::Show.act { go_to_first_job }
Page::Project::Job::Show.perform do |job|
expect(job.output).to include(sha1sum)
end
end
end
end
......@@ -69,7 +69,7 @@ module QA
tags:
- qa
- test
script: echo "CONTENTS" > my-artifacts/artifact.txt
script: mkdir my-artifacts; echo "CONTENTS" > my-artifacts/artifact.txt
artifacts:
paths:
- my-artifacts/
......@@ -95,7 +95,7 @@ module QA
expect(pipeline).to have_build('test-success', status: :success)
expect(pipeline).to have_build('test-failure', status: :failed)
expect(pipeline).to have_build('test-tags', status: :pending)
expect(pipeline).to have_build('test-artifacts', status: :failed)
expect(pipeline).to have_build('test-artifacts', status: :success)
end
end
end
......
describe QA::Git::Location do
describe '.new' do
context 'when URI starts with ssh://' do
context 'when URI has port' do
it 'parses correctly' do
uri = described_class
.new('ssh://git@qa.test:2222/sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(2222)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
context 'when URI does not have port' do
it 'parses correctly' do
uri = described_class
.new('ssh://git@qa.test/sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
end
context 'when URI does not start with ssh://' do
context 'when host does not have colons' do
it 'parses correctly' do
uri = described_class
.new('git@qa.test:sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa.test')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
context 'when host has a colon' do
it 'parses correctly' do
uri = described_class
.new('[git@qa:test]:sandbox/qa/repo.git')
expect(uri.user).to eq('git')
expect(uri.host).to eq('qa%3Atest')
expect(uri.port).to eq(22)
expect(uri.path).to eq('/sandbox/qa/repo.git')
end
end
end
end
end
#!/usr/bin/env ruby
require 'digest'
require 'fileutils'
harness_path = File.expand_path('../.git/security_harness', __dir__)
hook_path = File.expand_path("../.git/hooks/pre-push", __dir__)
if File.exist?(hook_path)
# Deal with a pre-existing hook
source_sum = Digest::SHA256.hexdigest(DATA.read)
dest_sum = Digest::SHA256.file(hook_path).hexdigest
if source_sum != dest_sum
puts "#{hook_path} exists and is different from our hook!"
puts "Remove it and re-run this script to continue."
exit 1
end
else
File.open(hook_path, 'w') do |file|
IO.copy_stream(DATA, file)
end
end
# Toggle the harness on or off
if File.exist?(harness_path)
FileUtils.rm(harness_path)
puts "Security harness removed -- you can now push to all remotes."
else
FileUtils.touch(harness_path)
puts "Security harness installed -- you will only be able to push to dev.gitlab.org!"
end
__END__
#!/bin/sh
set -e
url="$2"
harness=`dirname "$0"`/../security_harness
if [ -e "$harness" ]
then
if [[ "$url" != *"dev.gitlab.org"* ]]
then
echo "Pushing to remotes other than dev.gitlab.org has been disabled!"
echo "Run scripts/security-harness to disable this check."
echo
exit 1
fi
fi
require 'spec_helper'
describe Gitlab::Geo::BaseRequest, :geo do
include ::EE::GeoHelpers
let(:geo_node) { create(:geo_node) }
before do
stub_current_geo_node(geo_node)
end
describe '#authorization' do
let(:request) { described_class.new }
let(:token) { request.authorization }
let(:data) { token.split(' ').second.split(':') }
let(:access_key) { data.first }
let(:encoded_jwt) { data.second }
let(:jwt) { JWT.decode(encoded_jwt, geo_node.secret_access_key) }
it 'token is formatted properly' do
expect(access_key).to eq(geo_node.access_key)
expect(token).to start_with(Gitlab::Geo::BaseRequest::GITLAB_GEO_AUTH_TOKEN_TYPE)
end
it 'defaults to 1-minute expiration time' do
Timecop.freeze do
expect(jwt.first['exp']).to eq((Time.now + 1.minute).to_i)
end
end
end
end
require 'spec_helper'
describe 'User activates issue tracker', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:url) { 'http://tracker.example.com' }
def fill_form(active = true)
check 'Active' if active
fill_in 'service_project_url', with: url
fill_in 'service_issues_url', with: "#{url}/:id"
fill_in 'service_new_issue_url', with: url
end
before do
project.add_master(user)
sign_in(user)
visit project_settings_integrations_path(project)
end
shared_examples 'external issue tracker activation' do |tracker:|
describe 'user sets and activates the Service' do
context 'when the connection test succeeds' do
before do
stub_request(:head, url).to_return(headers: { 'Content-Type' => 'application/json' })
click_link(tracker)
fill_form
click_button('Test settings and save changes')
wait_for_requests
end
it 'activates the service' do
expect(page).to have_content("#{tracker} activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'shows the link in the menu' do
page.within('.nav-sidebar') do
expect(page).to have_link(tracker, href: url)
end
end
end
context 'when the connection test fails' do
it 'activates the service' do
stub_request(:head, url).to_raise(HTTParty::Error)
click_link(tracker)
fill_form
click_button('Test settings and save changes')
wait_for_requests
expect(find('.flash-container-page')).to have_content 'Test failed.'
expect(find('.flash-container-page')).to have_content 'Save anyway'
find('.flash-alert .flash-action').click
wait_for_requests
expect(page).to have_content("#{tracker} activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
end
end
describe 'user sets the service but keeps it disabled' do
before do
click_link(tracker)
fill_form(false)
click_button('Save changes')
end
it 'saves but does not activate the service' do
expect(page).to have_content("#{tracker} settings saved, but not activated.")
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'does not show the external tracker link in the menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link(tracker, href: url)
end
end
end
end
it_behaves_like 'external issue tracker activation', tracker: 'Redmine'
it_behaves_like 'external issue tracker activation', tracker: 'Bugzilla'
it_behaves_like 'external issue tracker activation', tracker: 'Custom Issue Tracker'
end
......@@ -3,7 +3,6 @@ require 'spec_helper'
describe 'User activates Jira', :js do
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:service) { project.create_jira_service }
let(:url) { 'http://jira.example.com' }
let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
......@@ -26,7 +25,7 @@ describe 'User activates Jira', :js do
describe 'user sets and activates Jira Service' do
context 'when Jira connection test succeeds' do
it 'activates the JIRA service' do
before do
server_info = { key: 'value' }.to_json
WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
......@@ -34,10 +33,18 @@ describe 'User activates Jira', :js do
fill_form
click_button('Test settings and save changes')
wait_for_requests
end
it 'activates the JIRA service' do
expect(page).to have_content('JIRA activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'shows the JIRA link in the menu' do
page.within('.nav-sidebar') do
expect(page).to have_link('JIRA', href: url)
end
end
end
context 'when Jira connection test fails' do
......@@ -75,15 +82,21 @@ describe 'User activates Jira', :js do
end
describe 'user sets Jira Service but keeps it disabled' do
context 'when Jira connection test succeeds' do
it 'activates the JIRA service' do
before do
click_link('JIRA')
fill_form(false)
click_button('Save changes')
end
it 'saves but does not activate the JIRA service' do
expect(page).to have_content('JIRA settings saved, but not activated.')
expect(current_path).to eq(project_settings_integrations_path(project))
end
it 'does not show the JIRA link in the menu' do
page.within('.nav-sidebar') do
expect(page).not_to have_link('JIRA', href: url)
end
end
end
end
......@@ -7,11 +7,12 @@
"data": { "type": "string" },
"filename": { "type": ["string"] },
"id": { "type": ["string", "null"] },
"project_id": { "type": "integer" },
"ref": { "type": "string" },
"startline": { "type": "integer" }
},
"required": [
"basename", "data", "filename", "id", "ref", "startline"
"basename", "data", "filename", "id", "ref", "startline", "project_id"
],
"additionalProperties": false
}
......
......@@ -4,9 +4,9 @@
{ "$ref": "basic.json" },
{
"required" : [
"stats",
"status",
"last_pipeline"
"last_pipeline",
"project_id"
],
"properties": {
"stats": { "$ref": "../commit_stats.json" },
......@@ -16,7 +16,8 @@
{ "type": "null" },
{ "$ref": "../pipeline/basic.json" }
]
}
},
"project_id": { "type": "integer" }
}
}
]
......
{
"type": "array",
"items": { "$ref": "commit/detail.json" }
}
import ApproversSelect from 'ee/approvers_select';
import ClassSpecHelper from './helpers/class_spec_helper';
describe('ApproversSelect', () => {
describe('saveApprovers', () => {
let complete;
const $input = jasmine.createSpyObj('$input', ['val']);
describe('saveApproversComplete', () => {
let $input;
let $approverSelect;
let $loadWrapper;
beforeEach(() => {
spyOn(window, '$').and.returnValue($input);
spyOn(window.$, 'ajax').and.callFake((options) => {
complete = options.complete;
});
$input = {
val: jasmine.createSpy(),
};
$input.val.and.returnValue('newValue');
$approverSelect = {
select2: jasmine.createSpy(),
};
ApproversSelect.saveApprovers('fieldName');
});
$loadWrapper = {
addClass: jasmine.createSpy(),
};
ClassSpecHelper.itShouldBeAStaticMethod(ApproversSelect, 'saveApprovers');
ApproversSelect.saveApproversComplete($input, $approverSelect, $loadWrapper);
});
describe('when request completes', () => {
it('should empty the $input value', () => {
$input.val.calls.reset();
complete();
expect($input.val).toHaveBeenCalledWith('');
});
it('should empty the select2 value', () => {
expect($approverSelect.select2).toHaveBeenCalledWith('val', '');
});
it('should hide loadWrapper', () => {
expect($loadWrapper.addClass).toHaveBeenCalledWith('hidden');
});
});
});
import initSettingsPanels from '~/settings_panels';
describe('Settings Panels', () => {
preloadFixtures('projects/ci_cd_settings.html.raw');
beforeEach(() => {
loadFixtures('projects/ci_cd_settings.html.raw');
});
describe('initSettingsPane', () => {
afterEach(() => {
location.hash = '';
});
it('should expand linked hash fragment panel', () => {
location.hash = '#js-general-pipeline-settings';
const pipelineSettingsPanel = document.querySelector('#js-general-pipeline-settings');
// Our test environment automatically expands everything so we need to clear that out first
pipelineSettingsPanel.classList.remove('expanded');
expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(false);
initSettingsPanels();
expect(pipelineSettingsPanel.classList.contains('expanded')).toBe(true);
});
});
});
import Vue from 'vue';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetNotAllowed', () => {
describe('template', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(notAllowedComponent);
const vm = new Component({
el: document.createElement('div'),
vm = mountComponent(Component);
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
afterEach(() => {
vm.$destroy();
});
it('renders success icon', () => {
expect(vm.$el.querySelector('.ci-status-icon-success')).not.toBe(null);
});
it('renders informative text', () => {
expect(vm.$el.innerText).toContain('Ready to be merged automatically.');
expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request');
});
});
});
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
describe('MRWidgetPipelineBlocked', () => {
describe('template', () => {
let vm;
beforeEach(() => {
const Component = Vue.extend(pipelineBlockedComponent);
const vm = new Component({
el: document.createElement('div'),
vm = mountComponent(Component);
});
it('should have correct elements', () => {
expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
afterEach(() => {
vm.$destroy();
});
it('renders warning icon', () => {
expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null);
});
it('renders information text', () => {
expect(vm.$el.textContent.trim().replace(/[\r\n]+/g, ' ')).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed');
});
});
......@@ -14,16 +14,10 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra
let!(:uploads) { described_class::Upload }
before do
DatabaseCleaner.clean
drop_temp_table_if_exists
ensure_temporary_tracking_table_exists
uploads.delete_all
end
after(:all) do
drop_temp_table_if_exists
end
context 'with untracked files and tracked files in untracked_files_for_uploads' do
let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
let!(:user1) { create(:user, :with_avatar) }
......@@ -122,9 +116,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra
end
it 'drops the temporary tracking table after processing the batch, if there are no untracked rows left' do
subject.perform(1, untracked_files_for_uploads.last.id)
expect(subject).to receive(:drop_temp_table_if_finished)
expect(ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)).to be_falsey
subject.perform(1, untracked_files_for_uploads.last.id)
end
it 'does not block a whole batch because of one bad path' do
......@@ -260,10 +254,6 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do
ensure_temporary_tracking_table_exists
end
after(:all) do
drop_temp_table_if_exists
end
describe '#upload_path' do
def assert_upload_path(file_path, expected_upload_path)
untracked_file = create_untracked_file(file_path)
......
require 'spec_helper'
describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do
include TrackUntrackedUploadsHelpers
include MigrationsHelpers
let!(:untracked_files_for_uploads) { described_class::UntrackedFile }
before do
DatabaseCleaner.clean
drop_temp_table_if_exists
end
after do
drop_temp_table_if_exists
end
around do |example|
# Especially important so the follow-up migration does not get run
Sidekiq::Testing.fake! do
......@@ -23,31 +13,21 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
# E.g. The installation is in use at the time of migration, and someone has
# just uploaded a file
shared_examples 'does not add files in /uploads/tmp' do
let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
shared_examples 'prepares the untracked_files_for_uploads table' do
context 'when files were uploaded before and after hashed storage was enabled' do
let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
let!(:user) { create(:user, :with_avatar) }
let!(:project1) { create(:project, :with_avatar, :legacy_storage) }
let(:project2) { create(:project) } # instantiate after enabling hashed_storage
before do
FileUtils.mkdir(File.dirname(tmp_file))
FileUtils.touch(tmp_file)
end
after do
FileUtils.rm(tmp_file)
end
it 'does not add files from /uploads/tmp' do
described_class.new.perform
# Markdown upload before enabling hashed_storage
UploadService.new(project1, uploaded_file, FileUploader).execute
expect(untracked_files_for_uploads.count).to eq(5)
end
end
stub_application_setting(hashed_storage_enabled: true)
it 'ensures the untracked_files_for_uploads table exists' do
expect do
described_class.new.perform
end.to change { ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads) }.from(false).to(true)
# Markdown upload after enabling hashed_storage
UploadService.new(project2, uploaded_file, FileUploader).execute
end
it 'has a path field long enough for really long paths' do
......@@ -65,31 +45,6 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
expect(record.reload.path.size).to eq(519)
end
context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE" do
around do |example|
# If this is CI, we use Postgres 9.2 so this whole context should be
# skipped since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
if described_class.new.send(:can_bulk_insert_and_ignore_duplicates?)
example.run
end
end
context 'when files were uploaded before and after hashed storage was enabled' do
let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
let!(:user) { create(:user, :with_avatar) }
let!(:project1) { create(:project, :with_avatar, :legacy_storage) }
let(:project2) { create(:project) } # instantiate after enabling hashed_storage
before do
# Markdown upload before enabling hashed_storage
UploadService.new(project1, uploaded_file, FileUploader).execute
stub_application_setting(hashed_storage_enabled: true)
# Markdown upload after enabling hashed_storage
UploadService.new(project2, uploaded_file, FileUploader).execute
end
it 'adds unhashed files to the untracked_files_for_uploads table' do
described_class.new.perform
......@@ -113,7 +68,8 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
it 'correctly schedules the follow-up background migration jobs' do
described_class.new.perform
expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
ids = described_class::UntrackedFile.all.order(:id).pluck(:id)
expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(ids.first, ids.last)
expect(BackgroundMigrationWorker.jobs.size).to eq(1)
end
......@@ -130,91 +86,68 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq do
end
end
# E.g. The installation is in use at the time of migration, and someone has
# just uploaded a file
context 'when there are files in /uploads/tmp' do
it_behaves_like 'does not add files in /uploads/tmp'
end
end
end
let(:tmp_file) { Rails.root.join(described_class::ABSOLUTE_UPLOAD_DIR, 'tmp', 'some_file.jpg') }
context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
before do
# If this is CI, we use Postgres 9.2 so this stub has no effect.
#
# If this is being run on Postgres 9.5+ or MySQL, then this stub allows us
# to test the bulk insert functionality without ON CONFLICT DO NOTHING or
# IGNORE.
allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
FileUtils.mkdir(File.dirname(tmp_file))
FileUtils.touch(tmp_file)
end
context 'when files were uploaded before and after hashed storage was enabled' do
let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) }
let!(:user) { create(:user, :with_avatar) }
let!(:project1) { create(:project, :with_avatar, :legacy_storage) }
let(:project2) { create(:project) } # instantiate after enabling hashed_storage
before do
# Markdown upload before enabling hashed_storage
UploadService.new(project1, uploaded_file, FileUploader).execute
stub_application_setting(hashed_storage_enabled: true)
# Markdown upload after enabling hashed_storage
UploadService.new(project2, uploaded_file, FileUploader).execute
after do
FileUtils.rm(tmp_file)
end
it 'adds unhashed files to the untracked_files_for_uploads table' do
it 'does not add files from /uploads/tmp' do
described_class.new.perform
expect(untracked_files_for_uploads.count).to eq(5)
end
it 'adds files with paths relative to CarrierWave.root' do
described_class.new.perform
untracked_files_for_uploads.all.each do |file|
expect(file.path.start_with?('uploads/')).to be_truthy
end
end
it 'does not add hashed files to the untracked_files_for_uploads table' do
described_class.new.perform
hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path
expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey
end
context 'when the last batch size exactly matches the max batch size' do
it 'does not raise error' do
stub_const("#{described_class}::FIND_BATCH_SIZE", 5)
it 'correctly schedules the follow-up background migration jobs' do
expect do
described_class.new.perform
end.not_to raise_error
expect(described_class::FOLLOW_UP_MIGRATION).to be_scheduled_migration(1, 5)
expect(BackgroundMigrationWorker.jobs.size).to eq(1)
expect(untracked_files_for_uploads.count).to eq(5)
end
# E.g. from a previous failed run of this background migration
context 'when there is existing data in untracked_files_for_uploads' do
before do
described_class.new.perform
end
it 'does not error or produce duplicates of existing data' do
expect do
described_class.new.perform
end.not_to change { untracked_files_for_uploads.count }.from(5)
end
end
context 'when there are files in /uploads/tmp' do
it_behaves_like 'does not add files in /uploads/tmp'
# If running on Postgres 9.2 (like on CI), this whole context is skipped
# since we're unable to use ON CONFLICT DO NOTHING or IGNORE.
context "test bulk insert with ON CONFLICT DO NOTHING or IGNORE", if: described_class.new.send(:can_bulk_insert_and_ignore_duplicates?) do
it_behaves_like 'prepares the untracked_files_for_uploads table'
end
# If running on Postgres 9.2 (like on CI), the stubbed method has no effect.
#
# If running on Postgres 9.5+ or MySQL, then this context effectively tests
# the bulk insert functionality without ON CONFLICT DO NOTHING or IGNORE.
context 'test bulk insert without ON CONFLICT DO NOTHING or IGNORE' do
before do
allow_any_instance_of(described_class).to receive(:postgresql_pre_9_5?).and_return(true)
end
it_behaves_like 'prepares the untracked_files_for_uploads table'
end
# Very new or lightly-used installations that are running this migration
# may not have an upload directory because they have no uploads.
context 'when no files were ever uploaded' do
it 'does not add to the untracked_files_for_uploads table (and does not raise error)' do
described_class.new.perform
it 'deletes the `untracked_files_for_uploads` table (and does not raise error)' do
background_migration = described_class.new
expect(background_migration).to receive(:drop_temp_table)
expect(untracked_files_for_uploads.count).to eq(0)
background_migration.perform
end
end
end
require 'spec_helper'
describe Gitlab::Email::Handler do
before do
stub_incoming_email_setting(enabled: true, address: "incoming+%{key}@appmail.adventuretime.ooo")
stub_config_setting(host: 'localhost')
end
describe '.for' do
it 'picks issue handler if there is not merge request prefix' do
expect(described_class.for('email', 'project+key')).to be_an_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
......@@ -18,43 +13,5 @@ describe Gitlab::Email::Handler do
it 'returns nil if no handler is found' do
expect(described_class.for('email', '')).to be_nil
end
def handler_for(fixture, mail_key)
described_class.for(fixture_file(fixture), mail_key)
end
context 'a Service Desk email' do
it 'uses the Service Desk handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_instance_of(Gitlab::Email::Handler::EE::ServiceDeskHandler)
end
it 'uses no handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/service_desk.eml', 'some/project')).to be_nil
end
end
context 'a new issue email' do
let!(:user) { create(:user, email: 'jake@adventuretime.ooo', incoming_email_token: 'auth_token') }
it 'uses the create issue handler when Service Desk is enabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(true)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
it 'uses the create issue handler when Service Desk is disabled' do
allow(License).to receive(:feature_available?).and_call_original
allow(License).to receive(:feature_available?).with(:service_desk).and_return(false)
expect(handler_for('emails/valid_new_issue.eml', 'some/project+auth_token')).to be_instance_of(Gitlab::Email::Handler::CreateIssueHandler)
end
end
end
end
......@@ -66,15 +66,6 @@ describe Gitlab::LDAP::Person do
end
end
describe '.validate_entry' do
it 'raises InvalidEntryError' do
entry['foo'] = 'bar'
expect { described_class.new(entry, 'ldapmain') }
.to raise_error(Gitlab::LDAP::Person::InvalidEntryError)
end
end
describe '#name' do
it 'uses the configured name attribute and handles values as an array' do
name = 'John Doe'
......
......@@ -59,18 +59,6 @@ describe Gitlab::QueryLimiting::Transaction do
expect { transaction.act_upon_results }
.to raise_error(described_class::ThresholdExceededError)
end
it 'reports the error in Sentry if raising an error is disabled' do
expect(transaction)
.to receive(:raise_error?)
.and_return(false)
expect(Raven)
.to receive(:capture_exception)
.with(an_instance_of(described_class::ThresholdExceededError))
transaction.act_upon_results
end
end
end
......
class IssuesCsvMailerPreview < ActionMailer::Preview
def issues_csv_export
user = OpenStruct.new(notification_email: 'a@example.com')
project = Project.unscoped.first
Notify.issues_csv_email(user, project, "Dummy,Csv\n0,1", export_status)
end
private
def export_status
{
truncated: [true, false].sample,
rows_written: 632,
rows_expected: 891
}
end
end
......@@ -89,5 +89,5 @@ end
## Best practices
1. Note that this type of tests do not run within the transaction, we use
a truncation database cleanup strategy. Do not depend on transaction being
a deletion database cleanup strategy. Do not depend on transaction being
present.
......@@ -473,6 +473,72 @@ describe API::Commits do
end
end
describe 'GET /projects/:id/repository/commits/:sha/refs' do
let(:project) { create(:project, :public, :repository) }
let(:tag) { project.repository.find_tag('v1.1.0') }
let(:commit_id) { tag.dereferenced_target.id }
let(:route) { "/projects/#{project_id}/repository/commits/#{commit_id}/refs" }
context 'when ref does not exist' do
let(:commit_id) { 'unknown' }
it_behaves_like '404 response' do
let(:request) { get api(route, current_user) }
let(:message) { '404 Commit Not Found' }
end
end
context 'when repository is disabled' do
include_context 'disabled repository'
it_behaves_like '403 response' do
let(:request) { get api(route, current_user) }
end
end
context 'for a valid commit' do
it 'returns all refs with no scope' do
get api(route, current_user), per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]})
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 { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns all refs' do
get api(route, current_user), type: 'all', per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
refs.concat(project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]})
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns the branch refs' do
get api(route, current_user), type: 'branch', per_page: 100
refs = project.repository.branch_names_contains(commit_id).map {|name| ['branch', name]}
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
it 'returns the tag refs' do
get api(route, current_user), type: 'tag', per_page: 100
refs = project.repository.tag_names_contains(commit_id).map {|name| ['tag', name]}
expect(response).to have_gitlab_http_status(200)
expect(json_response.map { |r| [r['type'], r['name']] }.compact).to eq(refs)
end
end
end
describe 'GET /projects/:id/repository/commits/:sha' do
let(:commit) { project.repository.commit }
let(:commit_id) { commit.id }
......
......@@ -66,7 +66,7 @@ describe API::Search do
get api(endpoint, user), scope: 'commits', search: 'folder'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits', size: 2
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details', size: 2
end
context 'for blobs scope' do
......@@ -167,7 +167,7 @@ describe API::Search do
it_behaves_like 'response is correct', schema: 'public_api/v4/snippets'
end
context 'when elasticsearch is enabled' do
context 'when elasticsearch is disabled' do
it_behaves_like 'elasticsearch disabled' do
let(:endpoint) { '/search' }
end
......@@ -391,7 +391,7 @@ describe API::Search do
get api("/projects/#{repo_project.id}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits'
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
end
context 'for commits scope with project path as id' do
......@@ -399,7 +399,7 @@ describe API::Search do
get api("/projects/#{CGI.escape(repo_project.full_path)}/-/search", user), scope: 'commits', search: '498214de67004b1da3d820901307bed2a68a8ef6'
end
it_behaves_like 'response is correct', schema: 'public_api/v4/commits'
it_behaves_like 'response is correct', schema: 'public_api/v4/commits_details'
end
context 'for blobs scope' do
......
......@@ -81,7 +81,7 @@ describe 'OpenID Connect requests' do
it 'includes all user information and group memberships' do
request_user_info
expect(json_response).to eq({
expect(json_response).to match(a_hash_including({
'sub' => hashed_subject,
'name' => 'Alice',
'nickname' => 'alice',
......@@ -90,13 +90,12 @@ describe 'OpenID Connect requests' do
'website' => 'https://example.com',
'profile' => 'http://localhost/alice',
'picture' => "http://localhost/uploads/-/system/user/avatar/#{user.id}/dk.png",
'groups' =>
if Group.supports_nested_groups?
['group1', 'group2/group3', 'group2/group3/group4']
else
['group1', 'group2/group3']
end
})
'groups' => anything
}))
expected_groups = %w[group1 group2/group3]
expected_groups << 'group2/group3/group4' if Group.supports_nested_groups?
expect(json_response['groups']).to match_array(expected_groups)
end
end
......
require 'spec_helper.rb'
describe Issues::FetchReferencedMergeRequestsService do
let(:project) { create(:project) }
let(:issue) { create(:issue, project: project) }
let(:other_project) { create(:project) }
let(:mr) { create(:merge_request, source_project: project, target_project: project, id: 2)}
let(:other_mr) { create(:merge_request, source_project: other_project, target_project: other_project, id: 1)}
let(:user) { create(:user) }
let(:service) { described_class.new(project, user) }
context 'with mentioned merge requests' do
it 'returns a list of sorted merge requests' do
allow(issue).to receive(:referenced_merge_requests).with(user).and_return([other_mr, mr])
mrs, closed_by_mrs = service.execute(issue)
expect(mrs).to match_array([mr, other_mr])
expect(closed_by_mrs).to match_array([])
end
end
context 'with closed-by merge requests' do
it 'returns a list of sorted merge requests' do
allow(issue).to receive(:closed_by_merge_requests).with(user).and_return([other_mr, mr])
mrs, closed_by_mrs = service.execute(issue)
expect(mrs).to match_array([])
expect(closed_by_mrs).to match_array([mr, other_mr])
end
end
end
......@@ -21,6 +21,15 @@ describe Members::AuthorizedDestroyService do
.to change { Member.count }.from(3).to(2)
end
it "doesn't destroy invited project member notification_settings" do
project.add_developer(member_user)
member = create :project_member, :invited, project: project
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
it 'destroys invited group member' do
group.add_developer(member_user)
......@@ -29,38 +38,73 @@ describe Members::AuthorizedDestroyService do
expect { described_class.new(member, member_user).execute }
.to change { Member.count }.from(2).to(1)
end
it "doesn't destroy invited group member notification_settings" do
group.add_developer(member_user)
member = create :group_member, :invited, group: group
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Requested user' do
it "doesn't destroy member notification_settings" do
member = create(:project_member, user: member_user, requested_at: Time.now)
expect { described_class.new(member, member_user).execute }
.not_to change { NotificationSetting.count }
end
end
context 'Group member' do
it "unassigns issues and merge requests" do
let(:member) { group.members.find_by(user_id: member_user.id) }
before do
group.add_developer(member_user)
end
it "unassigns issues and merge requests" do
issue = create :issue, project: group_project, assignees: [member_user]
create :issue, assignees: [member_user]
merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
create :merge_request, target_project: project, source_project: project, assignee: member_user
member = group.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
expect(issue.reload.assignee_ids).to be_empty
expect(merge_request.reload.assignee_id).to be_nil
end
it 'destroys member notification_settings' do
group.add_developer(member_user)
member = group.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
context 'Project member' do
it "unassigns issues and merge requests" do
let(:member) { project.members.find_by(user_id: member_user.id) }
before do
project.add_developer(member_user)
end
it "unassigns issues and merge requests" do
create :issue, project: project, assignees: [member_user]
create :merge_request, target_project: project, source_project: project, assignee: member_user
member = project.members.find_by(user_id: member_user.id)
expect { described_class.new(member, member_user).execute }
.to change { number_of_assigned_issuables(member_user) }.from(2).to(0)
end
it 'destroys member notification_settings' do
expect { described_class.new(member, member_user).execute }
.to change { member_user.notification_settings.count }.by(-1)
end
end
end
module FixtureHelpers
def fixture_file(filename)
def fixture_file(filename, dir: '')
return '' if filename.blank?
File.read(expand_fixture_path(filename))
File.read(expand_fixture_path(filename, dir: dir))
end
def fixture_file_ee(filename)
return '' if filename.blank?
File.read(expand_fixture_ee_path(filename))
end
def expand_fixture_path(filename)
File.expand_path(Rails.root.join('spec/fixtures/', filename))
end
def expand_fixture_ee_path(filename)
File.expand_path(Rails.root.join('ee/spec/fixtures/', filename))
def expand_fixture_path(filename, dir: '')
File.expand_path(Rails.root.join(dir, 'spec', 'fixtures', filename))
end
end
......
......@@ -17,6 +17,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
end
end
context 'with an authorized user' do
it 'filters by custom attributes' do
get api("/#{attributable_name}", admin), custom_attributes: { foo: 'foo', bar: 'bar' }
......@@ -25,6 +26,81 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
expect(json_response.first['id']).to eq attributable.id
end
end
end
describe "GET /#{attributable_name} with custom attributes" do
before do
other_attributable
end
context 'with an unauthorized user' do
it 'does not include custom attributes' do
get api("/#{attributable_name}", user), with_custom_attributes: true
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 2
expect(json_response.first).not_to include 'custom_attributes'
end
end
context 'with an authorized user' do
it 'does not include custom attributes by default' do
get api("/#{attributable_name}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 2
expect(json_response.first).not_to include 'custom_attributes'
expect(json_response.second).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
get api("/#{attributable_name}", admin), with_custom_attributes: true
expect(response).to have_gitlab_http_status(200)
expect(json_response.size).to be 2
attributable_response = json_response.find { |r| r['id'] == attributable.id }
other_attributable_response = json_response.find { |r| r['id'] == other_attributable.id }
expect(attributable_response['custom_attributes']).to contain_exactly(
{ 'key' => 'foo', 'value' => 'foo' },
{ 'key' => 'bar', 'value' => 'bar' }
)
expect(other_attributable_response['custom_attributes']).to eq []
end
end
end
describe "GET /#{attributable_name}/:id with custom attributes" do
context 'with an unauthorized user' do
it 'does not include custom attributes' do
get api("/#{attributable_name}/#{attributable.id}", user), with_custom_attributes: true
expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to include 'custom_attributes'
end
end
context 'with an authorized user' do
it 'does not include custom attributes by default' do
get api("/#{attributable_name}/#{attributable.id}", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).not_to include 'custom_attributes'
end
it 'includes custom attributes if requested' do
get api("/#{attributable_name}/#{attributable.id}", admin), with_custom_attributes: true
expect(response).to have_gitlab_http_status(200)
expect(json_response['custom_attributes']).to contain_exactly(
{ 'key' => 'foo', 'value' => 'foo' },
{ 'key' => 'bar', 'value' => 'bar' }
)
end
end
end
describe "GET /#{attributable_name}/:id/custom_attributes" do
context 'with an unauthorized user' do
......@@ -33,6 +109,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it_behaves_like 'an unauthorized API user'
end
context 'with an authorized user' do
it 'returns all custom attributes' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes", admin)
......@@ -43,6 +120,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
)
end
end
end
describe "GET /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
......@@ -51,13 +129,15 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it_behaves_like 'an unauthorized API user'
end
it 'returns a single custom attribute' do
context 'with an authorized user' do
it'returns a single custom attribute' do
get api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to eq({ 'key' => 'foo', 'value' => 'foo' })
end
end
end
describe "PUT /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
......@@ -66,6 +146,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it_behaves_like 'an unauthorized API user'
end
context 'with an authorized user' do
it 'creates a new custom attribute' do
expect do
put api("/#{attributable_name}/#{attributable.id}/custom_attributes/new", admin), value: 'new'
......@@ -86,6 +167,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
expect(custom_attribute1.reload.value).to eq 'new'
end
end
end
describe "DELETE /#{attributable_name}/:id/custom_attributes/:key" do
context 'with an unauthorized user' do
......@@ -94,6 +176,7 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
it_behaves_like 'an unauthorized API user'
end
context 'with an authorized user' do
it 'deletes an existing custom attribute' do
expect do
delete api("/#{attributable_name}/#{attributable.id}/custom_attributes/foo", admin)
......@@ -103,4 +186,5 @@ shared_examples 'custom attributes endpoints' do |attributable_name|
expect(attributable.custom_attributes.find_by(key: 'foo')).to be_nil
end
end
end
end
require_dependency Rails.root.join('ee', 'spec', 'support', 'stub_configuration')
require_dependency Rails.root.join('ee', 'spec', 'support', 'ee', 'stub_configuration')
module StubConfiguration
prepend EE::StubConfiguration
......
......@@ -8,10 +8,6 @@ module TrackUntrackedUploadsHelpers
Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists)
end
def drop_temp_table_if_exists
ActiveRecord::Base.connection.drop_table(:untracked_files_for_uploads) if ActiveRecord::Base.connection.table_exists?(:untracked_files_for_uploads)
end
def create_or_update_appearance(attrs)
a = Appearance.first_or_initialize(title: 'foo', description: 'bar')
a.update!(attrs)
......
......@@ -2,7 +2,8 @@ controller:
image:
tag: "0.10.2"
repository: "quay.io/kubernetes-ingress-controller/nginx-ingress-controller"
stats.enabled: true
stats:
enabled: true
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "10254"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment