Commit e3bdfa1a authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent c1a50b81
......@@ -259,7 +259,7 @@ export default class Clusters {
eventHub.$on('installApplication', this.installApplication);
eventHub.$on('updateApplication', data => this.updateApplication(data));
eventHub.$on('saveKnativeDomain', data => this.saveKnativeDomain(data));
eventHub.$on('setKnativeHostname', data => this.setKnativeHostname(data));
eventHub.$on('setKnativeDomain', data => this.setKnativeDomain(data));
eventHub.$on('uninstallApplication', data => this.uninstallApplication(data));
eventHub.$on('setCrossplaneProviderStack', data => this.setCrossplaneProviderStack(data));
eventHub.$on('setIngressModSecurityEnabled', data => this.setIngressModSecurityEnabled(data));
......@@ -275,7 +275,7 @@ export default class Clusters {
eventHub.$off('installApplication', this.installApplication);
eventHub.$off('updateApplication', this.updateApplication);
eventHub.$off('saveKnativeDomain');
eventHub.$off('setKnativeHostname');
eventHub.$off('setKnativeDomain');
eventHub.$off('setCrossplaneProviderStack');
eventHub.$off('uninstallApplication');
eventHub.$off('setIngressModSecurityEnabled');
......@@ -521,10 +521,10 @@ export default class Clusters {
});
}
setKnativeHostname(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'isEditingHostName', true);
this.store.updateAppProperty(appId, 'hostname', data.hostname);
setKnativeDomain({ id: appId, domain, domainId }) {
this.store.updateAppProperty(appId, 'isEditingDomain', true);
this.store.updateAppProperty(appId, 'hostname', domain);
this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
}
setCrossplaneProviderStack(data) {
......
......@@ -240,16 +240,20 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
this.helmInstallIllustration = helmInstallIllustration;
},
methods: {
saveKnativeDomain(hostname) {
saveKnativeDomain() {
eventHub.$emit('saveKnativeDomain', {
id: 'knative',
params: { hostname },
params: {
hostname: this.applications.knative.hostname,
pages_domain_id: this.applications.knative.pagesDomain?.id,
},
});
},
setKnativeHostname(hostname) {
eventHub.$emit('setKnativeHostname', {
setKnativeDomain({ domainId, domain }) {
eventHub.$emit('setKnativeDomain', {
id: 'knative',
hostname,
domainId,
domain,
});
},
setCrossplaneProviderStack(stack) {
......@@ -591,7 +595,10 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:request-reason="applications.knative.requestReason"
:installed="applications.knative.installed"
:install-failed="applications.knative.installFailed"
:install-application-request-params="{ hostname: applications.knative.hostname }"
:install-application-request-params="{
hostname: applications.knative.hostname,
pages_domain_id: applications.knative.pagesDomain && applications.knative.pagesDomain.id,
}"
:installed-via="installedVia"
:uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful"
......@@ -628,7 +635,7 @@ Crossplane runs inside your Kubernetes cluster and supports secure connectivity
:knative="knative"
:ingress-dns-help-path="ingressDnsHelpPath"
@save="saveKnativeDomain"
@set="setKnativeHostname"
@set="setKnativeDomain"
/>
</div>
</application-row>
......
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import {
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlLoadingIcon,
GlSearchBoxByType,
GlSprintf,
} from '@gitlab/ui';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { __, s__ } from '~/locale';
......@@ -13,6 +20,11 @@ export default {
LoadingButton,
ClipboardButton,
GlLoadingIcon,
GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlSearchBoxByType,
GlSprintf,
},
props: {
knative: {
......@@ -25,6 +37,11 @@ export default {
required: false,
},
},
data() {
return {
searchQuery: '',
};
},
computed: {
saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.knative.status);
......@@ -49,9 +66,22 @@ export default {
return this.knative.hostname;
},
set(hostname) {
this.$emit('set', hostname);
this.selectCustomDomain(hostname);
},
},
domainDropdownText() {
return this.knativeHostname || s__('ClusterIntegration|Select existing domain or use new');
},
availableDomains() {
return this.knative.availableDomains || [];
},
filteredDomains() {
const query = this.searchQuery.toLowerCase();
return this.availableDomains.filter(({ domain }) => domain.toLowerCase().includes(query));
},
showDomainsDropdown() {
return this.availableDomains.length > 0;
},
},
watch: {
knativeUpdateSuccessful(updateSuccessful) {
......@@ -60,6 +90,14 @@ export default {
}
},
},
methods: {
selectDomain({ id, domain }) {
this.$emit('set', { domain, domainId: id });
},
selectCustomDomain(domain) {
this.$emit('set', { domain, domainId: null });
},
},
};
</script>
......@@ -72,22 +110,55 @@ export default {
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
</div>
<template>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
class="form-group col-sm-12 mb-0"
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<gl-dropdown
v-if="showDomainsDropdown"
:text="domainDropdownText"
toggle-class="dropdown-menu-toggle"
class="w-100 mb-2"
>
<label for="knative-domainname">
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
<gl-search-box-by-type
v-model.trim="searchQuery"
:placeholder="s__('ClusterIntegration|Search domains')"
class="m-2"
/>
</div>
</template>
<gl-dropdown-item
v-for="domain in filteredDomains"
:key="domain.id"
@click="selectDomain(domain)"
>
<span class="ml-1">{{ domain.domain }}</span>
</gl-dropdown-item>
<template v-if="searchQuery">
<gl-dropdown-divider />
<gl-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
<span class="ml-1">
<gl-sprintf :message="s__('ClusterIntegration|Use %{query}')">
<template #query>
<code>{{ searchQuery }}</code>
</template>
</gl-sprintf>
</span>
</gl-dropdown-item>
</template>
</gl-dropdown>
<input
v-else
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/>
</div>
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
......@@ -144,7 +215,7 @@ export default {
:loading="saving"
:disabled="saveButtonDisabled"
:label="saveButtonLabel"
@click="$emit('save', knativeHostname)"
@click="$emit('save')"
/>
</template>
</div>
......
......@@ -93,7 +93,7 @@ export default class ClusterStore {
...applicationInitialState,
title: s__('ClusterIntegration|Knative'),
hostname: null,
isEditingHostName: false,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
updateSuccessful: false,
......@@ -234,7 +234,12 @@ export default class ClusterStore {
'jupyter',
);
} else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
if (serverAppEntry.available_domains) {
this.state.applications.knative.availableDomains = serverAppEntry.available_domains;
}
if (!this.state.applications.knative.isEditingDomain) {
this.state.applications.knative.pagesDomain =
serverAppEntry.pages_domain || this.state.applications.knative.pagesDomain;
this.state.applications.knative.hostname =
serverAppEntry.hostname || this.state.applications.knative.hostname;
}
......
......@@ -318,3 +318,11 @@ export const setUrlParams = (params, url = window.location.href, clearParams = f
export function urlIsDifferent(url, compare = String(window.location)) {
return url !== compare;
}
export function getHTTPProtocol(url) {
if (!url) {
return window.location.protocol.slice(0, -1);
}
const protocol = url.split(':');
return protocol.length > 1 ? protocol[0] : undefined;
}
<script>
import {
GlNewDropdown,
GlNewDropdownHeader,
GlFormInputGroup,
GlNewButton,
GlIcon,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { getHTTPProtocol } from '~/lib/utils/url_utility';
export default {
components: {
GlNewDropdown,
GlNewDropdownHeader,
GlFormInputGroup,
GlNewButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
sshLink: {
type: String,
required: false,
default: '',
},
httpLink: {
type: String,
required: false,
default: '',
},
},
computed: {
httpLabel() {
const protocol = this.httpLink ? getHTTPProtocol(this.httpLink)?.toUpperCase() : '';
return sprintf(__('Clone with %{protocol}'), { protocol });
},
},
labels: {
defaultLabel: __('Clone'),
ssh: __('Clone with SSH'),
},
copyURLTooltip: __('Copy URL'),
};
</script>
<template>
<gl-new-dropdown :text="$options.labels.defaultLabel" category="primary" variant="info">
<div class="pb-2 mx-1">
<template v-if="sshLink">
<gl-new-dropdown-header>{{ $options.labels.ssh }}</gl-new-dropdown-header>
<div class="mx-3">
<gl-form-input-group :value="sshLink" readonly select-on-click>
<template #append>
<gl-new-button
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="sshLink"
>
<gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
</gl-new-button>
</template>
</gl-form-input-group>
</div>
</template>
<template v-if="httpLink">
<gl-new-dropdown-header>{{ httpLabel }}</gl-new-dropdown-header>
<div class="mx-3">
<gl-form-input-group :value="httpLink" readonly select-on-click>
<template #append>
<gl-new-button
v-gl-tooltip.hover
:title="$options.copyURLTooltip"
:data-clipboard-text="httpLink"
>
<gl-icon name="copy-to-clipboard" :title="$options.copyURLTooltip" />
</gl-new-button>
</template>
</gl-form-input-group>
</div>
</template>
</div>
</gl-new-dropdown>
</template>
......@@ -5,6 +5,12 @@ class Admin::IntegrationsController < Admin::ApplicationController
private
def find_or_initialize_integration(name)
if name.in?(Service.available_services_names)
"#{name}_service".camelize.constantize.find_or_initialize_by(instance: true) # rubocop:disable CodeReuse/ActiveRecord
end
end
def integrations_enabled?
Feature.enabled?(:instance_level_integrations)
end
......
......@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def cluster_application_params
params.permit(:application, :hostname, :email, :stack, :modsecurity_enabled, :modsecurity_mode)
params.permit(:application, :hostname, :pages_domain_id, :email, :stack, :modsecurity_enabled, :modsecurity_mode)
end
def cluster_application_destroy_params
......
......@@ -37,11 +37,7 @@ module IntegrationsActions
end
def test
if integration.can_test?
render json: service_test_response, status: :ok
else
render json: {}, status: :not_found
end
render json: {}, status: :ok
end
private
......@@ -50,17 +46,11 @@ module IntegrationsActions
false
end
# TODO: Use actual integrations on the group / instance level
# To be completed in https://gitlab.com/groups/gitlab-org/-/epics/2430
def project
Project.first
end
def integration
# Using instance variable `@service` still required as it's used in ServiceParams
# and app/views/shared/_service_settings.html.haml. Should be removed once
# those 2 are refactored to use `@integration`.
@integration = @service ||= project.find_or_initialize_service(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
@integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
def success_message
......@@ -74,21 +64,4 @@ module IntegrationsActions
.as_json(only: integration.json_fields)
.merge(errors: integration.errors.as_json)
end
def service_test_response
unless integration.update(service_params[:service])
return { error: true, message: _('Validations failed.'), service_response: integration.errors.full_messages.join(','), test_failed: false }
end
data = integration.test_data(project, current_user)
outcome = integration.test(data)
unless outcome[:success]
return { error: true, message: _('Test failed.'), service_response: outcome[:result].to_s, test_failed: true }
end
{}
rescue Gitlab::HTTP::BlockedUrlError => e
{ error: true, message: _('Test failed.'), service_response: e.message, test_failed: true }
end
end
......@@ -9,6 +9,12 @@ module Groups
private
# TODO: Make this compatible with group-level integration
# https://gitlab.com/groups/gitlab-org/-/epics/2543
def find_or_initialize_integration(name)
Project.first.find_or_initialize_service(name)
end
def integrations_enabled?
Feature.enabled?(:group_level_integrations, group)
end
......
......@@ -201,8 +201,7 @@ class JiraService < IssueTrackerService
end
# Jira does not need test data.
# We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil)
def test_data(_, _)
nil
end
......@@ -221,7 +220,6 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
# Test settings by getting the project
jira_request { client.ServerInfo.all.attrs }
end
......
......@@ -53,7 +53,7 @@ class PipelinesEmailService < Service
end
def can_test?
project.ci_pipelines.any?
project&.ci_pipelines&.any?
end
def test_data(project, user)
......
......@@ -184,8 +184,10 @@ class Service < ApplicationRecord
{ success: result.present?, result: result }
end
# Disable test for instance-level services.
# https://gitlab.com/gitlab-org/gitlab/-/issues/213138
def can_test?
true
!instance?
end
# Provide convenient accessor methods
......
---
title: Consume remaining LinkLFsObjectsProjects jobs
merge_request: 27558
author:
type: other
---
title: Remove unnecessary index index_ci_builds_on_name_for_security_reports_values
merge_request: 28224
author:
type: performance
# frozen_string_literal: true
class RemoveIndexCiBuildsOnNameForSecurityReportsValues < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
INDEX_NAME = 'index_ci_builds_on_name_for_security_reports_values'
def up
remove_concurrent_index_by_name :ci_builds, INDEX_NAME
end
def down
add_concurrent_index :ci_builds,
:name,
name: INDEX_NAME,
where: "((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text]))"
end
end
# frozen_string_literal: true
class ConsumeRemainingLinkLfsObjectsProjectsJobs < ActiveRecord::Migration[6.0]
DOWNTIME = false
disable_ddl_transaction!
def up
Gitlab::BackgroundMigration.steal('LinkLfsObjectsProjects')
end
def down
# no-op as there is no need to do anything if this gets rolled back
end
end
......@@ -8672,8 +8672,6 @@ CREATE INDEX index_ci_builds_on_commit_id_and_type_and_ref ON public.ci_builds U
CREATE INDEX index_ci_builds_on_name_and_security_type_eq_ci_build ON public.ci_builds USING btree (name, id) WHERE (((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text])) AND ((type)::text = 'Ci::Build'::text));
CREATE INDEX index_ci_builds_on_name_for_security_reports_values ON public.ci_builds USING btree (name) WHERE ((name)::text = ANY (ARRAY[('container_scanning'::character varying)::text, ('dast'::character varying)::text, ('dependency_scanning'::character varying)::text, ('license_management'::character varying)::text, ('sast'::character varying)::text, ('license_scanning'::character varying)::text]));
CREATE INDEX index_ci_builds_on_project_id_and_id ON public.ci_builds USING btree (project_id, id);
CREATE INDEX index_ci_builds_on_project_id_and_name_and_ref ON public.ci_builds USING btree (project_id, name, ref) WHERE (((type)::text = 'Ci::Build'::text) AND ((status)::text = 'success'::text) AND ((retried = false) OR (retried IS NULL)));
......@@ -12902,6 +12900,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200318164448
20200318165448
20200318175008
20200319071702
20200319123041
20200319203901
20200320112455
......@@ -12927,6 +12926,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200326145443
20200330074719
20200330121000
20200330123739
20200330132913
20200331220930
\.
......
......@@ -4034,6 +4034,9 @@ msgstr ""
msgid "Clone with %{http_label}"
msgstr ""
msgid "Clone with %{protocol}"
msgstr ""
msgid "Clone with KRB5"
msgstr ""
......@@ -4691,6 +4694,9 @@ msgstr ""
msgid "ClusterIntegration|Search VPCs"
msgstr ""
msgid "ClusterIntegration|Search domains"
msgstr ""
msgid "ClusterIntegration|Search instance types"
msgstr ""
......@@ -4748,6 +4754,9 @@ msgstr ""
msgid "ClusterIntegration|Select a zone to choose a network"
msgstr ""
msgid "ClusterIntegration|Select existing domain or use new"
msgstr ""
msgid "ClusterIntegration|Select machine type"
msgstr ""
......@@ -4868,6 +4877,9 @@ msgstr ""
msgid "ClusterIntegration|Update failed. Please check the logs and try again."
msgstr ""
msgid "ClusterIntegration|Use %{query}"
msgstr ""
msgid "ClusterIntegration|Uses the Cloud Run, Istio, and HTTP Load Balancing addons for this cluster."
msgstr ""
......
......@@ -3,7 +3,6 @@
require 'spec_helper'
describe Admin::IntegrationsController do
let_it_be(:project) { create(:project) }
let(:admin) { create(:admin) }
before do
......@@ -34,7 +33,7 @@ describe Admin::IntegrationsController do
end
describe '#update' do
let(:integration) { create(:jira_service, project: project) }
let(:integration) { create(:jira_service, :instance) }
before do
put :update, params: { id: integration.class.to_param, service: { url: url } }
......@@ -52,34 +51,9 @@ describe Admin::IntegrationsController do
context 'invalid params' do
let(:url) { 'https://jira.localhost' }
it 'does not update the integration' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:edit)
expect(integration.reload.url).not_to eq(url)
end
end
end
describe '#test' do
context 'testable' do
let(:integration) { create(:jira_service, project: project) }
it 'returns ok' do
allow_any_instance_of(integration.class).to receive(:test) { { success: true } }
put :test, params: { id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'not testable' do
let(:integration) { create(:alerts_service, project: project) }
it 'returns not found' do
put :test, params: { id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:not_found)
it 'updates the integration' do
expect(response).to have_gitlab_http_status(:found)
expect(integration.reload.url).to eq(url)
end
end
end
......
......@@ -67,40 +67,11 @@ describe Groups::Settings::IntegrationsController do
end
context 'invalid params' do
let(:url) { 'ftp://jira.localhost' }
let(:url) { 'https://jira.localhost' }
it 'does not update the integration' do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:edit)
expect(integration.reload.url).not_to eq(url)
end
end
end
describe '#test' do
context 'testable' do
let(:integration) { create(:jira_service, project: project) }
before do
group.add_owner(user)
end
it 'returns ok' do
allow_any_instance_of(integration.class).to receive(:test) { { success: true } }
put :test, params: { group_id: group, id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:ok)
end
end
context 'not testable' do
let(:integration) { create(:alerts_service, project: project) }
it 'returns not found' do
put :test, params: { group_id: group, id: integration.class.to_param }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:found)
expect(integration.reload.url).to eq(url)
end
end
end
......
// `lodash/debounce` has a non-trivial implementation which can lead to
// [flaky spec errors][1]. This mock simply makes `debounce` calls synchronous.
//
// In the future we could enhance this by injecting some test values in
// the function passed to it. See [this issue][2] for more information.
//
// [1]: https://gitlab.com/gitlab-org/gitlab/-/issues/212532
// [2]: https://gitlab.com/gitlab-org/gitlab/-/issues/213378
// Further reference: https://github.com/facebook/jest/issues/3465
export default fn => fn;
......@@ -400,6 +400,10 @@ describe('Applications', () => {
});
describe('Knative application', () => {
const availableDomain = {
id: 4,
domain: 'newhostname.com',
};
const propsData = {
applications: {
...APPLICATIONS_MOCK_STATE,
......@@ -409,10 +413,11 @@ describe('Applications', () => {
status: 'installed',
externalIp: '1.1.1.1',
installed: true,
availableDomains: [availableDomain],
pagesDomain: null,
},
},
};
const newHostname = 'newhostname.com';
let wrapper;
let knativeDomainEditor;
......@@ -428,20 +433,44 @@ describe('Applications', () => {
});
it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
knativeDomainEditor.vm.$emit('save', newHostname);
propsData.applications.knative.hostname = availableDomain.domain;
propsData.applications.knative.pagesDomain = availableDomain;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: {
hostname: availableDomain.domain,
pages_domain_id: availableDomain.id,
},
});
});
it('emits saveKnativeDomain event when knative domain editor emits save event with custom domain', () => {
const newHostName = 'someothernewhostname.com';
propsData.applications.knative.hostname = newHostName;
propsData.applications.knative.pagesDomain = null;
knativeDomainEditor.vm.$emit('save');
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: { hostname: newHostname },
params: {
hostname: newHostName,
pages_domain_id: undefined,
},
});
});
it('emits setKnativeHostname event when knative domain editor emits change event', () => {
wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname);
wrapper.find(KnativeDomainEditor).vm.$emit('set', {
domain: availableDomain.domain,
domainId: availableDomain.id,
});
expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', {
expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeDomain', {
id: 'knative',
hostname: newHostname,
domain: availableDomain.domain,
domainId: availableDomain.id,
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem } from '@gitlab/ui';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
......@@ -80,7 +81,7 @@ describe('KnativeDomainEditor', () => {
it('triggers save event and pass current knative hostname', () => {
wrapper.find(LoadingButton).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]);
expect(wrapper.emitted('save').length).toEqual(1);
});
});
});
......@@ -104,14 +105,43 @@ describe('KnativeDomainEditor', () => {
describe('when knative domain name input changes', () => {
it('emits "set" event with updated domain name', () => {
createComponent({ knative });
const newDomain = {
id: 4,
domain: 'newhostname.com',
};
createComponent({ knative: { ...knative, availableDomains: [newDomain] } });
jest.spyOn(wrapper.vm, 'selectDomain');
wrapper.find(GlDropdownItem).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newDomain.domain,
domainId: newDomain.id,
},
]);
});
});
it('emits "set" event with updated custom domain name', () => {
const newHostname = 'newhostname.com';
createComponent({ knative });
jest.spyOn(wrapper.vm, 'selectCustomDomain');
wrapper.setData({ knativeHostname: newHostname });
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted('set')[0]).toEqual([newHostname]);
expect(wrapper.vm.selectCustomDomain).toHaveBeenCalledWith(newHostname);
expect(wrapper.emitted('set')[0]).toEqual([
{
domain: newHostname,
domainId: null,
},
]);
});
});
});
......
......@@ -140,7 +140,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[5].status_reason,
requestReason: null,
hostname: null,
isEditingHostName: false,
isEditingDomain: false,
externalIp: null,
externalHostname: null,
installed: false,
......
......@@ -9,8 +9,6 @@ import { branches } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('lodash/debounce', () => jest.fn);
describe('IDE branches search list', () => {
let wrapper;
const fetchBranchesMock = jest.fn();
......
......@@ -134,9 +134,7 @@ describe('IDE merge requests list', () => {
createComponent(defaultStateWithMergeRequests);
const input = findTokenedInput();
input.vm.$emit('input', 'something');
fetchMergeRequestsMock.mockClear();
jest.runAllTimers();
return wrapper.vm.$nextTick().then(() => {
expect(fetchMergeRequestsMock).toHaveBeenCalledWith(
expect.any(Object),
......
......@@ -485,4 +485,30 @@ describe('URL utility', () => {
);
});
});
describe('getHTTPProtocol', () => {
const httpProtocol = 'http:';
const httpsProtocol = 'https:';
it.each([[httpProtocol], [httpsProtocol]])(
'when no url passed, returns correct protocol for %i from window location',
protocol => {
setWindowLocation({
protocol,
});
expect(urlUtils.getHTTPProtocol()).toBe(protocol.slice(0, -1));
},
);
it.each`
url | expectation
${'not-a-url'} | ${undefined}
${'wss://example.com'} | ${'wss'}
${'https://foo.bar'} | ${'https'}
${'http://foo.bar'} | ${'http'}
${'http://foo.bar:8080'} | ${'http'}
`('returns correct protocol for $url', ({ url, expectation }) => {
expect(urlUtils.getHTTPProtocol(url)).toBe(expectation);
});
});
});
......@@ -20,7 +20,7 @@ afterEach(() =>
// give Promises a bit more time so they fail the right test
new Promise(setImmediate).then(() => {
// wait for pending setTimeout()s
jest.runAllTimers();
jest.runOnlyPendingTimers();
}),
);
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-new-dropdown-stub
category="primary"
headertext=""
size="medium"
text="Clone"
variant="info"
>
<div
class="pb-2 mx-1"
>
<gl-new-dropdown-header-stub>
Clone with SSH
</gl-new-dropdown-header-stub>
<div
class="mx-3"
>
<div
readonly="readonly"
>
<b-input-group-stub
tag="div"
>
<b-input-group-prepend-stub
tag="div"
>
<!---->
</b-input-group-prepend-stub>
<b-form-input-stub
class="gl-form-input"
debounce="0"
readonly="true"
type="text"
value="ssh://foo.bar"
/>
<b-input-group-append-stub
tag="div"
>
<gl-new-button-stub
category="tertiary"
data-clipboard-text="ssh://foo.bar"
icon=""
size="medium"
title="Copy URL"
variant="default"
>
<gl-icon-stub
name="copy-to-clipboard"
size="16"
title="Copy URL"
/>
</gl-new-button-stub>
</b-input-group-append-stub>
</b-input-group-stub>
</div>
</div>
<gl-new-dropdown-header-stub>
Clone with HTTP
</gl-new-dropdown-header-stub>
<div
class="mx-3"
>
<div
readonly="readonly"
>
<b-input-group-stub
tag="div"
>
<b-input-group-prepend-stub
tag="div"
>
<!---->
</b-input-group-prepend-stub>
<b-form-input-stub
class="gl-form-input"
debounce="0"
readonly="true"
type="text"
value="http://foo.bar"
/>
<b-input-group-append-stub
tag="div"
>
<gl-new-button-stub
category="tertiary"
data-clipboard-text="http://foo.bar"
icon=""
size="medium"
title="Copy URL"
variant="default"
>
<gl-icon-stub
name="copy-to-clipboard"
size="16"
title="Copy URL"
/>
</gl-new-button-stub>
</b-input-group-append-stub>
</b-input-group-stub>
</div>
</div>
</div>
</gl-new-dropdown-stub>
`;
import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
import { shallowMount } from '@vue/test-utils';
import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui';
describe('Clone Dropdown Button', () => {
let wrapper;
const sshLink = 'ssh://foo.bar';
const httpLink = 'http://foo.bar';
const httpsLink = 'https://foo.bar';
const defaultPropsData = {
sshLink,
httpLink,
};
const createComponent = (propsData = defaultPropsData) => {
wrapper = shallowMount(CloneDropdown, {
propsData,
stubs: {
'gl-form-input-group': GlFormInputGroup,
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('rendering', () => {
it('matches the snapshot', () => {
createComponent();
expect(wrapper.element).toMatchSnapshot();
});
it.each`
name | index | value
${'SSH'} | ${0} | ${sshLink}
${'HTTP'} | ${1} | ${httpLink}
`('renders correct link and a copy-button for $name', ({ index, value }) => {
createComponent();
const group = wrapper.findAll(GlFormInputGroup).at(index);
expect(group.props('value')).toBe(value);
expect(group.contains(GlFormInputGroup)).toBe(true);
});
it.each`
name | value
${'sshLink'} | ${sshLink}
${'httpLink'} | ${httpLink}
`('does not fail if only $name is set', ({ name, value }) => {
createComponent({ [name]: value });
expect(wrapper.find(GlFormInputGroup).props('value')).toBe(value);
expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
});
});
describe('functionality', () => {
it.each`
name | value
${'sshLink'} | ${null}
${'httpLink'} | ${null}
`('allows null values for the props', ({ name, value }) => {
createComponent({ ...defaultPropsData, [name]: value });
expect(wrapper.findAll(GlNewDropdownHeader).length).toBe(1);
});
it('correctly calculates httpLabel for HTTPS protocol', () => {
createComponent({ httpLink: httpsLink });
expect(wrapper.find(GlNewDropdownHeader).text()).toContain('HTTPS');
});
});
});
......@@ -104,21 +104,29 @@ describe Service do
describe "Test Button" do
describe '#can_test?' do
subject { service.can_test? }
let(:service) { create(:service, project: project) }
context 'when repository is not empty' do
let(:project) { create(:project, :repository) }
it 'returns true' do
expect(service.can_test?).to be true
end
it { is_expected.to be true }
end
context 'when repository is empty' do
let(:project) { create(:project) }
it 'returns true' do
expect(service.can_test?).to be true
it { is_expected.to be true }
end
context 'when instance-level service' do
Service.available_services_types.each do |service_type|
let(:service) do
service_type.constantize.new(instance: true)
end
it { is_expected.to be_falsey }
end
end
end
......
......@@ -27,14 +27,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
end
context 'user does not have push right to repository' do
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:forbidden)
expect(result[:message]).to eq("You are not allowed to push into this branch. Create another branch or open a merge request.")
end
it_behaves_like 'misconfigured dashboard service response with stepable', :forbidden, 'You are not allowed to push into this branch. Create another branch or open a merge request.'
end
context 'with rights to push to the repository' do
......@@ -46,27 +39,13 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
context 'with a yml extension' do
let(:file_name) { 'config/prometheus/../database.yml' }
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("A file with this name doesn't exist")
end
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, "A file with this name doesn't exist"
end
context 'without a yml extension' do
let(:file_name) { '../../..../etc/passwd' }
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("The file name should have a .yml extension")
end
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'The file name should have a .yml extension'
end
end
......@@ -81,14 +60,7 @@ describe Metrics::Dashboard::UpdateDashboardService, :use_clean_rails_memory_sto
project.repository.add_branch(user, branch, 'master')
end
it 'returns an appropriate message and status code', :aggregate_failures do
result = service_call
expect(result.keys).to contain_exactly(:message, :http_status, :status, :last_step)
expect(result[:status]).to eq(:error)
expect(result[:http_status]).to eq(:bad_request)
expect(result[:message]).to eq("There was an error updating the dashboard, branch named: existing_branch already exists.")
end
it_behaves_like 'misconfigured dashboard service response with stepable', :bad_request, 'There was an error updating the dashboard, branch named: existing_branch already exists.'
end
context 'Files::UpdateService success' do
......
......@@ -786,10 +786,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.117.0.tgz#05239ddcf529c62ca29e1ec1a25a7e24efb98207"
integrity sha512-dGy/VWuRAFCTZX3Yqu1+RnAHTSUWafteIk/RMfUCN9B/EMbYzjhYsNy0NLVoZ23Rj/KGv1bUGHvyQCoPP6VzpA==
"@gitlab/ui@11.0.0":
version "11.0.0"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-11.0.0.tgz#2dc6c4e92201911b80c1c24ff4ea6c5247810451"
integrity sha512-Cu50RQMbNGxEfMIxr3iv6i09hNs3deRS01CnkxNEdxmmokyKr86a2TItHwrAKyacjkb8IEfkn0Q/yoBMTpfPAw==
"@gitlab/ui@11.0.1":
version "11.0.1"
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-11.0.1.tgz#7d9fdb823590c72c232b7dee06b86c3e8766ba28"
integrity sha512-JlZULrpmm2jELsVHfcMpE0uiam+hA+5tL4+xZxiHoG+i9UlTQCAteMHOgJVT7pQYvjPAoSnw9XzTATEEcHVcOw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"
......
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