diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue index ff7d14ed95a..5bccd91e55e 100644 --- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue +++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue @@ -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.') }}

+ +
+ + +
diff --git a/app/assets/javascripts/security_configuration/graphql/cache_utils.js b/app/assets/javascripts/security_configuration/graphql/cache_utils.js new file mode 100644 index 00000000000..6d5258b01dc --- /dev/null +++ b/app/assets/javascripts/security_configuration/graphql/cache_utils.js @@ -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 }, + }); +}; diff --git a/app/assets/javascripts/security_configuration/graphql/utils/optimistic_response.js b/app/assets/javascripts/security_configuration/graphql/utils/optimistic_response.js deleted file mode 100644 index 8d1082d081a..00000000000 --- a/app/assets/javascripts/security_configuration/graphql/utils/optimistic_response.js +++ /dev/null @@ -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: [], - }, -}); diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb index f16d9f6325b..22243b606ed 100644 --- a/app/helpers/markup_helper.rb +++ b/app/helpers/markup_helper.rb @@ -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) diff --git a/app/views/explore/groups/_groups.html.haml b/app/views/explore/groups/_groups.html.haml index 0358fc524d3..bb2bd193565 100644 --- a/app/views/explore/groups/_groups.html.haml +++ b/app/views/explore/groups/_groups.html.haml @@ -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') diff --git a/doc/api/group_wikis.md b/doc/api/group_wikis.md index cff30589d0c..a4de537004d 100644 --- a/doc/api/group_wikis.md +++ b/doc/api/group_wikis.md @@ -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: " "https://gitlab.example.com/api/v4/groups/1/wikis/home" diff --git a/doc/api/wikis.md b/doc/api/wikis.md index e2c6c93f3cc..281ee414442 100644 --- a/doc/api/wikis.md +++ b/doc/api/wikis.md @@ -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: " "https://gitlab.example.com/api/v4/projects/1/wikis/home" diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md index 983de80c169..fb41ec3eff6 100644 --- a/doc/user/project/merge_requests/cherry_pick_changes.md +++ b/doc/user/project/merge_requests/cherry_pick_changes.md @@ -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. diff --git a/lib/api/entities/wiki_page.rb b/lib/api/entities/wiki_page.rb index 6944a5a785b..c6dba10e441 100644 --- a/lib/api/entities/wiki_page.rb +++ b/lib/api/entities/wiki_page.rb @@ -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 diff --git a/lib/api/wikis.rb b/lib/api/wikis.rb index fdce3c5ce18..fca690df916 100644 --- a/lib/api/wikis.rb +++ b/lib/api/wikis.rb @@ -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 diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb index d134350775d..56c2af1910e 100644 --- a/lib/gitlab/auth/ldap/user.rb +++ b/lib/gitlab/auth/ldap/user.rb @@ -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 diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb index 9f142727ebb..8b114d6d5d8 100644 --- a/lib/gitlab/auth/o_auth/user.rb +++ b/lib/gitlab/auth/o_auth/user.rb @@ -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? diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb index 205d5fe0015..d14da41deb6 100644 --- a/lib/gitlab/auth/saml/user.rb +++ b/lib/gitlab/auth/saml/user.rb @@ -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 diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 39789cdfd6e..d768d34a044 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -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 "" diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index b5b572890c1..5ca3b0c51f8 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -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 diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index 17ffb901e5a..fc221c963b1 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -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 diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 8764ac90af8..eb89cb0a40a 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -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 diff --git a/spec/frontend/security_configuration/components/training_provider_list_spec.js b/spec/frontend/security_configuration/components/training_provider_list_spec.js index 30dd47e6b09..2683c7665c8 100644 --- a/spec/frontend/security_configuration/components/training_provider_list_spec.js +++ b/spec/frontend/security_configuration/components/training_provider_list_spec.js @@ -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({ diff --git a/spec/frontend/security_configuration/graphql/cache_utils_spec.js b/spec/frontend/security_configuration/graphql/cache_utils_spec.js new file mode 100644 index 00000000000..a40611cc022 --- /dev/null +++ b/spec/frontend/security_configuration/graphql/cache_utils_spec.js @@ -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, + }, + }, + }), + ); + }); + }); +}); diff --git a/spec/frontend/security_configuration/mock_data.js b/spec/frontend/security_configuration/mock_data.js index c3807a20f42..8b9730d33da 100644 --- a/spec/frontend/security_configuration/mock_data.js +++ b/spec/frontend/security_configuration/mock_data.js @@ -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 = { diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index ab2f6fa5b7e..e5c48c0c9c4 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -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 diff --git a/spec/lib/api/entities/wiki_page_spec.rb b/spec/lib/api/entities/wiki_page_spec.rb index 934fd43ad88..de36ebc3a76 100644 --- a/spec/lib/api/entities/wiki_page_spec.rb +++ b/spec/lib/api/entities/wiki_page_spec.rb @@ -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 "

#{wiki_page.content}

" + 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 diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb index 9e269f84b7e..1fcdd678746 100644 --- a/spec/lib/gitlab/auth/ldap/access_spec.rb +++ b/spec/lib/gitlab/auth/ldap/access_spec.rb @@ -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) } diff --git a/spec/lib/gitlab/auth/ldap/authentication_spec.rb b/spec/lib/gitlab/auth/ldap/authentication_spec.rb index 42a893417d8..4b0e21da6c6 100644 --- a/spec/lib/gitlab/auth/ldap/authentication_spec.rb +++ b/spec/lib/gitlab/auth/ldap/authentication_spec.rb @@ -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' } diff --git a/spec/lib/gitlab/auth/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb index 8d36507ec7a..1a9e2f02de6 100644 --- a/spec/lib/gitlab/auth/o_auth/user_spec.rb +++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 0507bd4bb4d..c41f62e68d2 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb index 5427add25e9..7f5b6661695 100644 --- a/spec/requests/api/users_spec.rb +++ b/spec/requests/api/users_spec.rb @@ -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 diff --git a/spec/requests/api/wikis_spec.rb b/spec/requests/api/wikis_spec.rb index 7f44ba1d796..45b7a2090cd 100644 --- a/spec/requests/api/wikis_spec.rb +++ b/spec/requests/api/wikis_spec.rb @@ -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' diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb index 8008daf2eb9..697bfaca558 100644 --- a/spec/support/shared_examples/lib/wikis_api_examples.rb +++ b/spec/support/shared_examples/lib/wikis_api_examples.rb @@ -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) { '

Content for wiki page

' } + + it_behaves_like 'returns wiki page' end end diff --git a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb index bf050d601e3..0e7b657a154 100644 --- a/spec/views/shared/wikis/_sidebar.html.haml_spec.rb +++ b/spec/views/shared/wikis/_sidebar.html.haml_spec.rb @@ -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