diff options
30 files changed, 347 insertions, 48 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index 7053c91ad7a..5f653fbfc26 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -7ad4fcea44bc4c1a8682d77ad6b46aeacb8c6e1d +4f0cd9404f31511f5051e49b363adc06aa3ec365 @@ -18,7 +18,7 @@ gem 'default_value_for', '~> 3.4.0' gem 'pg', '~> 1.1' gem 'rugged', '~> 1.1' -gem 'grape-path-helpers', '~> 1.6.1' +gem 'grape-path-helpers', '~> 1.6.3' gem 'faraday', '~> 1.0' gem 'marginalia', '~> 1.10.0' diff --git a/Gemfile.lock b/Gemfile.lock index a703d29bc4e..0efe172d134 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -541,7 +541,7 @@ GEM grape-entity (0.7.1) activesupport (>= 4.0) multi_json (>= 1.3.2) - grape-path-helpers (1.6.1) + grape-path-helpers (1.6.3) activesupport grape (~> 1.3) rake (> 12) @@ -1471,7 +1471,7 @@ DEPENDENCIES gpgme (~> 2.0.19) grape (~> 1.5.2) grape-entity (~> 0.7.1) - grape-path-helpers (~> 1.6.1) + grape-path-helpers (~> 1.6.3) grape_logging (~> 1.7) graphiql-rails (~> 1.4.10) graphlient (~> 0.4.0) diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue index 4b5d12b3a5f..ab7bc65075d 100644 --- a/app/assets/javascripts/admin/users/components/users_table.vue +++ b/app/assets/javascripts/admin/users/components/users_table.vue @@ -1,7 +1,10 @@ <script> -import { GlTable } from '@gitlab/ui'; -import { __ } from '~/locale'; +import { GlSkeletonLoader, GlTable } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { convertNodeIdsFromGraphQLIds } from '~/graphql_shared/utils'; +import { s__, __ } from '~/locale'; import UserDate from '~/vue_shared/components/user_date.vue'; +import getUsersGroupCountsQuery from '../graphql/queries/get_users_group_counts.query.graphql'; import UserActions from './user_actions.vue'; import UserAvatar from './user_avatar.vue'; @@ -11,6 +14,7 @@ const thWidthClass = (width) => `gl-w-${width}p ${DEFAULT_TH_CLASSES}`; export default { components: { + GlSkeletonLoader, GlTable, UserAvatar, UserActions, @@ -26,6 +30,45 @@ export default { required: true, }, }, + data() { + return { + groupCounts: [], + }; + }, + apollo: { + groupCounts: { + query: getUsersGroupCountsQuery, + variables() { + return { + usernames: this.users.map((user) => user.username), + }; + }, + update(data) { + const nodes = data?.users?.nodes || []; + const parsedIds = convertNodeIdsFromGraphQLIds(nodes); + + return parsedIds.reduce((acc, { id, groupCount }) => { + acc[id] = groupCount || 0; + return acc; + }, {}); + }, + error(error) { + createFlash({ + message: this.$options.i18n.groupCountFetchError, + captureError: true, + error, + }); + }, + skip() { + return !this.users.length; + }, + }, + }, + i18n: { + groupCountFetchError: s__( + 'AdminUsers|Could not load user group counts. Please refresh the page to try again.', + ), + }, fields: [ { key: 'name', @@ -38,6 +81,11 @@ export default { thClass: thWidthClass(10), }, { + key: 'groupCount', + label: __('Groups'), + thClass: thWidthClass(10), + }, + { key: 'createdAt', label: __('Created on'), thClass: thWidthClass(15), @@ -50,7 +98,7 @@ export default { { key: 'settings', label: '', - thClass: thWidthClass(20), + thClass: thWidthClass(10), }, ], }; @@ -77,6 +125,13 @@ export default { <user-date :date="lastActivityOn" show-never /> </template> + <template #cell(groupCount)="{ item: { id } }"> + <div :data-testid="`user-group-count-${id}`"> + <gl-skeleton-loader v-if="$apollo.loading" :width="40" :lines="1" /> + <span v-else>{{ groupCounts[id] }}</span> + </div> + </template> + <template #cell(projectsCount)="{ item: { id, projectsCount } }"> <div :data-testid="`user-project-count-${id}`">{{ projectsCount }}</div> </template> diff --git a/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql new file mode 100644 index 00000000000..0d8e199f16e --- /dev/null +++ b/app/assets/javascripts/admin/users/graphql/queries/get_users_group_counts.query.graphql @@ -0,0 +1,8 @@ +query getUsersGroupCounts($usernames: [String!]) { + users(usernames: $usernames) { + nodes { + id + groupCount + } + } +} diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js index eceae8a8674..54c8edc080b 100644 --- a/app/assets/javascripts/admin/users/index.js +++ b/app/assets/javascripts/admin/users/index.js @@ -1,7 +1,15 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import AdminUsersApp from './components/app.vue'; +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient({}, { assumeImmutableResults: true }), +}); + export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => { if (!el) { return false; @@ -11,6 +19,7 @@ export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-a return new Vue({ el, + apolloProvider, render: (createElement) => createElement(AdminUsersApp, { props: { diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb index bc6e11d3ab9..e5abc033155 100644 --- a/app/graphql/types/user_interface.rb +++ b/app/graphql/types/user_interface.rb @@ -64,8 +64,7 @@ module Types description: 'Group memberships of the user.' field :group_count, resolver: Resolvers::Users::GroupCountResolver, - description: 'Group count for the user.', - feature_flag: :user_group_counts + description: 'Group count for the user.' field :status, type: Types::UserStatusType, null: true, diff --git a/app/helpers/webpack_helper.rb b/app/helpers/webpack_helper.rb index 170e3c45a21..90b8a8e94b0 100644 --- a/app/helpers/webpack_helper.rb +++ b/app/helpers/webpack_helper.rb @@ -5,6 +5,10 @@ module WebpackHelper javascript_include_tag(*webpack_entrypoint_paths(bundle)) end + def webpack_preload_asset_tag(asset, options = {}) + preload_link_tag(Gitlab::Webpack::Manifest.asset_paths(asset).first, options) + end + def webpack_controller_bundle_tags chunks = [] diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml index 6694ad5968a..8aa163a26ec 100644 --- a/app/views/layouts/_head.html.haml +++ b/app/views/layouts/_head.html.haml @@ -32,6 +32,8 @@ - if page_canonical_link %link{ rel: 'canonical', href: page_canonical_link } + = webpack_preload_asset_tag("monaco") + = favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png' = render 'layouts/startup_css' diff --git a/changelogs/unreleased/234005-extend-conantoken-expiration.yml b/changelogs/unreleased/234005-extend-conantoken-expiration.yml new file mode 100644 index 00000000000..daa92bd27c9 --- /dev/null +++ b/changelogs/unreleased/234005-extend-conantoken-expiration.yml @@ -0,0 +1,5 @@ +--- +title: Change conan token expiration from 1 hour to 24 hours +merge_request: 60763 +author: +type: changed diff --git a/changelogs/unreleased/276215-admin-users-show-group-count.yml b/changelogs/unreleased/276215-admin-users-show-group-count.yml new file mode 100644 index 00000000000..7750bee17fe --- /dev/null +++ b/changelogs/unreleased/276215-admin-users-show-group-count.yml @@ -0,0 +1,5 @@ +--- +title: Show total group counts in admin users table +merge_request: 60998 +author: +type: added diff --git a/changelogs/unreleased/default-csp.yml b/changelogs/unreleased/default-csp.yml new file mode 100644 index 00000000000..5ae79627dd8 --- /dev/null +++ b/changelogs/unreleased/default-csp.yml @@ -0,0 +1,5 @@ +--- +title: Enable Content-Security-Policy header by default +merge_request: 56923 +author: +type: other diff --git a/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml b/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml new file mode 100644 index 00000000000..464b6807b3b --- /dev/null +++ b/changelogs/unreleased/sh-upgrade-grape-path-helpers.yml @@ -0,0 +1,5 @@ +--- +title: Update grape-path-helpers to v1.6.3 +merge_request: 61196 +author: +type: performance diff --git a/config/feature_flags/development/find_remote_root_refs_inmemory.yml b/config/feature_flags/development/find_remote_root_refs_inmemory.yml index 18e2e2b366a..c78eadceaad 100644 --- a/config/feature_flags/development/find_remote_root_refs_inmemory.yml +++ b/config/feature_flags/development/find_remote_root_refs_inmemory.yml @@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329664 milestone: '13.12' type: development group: group::gitaly -default_enabled: true +default_enabled: false diff --git a/config/feature_flags/development/user_group_counts.yml b/config/feature_flags/development/user_group_counts.yml deleted file mode 100644 index 98798ea3ead..00000000000 --- a/config/feature_flags/development/user_group_counts.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: user_group_counts -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/44069/ -rollout_issue_url: -milestone: '13.6' -type: development -group: group::compliance -default_enabled: false diff --git a/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml b/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml index b7567777135..c0fd0561ee4 100644 --- a/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml +++ b/config/metrics/counts_28d/20210216175004_g_analytics_merge_request_monthly.yml @@ -6,7 +6,7 @@ product_stage: manage product_group: group::optimize product_category: value_type: number -status: data_available +status: removed time_frame: 28d data_source: redis_hll distribution: diff --git a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml b/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml index fef77d5c3ac..bbd3bfca2b2 100644 --- a/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml +++ b/config/metrics/counts_all/20210216174902_g_analytics_merge_request.yml @@ -6,8 +6,9 @@ product_stage: manage product_group: group::optimize product_category: value_type: number -status: data_available -time_frame: all +status: removed +time_frame: 7d +data_source: redis_hll data_source: distribution: - ce diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 3626fe428b1..6aa1c50ded1 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -9844,7 +9844,7 @@ A user assigned to a merge request. | <a id="mergerequestassigneebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. | | <a id="mergerequestassigneecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) | | <a id="mergerequestassigneeemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). | -| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. | +| <a id="mergerequestassigneegroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. | | <a id="mergerequestassigneegroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) | | <a id="mergerequestassigneeid"></a>`id` | [`ID!`](#id) | ID of the user. | | <a id="mergerequestassigneelocation"></a>`location` | [`String`](#string) | The location of the user. | @@ -10050,7 +10050,7 @@ A user assigned to a merge request as a reviewer. | <a id="mergerequestreviewerbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. | | <a id="mergerequestreviewercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) | | <a id="mergerequestrevieweremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). | -| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. | +| <a id="mergerequestreviewergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. | | <a id="mergerequestreviewergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) | | <a id="mergerequestreviewerid"></a>`id` | [`ID!`](#id) | ID of the user. | | <a id="mergerequestreviewerlocation"></a>`location` | [`String`](#string) | The location of the user. | @@ -12679,7 +12679,7 @@ Core represention of a GitLab user. | <a id="usercorebot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. | | <a id="usercorecallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) | | <a id="usercoreemail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). | -| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. | +| <a id="usercoregroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. | | <a id="usercoregroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) | | <a id="usercoreid"></a>`id` | [`ID!`](#id) | ID of the user. | | <a id="usercorelocation"></a>`location` | [`String`](#string) | The location of the user. | @@ -15346,7 +15346,7 @@ Implementations: | <a id="userbot"></a>`bot` | [`Boolean!`](#boolean) | Indicates if the user is a bot. | | <a id="usercallouts"></a>`callouts` | [`UserCalloutConnection`](#usercalloutconnection) | User callouts that belong to the user. (see [Connections](#connections)) | | <a id="useremail"></a>`email` **{warning-solid}** | [`String`](#string) | **Deprecated** in 13.7. This was renamed. Use: [`User.publicEmail`](#userpublicemail). | -| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. Available only when feature flag `user_group_counts` is enabled. | +| <a id="usergroupcount"></a>`groupCount` | [`Int`](#int) | Group count for the user. | | <a id="usergroupmemberships"></a>`groupMemberships` | [`GroupMemberConnection`](#groupmemberconnection) | Group memberships of the user. (see [Connections](#connections)) | | <a id="userid"></a>`id` | [`ID!`](#id) | ID of the user. | | <a id="userlocation"></a>`location` | [`String`](#string) | The location of the user. | diff --git a/doc/development/usage_ping/dictionary.md b/doc/development/usage_ping/dictionary.md index d63fbf96336..094d5cce21b 100644 --- a/doc/development/usage_ping/dictionary.md +++ b/doc/development/usage_ping/dictionary.md @@ -114,7 +114,7 @@ Unique visitors to /groups/:group/-/analytics/merge_request_analytics Group: `group::optimize` -Status: `data_available` +Status: `removed` Tiers: `free` @@ -7410,7 +7410,7 @@ Missing description Group: `group::optimize` -Status: `data_available` +Status: `removed` Tiers: `free` @@ -7422,7 +7422,7 @@ Missing description Group: `group::optimize` -Status: `data_available` +Status: `removed` Tiers: diff --git a/lib/gitlab/conan_token.rb b/lib/gitlab/conan_token.rb index c3d90aa78fb..d0560807f45 100644 --- a/lib/gitlab/conan_token.rb +++ b/lib/gitlab/conan_token.rb @@ -8,6 +8,7 @@ module Gitlab class ConanToken HMAC_KEY = 'gitlab-conan-packages' + CONAN_TOKEN_EXPIRE_TIME = 1.day.freeze attr_reader :access_token_id, :user_id @@ -57,7 +58,7 @@ module Gitlab JSONWebToken::HMACToken.new(self.class.secret).tap do |token| token['access_token'] = access_token_id token['user_id'] = user_id - token.expire_time = token.issued_at + 1.hour + token.expire_time = token.issued_at + CONAN_TOKEN_EXPIRE_TIME end end end diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb index ff844645b11..6f6147f0f32 100644 --- a/lib/gitlab/content_security_policy/config_loader.rb +++ b/lib/gitlab/content_security_policy/config_loader.rb @@ -8,11 +8,33 @@ module Gitlab media_src object_src report_uri script_src style_src worker_src).freeze def self.default_settings_hash - { - 'enabled' => false, + settings_hash = { + 'enabled' => true, 'report_only' => false, - 'directives' => DIRECTIVES.each_with_object({}) { |directive, hash| hash[directive] = nil } + 'directives' => { + 'default_src' => "'self'", + 'base_uri' => "'self'", + 'child_src' => "'none'", + 'connect_src' => "'self'", + 'font_src' => "'self'", + 'form_action' => "'self' https: http:", + 'frame_ancestors' => "'self'", + 'frame_src' => "'self' https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com", + 'img_src' => "'self' data: blob: http: https:", + 'manifest_src' => "'self'", + 'media_src' => "'self'", + 'script_src' => "'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com", + 'style_src' => "'self' 'unsafe-inline'", + 'worker_src' => "'self'", + 'object_src' => "'none'", + 'report_uri' => nil + } } + + allow_webpack_dev_server(settings_hash) if Rails.env.development? + allow_cdn(settings_hash) if ENV['GITLAB_CDN_HOST'].present? + + settings_hash end def initialize(csp_directives) @@ -38,6 +60,26 @@ module Gitlab arguments.strip.split(' ').map(&:strip) end + + def self.allow_webpack_dev_server(settings_hash) + secure = Settings.webpack.dev_server['https'] + host_and_port = "#{Settings.webpack.dev_server['host']}:#{Settings.webpack.dev_server['port']}" + http_url = "#{secure ? 'https' : 'http'}://#{host_and_port}" + ws_url = "#{secure ? 'wss' : 'ws'}://#{host_and_port}" + + append_to_directive(settings_hash, 'connect_src', "#{http_url} #{ws_url}") + end + + def self.allow_cdn(settings_hash) + cdn_host = ENV['GITLAB_CDN_HOST'] + + append_to_directive(settings_hash, 'script_src', cdn_host) + append_to_directive(settings_hash, 'style_src', cdn_host) + end + + def self.append_to_directive(settings_hash, directive, text) + settings_hash['directives'][directive] = "#{settings_hash['directives'][directive]} #{text}".strip + end end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index b8dde3291b8..085d16b7bd5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2528,6 +2528,9 @@ msgstr "" msgid "AdminUsers|Cohorts" msgstr "" +msgid "AdminUsers|Could not load user group counts. Please refresh the page to try again." +msgstr "" + msgid "AdminUsers|Deactivate" msgstr "" diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index e38376b0741..36907d4aa60 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -547,6 +547,32 @@ RSpec.describe 'Admin::Users' do end end + # TODO: Move to main GET /admin/users block once feature flag is removed. Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/290737 + context 'with vue_admin_users feature flag enabled', :js do + before do + stub_feature_flags(vue_admin_users: true) + end + + describe 'GET /admin/users' do + context 'user group count', :js do + before do + group = create(:group) + group.add_developer(current_user) + project = create(:project, group: create(:group)) + project.add_reporter(current_user) + end + + it 'displays count of the users authorized groups' do + visit admin_users_path + + wait_for_requests + + expect(page.find("[data-testid='user-group-count-#{current_user.id}']").text).to eq("2") + end + end + end + end + def click_user_dropdown_toggle(user_id) page.within("[data-testid='user-actions-#{user_id}']") do find("[data-testid='dropdown-toggle']").click diff --git a/spec/features/projects/new_project_from_template_spec.rb b/spec/features/projects/new_project_from_template_spec.rb new file mode 100644 index 00000000000..1c8647d859a --- /dev/null +++ b/spec/features/projects/new_project_from_template_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'New project from template', :js do + let(:user) { create(:user) } + + before do + sign_in(user) + + visit new_project_path + end + + context 'create from template' do + before do + page.find('a[href="#create_from_template"]').click + wait_for_requests + end + + it 'shows template tabs' do + page.within('#create-from-template-pane') do + expect(page).to have_link('Built-in', href: '#built-in') + end + end + end +end diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index 424b0deebd3..708c9e1979e 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -1,16 +1,36 @@ -import { GlTable } from '@gitlab/ui'; -import { mount } from '@vue/test-utils'; +import { GlTable, GlSkeletonLoader } from '@gitlab/ui'; +import { createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; + +import createMockApollo from 'helpers/mock_apollo_helper'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; import AdminUserActions from '~/admin/users/components/user_actions.vue'; import AdminUserAvatar from '~/admin/users/components/user_avatar.vue'; import AdminUsersTable from '~/admin/users/components/users_table.vue'; +import getUsersGroupCountsQuery from '~/admin/users/graphql/queries/get_users_group_counts.query.graphql'; +import createFlash from '~/flash'; import AdminUserDate from '~/vue_shared/components/user_date.vue'; -import { users, paths } from '../mock_data'; +import { users, paths, createGroupCountResponse } from '../mock_data'; + +jest.mock('~/flash'); + +const localVue = createLocalVue(); +localVue.use(VueApollo); describe('AdminUsersTable component', () => { let wrapper; + const user = users[0]; + const createFetchGroupCount = (data) => + jest.fn().mockResolvedValue(createGroupCountResponse(data)); + const fetchGroupCountsLoading = jest.fn().mockResolvedValue(new Promise(() => {})); + const fetchGroupCountsError = jest.fn().mockRejectedValue(new Error('Network error')); + const fetchGroupCountsResponse = createFetchGroupCount([{ id: user.id, groupCount: 5 }]); + + const findUserGroupCount = (id) => wrapper.findByTestId(`user-group-count-${id}`); + const findUserGroupCountLoader = (id) => findUserGroupCount(id).find(GlSkeletonLoader); const getCellByLabel = (trIdx, label) => { return wrapper .find(GlTable) @@ -20,8 +40,16 @@ describe('AdminUsersTable component', () => { .find(`[data-label="${label}"][role="cell"]`); }; - const initComponent = (props = {}) => { - wrapper = mount(AdminUsersTable, { + function createMockApolloProvider(resolverMock) { + const requestHandlers = [[getUsersGroupCountsQuery, resolverMock]]; + + return createMockApollo(requestHandlers); + } + + const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => { + wrapper = mountExtended(AdminUsersTable, { + localVue, + apolloProvider: createMockApolloProvider(resolverMock), propsData: { users, paths, @@ -36,8 +64,6 @@ describe('AdminUsersTable component', () => { }); describe('when there are users', () => { - const user = users[0]; - beforeEach(() => { initComponent(); }); @@ -69,4 +95,51 @@ describe('AdminUsersTable component', () => { expect(wrapper.text()).toContain('No users found'); }); }); + + describe('group counts', () => { + describe('when fetching the data', () => { + beforeEach(() => { + initComponent({}, fetchGroupCountsLoading); + }); + + it('renders a loader for each user', () => { + expect(findUserGroupCountLoader(user.id).exists()).toBe(true); + }); + }); + + describe('when the data has been fetched', () => { + beforeEach(() => { + initComponent(); + }); + + it("renders the user's group count", () => { + expect(findUserGroupCount(user.id).text()).toBe('5'); + }); + + describe("and a user's group count is null", () => { + beforeEach(() => { + initComponent({}, createFetchGroupCount([{ id: user.id, groupCount: null }])); + }); + + it("renders the user's group count as 0", () => { + expect(findUserGroupCount(user.id).text()).toBe('0'); + }); + }); + }); + + describe('when there is an error while fetching the data', () => { + beforeEach(() => { + initComponent({}, fetchGroupCountsError); + }); + + it('creates a flash message and captures the error', () => { + expect(createFlash).toHaveBeenCalledTimes(1); + expect(createFlash).toHaveBeenCalledWith({ + message: 'Could not load user group counts. Please refresh the page to try again.', + captureError: true, + error: expect.any(Error), + }); + }); + }); + }); }); diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js index c3918ef5173..4689ab36773 100644 --- a/spec/frontend/admin/users/mock_data.js +++ b/spec/frontend/admin/users/mock_data.js @@ -10,7 +10,7 @@ export const users = [ 'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon', badges: [ { text: 'Admin', variant: 'success' }, - { text: "It's you!", variant: null }, + { text: "It's you!", variant: 'muted' }, ], projectsCount: 0, actions: [], @@ -31,3 +31,16 @@ export const paths = { deleteWithContributions: '/admin/users/id', adminUser: '/admin/users/id', }; + +export const createGroupCountResponse = (groupCounts) => ({ + data: { + users: { + nodes: groupCounts.map(({ id, groupCount }) => ({ + id: `gid://gitlab/User/${id}`, + groupCount, + __typename: 'UserCore', + })), + __typename: 'UserCoreConnection', + }, + }, +}); diff --git a/spec/lib/gitlab/conan_token_spec.rb b/spec/lib/gitlab/conan_token_spec.rb index 00683cf6e47..b6180f69044 100644 --- a/spec/lib/gitlab/conan_token_spec.rb +++ b/spec/lib/gitlab/conan_token_spec.rb @@ -20,7 +20,7 @@ RSpec.describe Gitlab::ConanToken do JSONWebToken::HMACToken.new(jwt_secret).tap do |jwt| jwt['access_token'] = access_token_id jwt['user_id'] = user_id || user_id - jwt.expire_time = expire_time || jwt.issued_at + 1.hour + jwt.expire_time = expire_time || jwt.issued_at + ::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME end end @@ -75,7 +75,7 @@ RSpec.describe Gitlab::ConanToken do it 'returns nil for expired JWT' do jwt = build_jwt(access_token_id: 123, user_id: 456, - expire_time: Time.zone.now - 2.hours) + expire_time: Time.zone.now - (::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME + 1.hour)) expect(described_class.decode(jwt.encoded)).to be_nil end diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb index a94fd6acd32..41a6c06f9c9 100644 --- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb +++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb @@ -20,15 +20,34 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do end describe '.default_settings_hash' do - it 'returns empty defaults' do + it 'returns defaults for all keys' do settings = described_class.default_settings_hash - expect(settings['enabled']).to be_falsey + expect(settings['enabled']).to be_truthy expect(settings['report_only']).to be_falsey - described_class::DIRECTIVES.each do |directive| - expect(settings['directives'].has_key?(directive)).to be_truthy - expect(settings['directives'][directive]).to be_nil + directives = settings['directives'] + directive_names = (described_class::DIRECTIVES - ['report_uri']) + directive_names.each do |directive| + expect(directives.has_key?(directive)).to be_truthy + expect(directives[directive]).to be_truthy + end + + expect(directives.has_key?('report_uri')).to be_truthy + expect(directives['report_uri']).to be_nil + end + + context 'when GITLAB_CDN_HOST is set' do + before do + stub_env('GITLAB_CDN_HOST', 'https://example.com') + end + + it 'adds GITLAB_CDN_HOST to CSP' do + settings = described_class.default_settings_hash + directives = settings['directives'] + + expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com https://example.com") + expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com") end end end diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index 1afe8eafa5f..c938c6432fe 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -106,7 +106,7 @@ RSpec.shared_examples 'conan authenticate endpoint' do expect(payload['user_id']).to eq(personal_access_token.user_id) duration = payload['exp'] - payload['iat'] - expect(duration).to eq(1.hour) + expect(duration).to eq(::Gitlab::ConanToken::CONAN_TOKEN_EXPIRE_TIME) end end end diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index 6752bdc8337..ef0bd97cbcf 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -62,6 +62,12 @@ RSpec.describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="print" href="/stylesheets/highlight/themes/solarised-light.css" />') end + it 'preloads Monaco' do + render + + expect(rendered).to match('<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">') + end + context 'when an asset_host is set and snowplow url is set' do let(:asset_host) { 'http://test.host' } let(:snowplow_collector_hostname) { 'www.snow.plow' } |