Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-08 06:11:46 +00:00
parent d27e35761a
commit a2b477802d
30 changed files with 353 additions and 109 deletions

View File

@ -1,13 +1,10 @@
import dateFormat from 'dateformat';
import { unescape } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants';
import { hideFlash } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { formatTimeAsSummary } from '~/lib/utils/datetime/date_format_utility';
import { slugify } from '~/lib/utils/text_utility';
import { s__, sprintf } from '../locale';
export const removeFlash = (type = 'alert') => {
const flashEl = document.querySelector(`.flash-${type}`);
@ -45,29 +42,6 @@ export const transformStagesForPathNavigation = ({
return formattedStages;
};
export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, weeks, months }) => {
if (months) {
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
} else if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
} else if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
} else if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
} else if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
} else if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|<1m'), { ALLOWED_TAGS: [] }));
}
return '-';
};
/**
* Takes a raw median value in seconds and converts it to a string representation
* ie. converts 172800 => 2d (2 days)
@ -76,7 +50,7 @@ export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, we
* @returns {String} String representation ie 2w
*/
export const medianTimeToParsedSeconds = (value) =>
timeSummaryForPathNavigation({
formatTimeAsSummary({
...parseSeconds(value, { daysPerWeek: 7, hoursPerDay: 24 }),
seconds: value,
});

View File

@ -1,6 +1,8 @@
import dateFormat from 'dateformat';
import { isString, mapValues, reduce, isDate } from 'lodash';
import { s__, n__, __ } from '../../../locale';
import { isString, mapValues, reduce, isDate, unescape } from 'lodash';
import { roundToNearestHalf } from '~/lib/utils/common_utils';
import { sanitize } from '~/lib/dompurify';
import { s__, n__, __, sprintf } from '../../../locale';
/**
* Returns i18n month names array.
@ -361,3 +363,26 @@ export const dateToTimeInputValue = (date) => {
hour12: false,
});
};
export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, months }) => {
if (months) {
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
} else if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
} else if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
} else if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
} else if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
} else if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|<1m'), { ALLOWED_TAGS: [] }));
}
return '-';
};

View File

@ -11,15 +11,26 @@ import {
GlButton,
GlTooltipDirective,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
import CanCreateProjectSnippet from 'shared_queries/snippet/project_permissions.query.graphql';
import CanCreatePersonalSnippet from 'shared_queries/snippet/user_permissions.query.graphql';
import { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { __, s__, sprintf } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import createFlash, { FLASH_TYPES } from '~/flash';
import DeleteSnippetMutation from '../mutations/deleteSnippet.mutation.graphql';
export const i18n = {
snippetSpamSuccess: sprintf(
s__('Snippets|%{spammable_titlecase} was submitted to Akismet successfully.'),
{ spammable_titlecase: __('Snippet') },
),
snippetSpamFailure: s__('Snippets|Error with Akismet. Please check the logs for more info.'),
};
export default {
components: {
GlAvatar,
@ -54,7 +65,7 @@ export default {
},
},
},
inject: ['reportAbusePath'],
inject: ['reportAbusePath', 'canReportSpam'],
props: {
snippet: {
type: Object,
@ -63,7 +74,8 @@ export default {
},
data() {
return {
isDeleting: false,
isLoading: false,
isSubmittingSpam: false,
errorMessage: '',
canCreateSnippet: false,
};
@ -105,10 +117,11 @@ export default {
category: 'secondary',
},
{
condition: this.reportAbusePath,
condition: this.canReportSpam && !isEmpty(this.reportAbusePath),
text: __('Submit as spam'),
href: this.reportAbusePath,
click: this.submitAsSpam,
title: __('Submit as spam'),
loading: this.isSubmittingSpam,
},
];
},
@ -157,7 +170,7 @@ export default {
this.$refs.deleteModal.show();
},
deleteSnippet() {
this.isDeleting = true;
this.isLoading = true;
this.$apollo
.mutate({
mutation: DeleteSnippetMutation,
@ -167,17 +180,34 @@ export default {
if (data?.destroySnippet?.errors.length) {
throw new Error(data?.destroySnippet?.errors[0]);
}
this.isDeleting = false;
this.errorMessage = undefined;
this.closeDeleteModal();
this.redirectToSnippets();
})
.catch((err) => {
this.isDeleting = false;
this.isLoading = false;
this.errorMessage = err.message;
})
.finally(() => {
this.isLoading = false;
});
},
async submitAsSpam() {
try {
this.isSubmittingSpam = true;
await axios.post(this.reportAbusePath);
createFlash({
message: this.$options.i18n.snippetSpamSuccess,
type: FLASH_TYPES.SUCCESS,
});
} catch (error) {
createFlash({ message: this.$options.i18n.snippetSpamFailure });
} finally {
this.isSubmittingSpam = false;
}
},
},
i18n,
};
</script>
<template>
@ -189,9 +219,7 @@ export default {
:title="snippetVisibilityLevelDescription"
data-container="body"
>
<span class="sr-only">
{{ s__(`VisibilityLevel|${visibility}`) }}
</span>
<span class="sr-only">{{ s__(`VisibilityLevel|${visibility}`) }}</span>
<gl-icon :name="visibilityLevelIcon" :size="14" />
</div>
<div class="creator" data-testid="authored-message">
@ -233,6 +261,7 @@ export default {
>
<gl-button
:disabled="action.disabled"
:loading="action.loading"
:variant="action.variant"
:category="action.category"
:class="action.cssClass"
@ -240,9 +269,8 @@ export default {
data-qa-selector="snippet_action_button"
:data-qa-action="action.text"
@click="action.click ? action.click() : undefined"
>{{ action.text }}</gl-button
>
{{ action.text }}
</gl-button>
</div>
</template>
</div>
@ -266,14 +294,14 @@ export default {
<gl-modal ref="deleteModal" modal-id="delete-modal" title="Example title">
<template #modal-title>{{ __('Delete snippet?') }}</template>
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">{{
errorMessage
}}</gl-alert>
<gl-alert v-if="errorMessage" variant="danger" class="mb-2" @dismiss="errorMessage = ''">
{{ errorMessage }}
</gl-alert>
<gl-sprintf :message="__('Are you sure you want to delete %{name}?')">
<template #name
><strong>{{ snippet.title }}</strong></template
>
<template #name>
<strong>{{ snippet.title }}</strong>
</template>
</gl-sprintf>
<template #modal-footer>
@ -281,11 +309,11 @@ export default {
<gl-button
variant="danger"
category="primary"
:disabled="isDeleting"
:disabled="isLoading"
data-qa-selector="delete_snippet_button"
@click="deleteSnippet"
>
<gl-loading-icon v-if="isDeleting" size="sm" inline />
<gl-loading-icon v-if="isLoading" size="sm" inline />
{{ __('Delete snippet') }}
</gl-button>
</template>

View File

@ -27,6 +27,7 @@ export default function appFactory(el, Component) {
visibilityLevels = '[]',
selectedLevel,
multipleLevelsRestricted,
canReportSpam,
reportAbusePath,
...restDataset
} = el.dataset;
@ -39,6 +40,7 @@ export default function appFactory(el, Component) {
selectedLevel: SNIPPET_LEVELS_MAP[selectedLevel] ?? SNIPPET_VISIBILITY_PRIVATE,
multipleLevelsRestricted: 'multipleLevelsRestricted' in el.dataset,
reportAbusePath,
canReportSpam,
},
render(createElement) {
return createElement(Component, {

View File

@ -41,6 +41,7 @@ const populateUserInfo = (user) => {
workInformation: userData.work_information,
websiteUrl: userData.website_url,
pronouns: userData.pronouns,
localTime: userData.local_time,
loaded: true,
});
}

View File

@ -93,19 +93,27 @@ export default {
</div>
<div class="gl-text-gray-500">
<div v-if="user.bio" class="gl-display-flex gl-mb-2">
<gl-icon name="profile" class="gl-text-gray-400 gl-flex-shrink-0" />
<gl-icon name="profile" class="gl-flex-shrink-0" />
<span ref="bio" class="gl-ml-2 gl-overflow-hidden">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="gl-display-flex gl-mb-2">
<gl-icon name="work" class="gl-text-gray-400 gl-flex-shrink-0" />
<gl-icon name="work" class="gl-flex-shrink-0" />
<span ref="workInformation" class="gl-ml-2">{{ user.workInformation }}</span>
</div>
<div v-if="user.location" class="gl-display-flex gl-mb-2">
<gl-icon name="location" class="gl-flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div
v-if="user.localTime && !user.bot"
class="gl-display-flex gl-mb-2"
data-testid="user-popover-local-time"
>
<gl-icon name="clock" class="gl-flex-shrink-0" />
<span class="gl-ml-2">{{ user.localTime }}</span>
</div>
</div>
<div v-if="user.location" class="js-location gl-text-gray-500 gl-display-flex">
<gl-icon name="location" class="gl-text-gray-400 flex-shrink-0" />
<span class="gl-ml-2">{{ user.location }}</span>
</div>
<div v-if="statusHtml" class="js-user-status gl-mt-3">
<div v-if="statusHtml" class="gl-mb-2" data-testid="user-popover-status">
<span v-safe-html:[$options.safeHtmlConfig]="statusHtml"></span>
</div>
<div v-if="user.bot" class="gl-text-blue-500">

View File

@ -10,7 +10,9 @@ module Clusters
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
delegate :project, to: :agent
def config_project
agent.project
end
end
end
end

View File

@ -6,12 +6,15 @@ module Clusters
attr_reader :agent
delegate :id, to: :agent, prefix: true
delegate :project, to: :agent
def initialize(agent:)
@agent = agent
end
def config_project
agent.project
end
def config
nil
end

View File

@ -9,6 +9,10 @@ module Clusters
belongs_to :project, class_name: '::Project', optional: false
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
def config_project
agent.project
end
end
end
end

View File

@ -12,5 +12,13 @@ module Users
validates :last_digits, allow_nil: true, numericality: {
greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
}
def similar_records
self.class.where(
expiration_date: expiration_date,
last_digits: last_digits,
holder_name: holder_name
).order(credit_card_validated_at: :desc).includes(:user)
end
end
end

View File

@ -61,7 +61,6 @@
= _('Disabled')
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
= render_if_exists 'admin/users/credit_card_info', user: @user
%li
%span.light= _('External User:')
@ -139,6 +138,8 @@
= render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
= render_if_exists 'admin/users/credit_card_info', user: @user, link_to_match_page: true
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
-# Rendered on desktop only so order of cards can be different on desktop vs mobile

View File

@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)

View File

@ -12,7 +12,7 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true

View File

@ -0,0 +1,8 @@
---
name: finding_ci_pipeline_disable_joins
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70216
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338665
milestone: '14.3'
type: development
group: group::threat insights
default_enabled: true

View File

@ -192,6 +192,20 @@ cannot change them:
This ensures that your job uses the settings you intend and that they are not overridden by
project-level pipelines.
##### Avoid parent and child pipelines
Compliance pipelines start on the run of _every_ pipeline in a relevant project. This means that if a pipeline in the relevant project
triggers a child pipeline, the compliance pipeline runs first. This can trigger the parent pipeline, instead of the child pipeline.
Therefore, in projects with compliance frameworks, we recommend replacing
[parent-child pipelines](../../../ci/pipelines/parent_child_pipelines.md) with the following:
- Direct [`include`](../../../ci/yaml/index.md#include) statements that provide the parent pipeline with child pipeline configuration.
- Child pipelines placed in another project that are run using the [trigger API](../../../ci/triggers/) rather than the parent-child
pipeline feature.
This alternative ensures the compliance pipeline does not re-start the parent pipeline.
### Sharing and permissions
For your repository, you can set up features such as public access, repository features,

View File

@ -5,7 +5,7 @@ module API
module Clusters
class AgentAuthorization < Grape::Entity
expose :agent_id, as: :id
expose :project, with: Entities::ProjectIdentity, as: :config_project
expose :config_project, with: Entities::ProjectIdentity
expose :config, as: :configuration
end
end

View File

@ -4,6 +4,7 @@ module API
module Entities
class User < UserBasic
include UsersHelper
include TimeZoneHelper
include ActionView::Helpers::SanitizeHelper
expose :created_at, if: ->(user, opts) { Ability.allowed?(opts[:current_user], :read_user_profile, user) }
@ -24,6 +25,10 @@ module API
expose :bio_html do |user|
strip_tags(user.bio)
end
expose :local_time do |user|
local_time(user.timezone)
end
end
end
end

View File

@ -1141,6 +1141,9 @@ msgstr ""
msgid "(revoked)"
msgstr ""
msgid "(target)"
msgstr ""
msgid "(we need your current password to confirm your changes)"
msgstr ""
@ -3341,6 +3344,9 @@ msgstr ""
msgid "All users must have a name."
msgstr ""
msgid "All users with matching cards"
msgstr ""
msgid "Allow \"%{group_name}\" to sign you in"
msgstr ""
@ -6309,6 +6315,9 @@ msgstr ""
msgid "Capacity threshold"
msgstr ""
msgid "Card number:"
msgstr ""
msgid "CascadingSettings|Enforce for all subgroups"
msgstr ""
@ -9924,10 +9933,7 @@ msgstr ""
msgid "CredentialsInventory|SSH Keys"
msgstr ""
msgid "Credit card validated at:"
msgstr ""
msgid "Credit card validated:"
msgid "Credit card:"
msgstr ""
msgid "Critical vulnerabilities present"
@ -9984,6 +9990,9 @@ msgstr ""
msgid "Current sign-in at:"
msgstr ""
msgid "Current sign-in ip"
msgstr ""
msgid "Current vulnerabilities count"
msgstr ""
@ -13806,6 +13815,9 @@ msgstr ""
msgid "Expiration date (optional)"
msgstr ""
msgid "Expiration date:"
msgstr ""
msgid "Expired"
msgstr ""
@ -16853,6 +16865,9 @@ msgstr ""
msgid "History of authentications"
msgstr ""
msgid "Holder name:"
msgstr ""
msgid "Home page URL"
msgstr ""
@ -22939,6 +22954,9 @@ msgstr ""
msgid "No contributions were found"
msgstr ""
msgid "No credit card data for matching"
msgstr ""
msgid "No credit card required."
msgstr ""
@ -31589,6 +31607,9 @@ msgstr ""
msgid "Smartcard authentication failed: client certificate header is missing."
msgstr ""
msgid "Snippet"
msgstr ""
msgid "Snippets"
msgstr ""
@ -31613,6 +31634,9 @@ msgstr ""
msgid "SnippetsEmptyState|There are no snippets to show."
msgstr ""
msgid "Snippets|%{spammable_titlecase} was submitted to Akismet successfully."
msgstr ""
msgid "Snippets|Add another file %{num}/%{total}"
msgstr ""
@ -31622,6 +31646,9 @@ msgstr ""
msgid "Snippets|Description (optional)"
msgstr ""
msgid "Snippets|Error with Akismet. Please check the logs for more info."
msgstr ""
msgid "Snippets|Files"
msgstr ""
@ -36953,6 +36980,9 @@ msgstr ""
msgid "User and IP rate limits"
msgstr ""
msgid "User created at"
msgstr ""
msgid "User does not have a pending request"
msgstr ""
@ -37313,6 +37343,15 @@ msgstr ""
msgid "Validate your GitLab CI configuration file"
msgstr ""
msgid "Validated at"
msgstr ""
msgid "Validated at:"
msgstr ""
msgid "Validated:"
msgstr ""
msgid "Validations failed."
msgstr ""
@ -40851,6 +40890,9 @@ msgstr ""
msgid "originating vulnerability"
msgstr ""
msgid "other card matches"
msgstr ""
msgid "out of %d total test"
msgid_plural "out of %d total tests"
msgstr[0] ""

View File

@ -98,7 +98,7 @@ function rspec_simple_job() {
}
function rspec_db_library_code() {
local db_files="spec/lib/gitlab/database/ spec/support/helpers/database/"
local db_files="spec/lib/gitlab/database/"
rspec_simple_job "-- ${db_files}"
}

View File

@ -1,7 +1,6 @@
import { useFakeDate } from 'helpers/fake_date';
import {
transformStagesForPathNavigation,
timeSummaryForPathNavigation,
medianTimeToParsedSeconds,
formatMedianValues,
filterStagesByHiddenStatus,
@ -47,21 +46,6 @@ describe('Value stream analytics utils', () => {
});
});
describe('timeSummaryForPathNavigation', () => {
it.each`
unit | value | result
${'months'} | ${1.5} | ${'1.5M'}
${'weeks'} | ${1.25} | ${'1.5w'}
${'days'} | ${2} | ${'2d'}
${'hours'} | ${10} | ${'10h'}
${'minutes'} | ${20} | ${'20m'}
${'seconds'} | ${10} | ${'<1m'}
${'seconds'} | ${0} | ${'-'}
`('will format $value $unit to $result', ({ unit, value, result }) => {
expect(timeSummaryForPathNavigation({ [unit]: value })).toBe(result);
});
});
describe('medianTimeToParsedSeconds', () => {
it.each`
value | result

View File

@ -118,3 +118,18 @@ describe('date_format_utility.js', () => {
});
});
});
describe('formatTimeAsSummary', () => {
it.each`
unit | value | result
${'months'} | ${1.5} | ${'1.5M'}
${'weeks'} | ${1.25} | ${'1.5w'}
${'days'} | ${2} | ${'2d'}
${'hours'} | ${10} | ${'10h'}
${'minutes'} | ${20} | ${'20m'}
${'seconds'} | ${10} | ${'<1m'}
${'seconds'} | ${0} | ${'-'}
`('will format $value $unit to $result', ({ unit, value, result }) => {
expect(utils.formatTimeAsSummary({ [unit]: value })).toBe(result);
});
});

View File

@ -41,19 +41,23 @@ describe('Snippet view app', () => {
},
});
}
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findEmbedDropdown = () => wrapper.findComponent(EmbedDropdown);
afterEach(() => {
wrapper.destroy();
});
it('renders loader while the query is in flight', () => {
createComponent({ loading: true });
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(true);
});
it('renders all simple components after the query is finished', () => {
it('renders all simple components required after the query is finished', () => {
createComponent();
expect(wrapper.find(SnippetHeader).exists()).toBe(true);
expect(wrapper.find(SnippetTitle).exists()).toBe(true);
expect(wrapper.findComponent(SnippetHeader).exists()).toBe(true);
expect(wrapper.findComponent(SnippetTitle).exists()).toBe(true);
});
it('renders embed dropdown component if visibility allows', () => {
@ -65,7 +69,7 @@ describe('Snippet view app', () => {
},
},
});
expect(wrapper.find(EmbedDropdown).exists()).toBe(true);
expect(findEmbedDropdown().exists()).toBe(true);
});
it('renders correct snippet-blob components', () => {
@ -98,7 +102,7 @@ describe('Snippet view app', () => {
},
},
});
expect(wrapper.find(EmbedDropdown).exists()).toBe(isRendered);
expect(findEmbedDropdown().exists()).toBe(isRendered);
});
});
@ -120,7 +124,7 @@ describe('Snippet view app', () => {
},
},
});
expect(wrapper.find(CloneDropdownButton).exists()).toBe(isRendered);
expect(wrapper.findComponent(CloneDropdownButton).exists()).toBe(isRendered);
},
);
});

View File

@ -1,23 +1,30 @@
import { GlButton, GlModal, GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetHeader, { i18n } from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
import axios from '~/lib/utils/axios_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
jest.mock('~/flash');
describe('Snippet header component', () => {
let wrapper;
let snippet;
let mutationTypes;
let mutationVariables;
let mock;
let errorMsg;
let err;
const originalRelativeUrlRoot = gon.relative_url_root;
const reportAbusePath = '/-/snippets/42/mark_as_spam';
const canReportSpam = true;
const GlEmoji = { template: '<img/>' };
@ -47,6 +54,7 @@ describe('Snippet header component', () => {
mocks: { $apollo },
provide: {
reportAbusePath,
canReportSpam,
...provide,
},
propsData: {
@ -118,10 +126,13 @@ describe('Snippet header component', () => {
RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
REJECT: jest.fn(() => Promise.reject(err)),
};
mock = new MockAdapter(axios);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
gon.relative_url_root = originalRelativeUrlRoot;
});
@ -186,7 +197,6 @@ describe('Snippet header component', () => {
{
category: 'primary',
disabled: false,
href: reportAbusePath,
text: 'Submit as spam',
variant: 'default',
},
@ -205,7 +215,6 @@ describe('Snippet header component', () => {
text: 'Delete',
},
{
href: reportAbusePath,
text: 'Submit as spam',
title: 'Submit as spam',
},
@ -249,6 +258,31 @@ describe('Snippet header component', () => {
);
});
describe('submit snippet as spam', () => {
beforeEach(async () => {
createComponent();
});
it.each`
request | variant | text
${200} | ${'SUCCESS'} | ${i18n.snippetSpamSuccess}
${500} | ${'DANGER'} | ${i18n.snippetSpamFailure}
`(
'renders a "$variant" flash message with "$text" message for a request with a "$request" response',
async ({ request, variant, text }) => {
const submitAsSpamBtn = findButtons().at(2);
mock.onPost(reportAbusePath).reply(request);
submitAsSpamBtn.trigger('click');
await waitForPromises();
expect(createFlash).toHaveBeenLastCalledWith({
message: expect.stringContaining(text),
type: FLASH_TYPES[variant],
});
},
);
});
describe('with guest user', () => {
beforeEach(() => {
createComponent({
@ -258,6 +292,7 @@ describe('Snippet header component', () => {
},
provide: {
reportAbusePath: null,
canReportSpam: false,
},
});
});

View File

@ -9,6 +9,7 @@ const DEFAULT_PROPS = {
username: 'root',
name: 'Administrator',
location: 'Vienna',
localTime: '2:30 PM',
bot: false,
bio: null,
workInformation: null,
@ -31,10 +32,11 @@ describe('User Popover Component', () => {
wrapper.destroy();
});
const findUserStatus = () => wrapper.find('.js-user-status');
const findUserStatus = () => wrapper.findByTestId('user-popover-status');
const findTarget = () => document.querySelector('.js-user-link');
const findUserName = () => wrapper.find(UserNameWithStatus);
const findSecurityBotDocsLink = () => wrapper.findByTestId('user-popover-bot-docs-link');
const findUserLocalTime = () => wrapper.findByTestId('user-popover-local-time');
const createWrapper = (props = {}, options = {}) => {
wrapper = mountExtended(UserPopover, {
@ -71,7 +73,6 @@ describe('User Popover Component', () => {
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.name);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.username);
expect(wrapper.text()).toContain(DEFAULT_PROPS.user.location);
});
it('shows icon for location', () => {
@ -164,6 +165,25 @@ describe('User Popover Component', () => {
});
});
describe('local time', () => {
it('should show local time when it is available', () => {
createWrapper();
expect(findUserLocalTime().exists()).toBe(true);
});
it('should not show local time when it is not available', () => {
const user = {
...DEFAULT_PROPS.user,
localTime: null,
};
createWrapper({ user });
expect(findUserLocalTime().exists()).toBe(false);
});
});
describe('status data', () => {
it('should show only message', () => {
const user = { ...DEFAULT_PROPS.user, status: { message_html: 'Hello World' } };
@ -256,5 +276,11 @@ describe('User Popover Component', () => {
const securityBotDocsLink = findSecurityBotDocsLink();
expect(securityBotDocsLink.text()).toBe('Learn more about %<>\';"');
});
it('does not display local time', () => {
createWrapper({ user: SECURITY_BOT_USER });
expect(findUserLocalTime().exists()).toBe(false);
});
});
});

View File

@ -3,15 +3,34 @@
require 'spec_helper'
RSpec.describe API::Entities::Clusters::AgentAuthorization do
let_it_be(:authorization) { create(:agent_group_authorization) }
subject { described_class.new(authorization).as_json }
it 'includes basic fields' do
expect(subject).to include(
id: authorization.agent_id,
config_project: a_hash_including(id: authorization.agent.project_id),
configuration: authorization.config
)
shared_examples 'generic authorization' do
it 'includes shared fields' do
expect(subject).to include(
id: authorization.agent_id,
config_project: a_hash_including(id: authorization.agent.project_id),
configuration: authorization.config
)
end
end
context 'project authorization' do
let(:authorization) { create(:agent_project_authorization) }
include_examples 'generic authorization'
end
context 'group authorization' do
let(:authorization) { create(:agent_group_authorization) }
include_examples 'generic authorization'
end
context 'implicit authorization' do
let(:agent) { create(:cluster_agent) }
let(:authorization) { Clusters::Agents::ImplicitAuthorization.new(agent: agent) }
include_examples 'generic authorization'
end
end

View File

@ -3,10 +3,13 @@
require 'spec_helper'
RSpec.describe API::Entities::User do
let(:user) { create(:user) }
let(:current_user) { create(:user) }
let_it_be(:timezone) { 'America/Los_Angeles' }
subject { described_class.new(user, current_user: current_user).as_json }
let(:user) { create(:user, timezone: timezone) }
let(:current_user) { create(:user) }
let(:entity) { described_class.new(user, current_user: current_user) }
subject { entity.as_json }
it 'exposes correct attributes' do
expect(subject).to include(:bio, :location, :public_email, :skype, :linkedin, :twitter, :website_url, :organization, :job_title, :work_information, :pronouns)
@ -35,4 +38,10 @@ RSpec.describe API::Entities::User do
expect(subject[:bot]).to eq(true)
end
end
it 'exposes local_time' do
local_time = '2:30 PM'
expect(entity).to receive(:local_time).with(timezone).and_return(local_time)
expect(subject[:local_time]).to eq(local_time)
end
end

View File

@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::GroupAuthorization do
it { is_expected.to belong_to(:group).class_name('::Group').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
describe '#config_project' do
let(:record) { create(:agent_group_authorization) }
it { expect(record.config_project).to eq(record.agent.project) }
end
end

View File

@ -9,6 +9,6 @@ RSpec.describe Clusters::Agents::ImplicitAuthorization do
it { expect(subject.agent).to eq(agent) }
it { expect(subject.agent_id).to eq(agent.id) }
it { expect(subject.project).to eq(agent.project) }
it { expect(subject.config_project).to eq(agent.project) }
it { expect(subject.config).to be_nil }
end

View File

@ -7,4 +7,10 @@ RSpec.describe Clusters::Agents::ProjectAuthorization do
it { is_expected.to belong_to(:project).class_name('Project').required }
it { expect(described_class).to validate_jsonb_schema(['config']) }
describe '#config_project' do
let(:record) { create(:agent_project_authorization) }
it { expect(record.config_project).to eq(record.agent.project) }
end
end

View File

@ -7,4 +7,19 @@ RSpec.describe Users::CreditCardValidation do
it { is_expected.to validate_length_of(:holder_name).is_at_most(26) }
it { is_expected.to validate_numericality_of(:last_digits).is_less_than_or_equal_to(9999) }
describe '.similar_records' do
let(:card_details) { subject.attributes.slice(:expiration_date, :last_digits, :holder_name) }
subject(:credit_card_validation) { create(:credit_card_validation) }
let!(:match1) { create(:credit_card_validation, card_details) }
let!(:other1) { create(:credit_card_validation, card_details.merge(last_digits: 9)) }
let!(:match2) { create(:credit_card_validation, card_details) }
let!(:other2) { create(:credit_card_validation, card_details.merge(holder_name: 'foo bar')) }
it 'returns records with matching credit card, ordered by credit_card_validated_at' do
expect(subject.similar_records).to eq([match2, match1, subject])
end
end
end