Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-26 18:07:33 +00:00
parent a8c410f8a1
commit b8915a9ca9
27 changed files with 595 additions and 149 deletions

View File

@ -510,7 +510,7 @@ gem 'gitaly', '~> 15.9.0-rc3'
# KAS GRPC protocol definitions
gem 'kas-grpc', '~> 0.1.0'
gem 'grpc', '~> 1.42.0'
gem 'grpc', '~> 1.55.0'
gem 'google-protobuf', '~> 3.23', '>= 3.23.1'

View File

@ -269,12 +269,13 @@
{"name":"graphql","version":"1.13.12","platform":"ruby","checksum":"1d82666cf201193a8d0cb54cea38576b820418db4869b549f61a35f3a2d97ac3"},
{"name":"graphql-client","version":"0.17.0","platform":"ruby","checksum":"5aaf02ce8f2dbc8e3ba05a7eaeb3ad9336762c4424c6093f4438fbb9490eeb5d"},
{"name":"graphql-docs","version":"2.1.0","platform":"ruby","checksum":"7eb82402f8fda455104b2b60364e9ada145d79d3121a8f915790d49da38bb576"},
{"name":"grpc","version":"1.42.0","platform":"ruby","checksum":"b3d2649e67c6a636544996843d9ec191699c54c1aca797dbfea4dff36c14584a"},
{"name":"grpc","version":"1.42.0","platform":"x64-mingw32","checksum":"6aac1b6576134b0a83e000b1269f60d502eb24aee96c64e2658c3f24f8e32ac0"},
{"name":"grpc","version":"1.42.0","platform":"x86-linux","checksum":"4aa50538aa929f1f3bcefb11c65ee1a1606b5aef838ea4d4e93c100b5f4263a5"},
{"name":"grpc","version":"1.42.0","platform":"x86-mingw32","checksum":"eeb2a9381bea43fafe879b6ddaa011351a44d0894d48bdc965a07bcb67c6eb56"},
{"name":"grpc","version":"1.42.0","platform":"x86_64-darwin","checksum":"20fa202d46d8a055628260622e98fb6439529fbac283f0552af620b909f78535"},
{"name":"grpc","version":"1.42.0","platform":"x86_64-linux","checksum":"92e2ceb2aca335d5755163dd8030082091d5b0e63c117b1ca07051b66c53eb2e"},
{"name":"grpc","version":"1.55.0","platform":"ruby","checksum":"529332f8e5e98f5b138afd5c4a9c7bdc9e247f4c10c84c1adbf1a114eba161ae"},
{"name":"grpc","version":"1.55.0","platform":"x64-mingw-ucrt","checksum":"6b5c7b7358476469c5ecb46f35e1eff6983efc9395d9db8db0a2eb4207c82ffb"},
{"name":"grpc","version":"1.55.0","platform":"x64-mingw32","checksum":"73755c256fc0fe5361a979cd609414ebdaa5862f5821fba20ea31110f1d87405"},
{"name":"grpc","version":"1.55.0","platform":"x86-linux","checksum":"37c20569a17b1cff91155f193b0df41eb42fd0aed9051fa91ccca273a259e393"},
{"name":"grpc","version":"1.55.0","platform":"x86-mingw32","checksum":"6b4144b5af8086b46b2e62b5fbda50fc19105a4efefafaca63e15b0384c42274"},
{"name":"grpc","version":"1.55.0","platform":"x86_64-darwin","checksum":"d7f57eb84811d7ea2a9464ec88d9296a92801f643a4d7cf76cf4896edf12a25c"},
{"name":"grpc","version":"1.55.0","platform":"x86_64-linux","checksum":"4ee73555759774db22ba23ff79c332cce7ae08b0ba4d4b33ab4747e83e0a8518"},
{"name":"gssapi","version":"1.3.1","platform":"ruby","checksum":"c51cf30842ee39bd93ce7fc33e20405ff8a04cda9dec6092071b61258284aee1"},
{"name":"guard","version":"2.16.2","platform":"ruby","checksum":"71ba7abaddecc8be91ab77bbaf78f767246603652ebbc7b976fda497ebdc8fbb"},
{"name":"guard-compat","version":"1.2.1","platform":"ruby","checksum":"3ad21ab0070107f92edfd82610b5cdc2fb8e368851e72362ada9703443d646fe"},

View File

