diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 18:07:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-11-08 18:07:19 +0300 |
commit | a34d7fd9a723d6cc9c7348be2afe522bdc2be67f (patch) | |
tree | 5971e13ca0832ae06c599b3d5eec2e2fe71d884f /spec | |
parent | 5f89187f0433fc84d8387de25220185235d61ed1 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
21 files changed, 677 insertions, 104 deletions
diff --git a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb index 6bb791d2fd4..810c773de00 100644 --- a/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb +++ b/spec/controllers/groups/dependency_proxy_for_containers_controller_spec.rb @@ -60,6 +60,42 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: it { is_expected.to have_gitlab_http_status(:not_found) } end + context 'with invalid group access token' do + let_it_be(:user) { create(:user, :project_bot) } + + context 'not under the group' do + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'with sufficient scopes, but not active' do + context 'expired' do + let_it_be(:pat) do + create(:personal_access_token, :expired, user: user).tap do |pat| + pat.update_column(:scopes, Gitlab::Auth::REGISTRY_SCOPES) + end + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + + context 'revoked' do + let_it_be(:pat) do + create(:personal_access_token, :revoked, user: user).tap do |pat| + pat.update_column(:scopes, Gitlab::Auth::REGISTRY_SCOPES) + end + end + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + end + + context 'with insufficient scopes' do + let_it_be(:pat) { create(:personal_access_token, user: user, scopes: [Gitlab::Auth::READ_API_SCOPE]) } + + it { is_expected.to have_gitlab_http_status(:not_found) } + end + end + context 'with deploy token from a different group,' do let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } @@ -119,11 +155,7 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: end shared_examples 'authorize action with permission' do - context 'with a valid user' do - before do - group.add_guest(user) - end - + shared_examples 'sends Workhorse instructions' do it 'sends Workhorse local file instructions', :aggregate_failures do subject @@ -144,6 +176,32 @@ RSpec.describe Groups::DependencyProxyForContainersController, feature_category: expect(json_response['MaximumSize']).to eq(maximum_size) end end + + before do + group.add_guest(user) + end + + context 'with a valid user' do + it_behaves_like 'sends Workhorse instructions' + end + + context 'with a valid group access token' do + let_it_be(:user) { create(:user, :project_bot) } + let_it_be_with_reload(:token) { create(:personal_access_token, user: user) } + + before do + token.update_column(:scopes, Gitlab::Auth::REGISTRY_SCOPES) + end + + it_behaves_like 'sends Workhorse instructions' + end + + context 'with a deploy token' do + let_it_be(:user) { create(:deploy_token, :dependency_proxy_scopes, :group) } + let_it_be(:group_deploy_token) { create(:group_deploy_token, deploy_token: user, group: group) } + + it_behaves_like 'sends Workhorse instructions' + end end shared_examples 'namespace statistics refresh' do diff --git a/spec/finders/organizations/user_organizations_finder_spec.rb b/spec/finders/organizations/user_organizations_finder_spec.rb new file mode 100644 index 00000000000..71c9b861831 --- /dev/null +++ b/spec/finders/organizations/user_organizations_finder_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Organizations::UserOrganizationsFinder, '#execute', feature_category: :cell do + let_it_be(:admin) { create(:user, :admin) } + let_it_be(:organization_user) { create(:organization_user) } + let_it_be(:organization) { organization_user.organization } + let_it_be(:another_organization) { create(:organization) } + let_it_be(:another_user) { create(:user) } + + let(:current_user) { organization_user.user } + let(:target_user) { organization_user.user } + + subject(:finder) { described_class.new(current_user, target_user).execute } + + context 'when the current user has access to the organization' do + it { is_expected.to contain_exactly(organization) } + end + + context 'when the current user is an admin' do + let(:current_user) { admin } + + context 'when admin mode is enabled', :enable_admin_mode do + it { is_expected.to contain_exactly(organization) } + end + + context 'when admin mode is disabled' do + it { is_expected.to be_empty } + end + end + + context 'when the current user does not access to the organization' do + let(:current_user) { another_user } + + it { is_expected.to be_empty } + end + + context 'when the current user is nil' do + let(:current_user) { nil } + + it { is_expected.to be_empty } + end + + context 'when the target user is nil' do + let(:target_user) { nil } + + it { is_expected.to be_empty } + end +end diff --git a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js b/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js deleted file mode 100644 index 4f8126aaacf..00000000000 --- a/spec/frontend/analytics/product_analytics/components/activity_chart_spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { GlColumnChart } from '@gitlab/ui/dist/charts'; -import { shallowMount } from '@vue/test-utils'; -import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue'; - -describe('Activity Chart Bundle', () => { - let wrapper; - function mountComponent({ provide }) { - wrapper = shallowMount(ActivityChart, { - provide: { - formattedData: {}, - ...provide, - }, - }); - } - - const findChart = () => wrapper.findComponent(GlColumnChart); - const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]'); - - describe('Activity Chart', () => { - it('renders an warning message with no data', () => { - mountComponent({ provide: { formattedData: {} } }); - expect(findNoData().exists()).toBe(true); - }); - - it('renders a chart with data', () => { - mountComponent({ - provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } }, - }); - - expect(findNoData().exists()).toBe(false); - expect(findChart().exists()).toBe(true); - }); - }); -}); diff --git a/spec/frontend/import/details/components/import_details_table_spec.js b/spec/frontend/import/details/components/import_details_table_spec.js index aee8573eb02..e2ba0ddad17 100644 --- a/spec/frontend/import/details/components/import_details_table_spec.js +++ b/spec/frontend/import/details/components/import_details_table_spec.js @@ -2,6 +2,7 @@ import { mount, shallowMount } from '@vue/test-utils'; import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; +import { getParameterValues } from '~/lib/utils/url_utility'; import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status'; import { createAlert } from '~/alert'; import waitForPromises from 'helpers/wait_for_promises'; @@ -11,13 +12,32 @@ import ImportDetailsTable from '~/import/details/components/import_details_table import { mockImportFailures, mockHeaders } from '../mock_data'; jest.mock('~/alert'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + getParameterValues: jest.fn().mockReturnValue([]), +})); describe('Import details table', () => { let wrapper; let mock; - const createComponent = ({ mountFn = shallowMount, provide = {} } = {}) => { + const mockFields = [ + { + key: 'type', + label: 'Type', + }, + { + key: 'title', + label: 'Title', + }, + ]; + + const createComponent = ({ mountFn = shallowMount, props = {}, provide = {} } = {}) => { wrapper = mountFn(ImportDetailsTable, { + propsData: { + fields: mockFields, + ...props, + }, provide, }); }; @@ -109,5 +129,49 @@ describe('Import details table', () => { }); }); }); + + describe('when bulk_import is true', () => { + const mockId = 144; + const mockEntityId = 68; + + beforeEach(() => { + gon.api_version = 'v4'; + getParameterValues.mockReturnValueOnce([mockId]); + getParameterValues.mockReturnValueOnce([mockEntityId]); + + mock + .onGet(`/api/v4/bulk_imports/${mockId}/entities/${mockEntityId}/failures`) + .reply(HTTP_STATUS_OK, mockImportFailures, mockHeaders); + + createComponent({ + mountFn: mount, + props: { + bulkImport: true, + }, + }); + }); + + it('renders loading icon', () => { + expect(findGlLoadingIcon().exists()).toBe(true); + }); + + it('does not render loading icon after fetch', async () => { + await waitForPromises(); + + expect(findGlLoadingIcon().exists()).toBe(false); + }); + + it('sets items and pagination info', async () => { + await waitForPromises(); + + expect(findGlTableRows().length).toBe(mockImportFailures.length); + expect(findPaginationBar().props('pageInfo')).toMatchObject({ + page: mockHeaders['x-page'], + perPage: mockHeaders['x-per-page'], + total: mockHeaders['x-total'], + totalPages: mockHeaders['x-total-pages'], + }); + }); + }); }); }); diff --git a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js index 9b012995ea4..efefcdb20df 100644 --- a/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js +++ b/spec/frontend/projects/settings_service_desk/components/custom_email_form_spec.js @@ -1,11 +1,17 @@ import { mount } from '@vue/test-utils'; -import { GlLink } from '@gitlab/ui'; +import { GlFormSelect, GlLink } from '@gitlab/ui'; import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { helpPagePath } from '~/helpers/help_page_helper'; import CustomEmailForm from '~/projects/settings_service_desk/components/custom_email_form.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import { I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE } from '~/projects/settings_service_desk/custom_email_constants'; +import { + I18N_FORM_FORWARDING_CLIPBOARD_BUTTON_TITLE, + I18N_FORM_SMTP_AUTHENTICATION_NONE, + I18N_FORM_SMTP_AUTHENTICATION_PLAIN, + I18N_FORM_SMTP_AUTHENTICATION_LOGIN, + I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5, +} from '~/projects/settings_service_desk/custom_email_constants'; describe('CustomEmailForm', () => { let wrapper; @@ -24,6 +30,7 @@ describe('CustomEmailForm', () => { const findSmtpPortInput = () => findInputByTestId('form-smtp-port'); const findSmtpUsernameInput = () => findInputByTestId('form-smtp-username'); const findSmtpPasswordInput = () => findInputByTestId('form-smtp-password'); + const findSmtpAuthenticationSelect = () => wrapper.findComponent(GlFormSelect).find('select'); const findSubmit = () => wrapper.findByTestId('form-submit'); const clickButtonAndExpectNoSubmitEvent = async () => { @@ -60,6 +67,23 @@ describe('CustomEmailForm', () => { ); }); + it('renders correct translations for options for SMTP authentication', () => { + createWrapper(); + + const translationStrings = [ + I18N_FORM_SMTP_AUTHENTICATION_NONE, + I18N_FORM_SMTP_AUTHENTICATION_PLAIN, + I18N_FORM_SMTP_AUTHENTICATION_LOGIN, + I18N_FORM_SMTP_AUTHENTICATION_CRAM_MD5, + ]; + + findSmtpAuthenticationSelect() + .findAll('option') + .wrappers.forEach((item, index) => { + expect(item.text()).toEqual(translationStrings[index]); + }); + }); + it('form inputs are disabled when submitting', () => { createWrapper({ isSubmitting: true }); @@ -68,6 +92,7 @@ describe('CustomEmailForm', () => { expect(findSmtpPortInput().attributes('disabled')).toBeDefined(); expect(findSmtpUsernameInput().attributes('disabled')).toBeDefined(); expect(findSmtpPasswordInput().attributes('disabled')).toBeDefined(); + expect(findSmtpAuthenticationSelect().attributes('disabled')).toBeDefined(); expect(findSubmit().props('loading')).toBe(true); }); @@ -99,6 +124,8 @@ describe('CustomEmailForm', () => { findSmtpPasswordInput().setValue('supersecret'); findSmtpPasswordInput().trigger('change'); + + findSmtpAuthenticationSelect().setValue('login'); }); it('is invalid when malformed email provided', async () => { @@ -200,9 +227,10 @@ describe('CustomEmailForm', () => { { custom_email: 'user@example.com', smtp_address: 'smtp.example.com', + smtp_username: 'user@example.com', smtp_password: 'supersecret', smtp_port: '587', - smtp_username: 'user@example.com', + smtp_authentication: 'login', }, ], ]); diff --git a/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb new file mode 100644 index 00000000000..8836ba73110 --- /dev/null +++ b/spec/graphql/resolvers/users/frecent_groups_resolver_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Users::FrecentGroupsResolver, feature_category: :navigation do + it_behaves_like 'namespace visits resolver' +end diff --git a/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb new file mode 100644 index 00000000000..30a0a7d93b3 --- /dev/null +++ b/spec/graphql/resolvers/users/frecent_projects_resolver_spec.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Users::FrecentProjectsResolver, feature_category: :navigation do + it_behaves_like 'namespace visits resolver' +end diff --git a/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb new file mode 100644 index 00000000000..43ce53fffcb --- /dev/null +++ b/spec/migrations/db/migrate/20231103162825_add_wolfi_purl_type_to_package_metadata_purl_types_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe AddWolfiPurlTypeToPackageMetadataPurlTypes, feature_category: :software_composition_analysis do + let(:settings) { table(:application_settings) } + + describe "#up" do + it 'updates setting' do + settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10]) + + disable_migrations_output do + migrate! + end + + expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10, 13]) + end + end + + describe "#down" do + context 'with default value' do + it 'updates setting' do + settings.create!(package_metadata_purl_types: [1, 2, 4, 5, 9, 10, 13]) + + disable_migrations_output do + migrate! + schema_migrate_down! + end + + expect(ApplicationSetting.last.package_metadata_purl_types).to eq([1, 2, 4, 5, 9, 10]) + end + end + end +end diff --git a/spec/models/concerns/enums/sbom_spec.rb b/spec/models/concerns/enums/sbom_spec.rb index 41670880630..4cfd50b6c8a 100644 --- a/spec/models/concerns/enums/sbom_spec.rb +++ b/spec/models/concerns/enums/sbom_spec.rb @@ -23,6 +23,7 @@ RSpec.describe Enums::Sbom, feature_category: :dependency_management do :rpm | 10 :deb | 11 :cbl_mariner | 12 + :wolfi | 13 'unknown-pkg-manager' | 0 'Python (unknown)' | 0 end diff --git a/spec/models/users/group_visit_spec.rb b/spec/models/users/group_visit_spec.rb index 63c4631ad7d..241cb537fad 100644 --- a/spec/models/users/group_visit_spec.rb +++ b/spec/models/users/group_visit_spec.rb @@ -7,10 +7,6 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do let_it_be(:user) { create(:user) } let_it_be(:base_time) { DateTime.now } - before do - described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time) - end - it_behaves_like 'namespace visits model' it_behaves_like 'cleanup by a loose foreign key' do @@ -22,4 +18,25 @@ RSpec.describe Users::GroupVisit, feature_category: :navigation do let!(:model) { create(:group_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) } let!(:parent) { user } end + + describe '#frecent_groups' do + let_it_be(:group1) { create(:group) } + let_it_be(:group2) { create(:group) } + + before do + [ + [group1.id, 1.day.ago], + [group2.id, 2.days.ago] + ].each do |id, datetime| + described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime) + end + end + + it "returns the associated frecently visited groups" do + expect(described_class.frecent_groups(user_id: user.id)).to eq([ + group1, + group2 + ]) + end + end end diff --git a/spec/models/users/project_visit_spec.rb b/spec/models/users/project_visit_spec.rb index 38747bd6462..50e9ef02fd8 100644 --- a/spec/models/users/project_visit_spec.rb +++ b/spec/models/users/project_visit_spec.rb @@ -7,10 +7,6 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do let_it_be(:user) { create(:user) } let_it_be(:base_time) { DateTime.now } - before do - described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time) - end - it_behaves_like 'namespace visits model' it_behaves_like 'cleanup by a loose foreign key' do @@ -22,4 +18,25 @@ RSpec.describe Users::ProjectVisit, feature_category: :navigation do let!(:model) { create(:project_visit, entity_id: entity.id, user_id: user.id, visited_at: base_time) } let!(:parent) { user } end + + describe '#frecent_projects' do + let_it_be(:project1) { create(:project) } + let_it_be(:project2) { create(:project) } + + before do + [ + [project1.id, 1.day.ago], + [project2.id, 2.days.ago] + ].each do |id, datetime| + described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime) + end + end + + it "returns the associated frecently visited projects" do + expect(described_class.frecent_projects(user_id: user.id)).to eq([ + project1, + project2 + ]) + end + end end diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index cb7884b141e..042dbb09436 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -1110,53 +1110,103 @@ RSpec.describe GroupPolicy, feature_category: :system_access do it { is_expected.to be_allowed(:admin_dependency_proxy) } end + shared_examples 'disallows all dependency proxy access' do + it { is_expected.to be_disallowed(:read_dependency_proxy) } + it { is_expected.to be_disallowed(:admin_dependency_proxy) } + end + + shared_examples 'allows dependency proxy read access but not admin' do + it { is_expected.to be_allowed(:read_dependency_proxy) } + it { is_expected.to be_disallowed(:admin_dependency_proxy) } + end + context 'feature disabled' do let(:current_user) { owner } - it { is_expected.to be_disallowed(:read_dependency_proxy) } - it { is_expected.to be_disallowed(:admin_dependency_proxy) } + before do + stub_config(dependency_proxy: { enabled: false }) + end + + it_behaves_like 'disallows all dependency proxy access' end context 'feature enabled' do before do - stub_config(dependency_proxy: { enabled: true }) + stub_config(dependency_proxy: { enabled: true }, registry: { enabled: true }) end - context 'reporter' do - let(:current_user) { reporter } + context 'human user' do + context 'reporter' do + let(:current_user) { reporter } - it { is_expected.to be_allowed(:read_dependency_proxy) } - it { is_expected.to be_disallowed(:admin_dependency_proxy) } - end + it_behaves_like 'allows dependency proxy read access but not admin' + end - context 'developer' do - let(:current_user) { developer } + context 'developer' do + let(:current_user) { developer } + + it_behaves_like 'allows dependency proxy read access but not admin' + end + + context 'maintainer' do + let(:current_user) { maintainer } + + it_behaves_like 'allows dependency proxy read access but not admin' + it_behaves_like 'disabling admin_package feature flag' + end + + context 'owner' do + let(:current_user) { owner } - it { is_expected.to be_allowed(:read_dependency_proxy) } - it { is_expected.to be_disallowed(:admin_dependency_proxy) } + it { is_expected.to be_allowed(:read_dependency_proxy) } + it { is_expected.to be_allowed(:admin_dependency_proxy) } + + it_behaves_like 'disabling admin_package feature flag' + end end - context 'maintainer' do - let(:current_user) { maintainer } + context 'deploy token user' do + let!(:group_deploy_token) do + create(:group_deploy_token, group: group, deploy_token: deploy_token) + end + + subject { described_class.new(deploy_token, group) } - it { is_expected.to be_allowed(:read_dependency_proxy) } - it { is_expected.to be_disallowed(:admin_dependency_proxy) } + context 'with insufficient scopes' do + let_it_be(:deploy_token) { create(:deploy_token, :group) } - it_behaves_like 'disabling admin_package feature flag' + it_behaves_like 'disallows all dependency proxy access' + end + + context 'with sufficient scopes' do + let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } + + it_behaves_like 'allows dependency proxy read access but not admin' + end end - context 'owner' do - let(:current_user) { owner } + context 'group access token user' do + let_it_be(:bot_user) { create(:user, :project_bot) } + let_it_be(:token) { create(:personal_access_token, user: bot_user, scopes: [Gitlab::Auth::READ_API_SCOPE]) } + + subject { described_class.new(bot_user, group) } - it { is_expected.to be_allowed(:read_dependency_proxy) } - it { is_expected.to be_allowed(:admin_dependency_proxy) } + context 'not a member of the group' do + it_behaves_like 'disallows all dependency proxy access' + end + + context 'a member of the group' do + before do + group.add_guest(bot_user) + end - it_behaves_like 'disabling admin_package feature flag' + it_behaves_like 'allows dependency proxy read access but not admin' + end end end end - context 'deploy token access' do + context 'deploy token user' do let!(:group_deploy_token) do create(:group_deploy_token, group: group, deploy_token: deploy_token) end @@ -1179,17 +1229,6 @@ RSpec.describe GroupPolicy, feature_category: :system_access do it { is_expected.to be_allowed(:read_group) } it { is_expected.to be_disallowed(:destroy_package) } end - - context 'a deploy token with dependency proxy scopes' do - let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } - - before do - stub_config(dependency_proxy: { enabled: true }) - end - - it { is_expected.to be_allowed(:read_dependency_proxy) } - it { is_expected.to be_disallowed(:admin_dependency_proxy) } - end end it_behaves_like 'Self-managed Core resource access tokens' diff --git a/spec/policies/user_policy_spec.rb b/spec/policies/user_policy_spec.rb index 9a2caeb7435..bbfd231ed38 100644 --- a/spec/policies/user_policy_spec.rb +++ b/spec/policies/user_policy_spec.rb @@ -247,6 +247,32 @@ RSpec.describe UserPolicy do end end + describe ':read_user_organizations' do + context 'when user is admin' do + let(:current_user) { admin } + + context 'when admin mode is enabled', :enable_admin_mode do + it { is_expected.to be_allowed(:read_user_organizations) } + end + + context 'when admin mode is disabled' do + it { is_expected.not_to be_allowed(:read_user_organizations) } + end + end + + context 'when user is not an admin' do + context 'requesting their own organizations' do + subject { described_class.new(current_user, current_user) } + + it { is_expected.to be_allowed(:read_user_organizations) } + end + + context "requesting a different user's orgnanizations" do + it { is_expected.not_to be_allowed(:read_user_organizations) } + end + end + end + describe ':read_user_email_address' do context 'when user is admin' do let(:current_user) { admin } diff --git a/spec/requests/api/graphql/user_spec.rb b/spec/requests/api/graphql/user_spec.rb index 41ee233dfc5..22ebc1be964 100644 --- a/spec/requests/api/graphql/user_spec.rb +++ b/spec/requests/api/graphql/user_spec.rb @@ -113,4 +113,36 @@ RSpec.describe 'User', feature_category: :user_profile do end end end + + describe 'organizations field' do + let_it_be(:organization_user) { create(:organization_user, user: current_user) } + let_it_be(:organization) { organization_user.organization } + let_it_be(:another_organization) { create(:organization) } + let_it_be(:another_user) { create(:user) } + + let(:query) do + graphql_query_for( + :user, + { username: current_user.username.to_s.upcase }, + 'organizations { nodes { path } }' + ) + end + + context 'with permission' do + it 'returns the relevant organization details' do + post_graphql(query, current_user: current_user) + + expect(graphql_data.dig('user', 'organizations', 'nodes').pluck('path')) + .to match_array(organization.path) + end + end + + context 'without permission' do + it 'does not return organization details' do + post_graphql(query, current_user: another_user) + + expect(graphql_data.dig('user', 'organizations', 'nodes')).to be_nil + end + end + end end diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index 966cc2d6d4e..0ac059b5ed3 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -92,7 +92,7 @@ RSpec.describe JwtController, feature_category: :system_access do context 'project with enabled CI' do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build)).permit!) } + it { expect(service_class).to have_received(:new).with(project, user, ActionController::Parameters.new(parameters.merge(auth_type: :build, raw_token: build.token)).permit!) } it_behaves_like 'user logging' end @@ -119,7 +119,7 @@ RSpec.describe JwtController, feature_category: :system_access do .with( nil, nil, - ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token)).permit! + ActionController::Parameters.new(parameters.merge(deploy_token: deploy_token, auth_type: :deploy_token, raw_token: deploy_token.token)).permit! ) end @@ -144,7 +144,7 @@ RSpec.describe JwtController, feature_category: :system_access do .with( nil, user, - ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token)).permit! + ActionController::Parameters.new(parameters.merge(auth_type: :personal_access_token, raw_token: pat.token)).permit! ) end @@ -160,7 +160,7 @@ RSpec.describe JwtController, feature_category: :system_access do subject! { get '/jwt/auth', params: parameters, headers: headers } - it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap)).permit!) } + it { expect(service_class).to have_received(:new).with(nil, user, ActionController::Parameters.new(parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)).permit!) } it_behaves_like 'rejecting a blocked user' @@ -180,7 +180,7 @@ RSpec.describe JwtController, feature_category: :system_access do ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } it_behaves_like 'user logging' end @@ -197,7 +197,7 @@ RSpec.describe JwtController, feature_category: :system_access do ActionController::Parameters.new({ service: service_name, scopes: %w[scope1 scope2] }).permit! end - it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap)) } + it { expect(service_class).to have_received(:new).with(nil, user, service_parameters.merge(auth_type: :gitlab_or_ldap, raw_token: user.password)) } end context 'when user has 2FA enabled' do diff --git a/spec/services/auth/dependency_proxy_authentication_service_spec.rb b/spec/services/auth/dependency_proxy_authentication_service_spec.rb index 3ef9c8fc96e..04f7e46daa6 100644 --- a/spec/services/auth/dependency_proxy_authentication_service_spec.rb +++ b/spec/services/auth/dependency_proxy_authentication_service_spec.rb @@ -4,15 +4,17 @@ require 'spec_helper' RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :dependency_proxy do let_it_be(:user) { create(:user) } + let_it_be(:params) { {} } + let_it_be(:authentication_abilities) { nil } - let(:service) { described_class.new(nil, user) } + let(:service) { described_class.new(nil, user, params) } before do - stub_config(dependency_proxy: { enabled: true }) + stub_config(dependency_proxy: { enabled: true }, registry: { enabled: true }) end describe '#execute' do - subject { service.execute(authentication_abilities: nil) } + subject { service.execute(authentication_abilities: authentication_abilities) } shared_examples 'returning' do |status:, message:| it "returns #{message}", :aggregate_failures do @@ -21,9 +23,23 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :de end end - shared_examples 'returning a token' do - it 'returns a token' do - expect(subject[:token]).not_to be_nil + shared_examples 'returning a token with encoded user_id' do + it 'returns a token with encoded user_id' do + token = subject[:token] + expect(token).not_to be_nil + + decoded_token = decode(token) + expect(decoded_token['user_id']).not_to be_nil + end + end + + shared_examples 'returning a token with encoded deploy_token' do + it 'returns a token with encoded deploy_token' do + token = subject[:token] + expect(token).not_to be_nil + + decoded_token = decode(token) + expect(decoded_token['deploy_token']).not_to be_nil end end @@ -41,14 +57,53 @@ RSpec.describe Auth::DependencyProxyAuthenticationService, feature_category: :de it_behaves_like 'returning', status: 403, message: 'access forbidden' end - context 'with a deploy token as user' do - let_it_be(:user) { create(:deploy_token, :group, :dependency_proxy_scopes) } + context 'with a deploy token' do + let_it_be(:deploy_token) { create(:deploy_token, :group, :dependency_proxy_scopes) } + let_it_be(:params) { { deploy_token: deploy_token } } + + it_behaves_like 'returning a token with encoded deploy_token' + end + + context 'with a human user' do + it_behaves_like 'returning a token with encoded user_id' + end + + context 'with a group access token' do + let_it_be(:user) { create(:user, :project_bot) } + let_it_be_with_reload(:token) { create(:personal_access_token, user: user) } - it_behaves_like 'returning a token' + context 'with insufficient authentication abilities' do + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + + context 'with sufficient authentication abilities' do + let_it_be(:authentication_abilities) { Auth::DependencyProxyAuthenticationService::REQUIRED_ABILITIES } + let_it_be(:params) { { raw_token: token.token } } + + subject { service.execute(authentication_abilities: authentication_abilities) } + + it_behaves_like 'returning a token with encoded user_id' + + context 'revoked' do + before do + token.revoke! + end + + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + + context 'expired' do + before do + token.update_column(:expires_at, 1.day.ago) + end + + it_behaves_like 'returning', status: 403, message: 'access forbidden' + end + end end - context 'with a user' do - it_behaves_like 'returning a token' + def decode(token) + DependencyProxy::AuthTokenService.new(token).execute end end end diff --git a/spec/services/service_desk/custom_emails/create_service_spec.rb b/spec/services/service_desk/custom_emails/create_service_spec.rb index 2029c9a0c3f..e165131bcf9 100644 --- a/spec/services/service_desk/custom_emails/create_service_spec.rb +++ b/spec/services/service_desk/custom_emails/create_service_spec.rb @@ -156,7 +156,7 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv } end - it 'creates all records returns a successful response' do + it 'creates all records and returns a successful response' do # Because we also log in ServiceDesk::CustomEmailVerifications::CreateService expect(Gitlab::AppLogger).to receive(:info).with({ category: 'custom_email_verification' }).once expect(Gitlab::AppLogger).to receive(:info).with(logger_params).once @@ -174,7 +174,8 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv smtp_address: params[:smtp_address], smtp_port: params[:smtp_port].to_i, smtp_username: params[:smtp_username], - smtp_password: params[:smtp_password] + smtp_password: params[:smtp_password], + smtp_authentication: nil ) expect(project.service_desk_custom_email_verification).to have_attributes( state: 'started', @@ -183,6 +184,30 @@ RSpec.describe ServiceDesk::CustomEmails::CreateService, feature_category: :serv ) end + context 'with optional smtp_authentication parameter' do + before do + params[:smtp_authentication] = 'login' + end + + it 'sets authentication and returns a successful response' do + response = service.execute + project.reset + + expect(response).to be_success + expect(project.service_desk_custom_email_credential.smtp_authentication).to eq 'login' + end + + context 'with unsupported value' do + let(:expected_error_message) { error_cannot_create_custom_email } + + before do + params[:smtp_authentication] = 'unsupported' + end + + it_behaves_like 'a failing service that does not create records' + end + end + context 'when custom email aready exists' do let!(:settings) { create(:service_desk_setting, project: project, custom_email: 'user@example.com') } let!(:credential) { create(:service_desk_custom_email_credential, project: project) } diff --git a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb index 434592ccd38..257ccc553fe 100644 --- a/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb +++ b/spec/support/shared_contexts/graphql/types/query_type_shared_context.rb @@ -13,6 +13,8 @@ RSpec.shared_context 'with FOSS query type fields' do :current_user, :design_management, :echo, + :frecent_groups, + :frecent_projects, :gitpod_enabled, :group, :groups, diff --git a/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb new file mode 100644 index 00000000000..0dca28a4e74 --- /dev/null +++ b/spec/support/shared_examples/graphql/resolvers/users/pages_visits_resolvers_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'namespace visits resolver' do + include GraphqlHelpers + + describe '#resolve' do + context 'when user is not logged in' do + let_it_be(:current_user) { nil } + + it 'returns nil' do + expect(resolve_items).to eq(nil) + end + end + + context 'when user is logged in' do + let_it_be(:current_user) { create(:user) } + + context 'when the frecent_namespaces_suggestions feature flag is disabled' do + before do + stub_feature_flags(frecent_namespaces_suggestions: false) + end + + it 'raises a "Resource not available" exception' do + expect(resolve_items).to be_a(::Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end + + it 'returns frecent groups' do + expect(resolve_items).to be_an_instance_of(Array) + end + end + end + + private + + def resolve_items + sync(resolve(described_class, ctx: { current_user: current_user })) + end +end diff --git a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb index b653b4e265a..7a3b3d6924c 100644 --- a/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/merge_request_interactions_type_shared_examples.rb @@ -28,6 +28,7 @@ RSpec.shared_examples "a user type with merge request interaction type" do authoredMergeRequests assignedMergeRequests reviewRequestedMergeRequests + organizations groupMemberships groupCount projectMemberships diff --git a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb index 0b3e8516d25..ec7dc1fb8b9 100644 --- a/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb +++ b/spec/support/shared_examples/models/users/pages_visits_shared_examples.rb @@ -6,6 +6,10 @@ RSpec.shared_examples 'namespace visits model' do it { is_expected.to validate_presence_of(:visited_at) } describe '#visited_around?' do + before do + described_class.create!(entity_id: entity.id, user_id: user.id, visited_at: base_time) + end + context 'when the checked time matches a recent visit' do [-15.minutes, 15.minutes].each do |time_diff| it 'returns true' do @@ -24,4 +28,104 @@ RSpec.shared_examples 'namespace visits model' do end end end + + describe '#frecent_visits_scores' do + def frecent_visits_scores_to_array(visits) + visits.map { |visit| [visit["entity_id"], visit["score"]] } + end + + context 'when there is lots of data' do + before do + create_visit_records + end + + it 'returns the frecent items, sorted by their frecency score' do + expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id, + limit: 10))).to eq([[2, 31], [1, 30], [3, 28], [6, 6], [7, 6], [8, 6], [4, 6], [5, 6]]) + end + + it 'limits the amount of returned entries' do + expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id, + limit: 2))).to eq([ + [2, 31], [1, 30] + ]) + end + end + + context 'when there is few data' do + before do + [ + # Multiplier: 4 + [1, Time.current], + + # Multiplier: 3 + [2, 2.weeks.ago], + [3, 2.weeks.ago], + + # Multiplier: 2 + [1, 3.weeks.ago], + [1, 3.weeks.ago], + + # Multiplier: 1 + [2, 5.weeks.ago] + ].each do |id, datetime| + described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime) + end + end + + it 'returns the frecent items, sorted by their frecency score' do + expect(frecent_visits_scores_to_array(described_class.frecent_visits_scores(user_id: user.id, + limit: 5))).to eq([ + [1, 8], # Entity 1 gets a score of (1 * 4) + (2 * 2) = 8 + [2, 4], # Entity 2 gets a score of (1 * 3) + (1 * 1) = 4 + [3, 3] # Entity 3 gets a score of 1 * 3 = 3 + ]) + end + end + end + + private + + # rubocop: disable Metrics/AbcSize -- Despite being long, this method is quite straightforward. Splitting it in smaller chunks would likely harm readability more than anything. + def create_visit_records + [ + [1, Time.current], + + [2, 1.week.ago], + [2, 1.week.ago], + + [2, 2.weeks.ago], + [3, 2.weeks.ago], + [3, 2.weeks.ago], + [4, 2.weeks.ago], + [5, 2.weeks.ago], + [6, 2.weeks.ago], + [7, 2.weeks.ago], + [8, 2.weeks.ago], + + [1, 3.weeks.ago], + [1, 3.weeks.ago], + [3, 3.weeks.ago], + [3, 3.weeks.ago], + + [1, 4.weeks.ago], + [2, 4.weeks.ago], + [2, 4.weeks.ago], + + [3, 7.weeks.ago], + [3, 7.weeks.ago], + + [1, 8.weeks.ago], + [1, 8.weeks.ago], + [1, 8.weeks.ago], + [1, 8.weeks.ago], + + [2, 9.weeks.ago], + [2, 9.weeks.ago], + [2, 9.weeks.ago] + ].each do |id, datetime| + described_class.create!(entity_id: id, user_id: user.id, visited_at: datetime) + end + end + # rubocop: enable Metrics/AbcSize end |