Commit 9f4701df authored by Robert Speicher's avatar Robert Speicher

Merge branch 'ce-to-ee-2018-12-10' into 'master'

CE upstream - 2018-12-10 16:21 UTC

Closes gitlab-ce#55079 and gitaly#1425

See merge request gitlab-org/gitlab-ee!8774
parents 749280c9 b9a8fc58
### Problem to solve
<!--- What problem do we solve? -->
### Target audience
<!--- For whom are we doing this? Include either a persona from https://design.gitlab.com/#/getting-started/personas or define a specific company role. e.a. "Release Manager" or "Security Analyst" -->
### Further details
(Include use cases, benefits, and/or goals)
<!--- Include use cases, benefits, and/or goals (contributes to our vision?) -->
### Proposal
<!--- How are we going to solve the problem? -->
### What does success look like, and how can we measure that?
(If no way to measure success, link to an issue that will implement a way to measure this)
<!--- If no way to measure success, link to an issue that will implement a way to measure this -->
### Links / references
......
<script>
import { mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import Flash from '../../flash';
import store from '../stores';
import collapsibleContainer from './collapsible_container.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
import CollapsibleContainer from './collapsible_container.vue';
export default {
name: 'RegistryListApp',
components: {
collapsibleContainer,
CollapsibleContainer,
GlLoadingIcon,
},
props: {
......@@ -26,7 +24,7 @@ export default {
this.setMainEndpoint(this.endpoint);
},
mounted() {
this.fetchRepos().catch(() => Flash(errorMessages[errorMessagesTypes.FETCH_REPOS]));
this.fetchRepos();
},
methods: {
...mapActions(['setMainEndpoint', 'fetchRepos']),
......@@ -38,9 +36,9 @@ export default {
<gl-loading-icon v-if="isLoading" :size="3" />
<collapsible-container
v-for="(item, index) in repos"
v-for="item in repos"
v-else-if="!isLoading && repos.length"
:key="index"
:key="item.id"
:repo="item"
/>
......
<script>
import { mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import tableRegistry from './table_registry.vue';
import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '../../vue_shared/components/icon.vue';
import TableRegistry from './table_registry.vue';
import { errorMessages, errorMessagesTypes } from '../constants';
import { __ } from '../../locale';
export default {
name: 'CollapsibeContainerRegisty',
components: {
clipboardButton,
tableRegistry,
ClipboardButton,
TableRegistry,
GlLoadingIcon,
GlButton,
Icon,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
props: {
repo: {
......@@ -29,30 +31,30 @@ export default {
isOpen: false,
};
},
computed: {
iconName() {
return this.isOpen ? 'angle-up' : 'angle-right';
},
},
methods: {
...mapActions(['fetchRepos', 'fetchList', 'deleteRepo']),
toggleRepo() {
this.isOpen = !this.isOpen;
if (this.isOpen) {
this.fetchList({ repo: this.repo }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY),
);
this.fetchList({ repo: this.repo });
}
},
handleDeleteRepository() {
this.deleteRepo(this.repo)
.then(() => {
Flash(__('This container registry has been scheduled for deletion.'), 'notice');
createFlash(__('This container registry has been scheduled for deletion.'), 'notice');
this.fetchRepos();
})
.catch(() => this.showError(errorMessagesTypes.DELETE_REPO));
},
showError(message) {
Flash(errorMessages[message]);
createFlash(errorMessages[message]);
},
},
};
......@@ -61,18 +63,9 @@ export default {
<template>
<div class="container-image">
<div class="container-image-head">
<button type="button" class="js-toggle-repo btn-link" @click="toggleRepo">
<i
:class="{
'fa-chevron-right': !isOpen,
'fa-chevron-up': isOpen,
}"
class="fa"
aria-hidden="true"
>
</i>
{{ repo.name }}
</button>
<gl-button class="js-toggle-repo btn-link align-baseline" @click="toggleRepo">
<icon :name="iconName" /> {{ repo.name }}
</gl-button>
<clipboard-button
v-if="repo.location"
......@@ -82,17 +75,17 @@ export default {
/>
<div class="controls d-none d-sm-block float-right">
<button
<gl-button
v-if="repo.canDelete"
v-tooltip
v-gl-tooltip
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
type="button"
class="js-remove-repo btn btn-danger"
class="js-remove-repo"
variant="danger"
@click="handleDeleteRepository"
>
<i class="fa fa-trash" aria-hidden="true"> </i>
</button>
<icon name="remove" />
</gl-button>
</div>
</div>
......
<script>
import { mapActions } from 'vuex';
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { n__ } from '../../locale';
import Flash from '../../flash';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import tooltip from '../../vue_shared/directives/tooltip';
import createFlash from '../../flash';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import TablePagination from '../../vue_shared/components/table_pagination.vue';
import Icon from '../../vue_shared/components/icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import { errorMessages, errorMessagesTypes } from '../constants';
import { numberToHumanSize } from '../../lib/utils/number_utils';
export default {
components: {
clipboardButton,
tablePagination,
ClipboardButton,
TablePagination,
GlButton,
Icon,
},
directives: {
tooltip,
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
......@@ -31,29 +34,24 @@ export default {
},
methods: {
...mapActions(['fetchList', 'deleteRegistry']),
layers(item) {
return item.layers ? n__('%d layer', '%d layers', item.layers) : '';
},
formatSize(size) {
return numberToHumanSize(size);
},
handleDeleteRegistry(registry) {
this.deleteRegistry(registry)
.then(() => this.fetchList({ repo: this.repo }))
.catch(() => this.showError(errorMessagesTypes.DELETE_REGISTRY));
},
onPageChange(pageNumber) {
this.fetchList({ repo: this.repo, page: pageNumber }).catch(() =>
this.showError(errorMessagesTypes.FETCH_REGISTRY),
);
},
showError(message) {
Flash(errorMessages[message]);
createFlash(errorMessages[message]);
},
},
};
......@@ -71,10 +69,9 @@ export default {
</tr>
</thead>
<tbody>
<tr v-for="(item, i) in repo.list" :key="i">
<tr v-for="item in repo.list" :key="item.tag">
<td>
{{ item.tag }}
<clipboard-button
v-if="item.location"
:title="item.location"
......@@ -83,37 +80,34 @@ export default {
/>
</td>
<td>
<span v-tooltip :title="item.revision" data-placement="bottom">
{{ item.shortRevision }}
</span>
<span v-gl-tooltip.bottom :title="item.revision">{{ item.shortRevision }}</span>
</td>
<td>
{{ formatSize(item.size) }}
<template v-if="item.size && item.layers">
&middot;
</template>
<template v-if="item.size && item.layers"
>&middot;</template
>
{{ layers(item) }}
</td>
<td>
<span v-tooltip :title="tooltipTitle(item.createdAt)" data-placement="bottom">
{{ timeFormated(item.createdAt) }}
</span>
<span v-gl-tooltip.bottom :title="tooltipTitle(item.createdAt)">{{
timeFormated(item.createdAt)
}}</span>
</td>
<td class="content">
<button
<gl-button
v-if="item.canDelete"
v-tooltip
v-gl-tooltip
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
type="button"
class="js-delete-registry btn btn-danger d-none d-sm-block float-right"
data-container="body"
variant="danger"
class="js-delete-registry d-none d-sm-block float-right"
@click="handleDeleteRegistry(item);"
>
<i class="fa fa-trash" aria-hidden="true"> </i>
</button>
<icon name="remove" />
</gl-button>
</td>
</tr>
</tbody>
......
import Vue from 'vue';
import VueResource from 'vue-resource';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as types from './mutation_types';
Vue.use(VueResource);
import { errorMessages, errorMessagesTypes } from '../constants';
export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING);
return Vue.http
return axios
.get(state.endpoint)
.then(res => res.json())
.then(response => {
.then(({ data }) => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, data);
})
.catch(() => {
commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response);
createFlash(errorMessages[errorMessagesTypes.FETCH_REPOS]);
});
};
export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
const { headers } = response;
return axios
.get(repo.tagsPath, { params: { page } })
.then(response => {
const { headers, data } = response;
return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
commit(types.SET_REGISTRY_LIST, { repo, resp: data, headers });
})
.catch(() => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
createFlash(errorMessages[errorMessagesTypes.FETCH_REGISTRY]);
});
});
};
// eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
export const deleteRepo = ({ commit }, repo) => axios.delete(repo.destroyPath);
// eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const deleteRegistry = ({ commit }, image) => axios.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_MAIN_LOADING);
......
......@@ -3,36 +3,12 @@ import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
},
state: createState(),
actions,
getters,
mutations,
......
......@@ -48,6 +48,7 @@ export default {
[types.TOGGLE_REGISTRY_LIST_LOADING](state, list) {
const listToUpdate = state.repos.find(el => el.id === list.id);
listToUpdate.isLoading = !listToUpdate.isLoading;
},
};
export default () => ({
isLoading: false,
endpoint: '', // initial endpoint to fetch the repos list
/**
* Each object in `repos` has the following strucure:
* {
* name: String,
* isLoading: Boolean,
* tagsPath: String // endpoint to request the list
* destroyPath: String // endpoit to delete the repo
* list: Array // List of the registry images
* }
*
* Each registry image inside `list` has the following structure:
* {
* tag: String,
* revision: String
* shortRevision: String
* size: Number
* layers: Number
* createdAt: String
* destroyPath: String // endpoit to delete each image
* }
*/
repos: [],
});
......@@ -176,9 +176,9 @@
display: block;
font-weight: $gl-font-weight-normal;
position: relative;
padding: 8px 16px;
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
color: $gl-text-color;
line-height: normal;
line-height: $gl-btn-line-height;
white-space: normal;
overflow: hidden;
text-align: left;
......@@ -319,8 +319,8 @@
.dropdown-header {
color: $gl-text-color-secondary;
font-size: 13px;
line-height: 22px;
padding: 8px 16px;
line-height: $gl-line-height;
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
}
&.capitalize-header .dropdown-header {
......@@ -329,13 +329,8 @@
.dropdown-bold-header {
font-weight: $gl-font-weight-bold;
line-height: 22px;
padding: 0 16px;
}
.separator + .dropdown-header,
.separator + .dropdown-bold-header {
padding-top: 10px;
line-height: $gl-line-height;
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
}
.unclickable {
......
......@@ -127,12 +127,6 @@
}
}
li.dropdown-bold-header {
color: $gl-text-color-secondary;
font-size: 12px;
padding: 0 16px;
}
.navbar-collapse {
flex: 0 0 auto;
border-top: 0;
......@@ -541,7 +535,7 @@
left: auto;
li.current-user {
padding: 5px 18px;
padding: $dropdown-item-padding-y $dropdown-item-padding-x;
.user-name {
display: block;
......
......@@ -21,6 +21,8 @@ $danger: $red-500;
$zindex-modal-backdrop: 1040;
$nav-divider-margin-y: ($grid-size / 2);
$dropdown-divider-bg: $theme-gray-200;
$dropdown-item-padding-y: 8px;
$dropdown-item-padding-x: 12px;
$popover-max-width: 300px;
$popover-border-width: 1px;
$popover-border-color: $border-color;
......
......@@ -101,8 +101,6 @@ input[type='checkbox']:hover {
.dropdown-header {
// Necessary because glDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
// .dropdown-menu li has 1px side padding
padding: $gl-padding-8 17px;
color: $gl-text-color;
font-size: $gl-font-size;
line-height: 16px;
......
......@@ -181,15 +181,15 @@ class Clusters::ClustersController < Clusters::BaseController
end
def gcp_cluster
@gcp_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_provider_gcp
end.present(current_user: current_user)
cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_provider_gcp
@gcp_cluster = cluster.present(current_user: current_user)
end
def user_cluster
@user_cluster = ::Clusters::Cluster.new.tap do |cluster|
cluster.build_platform_kubernetes
end.present(current_user: current_user)
cluster = Clusters::BuildService.new(clusterable.subject).execute
cluster.build_platform_kubernetes
@user_cluster = cluster.present(current_user: current_user)
end
def validate_gcp_token
......
......@@ -26,4 +26,10 @@ module RendersCommits
commits
end
def valid_ref?(ref_name)
return true unless ref_name.present?
Gitlab::GitRefValidator.validate(ref_name)
end
end
......@@ -11,6 +11,7 @@ class Projects::CommitsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root
before_action :authorize_download_code!
before_action :validate_ref!, except: :commits_root
before_action :set_commits, except: :commits_root
def commits_root
......@@ -54,6 +55,10 @@ class Projects::CommitsController < Projects::ApplicationController
private
def validate_ref!
render_404 unless valid_ref?(@ref)
end
def set_commits
render_404 unless @path.empty? || request.format == :atom || @repository.blob_at(@commit.id, @path) || @repository.tree(@commit.id, @path).entries.present?
@limit, @offset = (params[:limit] || 40).to_i, (params[:offset] || 0).to_i
......
......@@ -65,12 +65,6 @@ class Projects::CompareController < Projects::ApplicationController
private
def valid_ref?(ref_name)
return true unless ref_name.present?
Gitlab::GitRefValidator.validate(ref_name)
end
def validate_refs!
valid = [head_ref, start_ref].map { |ref| valid_ref?(ref) }
......
......@@ -38,6 +38,11 @@ module SelectsHelper
classes << 'ajax-groups-select'
classes << 'multiselect' if opts[:multiple]
# EE requires this line to be present, but there is no easy way of injecting
# this into EE without causing merge conflicts. Given this line is very
# simple and not really EE specific on its own, we just include it in CE.
classes << 'multiselect' if opts[:multiple]
opts[:class] = classes.join(' ')
select2_tag(id, opts)
......
......@@ -168,7 +168,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = [message_id(model)]
headers[:subject]&.prepend('Re: ')
headers[:subject] = "Re: #{headers[:subject]}" if headers[:subject]
mail_thread(model, headers)
end
......
# frozen_string_literal: true
module Clusters
class BuildService
def initialize(subject)
@subject = subject
end
def execute
::Clusters::Cluster.new.tap do |cluster|
case @subject
when ::Project
cluster.cluster_type = :project_type
when ::Group
cluster.cluster_type = :group_type
else
raise NotImplementedError
end
end
end
end
end
---
title: Fix a frozen string error in app/mailers/notify.rb
merge_request: 23683
author:
type: fixed
---
title: Only allow strings in URL::Sanitizer.valid?
merge_request: 23675
author:
type: fixed
---
title: Adjust dropdown item and header padding to comply with design specs
merge_request: 23552
author:
type: changed
......@@ -14,6 +14,7 @@ module Gitlab
def self.valid?(url)
return false unless url.present?
return false unless url.is_a?(String)
uri = Addressable::URI.parse(url.strip)
......
......@@ -71,6 +71,13 @@ module QA
options.add_argument("disable-gpu")
end
# Use the same profile on QA runs if CHROME_REUSE_PROFILE is true.
# Useful to speed up local QA.
if QA::Runtime::Env.reuse_chrome_profile?
qa_profile_dir = ::File.expand_path('../../tmp/qa-profile', __dir__)
options.add_argument("user-data-dir=#{qa_profile_dir}")
end
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab-ee/issues/4252
options.add_argument("disable-dev-shm-usage") if QA::Runtime::Env.running_in_ci?
......
......@@ -32,6 +32,11 @@ module QA
enabled?(ENV['CHROME_HEADLESS'])
end
# set to 'true' to have Chrome use a fixed profile directory
def reuse_chrome_profile?
enabled?(ENV['CHROME_REUSE_PROFILE'], default: false)
end
def accept_insecure_certs?
enabled?(ENV['ACCEPT_INSECURE_CERTS'])
end
......
......@@ -53,6 +53,12 @@ describe Projects::CommitsController do
it { is_expected.to respond_with(:not_found) }
end
context "branch with invalid format, valid file" do
let(:id) { 'branch with space/README.md' }
it { is_expected.to respond_with(:not_found) }
end
end
context "when the ref name ends in .atom" do
......@@ -94,6 +100,30 @@ describe Projects::CommitsController do
end
end
end
describe "GET /commits/:id/signatures" do
render_views
before do
get(:signatures,
namespace_id: project.namespace,
project_id: project,
id: id,
format: :json)
end
context "valid branch" do
let(:id) { 'master' }
it { is_expected.to respond_with(:success) }
end
context "invalid branch format" do
let(:id) { 'some branch' }
it { is_expected.to respond_with(:not_found) }
end
end
end
context 'token authentication' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'User Cluster', :js do
include GoogleApi::CloudPlatformHelpers
let(:group) { create(:group) }
let(:user) { create(:user) }
before do
group.add_maintainer(user)
gitlab_sign_in(user)
allow(Groups::ClustersController).to receive(:STATUS_POLLING_INTERVAL) { 100 }
allow_any_instance_of(Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService).to receive(:execute)
end
context 'when user does not have a cluster and visits cluster index page' do
before do
visit group_clusters_path(group)
click_link 'Add Kubernetes cluster'
click_link 'Add existing cluster'
end
context 'when user filled form with valid parameters' do
shared_examples 'valid cluster user form' do
it 'user sees a cluster details page' do
subject
expect(page).to have_content('Kubernetes cluster integration')
expect(page.find_field('cluster[name]').value).to eq('dev-cluster')
expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value)
.to have_content('http://example.com')
expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value)
.to have_content('my-token')
end
end
before do
fill_in 'cluster_name', with: 'dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token'
end
subject { click_button 'Add Kubernetes cluster' }
it_behaves_like 'valid cluster user form'
context 'RBAC is enabled for the cluster' do
before do
check 'cluster_platform_kubernetes_attributes_authorization_type'
end
it_behaves_like 'valid cluster user form'
it 'user sees a cluster details page with RBAC enabled' do
subject
expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked
end
end
end
context 'when user filled form with invalid parameters' do
before do
click_button 'Add Kubernetes cluster'
end
it 'user sees a validation error' do
expect(page).to have_css('#error_explanation')
end
end
end
context 'when user does have a cluster and visits cluster page' do
let(:cluster) { create(:cluster, :provided_by_user, cluster_type: :group_type, groups: [group]) }
before do
visit group_cluster_path(group, cluster)
end
it 'user sees a cluster details page' do
expect(page).to have_button('Save changes')
end
context 'when user disables the cluster' do
before do
page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
page.within('#cluster-integration') { click_button 'Save changes' }
end
it 'user sees the successful message' do
expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
end
context 'when user changes cluster parameters' do
before do
fill_in 'cluster_name', with: 'my-dev-cluster'
fill_in 'cluster_platform_kubernetes_attributes_token', with: 'new-token'
page.within('#js-cluster-details') { click_button 'Save changes' }
end
it 'user sees the successful message' do
expect(page).to have_content('Kubernetes cluster was successfully updated.')
expect(cluster.reload.name).to eq('my-dev-cluster')
expect(cluster.reload.platform_kubernetes.token).to eq('new-token')
end
end
context 'when user destroy the cluster' do
before do
page.accept_confirm do
click_link 'Remove integration'
end
end
it 'user sees creation form with the successful message' do
expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
expect(page).to have_link('Add Kubernetes cluster')
end
end
end
end
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { TEST_HOST } from 'spec/test_constants';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
const Component = Vue.extend(registry);
let vm;
let Component;
let mock;
beforeEach(() => {
Component = Vue.extend(registry);
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('with data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, reposServerResponse);
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render a list of repos', done => {
......@@ -64,9 +57,9 @@ describe('Registry List', () => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.js-toggle-repo i').className).toEqual(
'fa fa-chevron-up',
);
expect(
vm.$el.querySelector('.js-toggle-repo use').getAttribute('xlink:href'),
).toContain('angle-up');
done();
});
});
......@@ -76,21 +69,10 @@ describe('Registry List', () => {
});
describe('without data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify([]), {
status: 200,
}),
);
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render empty message', done => {
......@@ -109,21 +91,10 @@ describe('Registry List', () => {
});
describe('while loading data', () => {
const interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
vm = mountComponent(Component, { endpoint: 'foo' });
});
mock.onGet(`${TEST_HOST}/foo`).replyOnce(200, []);
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
vm = mountComponent(Component, { endpoint: `${TEST_HOST}/foo` });
});
it('should render a loading spinner', done => {
......
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Vue from 'vue';
import collapsibleComponent from '~/registry/components/collapsible_container.vue';
import store from '~/registry/stores';
import { repoPropsData } from '../mock_data';
import * as types from '~/registry/stores/mutation_types';
import { repoPropsData, registryServerResponse, reposServerResponse } from '../mock_data';
describe('collapsible registry container', () => {
let vm;
let Component;
let mock;
const Component = Vue.extend(collapsibleComponent);
beforeEach(() => {
Component = Vue.extend(collapsibleComponent);
mock = new MockAdapter(axios);
mock.onGet(repoPropsData.tagsPath).replyOnce(200, registryServerResponse, {});
store.commit(types.SET_REPOS_LIST, reposServerResponse);
vm = new Component({
store,
propsData: {
......@@ -18,24 +28,23 @@ describe('collapsible registry container', () => {
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('toggle', () => {
it('should be closed by default', () => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
'fa fa-chevron-right',
);
expect(vm.iconName).toEqual('angle-right');
});
it('should be open when user clicks on closed repo', done => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBeDefined();
expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
'fa fa-chevron-up',
);
expect(vm.$el.querySelector('.container-image-tags')).not.toBeNull();
expect(vm.iconName).toEqual('angle-up');
done();
});
});
......@@ -45,12 +54,12 @@ describe('collapsible registry container', () => {
Vue.nextTick(() => {
vm.$el.querySelector('.js-toggle-repo').click();
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.$el.querySelector('.container-image-head i').className).toEqual(
'fa fa-chevron-right',
);
done();
setTimeout(() => {
Vue.nextTick(() => {
expect(vm.$el.querySelector('.container-image-tags')).toBe(null);
expect(vm.iconName).toEqual('angle-right');
done();
});
});
});
});
......@@ -58,7 +67,7 @@ describe('collapsible registry container', () => {
describe('delete repo', () => {
it('should be possible to delete a repo', () => {
expect(vm.$el.querySelector('.js-remove-repo')).toBeDefined();
expect(vm.$el.querySelector('.js-remove-repo')).not.toBeNull();
});
});
});
import Vue from 'vue';
import VueResource from 'vue-resource';
import _ from 'underscore';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as actions from '~/registry/stores/actions';
import * as types from '~/registry/stores/mutation_types';
import state from '~/registry/stores/state';
import { TEST_HOST } from 'spec/test_constants';
import testAction from '../../helpers/vuex_action_helper';
import {
defaultState,
reposServerResponse,
registryServerResponse,
parsedReposServerResponse,
} from '../mock_data';
Vue.use(VueResource);
describe('Actions Registry Store', () => {
let interceptor;
let mockedState;
let mock;
beforeEach(() => {
mockedState = defaultState;
mockedState = state();
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
describe('server requests', () => {
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
});
afterEach(() => {
mock.restore();
});
describe('server requests', () => {
describe('fetchRepos', () => {
beforeEach(() => {
interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(reposServerResponse), {
status: 200,
}),
);
};
Vue.http.interceptors.push(interceptor);
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, reposServerResponse, {});
});
it('should set receveived repos', done => {
......@@ -56,23 +48,15 @@ describe('Actions Registry Store', () => {
});
describe('fetchList', () => {
let repo;
beforeEach(() => {
interceptor = (request, next) => {
next(
request.respondWith(JSON.stringify(registryServerResponse), {
status: 200,
}),
);
};
mockedState.repos = parsedReposServerResponse;
[, repo] = mockedState.repos;
Vue.http.interceptors.push(interceptor);
mock.onGet(repo.tagsPath).replyOnce(200, registryServerResponse, {});
});
it('should set received list', done => {
mockedState.repos = parsedReposServerResponse;
const repo = mockedState.repos[1];
testAction(
actions.fetchList,
{ repo },
......
......@@ -41,6 +41,7 @@ describe Gitlab::UrlSanitizer do
false | '123://invalid:url'
false | 'valid@project:url.git'
false | 'valid:pass@project:url.git'
false | %w(test array)
true | 'ssh://example.com'
true | 'ssh://:@example.com'
true | 'ssh://foo@example.com'
......
......@@ -28,8 +28,8 @@ describe Notify do
end
def have_referable_subject(referable, reply: false)
prefix = referable.project ? "#{referable.project.name} | " : ''
prefix.prepend('Re: ') if reply
prefix = (referable.project ? "#{referable.project.name} | " : '').freeze
prefix = "Re: #{prefix}" if reply
suffix = "#{referable.title} (#{referable.to_reference})"
......
# frozen_string_literal: true
require 'spec_helper'
describe Clusters::BuildService do
describe '#execute' do
subject { described_class.new(cluster_subject).execute }
describe 'when cluster subject is a project' do
let(:cluster_subject) { build(:project) }
it 'sets the cluster_type to project_type' do
is_expected.to be_project_type
end
end
describe 'when cluster subject is a group' do
let(:cluster_subject) { build(:group) }
it 'sets the cluster_type to group_type' do
is_expected.to be_group_type
end
end
end
end
......@@ -6,6 +6,13 @@ module JavaScriptFixturesHelpers
FIXTURE_PATHS = %w[spec/javascripts/fixtures ee/spec/javascripts/fixtures].freeze
def self.included(base)
base.around do |example|
# pick an arbitrary date from the past, so tests are not time dependent
Timecop.freeze(Time.utc(2015, 7, 3, 10)) { example.run }
end
end
# Public: Removes all fixture files from given directory
#
# directory_name - directory of the fixtures (relative to FIXTURE_PATHS)
......
require 'spec_helper'
IDENTIFIER = %r{\h+/\S+}
describe NamespaceFileUploader do
let(:group) { build_stubbed(:group) }
let(:uploader) { described_class.new(group) }
let(:upload) { create(:upload, :namespace_upload, model: group) }
let(:identifier) { %r{\h+/\S+} }
subject { uploader }
it_behaves_like 'builds correct paths',
store_dir: %r[uploads/-/system/namespace/\d+],
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{IDENTIFIER}]
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[uploads/-/system/namespace/\d+],
upload_path: identifier,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/namespace/\d+/#{identifier}]
}
end
end
context "object_store is REMOTE" do
before do
......@@ -21,9 +25,14 @@ describe NamespaceFileUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
store_dir: %r[namespace/\d+/\h+],
upload_path: IDENTIFIER
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[namespace/\d+/\h+],
upload_path: identifier
}
end
end
end
context '.base_dir' do
......
require 'spec_helper'
IDENTIFIER = %r{\h+/\S+}
describe PersonalFileUploader do
let(:model) { create(:personal_snippet) }
let(:uploader) { described_class.new(model) }
let(:upload) { create(:upload, :personal_snippet_upload) }
let(:identifier) { %r{\h+/\S+} }
subject { uploader }
it_behaves_like 'builds correct paths',
store_dir: %r[uploads/-/system/personal_snippet/\d+],
upload_path: IDENTIFIER,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{IDENTIFIER}]
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[uploads/-/system/personal_snippet/\d+],
upload_path: identifier,
absolute_path: %r[#{CarrierWave.root}/uploads/-/system/personal_snippet/\d+/#{identifier}]
}
end
end
context "object_store is REMOTE" do
before do
......@@ -21,9 +25,14 @@ describe PersonalFileUploader do
include_context 'with storage', described_class::Store::REMOTE
it_behaves_like 'builds correct paths',
store_dir: %r[\d+/\h+],
upload_path: IDENTIFIER
it_behaves_like 'builds correct paths' do
let(:patterns) do
{
store_dir: %r[\d+/\h+],
upload_path: identifier
}
end
end
end
describe '#to_h' do
......
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