@ -750,8 +750,8 @@ GEM
graphql (~> 1.12)
html-pipeline (~> 2.9)
sass (~> 3.4)
grpc (1.42.0)
google-protobuf (~> 3.18)
grpc (1.55.0)
google-protobuf (~> 3.23)
googleapis-common-protos-types (~> 1.0)
gssapi (1.3.1)
ffi (>= 1.0.1)
@ -1777,7 +1777,7 @@ DEPENDENCIES
graphlyte (~> 1.0.0)
graphql (~> 1.13.12)
graphql-docs (~> 2.1.0)
grpc (~> 1.42.0)
grpc (~> 1.55.0)
gssapi (~> 1.3.1)
guard-rspec
haml_lint (~> 0.40.0)

View File

@ -2,13 +2,16 @@
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import createEnvironment from '../graphql/mutations/create_environment.mutation.graphql';
import EnvironmentForm from './environment_form.vue';
export default {
components: {
EnvironmentForm,
},
inject: ['projectEnvironmentsPath'],
mixins: [glFeatureFlagsMixin()],
inject: ['projectEnvironmentsPath', 'projectPath'],
data() {
return {
environment: {
@ -23,6 +26,45 @@ export default {
this.environment = env;
},
onSubmit() {
if (this.glFeatures?.environmentSettingsToGraphql) {
this.createWithGraphql();
} else {
this.createWithAxios();
}
},
async createWithGraphql() {
this.loading = true;
try {
const { data } = await this.$apollo.mutate({
mutation: createEnvironment,
variables: {
input: {
name: this.environment.name,
externalUrl: this.environment.externalUrl,
projectPath: this.projectPath,
},
},
});
const { errors } = data.environmentCreate;
if (errors.length > 0) {
throw new Error(errors[0]?.message ?? errors[0]);
}
const { path } = data.environmentCreate.environment;
if (path) {
visitUrl(path);
}
} catch (error) {
const { message } = error;
createAlert({ message });
} finally {
this.loading = false;
}
},
createWithAxios() {
this.loading = true;
axios
.post(this.projectEnvironmentsPath, {

View File

@ -0,0 +1,9 @@
mutation createEnvironment($input: EnvironmentCreateInput!) {
environmentCreate(input: $input) {
environment {
id
path
}
errors
}
}

View File

@ -1,11 +1,23 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import NewEnvironment from './components/new_environment.vue';
import { apolloProvider } from './graphql/client';
export default (el) =>
new Vue({
Vue.use(VueApollo);
export default (el) => {
if (!el) {
return null;
}
const { projectEnvironmentsPath, projectPath } = el.dataset;
return new Vue({
el,
provide: { projectEnvironmentsPath: el.dataset.projectEnvironmentsPath },
apolloProvider: apolloProvider(),
provide: { projectEnvironmentsPath, projectPath },
render(h) {
return h(NewEnvironment);
},
});
};

View File

@ -180,7 +180,6 @@ export default {
},
},
epicLink: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353639',
openBetaLink: 'https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta',
featureFlagLink: helpPagePath('operations/error_tracking'),
created() {
if (this.errorTrackingEnabled) {
@ -476,10 +475,6 @@ export default {
__('How do I get started?')
}}</gl-link>
</div>
<div class="gl-mt-3">
<span>{{ __('Error tracking is currently in') }}</span>
<gl-link target="_blank" :href="$options.openBetaLink">{{ __('Open Beta.') }}</gl-link>
</div>
</template>
</gl-empty-state>
</div>

View File

@ -28,7 +28,7 @@ query getUserSnippets(
name
}
}
commenters {
notes {
nodes {
id
}

View File

@ -1,6 +1,27 @@
<script>
import { GlAvatar, GlLink, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__, sprintf, n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { SNIPPET_VISIBILITY } from '~/snippets/constants';
export default {
name: 'SnippetRow',
i18n: {
snippetInfo: s__('UserProfile|%{id} · created %{created} by %{author}'),
updatedInfo: s__('UserProfile|updated %{updated}'),
blobTooltip: s__('UserProfile|%{count} %{file}'),
},
components: {
GlAvatar,
GlLink,
GlSprintf,
GlIcon,
TimeAgo,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
snippet: {
type: Object,
@ -11,11 +32,88 @@ export default {
required: true,
},
},
computed: {
formattedId() {
return `$${getIdFromGraphQLId(this.snippet.id)}`;
},
profilePath() {
return `${gon.relative_url_root || ''}/${this.userInfo.username}`;
},
blobCount() {
return this.snippet.blobs?.nodes?.length || 0;
},
commentsCount() {
return this.snippet.notes?.nodes?.length || 0;
},
visibilityIcon() {
return SNIPPET_VISIBILITY[this.snippet.visibilityLevel]?.icon;
},
blobTooltip() {
return sprintf(this.$options.i18n.blobTooltip, {
count: this.blobCount,
file: n__('file', 'files', this.blobCount),
});
},
},
};
</script>
<template>
<div>
{{ snippet.title }}
<div class="gl-display-flex gl-align-items-center gl-py-5">
<gl-avatar :size="48" :src="userInfo.avatarUrl" class="gl-mr-3" />
<div class="gl-display-flex gl-flex-direction-column gl-align-items-flex-start">
<gl-link
data-testid="snippet-url"
:href="snippet.webUrl"
class="gl-text-gray-900 gl-font-weight-bold gl-mb-2"
>{{ snippet.title }}</gl-link
>
<span class="gl-text-gray-500">
<gl-sprintf :message="$options.i18n.snippetInfo">
<template #id>
<span data-testid="snippet-id">{{ formattedId }}</span>
</template>
<template #created>
<time-ago data-testid="snippet-created-at" :time="snippet.createdAt" />
</template>
<template #author>
<gl-link data-testid="snippet-author" :href="profilePath" class="gl-text-gray-900">{{
userInfo.name
}}</gl-link>
</template>
</gl-sprintf>
</span>
</div>
<div class="gl-ml-auto gl-display-flex gl-flex-direction-column gl-align-items-flex-end">
<div class="gl-display-flex gl-align-items-center gl-mb-2">
<span
v-gl-tooltip
data-testid="snippet-blob"
:title="blobTooltip"
class="gl-mr-4"
:class="{ 'gl-opacity-5': blobCount === 0 }"
>
<gl-icon name="documents" />
<span>{{ blobCount }}</span>
</span>
<gl-link
data-testid="snippet-comments"
:href="`${snippet.webUrl}#notes`"
class="gl-mr-4 gl-text-gray-900"
:class="{ 'gl-opacity-5': commentsCount === 0 }"
>
<gl-icon name="comments" />
<span>{{ commentsCount }}</span>
</gl-link>
<gl-icon data-testid="snippet-visibility" :name="visibilityIcon" />
</div>
<span class="gl-text-gray-500">
<gl-sprintf :message="$options.i18n.updatedInfo">
<template #updated>
<time-ago data-testid="snippet-updated-at" :time="snippet.updatedAt" />
</template>
</gl-sprintf>
</span>
</div>
</div>
</template>

View File

@ -1,7 +1,7 @@
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { badgeState } from '~/issuable/components/status_box.vue';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN } from '~/issues/constants';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
import { machine } from '~/lib/utils/finite_state_machine';
import {
MTWPS_MERGE_STRATEGY,
@ -341,7 +341,7 @@ export default class MergeRequestStore {
return '';
}
return format(date);
return format(date, timeagoLanguageCode);
}
static getPreferredAutoMergeStrategy(availableAutoMergeStrategies) {

View File

@ -9,6 +9,7 @@ module Integrations
:app_store_key_id,
:app_store_private_key,
:app_store_private_key_file_name,
:app_store_protected_refs,
:active,
:alert_events,
:api_key,

View File

@ -650,9 +650,8 @@ module Ci
def apple_app_store_variables
return [] unless apple_app_store_integration.try(:activated?)
return [] unless pipeline.protected_ref?
Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables)
Gitlab::Ci::Variables::Collection.new(apple_app_store_integration.ci_variables(protected_ref: pipeline.protected_ref?))
end
def google_play_variables

View File

@ -15,6 +15,7 @@ module Integrations
validates :app_store_key_id, presence: true, format: { with: KEY_ID_REGEX }
validates :app_store_private_key, presence: true, certificate_key: true
validates :app_store_private_key_file_name, presence: true
validates :app_store_protected_refs, inclusion: [true, false]
end
field :app_store_issuer_id,
@ -30,6 +31,12 @@ module Integrations
field :app_store_private_key_file_name, section: SECTION_TYPE_CONNECTION
field :app_store_private_key, api_only: true
field :app_store_protected_refs,
type: 'checkbox',
section: SECTION_TYPE_CONFIGURATION,
title: -> { s_('AppleAppStore|Protected branches and tags only') },
checkbox_label: -> { s_('AppleAppStore|Only set variables on protected branches and tags') }
def title
'Apple App Store Connect'
end
@ -85,8 +92,9 @@ module Integrations
end
end
def ci_variables
def ci_variables(protected_ref:)
return [] unless activated?
return [] if app_store_protected_refs && !protected_ref
[
{ key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID', value: app_store_issuer_id, masked: true, public: false },
@ -98,6 +106,11 @@ module Integrations
]
end
def initialize_properties
super
self.app_store_protected_refs = true if app_store_protected_refs.nil?
end
private
def client

View File

@ -3,4 +3,4 @@
- page_title s_("Environments|New Environment")
- add_page_specific_style 'page_bundles/environments'
#js-new-environment{ data: { project_environments_path: project_environments_path(@project) } }
#js-new-environment{ data: { project_environments_path: project_environments_path(@project), project_path: @project.full_path, } }

View File

@ -37,6 +37,7 @@ GitLab supports enabling the Apple App Store integration at the project level. C
- **Issuer ID**: The Apple App Store Connect issuer ID.
- **Key ID**: The key ID of the generated private key.
- **Private Key**: The generated private key. You can download this key only once.
- **Protected branches and tags only**: Enable to only set variables on protected branches and tags.
1. Select **Save changes**.

View File

@ -197,6 +197,12 @@ module API
name: :app_store_private_key_file_name,
type: String,
desc: 'The Apple App Store Connect Private Key File Name'
},
{
required: false,
name: :app_store_protected_refs,
type: Boolean,
desc: 'Only enable for protected refs'
}
],
'asana' => [

View File

@ -6,7 +6,8 @@ module Gitlab
module GrpcErrorProcessor
extend Gitlab::ErrorTracking::Processor::Concerns::ProcessesExceptions
DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:(.*)')
# Braces added by gRPC Ruby code: https://github.com/grpc/grpc/blob/0e38b075ffff72ab2ad5326e3f60ba6dcc234f46/src/ruby/lib/grpc/errors.rb#L46
DEBUG_ERROR_STRING_REGEX = RE2('(.*) debug_error_string:\{(.*)\}')
class << self
def call(event)

View File

@ -5409,6 +5409,12 @@ msgstr ""
msgid "AppleAppStore|Leave empty to use your current Private Key."
msgstr ""
msgid "AppleAppStore|Only set variables on protected branches and tags"
msgstr ""
msgid "AppleAppStore|Protected branches and tags only"
msgstr ""
msgid "AppleAppStore|The Apple App Store Connect Issuer ID."
msgstr ""
@ -17625,9 +17631,6 @@ msgstr ""
msgid "Error tracking"
msgstr ""
msgid "Error tracking is currently in"
msgstr ""
msgid "Error updating %{issuableType}"
msgstr ""
@ -31624,9 +31627,6 @@ msgstr ""
msgid "Open"
msgstr ""
msgid "Open Beta."
msgstr ""
msgid "Open Selection"
msgstr ""
@ -49193,6 +49193,12 @@ msgstr ""
msgid "UserProfiles|No snippets found."
msgstr ""
msgid "UserProfile|%{count} %{file}"
msgstr ""
msgid "UserProfile|%{id} · created %{created} by %{author}"
msgstr ""
msgid "UserProfile|Activity"
msgstr ""
@ -49334,6 +49340,9 @@ msgstr ""
msgid "UserProfile|made a private contribution"
msgstr ""
msgid "UserProfile|updated %{updated}"
msgstr ""
msgid "Username"
msgstr ""

View File

@ -297,6 +297,7 @@ FactoryBot.define do
app_store_key_id { 'ABC1' }
app_store_private_key_file_name { 'auth_key.p8' }
app_store_private_key { File.read('spec/fixtures/auth_key.p8') }
app_store_protected_refs { true }
end
factory :google_play_integration, class: 'Integrations::GooglePlay' do

View File

@ -1,103 +1,196 @@
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import createEnvironment from '~/environments/graphql/mutations/create_environment.mutation.graphql';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
const DEFAULT_OPTS = {
provide: {
projectEnvironmentsPath: '/projects/environments',
protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd',
},
const newName = 'test';
const newExternalUrl = 'https://google.ca';
const provide = {
projectEnvironmentsPath: '/projects/environments',
projectPath: '/path/to/project',
};
const environmentCreate = { environment: { id: '1', path: 'path/to/environment' }, errors: [] };
const environmentCreateError = {
environment: null,
errors: [{ message: 'uh oh!' }],
};
describe('~/environments/components/new.vue', () => {
let wrapper;
let mock;
let name;
let url;
let form;
const createWrapper = (opts = {}) =>
mountExtended(NewEnvironment, {
...DEFAULT_OPTS,
...opts,
const createMockApolloProvider = (mutationResult) => {
Vue.use(VueApollo);
return createMockApollo([
[
createEnvironment,
jest.fn().mockResolvedValue({ data: { environmentCreate: mutationResult } }),
],
]);
};
const createWrapperWithApollo = async (mutationResult = environmentCreate) => {
wrapper = mountExtended(NewEnvironment, {
provide: {
...provide,
glFeatures: {
environmentSettingsToGraphql: true,
},
},
apolloProvider: createMockApolloProvider(mutationResult),
});
beforeEach(() => {
mock = new MockAdapter(axios);
wrapper = createWrapper();
name = wrapper.findByLabelText('Name');
url = wrapper.findByLabelText('External URL');
form = wrapper.findByRole('form', { name: 'New environment' });
});
afterEach(() => {
mock.restore();
});
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
const submitForm = async (expected, response) => {
mock
.onPost(DEFAULT_OPTS.provide.projectEnvironmentsPath, {
name: expected.name,
external_url: expected.url,
})
.reply(...response);
await name.setValue(expected.name);
await url.setValue(expected.url);
await form.trigger('submit');
await waitForPromises();
};
it('sets the title to New environment', () => {
const header = wrapper.findByRole('heading', { name: 'New environment' });
expect(header.exists()).toBe(true);
const createWrapperWithAxios = () => {
wrapper = mountExtended(NewEnvironment, {
provide: {
...provide,
glFeatures: {
environmentSettingsToGraphql: false,
},
},
});
};
const findNameInput = () => wrapper.findByLabelText(__('Name'));
const findExternalUrlInput = () => wrapper.findByLabelText(__('External URL'));
const findForm = () => wrapper.findByRole('form', { name: __('New environment') });
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
const submitForm = async () => {
await findNameInput().setValue('test');
await findExternalUrlInput().setValue('https://google.ca');
await findForm().trigger('submit');
};
describe('default', () => {
beforeEach(() => {
createWrapperWithAxios();
});
it('sets the title to New environment', () => {
const header = wrapper.findByRole('heading', { name: 'New environment' });
expect(header.exists()).toBe(true);
});
it.each`
input | value
${() => findNameInput()} | ${'test'}
${() => findExternalUrlInput()} | ${'https://example.org'}
`('changes the value of the input to $value', ({ input, value }) => {
input().setValue(value);
expect(input().element.value).toBe(value);
});
});
it.each`
input | value
${() => name} | ${'test'}
${() => url} | ${'https://example.org'}
`('changes the value of the input to $value', async ({ input, value }) => {
await input().setValue(value);
describe('when environmentSettingsToGraphql feature is enabled', () => {
describe('when mutation successful', () => {
beforeEach(() => {
createWrapperWithApollo();
});
expect(input().element.value).toBe(value);
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
await submitForm();
expect(showsLoading()).toBe(true);
});
it('submits the new environment on submit', async () => {
submitForm();
await waitForPromises();
expect(visitUrl).toHaveBeenCalledWith('path/to/environment');
});
});
describe('when failed', () => {
beforeEach(async () => {
createWrapperWithApollo(environmentCreateError);
submitForm();
await waitForPromises();
});
it('shows errors on error', () => {
expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
});
});
it('shows loader after form is submitted', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
describe('when environmentSettingsToGraphql feature is disabled', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
createWrapperWithAxios();
});
expect(showsLoading()).toBe(false);
afterEach(() => {
mock.restore();
});
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
expect(showsLoading()).toBe(true);
});
mock
.onPost(provide.projectEnvironmentsPath, {
name: newName,
external_url: newExternalUrl,
})
.reply(HTTP_STATUS_OK, { path: '/test' });
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await submitForm();
await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
expect(visitUrl).toHaveBeenCalledWith('/test');
});
it('submits the new environment on submit', async () => {
mock
.onPost(provide.projectEnvironmentsPath, {
name: newName,
external_url: newExternalUrl,
})
.reply(HTTP_STATUS_OK, { path: '/test' });
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
await submitForm();
await waitForPromises();
await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
it('shows errors on error', async () => {
mock
.onPost(provide.projectEnvironmentsPath, {
name: newName,
external_url: newExternalUrl,
})
.reply(HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] });
await submitForm();
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
});
});
});

View File

@ -367,19 +367,12 @@ describe('ErrorTrackingList', () => {
const emptyStatePrimaryDescription = emptyStateComponent.find('span', {
exactText: 'Monitor your errors directly in GitLab.',
});
const emptyStateSecondaryDescription = emptyStateComponent.find('span', {
exactText: 'Error tracking is currently in',
});
const emptyStateLinks = emptyStateComponent.findAll('a');
expect(emptyStateComponent.isVisible()).toBe(true);
expect(emptyStatePrimaryDescription.exists()).toBe(true);
expect(emptyStateSecondaryDescription.exists()).toBe(true);
expect(emptyStateLinks.at(0).attributes('href')).toBe(
'/help/operations/error_tracking.html#integrated-error-tracking',
);
expect(emptyStateLinks.at(1).attributes('href')).toBe(
'https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta',
);
});
});

View File

@ -1,3 +1,11 @@
import { GlAvatar, GlSprintf, GlIcon } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
VISIBILITY_LEVEL_PRIVATE_STRING,
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
} from '~/visibility_level/constants';
import { SNIPPET_VISIBILITY } from '~/snippets/constants';
import SnippetRow from '~/profile/components/snippets/snippet_row.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { MOCK_USER, MOCK_SNIPPET } from 'jest/profile/mock_data';
@ -16,16 +24,123 @@ describe('UserProfileSnippetRow', () => {
...defaultProps,
...props,
},
stubs: {
GlSprintf,
},
});
};
const findGlAvatar = () => wrapper.findComponent(GlAvatar);
const findSnippetUrl = () => wrapper.findByTestId('snippet-url');
const findSnippetId = () => wrapper.findByTestId('snippet-id');
const findSnippetCreatedAt = () => wrapper.findByTestId('snippet-created-at');
const findSnippetAuthor = () => wrapper.findByTestId('snippet-author');
const findSnippetBlob = () => wrapper.findByTestId('snippet-blob');
const findSnippetComments = () => wrapper.findByTestId('snippet-comments');
const findSnippetVisibility = () => wrapper.findByTestId('snippet-visibility');
const findSnippetUpdatedAt = () => wrapper.findByTestId('snippet-updated-at');
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders snippet title', () => {
expect(wrapper.text()).toBe(MOCK_SNIPPET.title);
it('renders GlAvatar with user avatar', () => {
expect(findGlAvatar().exists()).toBe(true);
expect(findGlAvatar().attributes('src')).toBe(MOCK_USER.avatarUrl);
});
it('renders Snippet Url with snippet webUrl', () => {
expect(findSnippetUrl().exists()).toBe(true);
expect(findSnippetUrl().attributes('href')).toBe(MOCK_SNIPPET.webUrl);
});
it('renders Snippet ID correctly formatted', () => {
expect(findSnippetId().exists()).toBe(true);
expect(findSnippetId().text()).toBe(`$${getIdFromGraphQLId(MOCK_SNIPPET.id)}`);
});
it('renders Snippet Created At with correct date string', () => {
expect(findSnippetCreatedAt().exists()).toBe(true);
expect(findSnippetCreatedAt().attributes('time')).toBe(MOCK_SNIPPET.createdAt.toString());
});
it('renders Snippet Author with profileLink', () => {
expect(findSnippetAuthor().exists()).toBe(true);
expect(findSnippetAuthor().attributes('href')).toBe(`/${MOCK_USER.username}`);
});
it('renders Snippet Updated At with correct date string', () => {
expect(findSnippetUpdatedAt().exists()).toBe(true);
expect(findSnippetUpdatedAt().attributes('time')).toBe(MOCK_SNIPPET.updatedAt.toString());
});
});
describe.each`
nodes | hasOpacity | tooltip
${[]} | ${true} | ${'0 files'}
${[{ name: 'file.txt' }]} | ${false} | ${'1 file'}
${[{ name: 'file.txt' }, { name: 'file2.txt' }]} | ${false} | ${'2 files'}
`('Blob Icon', ({ nodes, hasOpacity, tooltip }) => {
describe(`when blobs length ${nodes.length}`, () => {
beforeEach(() => {
createComponent({ snippet: { ...MOCK_SNIPPET, blobs: { nodes } } });
});
it(`does${hasOpacity ? '' : ' not'} render icon with opacity`, () => {
expect(findSnippetBlob().findComponent(GlIcon).props('name')).toBe('documents');
expect(findSnippetBlob().classes('gl-opacity-5')).toBe(hasOpacity);
});
it('renders text and tooltip correctly', () => {
expect(findSnippetBlob().text()).toBe(nodes.length.toString());
expect(findSnippetBlob().attributes('title')).toBe(tooltip);
});
});
});
describe.each`
nodes | hasOpacity
${[]} | ${true}
${[{ id: 'note/1' }]} | ${false}
${[{ id: 'note/1' }, { id: 'note/2' }]} | ${false}
`('Comments Icon', ({ nodes, hasOpacity }) => {
describe(`when comments length ${nodes.length}`, () => {
beforeEach(() => {
createComponent({ snippet: { ...MOCK_SNIPPET, notes: { nodes } } });
});
it(`does${hasOpacity ? '' : ' not'} render icon with opacity`, () => {
expect(findSnippetComments().findComponent(GlIcon).props('name')).toBe('comments');
expect(findSnippetComments().classes('gl-opacity-5')).toBe(hasOpacity);
});
it('renders text correctly', () => {
expect(findSnippetComments().text()).toBe(nodes.length.toString());
});
it('renders link to comments correctly', () => {
expect(findSnippetComments().attributes('href')).toBe(`${MOCK_SNIPPET.webUrl}#notes`);
});
});
});
describe.each`
visibilityLevel
${VISIBILITY_LEVEL_PUBLIC_STRING}
${VISIBILITY_LEVEL_PRIVATE_STRING}
${VISIBILITY_LEVEL_INTERNAL_STRING}
`('Visibility Icon', ({ visibilityLevel }) => {
describe(`when visibilityLevel is ${visibilityLevel}`, () => {
beforeEach(() => {
createComponent({ snippet: { ...MOCK_SNIPPET, visibilityLevel } });
});
it(`renders the ${SNIPPET_VISIBILITY[visibilityLevel].icon} icon`, () => {
expect(findSnippetVisibility().findComponent(GlIcon).props('name')).toBe(
SNIPPET_VISIBILITY[visibilityLevel].icon,
);
});
});
});
});

View File

@ -45,10 +45,10 @@ const getMockSnippet = (id) => {
},
],
},
commenters: {
notes: {
nodes: [
{
id: 'git://gitlab/User/1',
id: 'git://gitlab/Note/1',
},
],
},

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry do
RSpec.describe Gitlab::ErrorTracking::Processor::GrpcErrorProcessor, :sentry, feature_category: :integrations do
describe '.call' do
let(:raven_required_options) do
{

View File

@ -3870,7 +3870,9 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
end
context 'for the apple_app_store integration' do
let_it_be(:apple_app_store_integration) { create(:apple_app_store_integration) }
before do
allow(build.pipeline).to receive(:protected_ref?).and_return(pipeline_protected_ref)
end
let(:apple_app_store_variables) do
[
@ -3881,41 +3883,72 @@ RSpec.describe Ci::Build, feature_category: :continuous_integration, factory_def
]
end
context 'when the apple_app_store exists' do
context 'when a build is protected' do
before do
allow(build.pipeline).to receive(:protected_ref?).and_return(true)
build.project.update!(apple_app_store_integration: apple_app_store_integration)
end
it 'includes apple_app_store variables' do
is_expected.to include(*apple_app_store_variables)
end
end
context 'when a build is not protected' do
before do
allow(build.pipeline).to receive(:protected_ref?).and_return(false)
build.project.update!(apple_app_store_integration: apple_app_store_integration)
end
it 'does not include the apple_app_store variables' do
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
end
end
end
context 'when the apple_app_store integration does not exist' do
it 'does not include apple_app_store variables' do
shared_examples 'does not include the apple_app_store variables' do
specify do
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_ISSUER_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_KEY_ID' }).to be_nil
expect(subject.find { |v| v[:key] == 'APP_STORE_CONNECT_API_KEY_IS_KEY_CONTENT_BASE64' }).to be_nil
end
end
shared_examples 'includes apple_app_store variables' do
specify do
expect(subject).to include(*apple_app_store_variables)
end
end
context 'when an Apple App Store integration exists' do
let_it_be(:apple_app_store_integration) do
create(:apple_app_store_integration, project: project)
end
context 'when app_store_protected_refs is true' do
context 'when a build is protected' do
let(:pipeline_protected_ref) { true }
include_examples 'includes apple_app_store variables'
end
context 'when a build is not protected' do
let(:pipeline_protected_ref) { false }
include_examples 'does not include the apple_app_store variables'
end
end
context 'when app_store_protected_refs is false' do
before do
apple_app_store_integration.update!(app_store_protected_refs: false)
end
context 'when a build is protected' do
let(:pipeline_protected_ref) { true }
include_examples 'includes apple_app_store variables'
end
context 'when a build is not protected' do
let(:pipeline_protected_ref) { false }
include_examples 'includes apple_app_store variables'
end
end
end
context 'when an Apple App Store integration does not exist' do
context 'when a build is protected' do
let(:pipeline_protected_ref) { true }
include_examples 'does not include the apple_app_store variables'
end
context 'when a build is not protected' do
let(:pipeline_protected_ref) { false }
include_examples 'does not include the apple_app_store variables'
end
end
end
context 'for the google_play integration' do

View File

@ -13,6 +13,9 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
it { is_expected.to validate_presence_of :app_store_key_id }
it { is_expected.to validate_presence_of :app_store_private_key }
it { is_expected.to validate_presence_of :app_store_private_key_file_name }
it { is_expected.to allow_value(true).for(:app_store_protected_refs) }
it { is_expected.to allow_value(false).for(:app_store_protected_refs) }
it { is_expected.not_to allow_value(nil).for(:app_store_protected_refs) }
it { is_expected.to allow_value('aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee').for(:app_store_issuer_id) }
it { is_expected.not_to allow_value('abcde').for(:app_store_issuer_id) }
it { is_expected.to allow_value(File.read('spec/fixtures/ssl_key.pem')).for(:app_store_private_key) }
@ -30,7 +33,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
describe '#fields' do
it 'returns custom fields' do
expect(apple_app_store_integration.fields.pluck(:name)).to match_array(%w[app_store_issuer_id app_store_key_id
app_store_private_key app_store_private_key_file_name])
app_store_private_key app_store_private_key_file_name app_store_protected_refs])
end
end
@ -62,8 +65,8 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
describe '#ci_variables' do
let(:apple_app_store_integration) { build_stubbed(:apple_app_store_integration) }
it 'returns vars when the integration is activated' do
ci_vars = [
let(:ci_vars) do
[
{
key: 'APP_STORE_CONNECT_API_KEY_ISSUER_ID',
value: apple_app_store_integration.app_store_issuer_id,
@ -89,13 +92,32 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
public: false
}
]
expect(apple_app_store_integration.ci_variables).to match_array(ci_vars)
end
it 'returns an empty array when the integration is disabled' do
apple_app_store_integration = build_stubbed(:apple_app_store_integration, active: false)
expect(apple_app_store_integration.ci_variables).to match_array([])
it 'returns the vars for protected branch' do
expect(apple_app_store_integration.ci_variables(protected_ref: true)).to match_array(ci_vars)
end
it 'doesn\'t return the vars for unprotected branch' do
expect(apple_app_store_integration.ci_variables(protected_ref: false)).to be_empty
end
end
describe '#initialize_properties' do
context 'when app_store_protected_refs is nil' do
let(:apple_app_store_integration) { described_class.new(app_store_protected_refs: nil) }
it 'sets app_store_protected_refs to true' do
expect(apple_app_store_integration.app_store_protected_refs).to be(true)
end
end
context 'when app_store_protected_refs is false' do
let(:apple_app_store_integration) { build(:apple_app_store_integration, app_store_protected_refs: false) }
it 'sets app_store_protected_refs to false' do
expect(apple_app_store_integration.app_store_protected_refs).to be(false)
end
end
end
end
@ -105,7 +127,7 @@ RSpec.describe Integrations::AppleAppStore, feature_category: :mobile_devops do
describe '#ci_variables' do
it 'returns an empty array' do
expect(apple_app_store_integration.ci_variables).to match_array([])
expect(apple_app_store_integration.ci_variables(protected_ref: true)).to be_empty
end
end
end

View File

@ -84,6 +84,8 @@ RSpec.shared_context 'with integration' do
hash.merge!(k => 'ABC1')
elsif integration == 'apple_app_store' && k == :app_store_private_key_file_name
hash.merge!(k => 'ssl_key.pem')
elsif integration == 'apple_app_store' && k == :app_store_protected_refs # rubocop:disable Lint/DuplicateBranch
hash.merge!(k => true)
elsif integration == 'google_play' && k == :package_name
hash.merge!(k => 'com.gitlab.foo.bar')
elsif integration == 'google_play' && k == :service_account_key