Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-08 15:15:55 +00:00
parent 6728ed6fe2
commit cce7638874
30 changed files with 576 additions and 244 deletions

View File

@ -2,7 +2,7 @@
import { GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Tracking from '~/tracking';
import { __ } from '~/locale';
import { __, s__ } from '~/locale';
import {
TRACK_TOGGLE_TRAINING_PROVIDER_ACTION,
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
@ -10,9 +10,12 @@ import {
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
} from '~/security_configuration/constants';
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/utils/optimistic_response';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import {
updateSecurityTrainingCache,
updateSecurityTrainingOptimisticResponse,
} from '~/security_configuration/graphql/cache_utils';
const i18n = {
providerQueryErrorMessage: __(
@ -21,6 +24,7 @@ const i18n = {
configMutationErrorMessage: __(
'Could not save configuration. Please refresh the page, or try again later.',
),
primaryTraining: s__('SecurityTraining|Primary Training'),
};
export default {
@ -57,6 +61,9 @@ export default {
};
},
computed: {
enabledProviders() {
return this.securityTrainingProviders.filter(({ isEnabled }) => isEnabled);
},
isLoading() {
return this.$apollo.queries.securityTrainingProviders.loading;
},
@ -91,14 +98,42 @@ export default {
Sentry.captureException(e);
}
},
toggleProvider(provider) {
const { isEnabled } = provider;
async toggleProvider(provider) {
const { isEnabled, isPrimary } = provider;
const toggledIsEnabled = !isEnabled;
this.trackProviderToggle(provider.id, toggledIsEnabled);
this.storeProvider({ ...provider, isEnabled: toggledIsEnabled });
// when the current primary provider gets disabled then set the first enabled to be the new primary
if (!toggledIsEnabled && isPrimary && this.enabledProviders.length > 1) {
const firstOtherEnabledProvider = this.enabledProviders.find(
({ id }) => id !== provider.id,
);
this.setPrimaryProvider(firstOtherEnabledProvider);
}
this.storeProvider({
...provider,
isEnabled: toggledIsEnabled,
});
},
async storeProvider({ id, isEnabled, isPrimary }) {
setPrimaryProvider(provider) {
this.storeProvider({ ...provider, isPrimary: true });
},
async storeProvider(provider) {
const { id, isEnabled, isPrimary } = provider;
let nextIsPrimary = isPrimary;
// if the current provider has been disabled it can't be primary
if (!isEnabled) {
nextIsPrimary = false;
}
// if the current provider is the only enabled provider it should be primary
if (isEnabled && !this.enabledProviders.length) {
nextIsPrimary = true;
}
try {
const {
data: {
@ -111,13 +146,17 @@ export default {
projectPath: this.projectFullPath,
providerId: id,
isEnabled,
isPrimary,
isPrimary: nextIsPrimary,
},
},
optimisticResponse: updateSecurityTrainingOptimisticResponse({
id,
isEnabled,
isPrimary,
isPrimary: nextIsPrimary,
}),
update: updateSecurityTrainingCache({
query: securityTrainingProvidersQuery,
variables: { fullPath: this.projectFullPath },
}),
});
@ -188,6 +227,27 @@ export default {
{{ __('Learn more.') }}
</gl-link>
</p>
<!-- Note: The following `div` and it's content will be replaced by 'GlFormRadio' once https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1720#note_857342988 is resolved -->
<div
class="gl-form-radio custom-control custom-radio"
data-testid="primary-provider-radio"
>
<input
:id="`security-training-provider-${provider.id}`"
type="radio"
:checked="provider.isPrimary"
name="radio-group-name"
class="custom-control-input"
:disabled="!provider.isEnabled"
@change="setPrimaryProvider(provider)"
/>
<label
class="custom-control-label"
:for="`security-training-provider-${provider.id}`"
>
{{ $options.i18n.primaryTraining }}
</label>
</div>
</div>
</div>
</gl-card>

View File

@ -0,0 +1,40 @@
import produce from 'immer';
export const updateSecurityTrainingOptimisticResponse = (changes) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
securityTrainingUpdate: {
__typename: 'SecurityTrainingUpdatePayload',
training: {
__typename: 'ProjectSecurityTraining',
...changes,
},
errors: [],
},
});
export const updateSecurityTrainingCache = ({ query, variables }) => (cache, { data }) => {
const {
securityTrainingUpdate: { training: updatedProvider },
} = data;
const { project } = cache.readQuery({ query, variables });
if (!updatedProvider.isPrimary) {
return;
}
// when we set a new primary provider, we need to unset the previous one(s)
const updatedProject = produce(project, (draft) => {
draft.securityTrainingProviders.forEach((provider) => {
// eslint-disable-next-line no-param-reassign
provider.isPrimary = provider.id === updatedProvider.id;
});
});
// write to the cache
cache.writeQuery({
query,
variables,
data: { project: updatedProject },
});
};

View File

@ -1,13 +0,0 @@
export const updateSecurityTrainingOptimisticResponse = (changes) => ({
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation',
securityTrainingUpdate: {
__typename: 'SecurityTrainingUpdatePayload',
training: {
__typename: 'ProjectSecurityTraining',
...changes,
},
errors: [],
},
});

View File

@ -127,7 +127,7 @@ module MarkupHelper
text = wiki_page.content
return '' unless text.present?
context = render_wiki_content_context(@wiki, wiki_page, context)
context = render_wiki_content_context(wiki_page.wiki, wiki_page, context)
html = markup_unsafe(wiki_page.path, text, context)
prepare_for_rendering(html, context)

View File

@ -1,4 +1,3 @@
.js-groups-list-holder
#js-groups-tree{ data: { hide_projects: 'true', endpoint: explore_groups_path(format: :json), path: explore_groups_path, form_sel: 'form#group-filter-form', filter_sel: '.js-groups-list-filter', holder_sel: '.js-groups-list-holder', dropdown_sel: '.js-group-filter-dropdown-wrap' } }
.loading-container.text-center.prepend-top-20
.gl-spinner.gl-spinner-md
= gl_loading_icon(size: 'md', css_class: 'gl-mt-6')

View File

@ -9,6 +9,7 @@ type: reference, api
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/212199) in GitLab 13.5.
> - The `encoding` field was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81150) in GitLab 14.9.
> - The `render_html` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
The [group wikis](../user/project/wiki/group.md) API is available only in APIv4.
An API for [project wikis](wikis.md) is also available.
@ -69,6 +70,7 @@ GET /groups/:id/wikis/:slug
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the group](index.md#namespaced-path-encoding) |
| `slug` | string | yes | URL-encoded slug (a unique string) of the wiki page, such as `dir%2Fpage_name` |
| `render_html` | boolean | no | Return the rendered HTML of the wiki page |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/groups/1/wikis/home"

View File

@ -6,7 +6,8 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Project wikis API **(FREE)**
> The `encoding` field was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81150) in GitLab 14.9.
> - The `encoding` field was [added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81150) in GitLab 14.9.
> - The `render_html` attribute was [added](https://gitlab.com/gitlab-org/gitlab/-/issues/336792) in GitLab 14.9.
The project [wikis](../user/project/wiki/index.md) API is available only in APIv4.
An API for [group wikis](group_wikis.md) is also available.
@ -67,6 +68,7 @@ GET /projects/:id/wikis/:slug
| --------- | ------- | -------- | --------------------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](index.md#namespaced-path-encoding) |
| `slug` | string | yes | URLencoded slug (a unique string) of the wiki page, such as `dir%2Fpage_name` |
| `render_html` | boolean | no | Return the rendered HTML of the wiki page |
```shell
curl --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/1/wikis/home"

View File

@ -66,8 +66,8 @@ git cherry-pick -m 2 7a39eb0
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/21268) in GitLab 13.11 behind a [feature flag](../../feature_flags.md), disabled by default.
> - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/324154) in GitLab 14.0.
You can use the GitLab UI to cherry-pick merge requests into a project, even if the
merge request is from a fork:
You can cherry-pick merge requests from the same project, or forks of the same
project, from the GitLab user interface:
1. In the merge request's secondary menu, click **Commits** to display the commit details page.
1. Click on the **Options** dropdown and select **Cherry-pick** to show the cherry-pick modal.

View File

@ -3,7 +3,11 @@
module API
module Entities
class WikiPage < WikiPageBasic
expose :content
include ::MarkupHelper
expose :content do |wiki_page, options|
options[:render_html] ? render_wiki_content(wiki_page) : wiki_page.content
end
expose :encoding do |wiki_page|
wiki_page.content.encoding.name

View File

@ -45,11 +45,12 @@ module API
end
params do
requires :slug, type: String, desc: 'The slug of a wiki page'
optional :render_html, type: Boolean, default: false, desc: 'Render content to HTML'
end
get ':id/wikis/:slug' do
authorize! :read_wiki, container
present wiki_page, with: Entities::WikiPage
present wiki_page, with: Entities::WikiPage, render_html: params[:render_html]
end
desc 'Create a wiki page' do

View File

@ -11,9 +11,6 @@ module Gitlab
module Ldap
class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
def save
super('LDAP')
end
# instance methods
def find_user
@ -44,6 +41,10 @@ module Gitlab
def auth_hash=(auth_hash)
@auth_hash = Gitlab::Auth::Ldap::AuthHash.new(auth_hash)
end
def protocol_name
'LDAP'
end
end
end
end

View File

@ -46,7 +46,7 @@ module Gitlab
valid? && persisted?
end
def save(provider = 'OAuth')
def save(provider = protocol_name)
raise SigninDisabledForProviderError if oauth_provider_disabled?
raise SignupDisabledError unless gl_user
@ -96,6 +96,10 @@ module Gitlab
end
end
def protocol_name
'OAuth'
end
protected
def should_save?

View File

@ -11,10 +11,6 @@ module Gitlab
class User < Gitlab::Auth::OAuth::User
extend ::Gitlab::Utils::Override
def save
super('SAML')
end
def find_user
user = find_by_uid_and_provider
@ -40,6 +36,10 @@ module Gitlab
saml_config.upstream_two_factor_authn_contexts&.include?(auth_hash.authn_context)
end
def protocol_name
'SAML'
end
protected
def saml_config

View File

@ -32827,6 +32827,9 @@ msgstr ""
msgid "SecurityReports|Change status"
msgstr ""
msgid "SecurityReports|Check the messages generated while parsing the following security reports, as they may prevent the results from being ingested by GitLab. Ensure the security report conforms to a supported %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgstr ""
msgid "SecurityReports|Comment added to '%{vulnerabilityName}'"
msgstr ""
@ -33013,7 +33016,7 @@ msgstr ""
msgid "SecurityReports|The Vulnerability Report shows the results of the latest successful pipeline on your project's default branch, as well as vulnerabilities from your latest container scan. %{linkStart}Learn more.%{linkEnd}"
msgstr ""
msgid "SecurityReports|The security reports below contain one or more vulnerability findings that could not be parsed and were not recorded. Download the artifacts in the job output to investigate. Ensure any security report created conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgid "SecurityReports|The following security reports contain one or more vulnerability findings that could not be parsed and were not recorded. To investigate a report, download the artifacts in the job output. Ensure the security report conforms to the relevant %{helpPageLinkStart}JSON schema%{helpPageLinkEnd}."
msgstr ""
msgid "SecurityReports|There was an error adding the comment."
@ -33073,6 +33076,9 @@ msgstr ""
msgid "SecurityReports|Vulnerability Report"
msgstr ""
msgid "SecurityReports|Warning parsing security reports"
msgstr ""
msgid "SecurityReports|While it's rare to have no vulnerabilities for your pipeline, it can happen. In any event, we ask that you double check your settings to make sure all security scanning jobs have passed successfully."
msgstr ""
@ -33094,6 +33100,9 @@ msgstr ""
msgid "SecurityReports|scanned resources"
msgstr ""
msgid "SecurityTraining|Primary Training"
msgstr ""
msgid "See example DevOps Score page in our documentation."
msgstr ""

View File

@ -8,12 +8,11 @@ module QA
AuthorizationError = Class.new(RuntimeError)
def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil, ip_limits: false)
def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil)
@address = address
@personal_access_token = personal_access_token
@is_new_session = is_new_session
@user = user
enable_ip_limits if ip_limits
end
# Personal access token
@ -68,24 +67,6 @@ module QA
private
def enable_ip_limits
Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
Runtime::Browser.visit(@address, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_admin_credentials)
Page::Main::Menu.perform(&:go_to_admin_area)
Page::Admin::Menu.perform(&:go_to_network_settings)
Page::Admin::Settings::Network.perform do |setting|
setting.expand_ip_limits do |page|
page.enable_throttles
page.save_settings
end
end
Page::Main::Menu.perform(&:sign_out)
end
# Create PAT
#
# Use api if admin personal access token is present and skip any UI actions otherwise perform creation via UI

View File

@ -1,19 +1,41 @@
# frozen_string_literal: true
require 'airborne'
module QA
RSpec.describe 'Manage with IP rate limits', :requires_admin, :skip_live_env do
describe 'Users API' do
let(:api_client) { Runtime::API::Client.new(:gitlab, ip_limits: true) }
let(:request) { Runtime::API::Request.new(api_client, '/users') }
RSpec.describe 'Manage', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do
describe 'rate limits' do
let(:rate_limited_user) { Resource::User.fabricate_via_api! }
let(:api_client) { Runtime::API::Client.new(:gitlab, user: rate_limited_user) }
let!(:request) { Runtime::API::Request.new(api_client, '/users') }
it 'GET /users', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347881' do
5.times do
get request.url
expect_status(200)
after do
rate_limited_user.remove_via_api!
end
it 'throttles authenticated api requests by user', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347881' do
with_application_settings(
throttle_authenticated_api_requests_per_period: 5,
throttle_authenticated_api_period_in_seconds: 60,
throttle_authenticated_api_enabled: true
) do
5.times do
res = RestClient.get request.url
expect(res.code).to be(200)
end
expect { RestClient.get request.url }.to raise_error do |e|
expect(e.class).to be(RestClient::TooManyRequests)
end
end
end
end
private
def with_application_settings(**hargs)
QA::Runtime::ApplicationSettings.set_application_settings(**hargs)
yield
ensure
QA::Runtime::ApplicationSettings.restore_application_settings(*hargs.keys)
end
end
end

View File

@ -151,7 +151,7 @@ FactoryBot.define do
transient do
extern_uid { '123456' }
provider { 'ldapmain' }
provider { 'twitter' }
end
after(:create) do |user, evaluator|
@ -166,6 +166,12 @@ FactoryBot.define do
user.identities << create(:identity, identity_attrs)
end
trait :ldap do
transient do
provider { 'ldapmain' }
end
end
end
factory :atlassian_user do

View File

@ -1,8 +1,8 @@
import * as Sentry from '@sentry/browser';
import { GlAlert, GlLink, GlToggle, GlCard, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import {
@ -12,7 +12,7 @@ import {
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
} from '~/security_configuration/constants';
import TrainingProviderList from '~/security_configuration/components/training_provider_list.vue';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/utils/optimistic_response';
import { updateSecurityTrainingOptimisticResponse } from '~/security_configuration/graphql/cache_utils';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
import configureSecurityTrainingProvidersMutation from '~/security_configuration/graphql/configure_security_training_providers.mutation.graphql';
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
@ -20,8 +20,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import {
dismissUserCalloutResponse,
dismissUserCalloutErrorResponse,
securityTrainingProviders,
securityTrainingProvidersResponse,
getSecurityTrainingProvidersData,
updateSecurityTrainingProvidersResponse,
updateSecurityTrainingProvidersErrorResponse,
testProjectPath,
@ -30,6 +29,19 @@ import {
Vue.use(VueApollo);
const TEST_TRAINING_PROVIDERS_ALL_DISABLED = getSecurityTrainingProvidersData();
const TEST_TRAINING_PROVIDERS_FIRST_ENABLED = getSecurityTrainingProvidersData({
providerOverrides: { first: { isEnabled: true, isPrimary: true } },
});
const TEST_TRAINING_PROVIDERS_ALL_ENABLED = getSecurityTrainingProvidersData({
providerOverrides: {
first: { isEnabled: true, isPrimary: true },
second: { isEnabled: true, isPrimary: false },
third: { isEnabled: true, isPrimary: false },
},
});
const TEST_TRAINING_PROVIDERS_DEFAULT = TEST_TRAINING_PROVIDERS_ALL_DISABLED;
describe('TrainingProviderList component', () => {
let wrapper;
let apolloProvider;
@ -38,7 +50,7 @@ describe('TrainingProviderList component', () => {
const defaultHandlers = [
[
securityTrainingProvidersQuery,
jest.fn().mockResolvedValue(securityTrainingProvidersResponse),
jest.fn().mockResolvedValue(TEST_TRAINING_PROVIDERS_DEFAULT.response),
],
[
configureSecurityTrainingProvidersMutation,
@ -53,7 +65,7 @@ describe('TrainingProviderList component', () => {
};
const createComponent = () => {
wrapper = shallowMount(TrainingProviderList, {
wrapper = shallowMountExtended(TrainingProviderList, {
provide: {
projectFullPath: testProjectPath,
},
@ -68,6 +80,7 @@ describe('TrainingProviderList component', () => {
const findLinks = () => wrapper.findAllComponents(GlLink);
const findToggles = () => wrapper.findAllComponents(GlToggle);
const findFirstToggle = () => findToggles().at(0);
const findPrimaryProviderRadios = () => wrapper.findAllByTestId('primary-provider-radio');
const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findErrorAlert = () => wrapper.findComponent(GlAlert);
@ -107,7 +120,7 @@ describe('TrainingProviderList component', () => {
Mutation: {
configureSecurityTrainingProviders: () => ({
errors: [],
securityTrainingProviders: [],
TEST_TRAINING_PROVIDERS_DEFAULT: [],
}),
},
},
@ -122,33 +135,48 @@ describe('TrainingProviderList component', () => {
});
it('renders correct amount of cards', () => {
expect(findCards()).toHaveLength(securityTrainingProviders.length);
expect(findCards()).toHaveLength(TEST_TRAINING_PROVIDERS_DEFAULT.data.length);
});
securityTrainingProviders.forEach(({ name, description, url, isEnabled }, index) => {
it(`shows the name for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(name);
});
it(`shows the description for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(description);
});
it(`shows the learn more link for card ${index}`, () => {
expect(findLinks().at(index).attributes()).toEqual({
target: '_blank',
href: url,
TEST_TRAINING_PROVIDERS_DEFAULT.data.forEach(
({ name, description, url, isEnabled }, index) => {
it(`shows the name for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(name);
});
});
it(`shows the toggle with the correct value for card ${index}`, () => {
expect(findToggles().at(index).props('value')).toEqual(isEnabled);
});
it(`shows the description for card ${index}`, () => {
expect(findCards().at(index).text()).toContain(description);
});
it('does not show loader when query is populated', () => {
expect(findLoader().exists()).toBe(false);
});
});
it(`shows the learn more link for card ${index}`, () => {
expect(findLinks().at(index).attributes()).toEqual({
target: '_blank',
href: url,
});
});
it(`shows the toggle with the correct value for card ${index}`, () => {
expect(findToggles().at(index).props('value')).toEqual(isEnabled);
});
it(`shows a radio button to select the provider as primary within card ${index}`, () => {
const primaryProviderRadioForCurrentCard = findPrimaryProviderRadios().at(index);
// if the given provider is not enabled it should not be possible select it as primary
expect(primaryProviderRadioForCurrentCard.find('input').attributes('disabled')).toBe(
isEnabled ? undefined : 'disabled',
);
expect(primaryProviderRadioForCurrentCard.text()).toBe(
TrainingProviderList.i18n.primaryTraining,
);
});
it('does not show loader when query is populated', () => {
expect(findLoader().exists()).toBe(false);
});
},
);
});
describe('storing training provider settings', () => {
@ -168,7 +196,7 @@ describe('TrainingProviderList component', () => {
input: {
providerId: testProviderIds[0],
isEnabled: true,
isPrimary: false,
isPrimary: true,
projectPath: testProjectPath,
},
},
@ -178,9 +206,9 @@ describe('TrainingProviderList component', () => {
it('returns an optimistic response when calling the mutation', () => {
const optimisticResponse = updateSecurityTrainingOptimisticResponse({
id: securityTrainingProviders[0].id,
id: TEST_TRAINING_PROVIDERS_DEFAULT.data[0].id,
isEnabled: true,
isPrimary: false,
isPrimary: true,
});
expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith(
@ -243,7 +271,7 @@ describe('TrainingProviderList component', () => {
// Once https://gitlab.com/gitlab-org/gitlab/-/issues/348985 and https://gitlab.com/gitlab-org/gitlab/-/merge_requests/79492
// are merged this will be much easer to do and should be tackled then.
expect(trackingSpy).toHaveBeenCalledWith(undefined, TRACK_TOGGLE_TRAINING_PROVIDER_ACTION, {
property: securityTrainingProviders[0].id,
property: TEST_TRAINING_PROVIDERS_DEFAULT.data[0].id,
label: TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
extra: {
providerIsEnabled: true,
@ -253,7 +281,7 @@ describe('TrainingProviderList component', () => {
it(`tracks when a provider's "Learn more" link is clicked`, () => {
const firstProviderLink = findLinks().at(0);
const [{ id: firstProviderId }] = securityTrainingProviders;
const [{ id: firstProviderId }] = TEST_TRAINING_PROVIDERS_DEFAULT.data;
expect(trackingSpy).not.toHaveBeenCalled();
@ -271,6 +299,37 @@ describe('TrainingProviderList component', () => {
});
});
describe('primary provider settings', () => {
it.each`
description | initialProviderData | expectedMutationInput
${'sets the provider to be non-primary when it gets disabled'} | ${TEST_TRAINING_PROVIDERS_FIRST_ENABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_FIRST_ENABLED.data[0].id, isEnabled: false, isPrimary: false }}
${'sets a provider to be primary when it is the only one enabled'} | ${TEST_TRAINING_PROVIDERS_ALL_DISABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_ALL_DISABLED.data[0].id, isEnabled: true, isPrimary: true }}
${'sets the first other enabled provider to be primary when the primary one gets disabled'} | ${TEST_TRAINING_PROVIDERS_ALL_ENABLED.response} | ${{ providerId: TEST_TRAINING_PROVIDERS_ALL_ENABLED.data[1].id, isEnabled: true, isPrimary: true }}
`('$description', async ({ initialProviderData, expectedMutationInput }) => {
createApolloProvider({
handlers: [
[securityTrainingProvidersQuery, jest.fn().mockResolvedValue(initialProviderData)],
],
});
jest.spyOn(apolloProvider.defaultClient, 'mutate');
createComponent();
await waitForQueryToBeLoaded();
await toggleFirstProvider();
expect(apolloProvider.defaultClient.mutate).toHaveBeenNthCalledWith(
1,
expect.objectContaining({
variables: {
input: expect.objectContaining({
...expectedMutationInput,
}),
},
}),
);
});
});
describe('with errors', () => {
const expectErrorAlertToExist = () => {
expect(findErrorAlert().props()).toMatchObject({

View File

@ -0,0 +1,108 @@
import {
updateSecurityTrainingCache,
updateSecurityTrainingOptimisticResponse,
} from '~/security_configuration/graphql/cache_utils';
describe('EE - Security configuration graphQL cache utils', () => {
describe('updateSecurityTrainingOptimisticResponse', () => {
it('returns an optimistic response in the correct shape', () => {
const changes = { isEnabled: true, isPrimary: true };
const mutationResponse = updateSecurityTrainingOptimisticResponse(changes);
expect(mutationResponse).toEqual({
__typename: 'Mutation',
securityTrainingUpdate: {
__typename: 'SecurityTrainingUpdatePayload',
training: {
__typename: 'ProjectSecurityTraining',
...changes,
},
errors: [],
},
});
});
});
describe('updateSecurityTrainingCache', () => {
let mockCache;
beforeEach(() => {
// freezing the data makes sure that we don't mutate the original project
const mockCacheData = Object.freeze({
project: {
securityTrainingProviders: [
{ id: 1, isEnabled: true, isPrimary: true },
{ id: 2, isEnabled: true, isPrimary: false },
{ id: 3, isEnabled: false, isPrimary: false },
],
},
});
mockCache = {
readQuery: () => mockCacheData,
writeQuery: jest.fn(),
};
});
it('does not update the cache when the primary provider is not getting disabled', () => {
const providerAfterUpdate = {
id: 2,
isEnabled: true,
isPrimary: false,
};
updateSecurityTrainingCache({
query: 'GraphQL query',
variables: { fullPath: 'gitlab/project' },
})(mockCache, {
data: {
securityTrainingUpdate: {
training: {
...providerAfterUpdate,
},
},
},
});
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});
it('sets the previous primary provider to be non-primary when another provider gets set as primary', () => {
const providerAfterUpdate = {
id: 2,
isEnabled: true,
isPrimary: true,
};
const expectedTrainingProvidersWrittenToCache = [
// this was the previous primary primary provider and it should not be primary any longer
{ id: 1, isEnabled: true, isPrimary: false },
{ id: 2, isEnabled: true, isPrimary: true },
{ id: 3, isEnabled: false, isPrimary: false },
];
updateSecurityTrainingCache({
query: 'GraphQL query',
variables: { fullPath: 'gitlab/project' },
})(mockCache, {
data: {
securityTrainingUpdate: {
training: {
...providerAfterUpdate,
},
},
},
});
expect(mockCache.writeQuery).toHaveBeenCalledWith(
expect.objectContaining({
data: {
project: {
securityTrainingProviders: expectedTrainingProvidersWrittenToCache,
},
},
}),
);
});
});
});

View File

@ -1,8 +1,8 @@
export const testProjectPath = 'foo/bar';
export const testProviderIds = [101, 102];
export const testProviderIds = [101, 102, 103];
export const securityTrainingProviders = [
const createSecurityTrainingProviders = ({ providerOverrides = {} }) => [
{
id: testProviderIds[0],
name: 'Vendor Name 1',
@ -10,33 +10,43 @@ export const securityTrainingProviders = [
url: 'https://www.example.org/security/training',
isEnabled: false,
isPrimary: false,
...providerOverrides.first,
},
{
id: testProviderIds[1],
name: 'Vendor Name 2',
description: 'Security training with guide and learning pathways.',
url: 'https://www.vendornametwo.com/',
isEnabled: true,
isEnabled: false,
isPrimary: false,
...providerOverrides.second,
},
{
id: testProviderIds[2],
name: 'Vendor Name 3',
description: 'Security training for the everyday developer.',
url: 'https://www.vendornamethree.com/',
isEnabled: false,
isPrimary: false,
...providerOverrides.third,
},
];
export const securityTrainingProvidersResponse = {
data: {
project: {
id: 1,
securityTrainingProviders,
export const getSecurityTrainingProvidersData = (providerOverrides = {}) => {
const securityTrainingProviders = createSecurityTrainingProviders(providerOverrides);
const response = {
data: {
project: {
id: 1,
securityTrainingProviders,
},
},
},
};
};
export const disabledSecurityTrainingProvidersResponse = {
data: {
project: {
id: 1,
securityTrainingProviders: [securityTrainingProviders[0]],
},
},
return {
response,
data: securityTrainingProviders,
};
};
export const dismissUserCalloutResponse = {

View File

@ -315,33 +315,26 @@ RSpec.describe MarkupHelper do
end
describe '#render_wiki_content' do
let(:wiki) { double('WikiPage', path: "file.#{extension}") }
let(:wiki_repository) { double('Repository') }
let(:wiki) { build(:wiki, container: project) }
let(:content) { 'wiki content' }
let(:slug) { 'nested/page' }
let(:wiki_page) { double('WikiPage', path: "file.#{extension}", content: content, slug: slug, wiki: wiki) }
let(:context) do
{
pipeline: :wiki, project: project, wiki: wiki,
page_slug: 'nested/page', issuable_reference_expansion_enabled: true,
repository: wiki_repository
page_slug: slug, issuable_reference_expansion_enabled: true,
repository: wiki.repository
}
end
before do
expect(wiki).to receive(:content).and_return(content)
expect(wiki).to receive(:slug).and_return('nested/page')
expect(wiki).to receive(:repository).and_return(wiki_repository)
allow(wiki).to receive(:container).and_return(project)
helper.instance_variable_set(:@wiki, wiki)
end
context 'when file is Markdown' do
let(:extension) { 'md' }
it 'renders using #markdown_unsafe helper method' do
expect(helper).to receive(:markdown_unsafe).with('wiki content', context)
helper.render_wiki_content(wiki)
helper.render_wiki_content(wiki_page)
end
context 'when context has labels' do
@ -350,7 +343,7 @@ RSpec.describe MarkupHelper do
let(:content) { '~Bug' }
it 'renders label' do
result = helper.render_wiki_content(wiki)
result = helper.render_wiki_content(wiki_page)
doc = Nokogiri::HTML.parse(result)
expect(doc.css('.gl-label-link')).not_to be_empty
@ -366,7 +359,7 @@ RSpec.describe MarkupHelper do
end
it 'renders uploads relative to project' do
result = helper.render_wiki_content(wiki)
result = helper.render_wiki_content(wiki_page)
expect(result).to include("#{project.full_path}#{upload_link}")
end
@ -379,7 +372,7 @@ RSpec.describe MarkupHelper do
it 'renders using Gitlab::Asciidoc' do
expect(Gitlab::Asciidoc).to receive(:render)
helper.render_wiki_content(wiki)
helper.render_wiki_content(wiki_page)
end
end
@ -398,7 +391,7 @@ FooBar
it 'renders using #markdown_unsafe helper method' do
expect(helper).to receive(:markdown_unsafe).with(content, context)
result = helper.render_wiki_content(wiki)
result = helper.render_wiki_content(wiki_page)
expect(result).to be_empty
end
@ -410,7 +403,7 @@ FooBar
it 'renders all other formats using Gitlab::OtherMarkup' do
expect(Gitlab::OtherMarkup).to receive(:render)
helper.render_wiki_content(wiki)
helper.render_wiki_content(wiki_page)
end
end
end

View File

@ -5,7 +5,10 @@ require 'spec_helper'
RSpec.describe API::Entities::WikiPage do
let_it_be_with_reload(:wiki_page) { create(:wiki_page) }
let(:entity) { described_class.new(wiki_page) }
let(:params) { {} }
let(:entity) { described_class.new(wiki_page, params) }
subject { entity.as_json }
it 'returns the proper encoding for the wiki page content' do
expect(entity.as_json[:encoding]).to eq 'UTF-8'
@ -14,4 +17,26 @@ RSpec.describe API::Entities::WikiPage do
expect(entity.as_json[:encoding]).to eq 'ISO-8859-1'
end
it 'returns the raw wiki page content' do
expect(subject[:content]).to eq wiki_page.content
end
context 'when render_html param is passed' do
context 'when it is true' do
let(:params) { { render_html: true } }
it 'returns the wiki page content rendered' do
expect(subject[:content]).to eq "<p data-sourcepos=\"1:1-1:#{wiki_page.content.size}\" dir=\"auto\">#{wiki_page.content}</p>"
end
end
context 'when it is false' do
let(:params) { { render_html: false } }
it 'returns the raw wiki page content' do
expect(subject[:content]).to eq wiki_page.content
end
end
end
end

View File

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::Ldap::Access do
include LdapHelpers
let(:user) { create(:omniauth_user) }
let(:user) { create(:omniauth_user, :ldap) }
subject(:access) { described_class.new(user) }

View File

@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe Gitlab::Auth::Ldap::Authentication do
let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
let(:user) { create(:omniauth_user, extern_uid: Gitlab::Auth::Ldap::Person.normalize_dn(dn)) }
let(:user) { create(:omniauth_user, :ldap, extern_uid: Gitlab::Auth::Ldap::Person.normalize_dn(dn)) }
let(:login) { 'john' }
let(:password) { 'password' }

View File

@ -577,28 +577,66 @@ RSpec.describe Gitlab::Auth::OAuth::User do
stub_omniauth_config(allow_single_sign_on: ['twitter'])
end
context 'signup with omniauth only' do
context 'dont block on create' do
before do
stub_omniauth_config(block_auto_created_users: false)
shared_examples 'being blocked on creation' do
context 'when blocking on creation' do
it 'creates a blocked user' do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
it do
context 'when a sign up user cap has been set up but has not been reached yet' do
it 'still creates a blocked user' do
stub_application_setting(new_user_signups_cap: 999)
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
end
end
end
end
shared_examples 'not being blocked on creation' do
context 'when not blocking on creation' do
it 'creates a non-blocked user' do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end
context 'block on create' do
context 'signup with SAML' do
let(:provider) { 'saml' }
before do
stub_omniauth_config({
allow_single_sign_on: ['saml'],
auto_link_saml_user: true,
block_auto_created_users: block_auto_created_users
})
end
it_behaves_like 'being blocked on creation' do
let(:block_auto_created_users) { true }
end
it_behaves_like 'not being blocked on creation' do
let(:block_auto_created_users) { false }
end
end
context 'signup with omniauth only' do
it_behaves_like 'being blocked on creation' do
before do
stub_omniauth_config(block_auto_created_users: true)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
it_behaves_like 'not being blocked on creation' do
before do
stub_omniauth_config(block_auto_created_users: false)
end
end
end
@ -614,31 +652,19 @@ RSpec.describe Gitlab::Auth::OAuth::User do
end
context "and no account for the LDAP user" do
context 'dont block on create (LDAP)' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: false)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
it_behaves_like 'being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: true)
end
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).to be_blocked
it_behaves_like 'not being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: false)
end
end
end
end
@ -646,32 +672,20 @@ RSpec.describe Gitlab::Auth::OAuth::User do
context 'and LDAP user has an account already' do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
context 'dont block on create (LDAP)' do
it_behaves_like 'not being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: false)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
it_behaves_like 'not being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: true)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end
end
@ -682,56 +696,32 @@ RSpec.describe Gitlab::Auth::OAuth::User do
oauth_user.gl_user.activate
end
context 'dont block on create' do
it_behaves_like 'not being blocked on creation' do
before do
stub_omniauth_config(block_auto_created_users: false)
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create' do
it_behaves_like 'not being blocked on creation' do
before do
stub_omniauth_config(block_auto_created_users: true)
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'dont block on create (LDAP)' do
it_behaves_like 'not being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: false)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
context 'block on create (LDAP)' do
it_behaves_like 'not being blocked on creation' do
before do
allow_next_instance_of(Gitlab::Auth::Ldap::Config) do |instance|
allow(instance).to receive_messages(block_auto_created_users: true)
end
end
it do
oauth_user.save # rubocop:disable Rails/SaveBang
expect(gl_user).to be_valid
expect(gl_user).not_to be_blocked
end
end
end
end
@ -1057,4 +1047,10 @@ RSpec.describe Gitlab::Auth::OAuth::User do
expect(oauth_user.bypass_two_factor?).to be_falsey
end
end
describe '#protocol_name' do
it 'is OAuth' do
expect(oauth_user.protocol_name).to eq('OAuth')
end
end
end

View File

@ -3090,7 +3090,7 @@ RSpec.describe User do
describe '#ldap_identity' do
it 'returns ldap identity' do
user = create :omniauth_user
user = create(:omniauth_user, :ldap)
expect(user.ldap_identity.provider).not_to be_empty
end

View File

@ -11,6 +11,7 @@ RSpec.describe API::Users do
let(:blocked_user) { create(:user, :blocked) }
let(:omniauth_user) { create(:omniauth_user) }
let(:ldap_user) { create(:omniauth_user, provider: 'ldapmain') }
let(:ldap_blocked_user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') }
let(:private_user) { create(:user, private_profile: true) }
let(:deactivated_user) { create(:user, state: 'deactivated') }
@ -1293,10 +1294,10 @@ RSpec.describe API::Users do
end
it "updates user's existing identity" do
put api("/users/#{omniauth_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' }
put api("/users/#{ldap_user.id}", admin), params: { provider: 'ldapmain', extern_uid: '654321' }
expect(response).to have_gitlab_http_status(:ok)
expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
expect(ldap_user.reload.identities.first.extern_uid).to eq('654321')
end
it 'updates user with new identity' do

View File

@ -130,41 +130,42 @@ RSpec.describe API::Wikis do
describe 'GET /projects/:id/wikis/:slug' do
let(:page) { create(:wiki_page, wiki: project.wiki) }
let(:url) { "/projects/#{project.id}/wikis/#{page.slug}" }
let(:params) { {} }
subject(:request) { get api(url, user), params: params }
context 'when wiki is disabled' do
let(:project) { project_wiki_disabled }
before do
request
end
context 'when user is guest' do
before do
get api(url)
end
let(:user) { nil }
include_examples 'wiki API 404 Project Not Found'
end
context 'when user is developer' do
before do
get api(url, developer)
end
let(:user) { developer }
include_examples 'wiki API 403 Forbidden'
end
context 'when user is maintainer' do
before do
get api(url, maintainer)
end
let(:user) { maintainer }
include_examples 'wiki API 403 Forbidden'
end
end
context 'when wiki is available only for team members' do
let(:project) { create(:project, :wiki_repo, :wiki_private) }
let_it_be_with_reload(:project) { create(:project, :wiki_repo, :wiki_private) }
context 'when user is guest' do
before do
get api(url)
request
end
include_examples 'wiki API 404 Project Not Found'
@ -173,7 +174,8 @@ RSpec.describe API::Wikis do
context 'when user is developer' do
before do
project.add_developer(user)
get api(url, user)
request
end
include_examples 'wikis API returns wiki page'
@ -189,7 +191,7 @@ RSpec.describe API::Wikis do
before do
project.add_maintainer(user)
get api(url, user)
request
end
include_examples 'wikis API returns wiki page'
@ -203,11 +205,13 @@ RSpec.describe API::Wikis do
end
context 'when wiki is available for everyone with access' do
let(:project) { create(:project, :wiki_repo) }
let_it_be_with_reload(:project) { create(:project, :wiki_repo) }
context 'when user is guest' do
let(:user) { nil }
before do
get api(url)
request
end
include_examples 'wiki API 404 Project Not Found'
@ -217,7 +221,7 @@ RSpec.describe API::Wikis do
before do
project.add_developer(user)
get api(url, user)
request
end
include_examples 'wikis API returns wiki page'
@ -233,7 +237,7 @@ RSpec.describe API::Wikis do
before do
project.add_maintainer(user)
get api(url, user)
request
end
include_examples 'wikis API returns wiki page'

View File

@ -44,14 +44,32 @@ RSpec.shared_examples_for 'wikis API returns list of wiki pages' do
end
RSpec.shared_examples_for 'wikis API returns wiki page' do
it 'returns the wiki page' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(5)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(page.content)
expect(json_response['slug']).to eq(page.slug)
expect(json_response['title']).to eq(page.title)
expect(json_response['encoding']).to eq('UTF-8')
shared_examples 'returns wiki page' do
specify do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response.size).to eq(5)
expect(json_response.keys).to match_array(expected_keys_with_content)
expect(json_response['content']).to eq(expected_content)
expect(json_response['slug']).to eq(page.slug)
expect(json_response['title']).to eq(page.title)
end
end
let(:expected_content) { page.content }
it_behaves_like 'returns wiki page'
context 'when render param is false' do
let(:params) { { render_html: false } }
it_behaves_like 'returns wiki page'
end
context 'when render param is true' do
let(:params) { { render_html: true } }
let(:expected_content) { '<p data-sourcepos="1:1-1:21" dir="auto">Content for wiki page</p>' }
it_behaves_like 'returns wiki page'
end
end

View File

@ -31,7 +31,7 @@ RSpec.describe 'shared/wikis/_sidebar.html.haml' do
context 'The sidebar comes from a custom page' do
before do
assign(:sidebar_page, double('WikiPage', path: 'sidebar.md', slug: 'sidebar', content: 'Some sidebar content'))
assign(:sidebar_page, double('WikiPage', path: 'sidebar.md', slug: 'sidebar', content: 'Some sidebar content', wiki: wiki))
end
it 'does not show an alert' do