Commit 3984a4cc authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'ee/master' into ce-to-ee-2018-03-06

* ee/master:
  Add project export API
  Lock gRPC gem to 1.8
  Add spec
  Fix http_io to make it return path is nil
  Add find-sec-bugs in the list of tools for java SAST. Refs #4123
  Changes after review
  Fix broken empty tab state
  Manage empty states in Pipelines page Adds i18n Adds test
  Merge from CE
parents 1d3e85ac b89accf8
...@@ -427,6 +427,10 @@ end ...@@ -427,6 +427,10 @@ end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
# Explicitly lock grpc as we know 1.9 is bad
# 1.10 is still being tested. See gitlab-org/gitaly#1059
gem 'grpc', '~> 1.8.3'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1' gem 'google-protobuf', '= 3.5.1'
......
...@@ -1108,6 +1108,7 @@ DEPENDENCIES ...@@ -1108,6 +1108,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0) grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0) grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.8.3)
gssapi gssapi
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
hamlit (~> 2.6.1) hamlit (~> 2.6.1)
......
...@@ -20,10 +20,6 @@ ...@@ -20,10 +20,6 @@
type: String, type: String,
required: true, required: true,
}, },
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: { errorStateSvgPath: {
type: String, type: String,
required: true, required: true,
...@@ -45,23 +41,14 @@ ...@@ -45,23 +41,14 @@
}, },
computed: { computed: {
/**
* Empty state is only rendered if after the first request we receive no pipelines.
*
* @return {Boolean}
*/
shouldRenderEmptyState() {
return !this.state.pipelines.length &&
!this.isLoading &&
this.hasMadeRequest &&
!this.hasError;
},
shouldRenderTable() { shouldRenderTable() {
return !this.isLoading && return !this.isLoading &&
this.state.pipelines.length > 0 && this.state.pipelines.length > 0 &&
!this.hasError; !this.hasError;
}, },
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
...@@ -92,25 +79,22 @@ ...@@ -92,25 +79,22 @@
<div class="content-list pipelines"> <div class="content-list pipelines">
<loading-icon <loading-icon
label="Loading pipelines" :label="s__('Pipelines|Loading Pipelines')"
size="3" size="3"
v-if="isLoading" v-if="isLoading"
class="prepend-top-20"
/> />
<empty-state <svg-blank-state
v-if="shouldRenderEmptyState" v-else-if="shouldRenderErrorState"
:help-page-path="helpPagePath" :svg-path="errorStateSvgPath"
:empty-state-svg-path="emptyStateSvgPath" :message="s__(`Pipelines|There was an error fetching the pipelines.
/> Try again in a few moments or contact your support team.`)"
<error-state
v-if="shouldRenderErrorState"
:error-state-svg-path="errorStateSvgPath"
/> />
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable" v-else-if="shouldRenderTable"
> >
<pipelines-table-component <pipelines-table-component
:pipelines="state.pipelines" :pipelines="state.pipelines"
......
...@@ -2,6 +2,7 @@ import Vue from 'vue'; ...@@ -2,6 +2,7 @@ import Vue from 'vue';
import PipelinesStore from '../../../../pipelines/stores/pipelines_store'; import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
import pipelinesComponent from '../../../../pipelines/components/pipelines.vue'; import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
import Translate from '../../../../vue_shared/translate'; import Translate from '../../../../vue_shared/translate';
import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
Vue.use(Translate); Vue.use(Translate);
...@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({ ...@@ -11,16 +12,28 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
pipelinesComponent, pipelinesComponent,
}, },
data() { data() {
const store = new PipelinesStore();
return { return {
store, store: new PipelinesStore(),
}; };
}, },
created() {
this.dataset = document.querySelector(this.$options.el).dataset;
},
render(createElement) { render(createElement) {
return createElement('pipelines-component', { return createElement('pipelines-component', {
props: { props: {
store: this.store, store: this.store,
endpoint: this.dataset.endpoint,
helpPagePath: this.dataset.helpPagePath,
emptyStateSvgPath: this.dataset.emptyStateSvgPath,
errorStateSvgPath: this.dataset.errorStateSvgPath,
noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
autoDevopsPath: this.dataset.helpAutoDevopsPath,
newPipelinePath: this.dataset.newPipelinePath,
canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
ciLintPath: this.dataset.ciLintPath,
resetCachePath: this.dataset.resetCachePath,
}, },
}); });
}, },
......
<script> <script>
export default { export default {
name: 'PipelinesSvgState',
props: { props: {
errorStateSvgPath: { svgPath: {
type: String,
required: true,
},
message: {
type: String, type: String,
required: true, required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="row empty-state js-pipelines-error-state"> <div class="row empty-state">
<div class="col-xs-12"> <div class="col-xs-12">
<div class="svg-content"> <div class="svg-content">
<img :src="errorStateSvgPath"/> <img :src="svgPath" />
</div> </div>
</div> </div>
<div class="col-xs-12 text-center"> <div class="col-xs-12 text-center">
<div class="text-content"> <div class="text-content">
<h4>The API failed to fetch the pipelines.</h4> <h4>{{ message }}</h4>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelinesEmptyState',
props: { props: {
helpPagePath: { helpPagePath: {
type: String, type: String,
...@@ -9,6 +10,10 @@ ...@@ -9,6 +10,10 @@
type: String, type: String,
required: true, required: true,
}, },
canSetCi: {
type: Boolean,
required: true,
},
}, },
}; };
</script> </script>
...@@ -22,22 +27,36 @@ ...@@ -22,22 +27,36 @@
<div class="col-xs-12"> <div class="col-xs-12">
<div class="text-content"> <div class="text-content">
<template v-if="canSetCi">
<h4 class="text-center"> <h4 class="text-center">
{{ s__("Pipelines|Build with confidence") }} {{ s__('Pipelines|Build with confidence') }}
</h4> </h4>
<p> <p>
{{ s__(`Pipelines|Continous Integration can help {{ s__(`Pipelines|Continous Integration can help
catch bugs by running your tests automatically, catch bugs by running your tests automatically,
while Continuous Deployment can help you deliver code to your product environment.`) }} while Continuous Deployment can help you deliver
code to your product environment.`) }}
</p> </p>
<div class="text-center"> <div class="text-center">
<a <a
:href="helpPagePath" :href="helpPagePath"
class="btn btn-info" class="btn btn-primary js-get-started-pipelines"
> >
{{ s__("Pipelines|Get started with Pipelines") }} {{ s__('Pipelines|Get started with Pipelines') }}
</a> </a>
</div> </div>
</template>
<p
v-else
class="text-center"
>
{{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
</div> </div>
</div> </div>
</div> </div>
......
<script> <script>
export default { export default {
name: 'PipelineNavControls', name: 'PipelineNavControls',
props: { props: {
newPipelinePath: { newPipelinePath: {
type: String, type: String,
required: true, required: false,
}, default: null,
hasCiEnabled: {
type: Boolean,
required: true,
},
helpPagePath: {
type: String,
required: true,
}, },
resetCachePath: { resetCachePath: {
type: String, type: String,
required: true, required: false,
default: null,
}, },
ciLintPath: { ciLintPath: {
type: String, type: String,
required: true, required: false,
}, default: null,
canCreatePipeline: {
type: Boolean,
required: true,
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="nav-controls"> <div class="nav-controls">
<a <a
v-if="canCreatePipeline" v-if="newPipelinePath"
:href="newPipelinePath" :href="newPipelinePath"
class="btn btn-create"> class="btn btn-create js-run-pipeline"
Run Pipeline >
</a> {{ s__('Pipelines|Run Pipeline') }}
<a
v-if="!hasCiEnabled"
:href="helpPagePath"
class="btn btn-info">
Get started with Pipelines
</a> </a>
<a <a
v-if="resetCachePath"
data-method="post" data-method="post"
rel="nofollow"
:href="resetCachePath" :href="resetCachePath"
class="btn btn-default"> class="btn btn-default js-clear-cache"
Clear runner caches >
{{ s__('Pipelines|Clear Runner Caches') }}
</a> </a>
<a <a
v-if="ciLintPath"
:href="ciLintPath" :href="ciLintPath"
class="btn btn-default"> class="btn btn-default js-ci-lint"
CI Lint >
{{ s__('Pipelines|CI Lint') }}
</a> </a>
</div> </div>
</template> </template>
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import { __, sprintf, s__ } from '../../locale';
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines'; import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import TablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from '../../vue_shared/components/navigation_tabs.vue'; import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import NavigationControls from './nav_controls.vue';
import { import {
convertPermissionToBoolean,
getParameterByName, getParameterByName,
parseQueryStringIntoObject, parseQueryStringIntoObject,
} from '../../lib/utils/common_utils'; } from '../../lib/utils/common_utils';
...@@ -14,9 +14,9 @@ ...@@ -14,9 +14,9 @@
export default { export default {
components: { components: {
tablePagination, TablePagination,
navigationTabs, NavigationTabs,
navigationControls, NavigationControls,
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
...@@ -36,111 +36,186 @@ ...@@ -36,111 +36,186 @@
required: false, required: false,
default: 'root', default: 'root',
}, },
endpoint: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
emptyStateSvgPath: {
type: String,
required: true,
},
errorStateSvgPath: {
type: String,
required: true,
},
noPipelinesSvgPath: {
type: String,
required: true,
},
autoDevopsPath: {
type: String,
required: true,
},
hasGitlabCi: {
type: Boolean,
required: true,
},
canCreatePipeline: {
type: Boolean,
required: true,
},
ciLintPath: {
type: String,
required: false,
default: null,
},
resetCachePath: {
type: String,
required: false,
default: null,
},
newPipelinePath: {
type: String,
required: false,
default: null,
},
}, },
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
return { return {
endpoint: pipelinesData.endpoint, // Start with loading state to avoid a glitch when the empty state will be rendered
helpPagePath: pipelinesData.helpPagePath, isLoading: true,
emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
errorStateSvgPath: pipelinesData.errorStateSvgPath,
autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
hasCi: pipelinesData.hasCi,
ciLintPath: pipelinesData.ciLintPath,
resetCachePath: pipelinesData.resetCachePath,
state: this.store.state, state: this.store.state,
scope: getParameterByName('scope') || 'all', scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1', page: getParameterByName('page') || '1',
requestData: {}, requestData: {},
}; };
}, },
computed: { stateMap: {
canCreatePipelineParsed() { // with tabs
return convertPermissionToBoolean(this.canCreatePipeline); loading: 'loading',
}, tableList: 'tableList',
error: 'error',
emptyTab: 'emptyTab',
// without tabs
emptyState: 'emptyState',
},
scopes: {
all: 'all',
pending: 'pending',
running: 'running',
finished: 'finished',
branches: 'branches',
tags: 'tags',
},
computed: {
/** /**
* The empty state should only be rendered when the request is made to fetch all pipelines * `hasGitlabCi` handles both internal and external CI.
* and none is returned. * The order on which the checks are made in this method is
* * important to guarantee we handle all the corner cases.
* @return {Boolean}
*/ */
shouldRenderEmptyState() { stateToRender() {
return !this.isLoading && const { stateMap } = this.$options;
!this.hasError &&
this.hasMadeRequest && if (this.isLoading) {
!this.state.pipelines.length && return stateMap.loading;
(this.scope === 'all' || this.scope === null); }
if (this.hasError) {
return stateMap.error;
}
if (this.state.pipelines.length) {
return stateMap.tableList;
}
if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
return stateMap.emptyTab;
}
return stateMap.emptyState;
}, },
/** /**
* When a specific scope does not have pipelines we render a message. * Tabs are rendered in all states except empty state.
* * They are not rendered before the first request to avoid a flicker on first load.
* @return {Boolean}
*/ */
shouldRenderNoPipelinesMessage() { shouldRenderTabs() {
return !this.isLoading && const { stateMap } = this.$options;
!this.hasError && return this.hasMadeRequest &&
!this.state.pipelines.length && [
this.scope !== 'all' && stateMap.loading,
this.scope !== null; stateMap.tableList,
stateMap.error,
stateMap.emptyTab,
].includes(this.stateToRender);
}, },
shouldRenderTable() { shouldRenderButtons() {
return !this.hasError && return (this.newPipelinePath ||
!this.isLoading && this.state.pipelines.length; this.resetCachePath ||
this.ciLintPath) && this.shouldRenderTabs;
}, },
/**
* Pagination should only be rendered when there is more than one page.
*
* @return {Boolean}
*/
shouldRenderPagination() { shouldRenderPagination() {
return !this.isLoading && return !this.isLoading &&
this.state.pipelines.length && this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage; this.state.pageInfo.total > this.state.pageInfo.perPage;
}, },
hasCiEnabled() {
return this.hasCi !== undefined; emptyTabMessage() {
const { scopes } = this.$options;
const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
if (possibleScopes.includes(this.scope)) {
return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
scope: this.scope,
});
}
return s__('Pipelines|There are currently no pipelines.');
}, },
tabs() { tabs() {
const { count } = this.state; const { count } = this.state;
const { scopes } = this.$options;
return [ return [
{ {
name: 'All', name: __('All'),
scope: 'all', scope: scopes.all,
count: count.all, count: count.all,
isActive: this.scope === 'all', isActive: this.scope === 'all',
}, },
{ {
name: 'Pending', name: __('Pending'),
scope: 'pending', scope: scopes.pending,
count: count.pending, count: count.pending,
isActive: this.scope === 'pending', isActive: this.scope === 'pending',
}, },
{ {
name: 'Running', name: __('Running'),
scope: 'running', scope: scopes.running,
count: count.running, count: count.running,
isActive: this.scope === 'running', isActive: this.scope === 'running',
}, },
{ {
name: 'Finished', name: __('Finished'),
scope: 'finished', scope: scopes.finished,
count: count.finished, count: count.finished,
isActive: this.scope === 'finished', isActive: this.scope === 'finished',
}, },
{ {
name: 'Branches', name: __('Branches'),
scope: 'branches', scope: scopes.branches,
isActive: this.scope === 'branches', isActive: this.scope === 'branches',
}, },
{ {
name: 'Tags', name: __('Tags'),
scope: 'tags', scope: scopes.tags,
isActive: this.scope === 'tags', isActive: this.scope === 'tags',
}, },
]; ];
...@@ -187,7 +262,7 @@ ...@@ -187,7 +262,7 @@
this.errorCallback(); this.errorCallback();
// restart polling // restart polling
this.poll.restart(); this.poll.restart({ data: this.requestData });
}); });
}, },
}, },
...@@ -197,69 +272,70 @@ ...@@ -197,69 +272,70 @@
<div class="pipelines-container"> <div class="pipelines-container">
<div <div
class="top-area scrolling-tabs-container inner-page-scroll-tabs" class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!shouldRenderEmptyState" v-if="shouldRenderTabs || shouldRenderButtons"
> >
<div class="fade-left"> <div class="fade-left">
<i <i
class="fa fa-angle-left" class="fa fa-angle-left"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</div> </div>
<div class="fade-right"> <div class="fade-right">
<i <i
class="fa fa-angle-right" class="fa fa-angle-right"
aria-hidden="true"> aria-hidden="true"
>
</i> </i>
</div> </div>
<navigation-tabs <navigation-tabs
v-if="shouldRenderTabs"
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="pipelines" scope="pipelines"
/> />
<navigation-controls <navigation-controls
v-if="shouldRenderButtons"
:new-pipeline-path="newPipelinePath" :new-pipeline-path="newPipelinePath"
:has-ci-enabled="hasCiEnabled"
:help-page-path="helpPagePath"
:reset-cache-path="resetCachePath" :reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath" :ci-lint-path="ciLintPath"
:can-create-pipeline="canCreatePipelineParsed "
/> />
</div> </div>
<div class="content-list pipelines"> <div class="content-list pipelines">
<loading-icon <loading-icon
label="Loading Pipelines" v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')"
size="3" size="3"
v-if="isLoading"
class="prepend-top-20" class="prepend-top-20"
/> />
<empty-state <empty-state
v-if="shouldRenderEmptyState" v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath" :help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath" :empty-state-svg-path="emptyStateSvgPath"
:can-set-ci="canCreatePipeline"
/> />
<error-state <svg-blank-state
v-if="shouldRenderErrorState" v-else-if="stateToRender === $options.stateMap.error"
:error-state-svg-path="errorStateSvgPath" :svg-path="errorStateSvgPath"
:message="s__(`Pipelines|There was an error fetching the pipelines.
Try again in a few moments or contact your support team.`)"
/> />
<div <svg-blank-state
class="blank-state-row" v-else-if="stateToRender === $options.stateMap.emptyTab"
v-if="shouldRenderNoPipelinesMessage" :svg-path="noPipelinesSvgPath"
> :message="emptyTabMessage"
<div class="blank-state-center"> />
<h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
</div>
</div>
<div <div
class="table-holder" class="table-holder"
v-if="shouldRenderTable" v-else-if="stateToRender === $options.stateMap.tableList"
> >
<pipelines-table-component <pipelines-table-component
......
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import { __ } from '../../locale';
import Flash from '../../flash'; import Flash from '../../flash';
import Poll from '../../lib/utils/poll'; import Poll from '../../lib/utils/poll';
import emptyState from '../components/empty_state.vue'; import EmptyState from '../components/empty_state.vue';
import errorState from '../components/error_state.vue'; import SvgBlankState from '../components/blank_state.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
import pipelinesTableComponent from '../components/pipelines_table.vue'; import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
export default { export default {
components: { components: {
pipelinesTableComponent, PipelinesTableComponent,
errorState, SvgBlankState,
emptyState, EmptyState,
loadingIcon, LoadingIcon,
},
computed: {
shouldRenderErrorState() {
return this.hasError && !this.isLoading;
},
}, },
data() { data() {
return { return {
...@@ -85,6 +81,7 @@ export default { ...@@ -85,6 +81,7 @@ export default {
this.hasError = true; this.hasError = true;
this.isLoading = false; this.isLoading = false;
this.updateGraphDropdown = false; this.updateGraphDropdown = false;
this.hasMadeRequest = true;
}, },
setIsMakingRequest(isMakingRequest) { setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest; this.isMakingRequest = isMakingRequest;
...@@ -96,7 +93,7 @@ export default { ...@@ -96,7 +93,7 @@ export default {
postAction(endpoint) { postAction(endpoint) {
this.service.postAction(endpoint) this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines')) .then(() => eventHub.$emit('refreshPipelines'))
.catch(() => new Flash('An error occurred while making the request.')); .catch(() => Flash(__('An error occurred while making the request.')));
}, },
}, },
}; };
...@@ -1539,16 +1539,34 @@ class Project < ActiveRecord::Base ...@@ -1539,16 +1539,34 @@ class Project < ActiveRecord::Base
end end
end end
def import_export_shared
@import_export_shared ||= Gitlab::ImportExport::Shared.new(self)
end
def export_path def export_path
return nil unless namespace.present? || hashed_storage?(:repository) return nil unless namespace.present? || hashed_storage?(:repository)
File.join(Gitlab::ImportExport.storage_path, disk_path) import_export_shared.archive_path
end end
def export_project_path def export_project_path
Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) } Dir.glob("#{export_path}/*export.tar.gz").max_by { |f| File.ctime(f) }
end end
def export_status
if export_in_progress?
:started
elsif export_project_path
:finished
else
:none
end
end
def export_in_progress?
import_export_shared.active_export_count > 0
end
def remove_exports def remove_exports
return nil unless export_path.present? return nil unless export_path.present?
......
...@@ -2,7 +2,7 @@ module Projects ...@@ -2,7 +2,7 @@ module Projects
module ImportExport module ImportExport
class ExportService < BaseService class ExportService < BaseService
def execute(_options = {}) def execute(_options = {})
@shared = Gitlab::ImportExport::Shared.new(relative_path: File.join(project.disk_path, 'work')) @shared = project.import_export_shared
save_all save_all
end end
......
...@@ -10,8 +10,9 @@ ...@@ -10,8 +10,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'), "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'), "empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'), "error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
"new-pipeline-path" => new_project_pipeline_path(@project), "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s, "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
"has-ci" => @repository.gitlab_ci_yml, "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
"ci-lint-path" => ci_lint_path, "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
"reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } } "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
"has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
---
title: Add project export API
merge_request: 15860
author: Travis Miller
type: added
---
title: Handle empty state in Pipelines page
merge_request:
author:
type: fixed
...@@ -140,28 +140,28 @@ ...@@ -140,28 +140,28 @@
priority: 5 priority: 5
metrics: metrics:
- title: "Memory Usage" - title: "Memory Usage"
y_label: "Memory Usage (MB)" y_label: "Memory Used per Pod"
required_metrics: required_metrics:
- container_memory_usage_bytes - container_memory_usage_bytes
weight: 1 weight: 1
queries: queries:
- query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024' - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
- query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job)) /1024/1024' - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Average label: Average
unit: MB unit: MB
track: canary track: canary
- title: "CPU Utilization" - title: "CPU Usage"
y_label: "CPU Utilization (%)" y_label: "Cores per Pod"
required_metrics: required_metrics:
- container_cpu_usage_seconds_total - container_cpu_usage_seconds_total
weight: 1 weight: 1
queries: queries:
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100' - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Average label: Average
unit: "%" unit: "cores"
- query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100' - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Average label: Average
unit: "%" unit: "cores"
track: canary track: canary
# Project import API # Project import/export API
[Introduced][ce-41899] in GitLab 10.6 [Introduced][ce-41899] in GitLab 10.6
[See also the project import/export documentation](../user/project/settings/import_export.md) [See also the project import/export documentation](../user/project/settings/import_export.md)
## Schedule an export
Start a new export.
```http
POST /projects/:id/export
```
| 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 |
```console
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
```
```json
{
"message": "202 Accepted"
}
```
## Export status
Get the status of export.
```http
GET /projects/:id/export
```
| 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 |
```console
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/export
```
Status can be one of `none`, `started`, or `finished`.
`_links` are only present when export has finished.
```json
{
"id": 1,
"description": "Itaque perspiciatis minima aspernatur corporis consequatur.",
"name": "Gitlab Test",
"name_with_namespace": "Gitlab Org / Gitlab Test",
"path": "gitlab-test",
"path_with_namespace": "gitlab-org/gitlab-test",
"created_at": "2017-08-29T04:36:44.383Z",
"export_status": "finished",
"_links": {
"api_url": "https://gitlab.example.com/api/v4/projects/1/export/download",
"web_url": "https://gitlab.example.com/gitlab-org/gitlab-test/download_export",
}
}
```
## Export download
Download the finished export.
```http
GET /projects/:id/export/download
```
| 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 |
```console
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --remote-header-name --remote-name https://gitlab.example.com/api/v4/projects/5/export/download
```
```console
ls *export.tar.gz
2017-12-05_22-11-148_namespace_project_export.tar.gz
```
## Import a file ## Import a file
```http ```http
......
...@@ -13,16 +13,15 @@ integration services must be enabled. ...@@ -13,16 +13,15 @@ integration services must be enabled.
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 | | Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 |
| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 | | Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) |
## Configuring Prometheus to monitor for Kubernetes node metrics ## Configuring Prometheus to monitor for Kubernetes metrics
In order for Prometheus to collect Kubernetes metrics, you first must have a Prometheus needs to be deployed into the cluster and configured properly in order to gather Kubernetes metrics. GitLab supports two methods for doing so:
Prometheus server up and running. You have two options here:
- If you have an Omnibus based GitLab installation within your Kubernetes cluster, you can leverage the bundled Prometheus server to [monitor Kubernetes](../../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes). - GitLab [integrates with Kubernetes](../../clusters/index.md), and can [deploy Prometheus into a connected cluster](../prometheus.html#managed-prometheus-on-kubernetes). It is automatically configured to collect Kubernetes metrics.
- To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/) or [our guide](../../../../administration/monitoring/prometheus/index.md#configuring-your-own-prometheus-server-within-kubernetes). - To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/).
## Specifying the Environment ## Specifying the Environment
...@@ -30,19 +29,17 @@ In order to isolate and only display relevant CPU and Memory metrics for a given ...@@ -30,19 +29,17 @@ In order to isolate and only display relevant CPU and Memory metrics for a given
Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment. Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment.
If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added.
## Displaying Canary metrics ## Displaying Canary metrics
> Introduced in [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15201). > Introduced in [GitLab 10.2](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15201).
GitLab also gathers Kubernetes metrics for [canary deployments](../../canary_deployments.md), allowing easy comparison between the current deployed version and the canary. GitLab also gathers Kubernetes metrics for [canary deployments](../../canary_deployments.md), allowing easy comparison between the current deployed version and the canary.
These metrics expect an `environment` label of the form `$CI_ENVIRONMENT_SLUG-canary` to isolate the canary metrics. If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md), this label will be automatically configured for you. These metrics expect the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name to begin with `$CI_ENVIRONMENT_SLUG-canary`, to isolate the canary metrics.
### Canary metrics supported ### Canary metrics supported
| Name | Query | | Name | Query |
| ---- | ----- | | ---- | ----- |
| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}-canary"}) without (job)) /1024/1024 | | Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 |
| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}-canary"}[2m])) without (job)) * 100 | | Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) |
...@@ -31,7 +31,7 @@ The following languages and frameworks are supported. ...@@ -31,7 +31,7 @@ The following languages and frameworks are supported.
| Python ([pip](https://pip.pypa.io/en/stable/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bandit](https://github.com/openstack/bandit) | | Python ([pip](https://pip.pypa.io/en/stable/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bandit](https://github.com/openstack/bandit) |
| Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) | | Ruby ([gem](https://rubygems.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [bundler-audit](https://github.com/rubysec/bundler-audit) |
| Ruby on Rails | [brakeman](https://brakemanscanner.org) | | Ruby on Rails | [brakeman](https://brakemanscanner.org) |
| Java ([Maven](http://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | | Java ([Maven](https://maven.apache.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium), [find-sec-bugs](https://find-sec-bugs.github.io/) |
| PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) | | PHP ([Composer](https://getcomposer.org/)) | [gemnasium](https://gitlab.com/gitlab-org/security-products/gemnasium) |
Some security scanners require to send a list of project dependencies to GitLab central servers to check for vulnerabilities. To learn more about this or to disable it please Some security scanners require to send a list of project dependencies to GitLab central servers to check for vulnerabilities. To learn more about this or to disable it please
......
...@@ -37,6 +37,10 @@ module Gitlab ...@@ -37,6 +37,10 @@ module Gitlab
end end
def path def path
nil
end
def url
@uri.to_s @uri.to_s
end end
......
...@@ -28,6 +28,12 @@ describe Gitlab::Ci::Trace::HttpIO do ...@@ -28,6 +28,12 @@ describe Gitlab::Ci::Trace::HttpIO do
describe '#path' do describe '#path' do
subject { http_io.path } subject { http_io.path }
it { is_expected.to be_nil }
end
describe '#url' do
subject { http_io.url }
it { is_expected.to eq(url) } it { is_expected.to eq(url) }
end end
......
...@@ -150,6 +150,8 @@ module API ...@@ -150,6 +150,8 @@ module API
mount ::API::PagesDomains mount ::API::PagesDomains
mount ::API::Pipelines mount ::API::Pipelines
mount ::API::PipelineSchedules mount ::API::PipelineSchedules
mount ::API::ProjectExport
mount ::API::ProjectImport
mount ::API::ProjectHooks mount ::API::ProjectHooks
mount ::API::Projects mount ::API::Projects
mount ::API::ProjectMilestones mount ::API::ProjectMilestones
...@@ -185,7 +187,6 @@ module API ...@@ -185,7 +187,6 @@ module API
mount ::API::Ldap mount ::API::Ldap
mount ::API::LdapGroupLinks mount ::API::LdapGroupLinks
mount ::API::License mount ::API::License
mount ::API::ProjectImport
mount ::API::ProjectPushRule mount ::API::ProjectPushRule
mount ::EE::API::Boards mount ::EE::API::Boards
## EE-specific API V4 endpoints END ## EE-specific API V4 endpoints END
......
...@@ -91,6 +91,21 @@ module API ...@@ -91,6 +91,21 @@ module API
expose :created_at expose :created_at
end end
class ProjectExportStatus < ProjectIdentity
include ::API::Helpers::RelatedResourcesHelpers
expose :export_status
expose :_links, if: lambda { |project, _options| project.export_status == :finished } do
expose :api_url do |project|
expose_url(api_v4_projects_export_download_path(id: project.id))
end
expose :web_url do |project|
Gitlab::Routing.url_helpers.download_export_project_url(project)
end
end
end
class ProjectImportStatus < ProjectIdentity class ProjectImportStatus < ProjectIdentity
expose :import_status expose :import_status
......
module API
class ProjectExport < Grape::API
before do
not_found! unless Gitlab::CurrentSettings.project_export_enabled?
authorize_admin_project
end
params do
requires :id, type: String, desc: 'The ID of a project'
end
resource :projects, requirements: { id: %r{[^/]+} } do
desc 'Get export status' do
detail 'This feature was introduced in GitLab 10.6.'
success Entities::ProjectExportStatus
end
get ':id/export' do
present user_project, with: Entities::ProjectExportStatus
end
desc 'Download export' do
detail 'This feature was introduced in GitLab 10.6.'
end
get ':id/export/download' do
path = user_project.export_project_path
render_api_error!('404 Not found or has expired', 404) unless path
present_file!(path, File.basename(path), 'application/gzip')
end
desc 'Start export' do
detail 'This feature was introduced in GitLab 10.6.'
end
post ':id/export' do
user_project.add_export_job(current_user: current_user)
accepted!
end
end
end
end
...@@ -8,7 +8,7 @@ module Gitlab ...@@ -8,7 +8,7 @@ module Gitlab
attr_reader :stream attr_reader :stream
delegate :close, :tell, :seek, :size, :path, :truncate, to: :stream, allow_nil: true delegate :close, :tell, :seek, :size, :path, :url, :truncate, to: :stream, allow_nil: true
delegate :valid?, to: :stream, as: :present?, allow_nil: true delegate :valid?, to: :stream, as: :present?, allow_nil: true
......
...@@ -9,7 +9,7 @@ module Gitlab ...@@ -9,7 +9,7 @@ module Gitlab
@archive_file = project.import_source @archive_file = project.import_source
@current_user = project.creator @current_user = project.creator
@project = project @project = project
@shared = Gitlab::ImportExport::Shared.new(relative_path: path_with_namespace) @shared = project.import_export_shared
end end
def execute def execute
......
module Gitlab module Gitlab
module ImportExport module ImportExport
class Shared class Shared
attr_reader :errors, :opts attr_reader :errors, :project
def initialize(opts) def initialize(project)
@opts = opts @project = project
@errors = [] @errors = []
end end
def active_export_count
Dir[File.join(archive_path, '*')].count { |name| File.directory?(name) }
end
def export_path def export_path
@export_path ||= Gitlab::ImportExport.export_path(relative_path: relative_path) @export_path ||= Gitlab::ImportExport.export_path(relative_path: relative_path)
end end
...@@ -31,11 +35,11 @@ module Gitlab ...@@ -31,11 +35,11 @@ module Gitlab
private private
def relative_path def relative_path
File.join(opts[:relative_path], SecureRandom.hex) File.join(relative_archive_path, SecureRandom.hex)
end end
def relative_archive_path def relative_archive_path
File.join(opts[:relative_path], '..') @project.disk_path
end end
def error_out(message, caller) def error_out(message, caller)
......
...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do ...@@ -86,7 +86,22 @@ describe 'Pipelines', :js do
it 'updates content when tab is clicked' do it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click page.find('.js-pipelines-tab-pending').click
wait_for_requests wait_for_requests
expect(page).to have_content('No pipelines to show.') expect(page).to have_content('There are currently no pending pipelines.')
end
end
context 'navigation links' do
before do
visit project_pipelines_path(project)
wait_for_requests
end
it 'renders run pipeline link' do
expect(page).to have_link('Run Pipeline')
end
it 'renders ci lint link' do
expect(page).to have_link('CI Lint')
end end
end end
...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do ...@@ -542,7 +557,7 @@ describe 'Pipelines', :js do
end end
it 'has a clear caches button' do it 'has a clear caches button' do
expect(page).to have_link 'Clear runner caches' expect(page).to have_link 'Clear Runner Caches'
end end
describe 'user clicks the button' do describe 'user clicks the button' do
...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do ...@@ -552,19 +567,31 @@ describe 'Pipelines', :js do
end end
it 'increments jobs_cache_index' do it 'increments jobs_cache_index' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
context 'when project does not have jobs_cache_index' do context 'when project does not have jobs_cache_index' do
it 'sets jobs_cache_index to 1' do it 'sets jobs_cache_index to 1' do
click_link 'Clear runner caches' click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.' expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end end
end end
end end
end end
describe 'Empty State' do
let(:project) { create(:project, :repository) }
before do
visit project_pipelines_path(project)
end
it 'renders empty state' do
expect(page).to have_content 'Build with confidence'
end
end
end end
context 'when user is not logged in' do context 'when user is not logged in' do
...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do ...@@ -575,7 +602,9 @@ describe 'Pipelines', :js do
context 'when project is public' do context 'when project is public' do
let(:project) { create(:project, :public, :repository) } let(:project) { create(:project, :public, :repository) }
it { expect(page).to have_content 'Build with confidence' } context 'without pipelines' do
it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
end
end end
context 'when project is private' do context 'when project is private' do
......
{
"type": "object",
"allOf": [
{ "$ref": "identity.json" },
{
"required": [
"export_status"
],
"properties": {
"export_status": {
"type": "string",
"enum": ["none", "started", "finished"]
}
}
}
]
}
{
"type": "object",
"required": [
"id",
"description",
"name",
"name_with_namespace",
"path",
"path_with_namespace",
"created_at"
],
"properties": {
"id": { "type": "integer" },
"description": { "type": ["string", "null"] },
"name": { "type": "string" },
"name_with_namespace": { "type": "string" },
"path": { "type": "string" },
"path_with_namespace": { "type": "string" },
"created_at": { "type": "date" }
}
}
import Vue from 'vue';
import component from '~/pipelines/components/blank_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Blank State', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(component);
vm = mountComponent(Component,
{
svgPath: 'foo',
message: 'Blank State',
},
);
});
it('should render svg', () => {
expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
});
it('should render message', () => {
expect(
vm.$el.querySelector('h4').textContent.trim(),
).toEqual('Blank State');
});
});
import Vue from 'vue'; import Vue from 'vue';
import emptyStateComp from '~/pipelines/components/empty_state.vue'; import emptyStateComp from '~/pipelines/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Empty State', () => { describe('Pipelines Empty State', () => {
let component; let component;
...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => { ...@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => {
beforeEach(() => { beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp); EmptyStateComponent = Vue.extend(emptyStateComp);
component = new EmptyStateComponent({ component = mountComponent(EmptyStateComponent, {
propsData: {
helpPagePath: 'foo', helpPagePath: 'foo',
emptyStateSvgPath: 'foo', emptyStateSvgPath: 'foo',
}, canSetCi: true,
}).$mount(); });
});
afterEach(() => {
component.$destroy();
}); });
it('should render empty state SVG', () => { it('should render empty state SVG', () => {
...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => { ...@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence'); expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continous Integration can help catch bugs by running your tests automatically'); ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
expect( expect(
component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '), component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
).toContain('Continuous Deployment can help you deliver code to your product environment'); ).toContain('while Continuous Deployment can help you deliver code to your product environment');
}); });
it('should render a link with provided help path', () => { it('should render a link with provided help path', () => {
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo'); expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo');
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines'); expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines');
}); });
}); });
import Vue from 'vue';
import errorStateComp from '~/pipelines/components/error_state.vue';
describe('Pipelines Error State', () => {
let component;
let ErrorStateComponent;
beforeEach(() => {
ErrorStateComponent = Vue.extend(errorStateComp);
component = new ErrorStateComponent({
propsData: {
errorStateSvgPath: 'foo',
},
}).$mount();
});
it('should render error state SVG', () => {
expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
});
it('should render emtpy state information', () => {
expect(
component.$el.querySelector('h4').textContent,
).toContain('The API failed to fetch the pipelines');
});
});
import Vue from 'vue'; import Vue from 'vue';
import navControlsComp from '~/pipelines/components/nav_controls.vue'; import navControlsComp from '~/pipelines/components/nav_controls.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Nav Controls', () => { describe('Pipelines Nav Controls', () => {
let NavControlsComponent; let NavControlsComponent;
let component;
beforeEach(() => { beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp); NavControlsComponent = Vue.extend(navControlsComp);
}); });
afterEach(() => {
component.$destroy();
});
it('should render link to create a new pipeline', () => { it('should render link to create a new pipeline', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline'); expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath); expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath);
}); });
it('should not render link to create pipeline if no permission is provided', () => { it('should not render link to create pipeline if no path is provided', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-create')).toEqual(null); expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
}); });
it('should render link for resetting runner caches', () => { it('should render link for resetting runner caches', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: false,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches'); expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches');
expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath); expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath);
}); });
it('should render link for CI lint', () => { it('should render link for CI lint', () => {
const mockData = { const mockData = {
newPipelinePath: 'foo', newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
});
it('should render link to help page when CI is not enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: false,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
canCreatePipeline: true,
};
const component = new NavControlsComponent({
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
});
it('should not render link to help page when CI is enabled', () => {
const mockData = {
newPipelinePath: 'foo',
hasCiEnabled: true,
helpPagePath: 'foo', helpPagePath: 'foo',
ciLintPath: 'foo', ciLintPath: 'foo',
resetCachePath: 'foo', resetCachePath: 'foo',
canCreatePipeline: true,
}; };
const component = new NavControlsComponent({ component = mountComponent(NavControlsComponent, mockData);
propsData: mockData,
}).$mount();
expect(component.$el.querySelector('.btn-info')).toEqual(null); expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath);
}); });
}); });
...@@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper'; ...@@ -7,36 +7,380 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines', () => { describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json'; const jsonFixtureName = 'pipelines/pipelines.json';
preloadFixtures('static/pipelines.html.raw');
preloadFixtures(jsonFixtureName); preloadFixtures(jsonFixtureName);
let PipelinesComponent; let PipelinesComponent;
let pipelines; let pipelines;
let component; let vm;
const paths = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
ciLintPath: '/ci/lint',
resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
newPipelinePath: '/twitter/flight/pipelines/new',
};
const noPermissions = {
endpoint: 'twitter/flight/pipelines.json',
autoDevopsPath: '/help/topics/autodevops/index.md',
helpPagePath: '/help/ci/quick_start/README',
emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
};
beforeEach(() => { beforeEach(() => {
loadFixtures('static/pipelines.html.raw');
pipelines = getJSONFixture(jsonFixtureName); pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp); PipelinesComponent = Vue.extend(pipelinesComp);
}); });
afterEach(() => { afterEach(() => {
component.$destroy(); vm.$destroy();
}); });
describe('successfull request', () => {
describe('with pipelines', () => {
const pipelinesInterceptor = (request, next) => { const pipelinesInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(pipelines), { next(request.respondWith(JSON.stringify(pipelines), {
status: 200, status: 200,
})); }));
}; };
const emptyStateInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
pipelines: [],
count: {
all: 0,
pending: 0,
running: 0,
finished: 0,
},
}), {
status: 200,
}));
};
const errorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
describe('With permission', () => {
describe('With pipelines in main tab', () => {
beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders Run Pipeline button', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
});
it('renders CI Lint button', () => {
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
});
it('renders Clear Runner Cache button', () => {
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders pipelines table', () => {
expect(
vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
});
});
describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders Run Pipeline button', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
});
it('renders CI Lint button', () => {
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
});
it('renders Clear Runner Cache button', () => {
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders tab empty state', () => {
expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
});
});
describe('Without pipelines nor CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state', () => {
expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence');
expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath);
});
it('does not render tabs nor buttons', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
});
describe('When API returns error', () => {
beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...paths,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('renders buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
});
it('renders error state', () => {
expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
});
});
});
describe('Without permission', () => {
describe('With pipelines in main tab', () => {
beforeEach((done) => {
Vue.http.interceptors.push(pipelinesInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, pipelinesInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not render buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
it('renders pipelines table', () => {
expect(
vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
});
});
describe('Without pipelines on main tab with CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not render buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
it('renders tab empty state', () => {
expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
});
});
describe('Without pipelines nor CI', () => {
beforeEach((done) => {
Vue.http.interceptors.push(emptyStateInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: false,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, emptyStateInterceptor,
);
});
it('renders empty state without button to set CI', () => {
expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.');
expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
});
it('does not render tabs or buttons', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
});
describe('When API returns error', () => {
beforeEach((done) => {
Vue.http.interceptors.push(errorInterceptor);
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: false,
canCreatePipeline: true,
...noPermissions,
});
setTimeout(() => {
done();
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, errorInterceptor,
);
});
it('renders tabs', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
});
it('does not renders buttons', () => {
expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
});
it('renders error state', () => {
expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
});
});
});
describe('successfull request', () => {
describe('with pipelines', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor); Vue.http.interceptors.push(pipelinesInterceptor);
component = mountComponent(PipelinesComponent, { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
}); });
}); });
...@@ -48,9 +392,9 @@ describe('Pipelines', () => { ...@@ -48,9 +392,9 @@ describe('Pipelines', () => {
it('should render table', (done) => { it('should render table', (done) => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelector('.table-holder')).toBeDefined(); expect(vm.$el.querySelector('.table-holder')).toBeDefined();
expect( expect(
component.$el.querySelectorAll('.gl-responsive-table-row').length, vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1); ).toEqual(pipelines.pipelines.length + 1);
done(); done();
}); });
...@@ -59,22 +403,22 @@ describe('Pipelines', () => { ...@@ -59,22 +403,22 @@ describe('Pipelines', () => {
it('should render navigation tabs', (done) => { it('should render navigation tabs', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
).toContain('Pending'); ).toContain('Pending');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
).toContain('All'); ).toContain('All');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
).toContain('Running'); ).toContain('Running');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
).toContain('Finished'); ).toContain('Finished');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
).toContain('Branches'); ).toContain('Branches');
expect( expect(
component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(), vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
).toContain('Tags'); ).toContain('Tags');
done(); done();
}); });
...@@ -82,10 +426,10 @@ describe('Pipelines', () => { ...@@ -82,10 +426,10 @@ describe('Pipelines', () => {
it('should make an API request when using tabs', (done) => { it('should make an API request when using tabs', (done) => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(vm, 'updateContent');
component.$el.querySelector('.js-pipelines-tab-finished').click(); vm.$el.querySelector('.js-pipelines-tab-finished').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done(); done();
}); });
}); });
...@@ -93,9 +437,9 @@ describe('Pipelines', () => { ...@@ -93,9 +437,9 @@ describe('Pipelines', () => {
describe('with pagination', () => { describe('with pagination', () => {
it('should make an API request when using pagination', (done) => { it('should make an API request when using pagination', (done) => {
setTimeout(() => { setTimeout(() => {
spyOn(component, 'updateContent'); spyOn(vm, 'updateContent');
// Mock pagination // Mock pagination
component.store.state.pageInfo = { vm.store.state.pageInfo = {
page: 1, page: 1,
total: 10, total: 10,
perPage: 2, perPage: 2,
...@@ -103,9 +447,9 @@ describe('Pipelines', () => { ...@@ -103,9 +447,9 @@ describe('Pipelines', () => {
totalPages: 5, totalPages: 5,
}; };
Vue.nextTick(() => { vm.$nextTick(() => {
component.$el.querySelector('.js-next-button a').click(); vm.$el.querySelector('.js-next-button a').click();
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' }); expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
done(); done();
}); });
...@@ -113,114 +457,249 @@ describe('Pipelines', () => { ...@@ -113,114 +457,249 @@ describe('Pipelines', () => {
}); });
}); });
}); });
});
describe('without pipelines', () => { describe('methods', () => {
const emptyInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(emptyInterceptor); spyOn(history, 'pushState').and.stub();
}); });
afterEach(() => { describe('updateContent', () => {
Vue.http.interceptors = _.without( it('should set given parameters', () => {
Vue.http.interceptors, emptyInterceptor, vm = mountComponent(PipelinesComponent, {
); store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
vm.updateContent({ scope: 'finished', page: '4' });
expect(vm.page).toEqual('4');
expect(vm.scope).toEqual('finished');
expect(vm.requestData.scope).toEqual('finished');
expect(vm.requestData.page).toEqual('4');
});
}); });
it('should render empty state', (done) => { describe('onChangeTab', () => {
component = new PipelinesComponent({ it('should set page to 1', () => {
propsData: { vm = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
}, hasGitlabCi: true,
}).$mount(); canCreatePipeline: true,
...paths,
});
spyOn(vm, 'updateContent');
setTimeout(() => { vm.onChangeTab('running');
expect(component.$el.querySelector('.empty-state')).not.toBe(null);
expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
spyOn(vm, 'updateContent');
vm.onChangePage(4);
expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
});
});
});
describe('computed properties', () => {
beforeEach(() => {
vm = mountComponent(PipelinesComponent, {
store: new Store(),
hasGitlabCi: true,
canCreatePipeline: true,
...paths,
});
});
describe('tabs', () => {
it('returns default tabs', () => {
expect(vm.tabs).toEqual([
{ name: 'All', scope: 'all', count: undefined, isActive: true },
{ name: 'Pending', scope: 'pending', count: undefined, isActive: false },
{ name: 'Running', scope: 'running', count: undefined, isActive: false },
{ name: 'Finished', scope: 'finished', count: undefined, isActive: false },
{ name: 'Branches', scope: 'branches', isActive: false },
{ name: 'Tags', scope: 'tags', isActive: false },
]);
});
});
describe('emptyTabMessage', () => {
it('returns message with scope', (done) => {
vm.scope = 'pending';
vm.$nextTick(() => {
expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
done(); done();
}); });
}); });
it('returns message without scope when scope is `all`', () => {
expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
}); });
}); });
describe('unsuccessfull request', () => { describe('stateToRender', () => {
const errorInterceptor = (request, next) => { it('returns loading state when the app is loading', () => {
next(request.respondWith(JSON.stringify([]), { expect(vm.stateToRender).toEqual('loading');
status: 500, });
}));
};
beforeEach(() => { it('returns error state when app has error', (done) => {
Vue.http.interceptors.push(errorInterceptor); vm.hasError = true;
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('error');
done();
});
}); });
afterEach(() => { it('returns table list when app has pipelines', (done) => {
Vue.http.interceptors = _.without( vm.isLoading = false;
Vue.http.interceptors, errorInterceptor, vm.hasError = false;
); vm.state.pipelines = pipelines.pipelines;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('tableList');
done();
});
}); });
it('should render error state', (done) => { it('returns empty tab when app does not have pipelines but project has pipelines', (done) => {
component = new PipelinesComponent({ vm.state.count.all = 10;
propsData: { vm.isLoading = false;
store: new Store(),
}, vm.$nextTick(() => {
}).$mount(); expect(vm.stateToRender).toEqual('emptyTab');
setTimeout(() => {
expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
done(); done();
}); });
}); });
it('returns empty tab when project has CI', (done) => {
vm.isLoading = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyTab');
done();
});
}); });
describe('methods', () => { it('returns empty state when project does not have pipelines nor CI', (done) => {
beforeEach(() => { vm.isLoading = false;
spyOn(history, 'pushState').and.stub(); vm.hasGitlabCi = false;
vm.$nextTick(() => {
expect(vm.stateToRender).toEqual('emptyState');
done();
});
});
}); });
describe('updateContent', () => { describe('shouldRenderTabs', () => {
it('should set given parameters', () => { it('returns true when state is loading & has already made the first request', (done) => {
component = mountComponent(PipelinesComponent, { vm.isLoading = true;
store: new Store(), vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
}); });
component.updateContent({ scope: 'finished', page: '4' }); });
it('returns true when state is tableList & has already made the first request', (done) => {
vm.isLoading = false;
vm.state.pipelines = pipelines.pipelines;
vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
expect(component.page).toEqual('4'); done();
expect(component.scope).toEqual('finished');
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4');
}); });
}); });
describe('onChangeTab', () => { it('returns true when state is error & has already made the first request', (done) => {
it('should set page to 1', () => { vm.isLoading = false;
component = mountComponent(PipelinesComponent, { vm.hasError = true;
store: new Store(), vm.hasMadeRequest = true;
vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
done();
});
}); });
spyOn(component, 'updateContent'); it('returns true when state is empty tab & has already made the first request', (done) => {
vm.isLoading = false;
vm.state.count.all = 10;
vm.hasMadeRequest = true;
component.onChangeTab('running'); vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(true);
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); done();
}); });
}); });
describe('onChangePage', () => { it('returns false when has not made first request', (done) => {
it('should update page and keep scope', () => { vm.hasMadeRequest = false;
component = mountComponent(PipelinesComponent, {
store: new Store(), vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(false);
done();
}); });
});
it('returns false when state is emtpy state', (done) => {
vm.isLoading = false;
vm.hasMadeRequest = true;
vm.hasGitlabCi = false;
spyOn(component, 'updateContent'); vm.$nextTick(() => {
expect(vm.shouldRenderTabs).toEqual(false);
done();
});
});
});
component.onChangePage(4); describe('shouldRenderButtons', () => {
it('returns true when it has paths & has made the first request', (done) => {
vm.hasMadeRequest = true;
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(true);
done();
});
});
it('returns false when it has not made the first request', (done) => {
vm.hasMadeRequest = false;
vm.$nextTick(() => {
expect(vm.shouldRenderButtons).toEqual(false);
done();
});
}); });
}); });
}); });
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::AvatarRestorer do describe Gitlab::ImportExport::AvatarRestorer do
include UploadHelpers include UploadHelpers
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:shared) { project.import_export_shared }
let(:project) { create(:project) } let(:project) { create(:project) }
before do before do
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::AvatarSaver do describe Gitlab::ImportExport::AvatarSaver do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:shared) { project.import_export_shared }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:project_with_avatar) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) } let(:project_with_avatar) { create(:project, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
let(:project) { create(:project) } let(:project) { create(:project) }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::FileImporter do describe Gitlab::ImportExport::FileImporter do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: 'test') } let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" } let(:export_path) { "#{Dir.tmpdir}/file_importer_spec" }
let(:valid_file) { "#{shared.export_path}/valid.json" } let(:valid_file) { "#{shared.export_path}/valid.json" }
let(:symlink_file) { "#{shared.export_path}/invalid.json" } let(:symlink_file) { "#{shared.export_path}/invalid.json" }
...@@ -12,6 +12,7 @@ describe Gitlab::ImportExport::FileImporter do ...@@ -12,6 +12,7 @@ describe Gitlab::ImportExport::FileImporter do
stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0) stub_const('Gitlab::ImportExport::FileImporter::MAX_RETRIES', 0)
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true) allow_any_instance_of(Gitlab::ImportExport::CommandLineUtil).to receive(:untar_zxf).and_return(true)
allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('test')
allow(SecureRandom).to receive(:hex).and_return('abcd') allow(SecureRandom).to receive(:hex).and_return('abcd')
setup_files setup_files
end end
......
...@@ -7,7 +7,7 @@ describe 'forked project import' do ...@@ -7,7 +7,7 @@ describe 'forked project import' do
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') } let!(:project) { create(:project, name: 'test-repo-restorer-no-repo', path: 'test-repo-restorer-no-repo') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:forked_from_project) { create(:project, :repository) } let(:forked_from_project) { create(:project, :repository) }
let(:forked_project) { fork_project(project_with_repo, nil, repository: true) } let(:forked_project) { fork_project(project_with_repo, nil, repository: true) }
let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:repo_saver) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
......
...@@ -7,9 +7,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -7,9 +7,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
@user = create(:user) @user = create(:user)
RSpec::Mocks.with_temporary_scope do RSpec::Mocks.with_temporary_scope do
@shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path')
allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
@project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') @project = create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
@shared = @project.import_export_shared
allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true) allow_any_instance_of(Repository).to receive(:fetch_ref).and_return(true)
allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false) allow_any_instance_of(Gitlab::Git::Repository).to receive(:branch_exists?).and_return(false)
...@@ -263,7 +263,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -263,7 +263,7 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'Light JSON' do context 'Light JSON' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') } let(:shared) { project.import_export_shared }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
......
...@@ -2,7 +2,7 @@ require 'spec_helper' ...@@ -2,7 +2,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeSaver do describe Gitlab::ImportExport::ProjectTreeSaver do
describe 'saves the project tree into a json object' do describe 'saves the project tree into a json object' do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) } let(:project_tree_saver) { described_class.new(project: project, current_user: user, shared: shared) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:user) { create(:user) } let(:user) { create(:user) }
......
require 'spec_helper' require 'spec_helper'
describe Gitlab::ImportExport::Reader do describe Gitlab::ImportExport::Reader do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
let(:test_config) { 'spec/support/import_export/import_export.yml' } let(:test_config) { 'spec/support/import_export/import_export.yml' }
let(:project_tree_hash) do let(:project_tree_hash) do
{ {
......
...@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::RepoRestorer do ...@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::RepoRestorer do
let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:project) } let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) } let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:restorer) do let(:restorer) do
......
...@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::RepoSaver do ...@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::RepoSaver do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, :public, name: 'searchable_project') } let!(:project) { create(:project, :public, name: 'searchable_project') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:bundler) { described_class.new(project: project, shared: shared) } let(:bundler) { described_class.new(project: project, shared: shared) }
before do before do
......
...@@ -3,7 +3,7 @@ require 'spec_helper' ...@@ -3,7 +3,7 @@ require 'spec_helper'
describe Gitlab::ImportExport::UploadsRestorer do describe Gitlab::ImportExport::UploadsRestorer do
describe 'bundle a project Git repo' do describe 'bundle a project Git repo' do
let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
before do before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
......
...@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::UploadsSaver do ...@@ -4,7 +4,7 @@ describe Gitlab::ImportExport::UploadsSaver do
describe 'bundle a project Git repo' do describe 'bundle a project Git repo' do
let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/uploads_saver_spec" }
let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
before do before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
......
...@@ -2,12 +2,13 @@ require 'spec_helper' ...@@ -2,12 +2,13 @@ require 'spec_helper'
include ImportExport::CommonUtil include ImportExport::CommonUtil
describe Gitlab::ImportExport::VersionChecker do describe Gitlab::ImportExport::VersionChecker do
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: '') } let(:shared) { Gitlab::ImportExport::Shared.new(nil) }
describe 'bundle a project Git repo' do describe 'bundle a project Git repo' do
let(:version) { Gitlab::ImportExport.version } let(:version) { Gitlab::ImportExport.version }
before do before do
allow_any_instance_of(Gitlab::ImportExport::Shared).to receive(:relative_archive_path).and_return('')
allow(File).to receive(:open).and_return(version) allow(File).to receive(:open).and_return(version)
end end
......
...@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do ...@@ -5,7 +5,7 @@ describe Gitlab::ImportExport::WikiRepoSaver do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, :public, name: 'searchable_project') } let!(:project) { create(:project, :public, name: 'searchable_project') }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:wiki_bundler) { described_class.new(project: project, shared: shared) } let(:wiki_bundler) { described_class.new(project: project, shared: shared) }
let!(:project_wiki) { ProjectWiki.new(project, user) } let!(:project_wiki) { ProjectWiki.new(project, user) }
......
...@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::WikiRestorer do ...@@ -6,7 +6,7 @@ describe Gitlab::ImportExport::WikiRestorer do
let!(:project_without_wiki) { create(:project) } let!(:project_without_wiki) { create(:project) }
let!(:project) { create(:project) } let!(:project) { create(:project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.full_path) } let(:shared) { project.import_export_shared }
let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) } let(:bundler) { Gitlab::ImportExport::WikiRepoSaver.new(project: project_with_wiki, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) } let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:restorer) do let(:restorer) do
......
require 'spec_helper'
describe API::ProjectExport do
set(:project) { create(:project) }
set(:project_none) { create(:project) }
set(:project_started) { create(:project) }
set(:project_finished) { create(:project) }
set(:user) { create(:user) }
set(:admin) { create(:admin) }
let(:path) { "/projects/#{project.id}/export" }
let(:path_none) { "/projects/#{project_none.id}/export" }
let(:path_started) { "/projects/#{project_started.id}/export" }
let(:path_finished) { "/projects/#{project_finished.id}/export" }
let(:download_path) { "/projects/#{project.id}/export/download" }
let(:download_path_none) { "/projects/#{project_none.id}/export/download" }
let(:download_path_started) { "/projects/#{project_started.id}/export/download" }
let(:download_path_finished) { "/projects/#{project_finished.id}/export/download" }
let(:export_path) { "#{Dir.tmpdir}/project_export_spec" }
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
# simulate exporting work directory
FileUtils.mkdir_p File.join(project_started.export_path, 'securerandom-hex')
# simulate exported
FileUtils.mkdir_p project_finished.export_path
FileUtils.touch File.join(project_finished.export_path, '_export.tar.gz')
end
after do
FileUtils.rm_rf(export_path, secure: true)
end
shared_examples_for 'when project export is disabled' do
before do
stub_application_setting(project_export_enabled?: false)
end
it_behaves_like '404 response'
end
describe 'GET /projects/:project_id/export' do
shared_examples_for 'get project export status not found' do
it_behaves_like '404 response' do
let(:request) { get api(path, user) }
end
end
shared_examples_for 'get project export status denied' do
it_behaves_like '403 response' do
let(:request) { get api(path, user) }
end
end
shared_examples_for 'get project export status ok' do
it 'is none' do
get api(path_none, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/project/export_status')
expect(json_response['export_status']).to eq('none')
end
it 'is started' do
get api(path_started, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/project/export_status')
expect(json_response['export_status']).to eq('started')
end
it 'is finished' do
get api(path_finished, user)
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/project/export_status')
expect(json_response['export_status']).to eq('finished')
end
end
it_behaves_like 'when project export is disabled' do
let(:request) { get api(path, admin) }
end
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
it_behaves_like 'get project export status ok'
end
context 'when user is a master' do
before do
project.add_master(user)
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
end
it_behaves_like 'get project export status ok'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like 'get project export status denied'
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like 'get project export status denied'
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'get project export status denied'
end
context 'when user is not a member' do
it_behaves_like 'get project export status not found'
end
end
end
describe 'GET /projects/:project_id/export/download' do
shared_examples_for 'get project export download not found' do
it_behaves_like '404 response' do
let(:request) { get api(download_path, user) }
end
end
shared_examples_for 'get project export download denied' do
it_behaves_like '403 response' do
let(:request) { get api(download_path, user) }
end
end
shared_examples_for 'get project export download' do
it_behaves_like '404 response' do
let(:request) { get api(download_path_none, user) }
end
it_behaves_like '404 response' do
let(:request) { get api(download_path_started, user) }
end
it 'downloads' do
get api(download_path_finished, user)
expect(response).to have_gitlab_http_status(200)
end
end
it_behaves_like 'when project export is disabled' do
let(:request) { get api(download_path, admin) }
end
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
it_behaves_like 'get project export download'
end
context 'when user is a master' do
before do
project.add_master(user)
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
end
it_behaves_like 'get project export download'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like 'get project export download denied'
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like 'get project export download denied'
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'get project export download denied'
end
context 'when user is not a member' do
it_behaves_like 'get project export download not found'
end
end
end
describe 'POST /projects/:project_id/export' do
shared_examples_for 'post project export start not found' do
it_behaves_like '404 response' do
let(:request) { post api(path, user) }
end
end
shared_examples_for 'post project export start denied' do
it_behaves_like '403 response' do
let(:request) { post api(path, user) }
end
end
shared_examples_for 'post project export start' do
it 'starts' do
post api(path, user)
expect(response).to have_gitlab_http_status(202)
end
end
it_behaves_like 'when project export is disabled' do
let(:request) { post api(path, admin) }
end
context 'when project export is enabled' do
context 'when user is an admin' do
let(:user) { admin }
it_behaves_like 'post project export start'
end
context 'when user is a master' do
before do
project.add_master(user)
project_none.add_master(user)
project_started.add_master(user)
project_finished.add_master(user)
end
it_behaves_like 'post project export start'
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it_behaves_like 'post project export start denied'
end
context 'when user is a reporter' do
before do
project.add_reporter(user)
end
it_behaves_like 'post project export start denied'
end
context 'when user is a guest' do
before do
project.add_guest(user)
end
it_behaves_like 'post project export start denied'
end
context 'when user is not a member' do
it_behaves_like 'post project export start not found'
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment