diff options
46 files changed, 636 insertions, 217 deletions
@@ -416,7 +416,7 @@ end gem 'octokit', '~> 4.15' # https://gitlab.com/gitlab-org/gitlab/issues/207207 -gem 'gitlab-mail_room', '~> 0.0.4', require: 'mail_room' +gem 'gitlab-mail_room', '~> 0.0.6', require: 'mail_room' gem 'email_reply_trimmer', '~> 0.1' gem 'html2text' diff --git a/Gemfile.lock b/Gemfile.lock index 319c4df3f9d..2e3537e223f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -390,7 +390,7 @@ GEM opentracing (~> 0.4) redis (> 3.0.0, < 5.0.0) gitlab-license (1.0.0) - gitlab-mail_room (0.0.4) + gitlab-mail_room (0.0.6) gitlab-markup (1.7.1) gitlab-net-dns (0.9.1) gitlab-puma (4.3.3.gitlab.2) @@ -1241,7 +1241,7 @@ DEPENDENCIES gitlab-chronic (~> 0.10.5) gitlab-labkit (= 0.12.0) gitlab-license (~> 1.0) - gitlab-mail_room (~> 0.0.4) + gitlab-mail_room (~> 0.0.6) gitlab-markup (~> 1.7.1) gitlab-net-dns (~> 0.9.1) gitlab-puma (~> 4.3.3.gitlab.2) diff --git a/app/assets/javascripts/monitoring/stores/utils.js b/app/assets/javascripts/monitoring/stores/utils.js index 058fab5f4fc..5795e756282 100644 --- a/app/assets/javascripts/monitoring/stores/utils.js +++ b/app/assets/javascripts/monitoring/stores/utils.js @@ -2,8 +2,8 @@ import { slugify } from '~/lib/utils/text_utility'; import createGqClient, { fetchPolicies } from '~/lib/graphql'; import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { parseTemplatingVariables } from './variable_mapping'; import { NOT_IN_DB_PREFIX, linkTypes } from '../constants'; +import { mergeURLVariables, parseTemplatingVariables } from './variable_mapping'; import { DATETIME_RANGE_TYPES } from '~/lib/utils/constants'; import { timeRangeToParams, getRangeType } from '~/lib/utils/datetime_range'; import { isSafeURL, mergeUrlParams } from '~/lib/utils/url_utility'; @@ -289,7 +289,7 @@ export const mapToDashboardViewModel = ({ }) => { return { dashboard, - variables: parseTemplatingVariables(templating), + variables: mergeURLVariables(parseTemplatingVariables(templating)), links: links.map(mapLinksToViewModel), panelGroups: panel_groups.map(mapToPanelGroupViewModel), }; diff --git a/app/assets/javascripts/monitoring/stores/variable_mapping.js b/app/assets/javascripts/monitoring/stores/variable_mapping.js index 66b9899f673..c0a8150063b 100644 --- a/app/assets/javascripts/monitoring/stores/variable_mapping.js +++ b/app/assets/javascripts/monitoring/stores/variable_mapping.js @@ -1,4 +1,5 @@ import { isString } from 'lodash'; +import { templatingVariablesFromUrl } from '../utils'; import { VARIABLE_TYPES } from '../constants'; /** @@ -164,4 +165,39 @@ export const parseTemplatingVariables = ({ variables = {} } = {}) => return acc; }, {}); +/** + * Custom variables are defined in the dashboard yml file + * and their values can be passed through the URL. + * + * On component load, this method merges variables data + * from the yml file with URL data to store in the Vuex store. + * Not all params coming from the URL need to be stored. Only + * the ones that have a corresponding variable defined in the + * yml file. + * + * This ensures that there is always a single source of truth + * for variables + * + * This method can be improved further. See the below issue + * https://gitlab.com/gitlab-org/gitlab/-/issues/217713 + * + * @param {Object} varsFromYML template variables from yml file + * @returns {Object} + */ +export const mergeURLVariables = (varsFromYML = {}) => { + const varsFromURL = templatingVariablesFromUrl(); + const variables = {}; + Object.keys(varsFromYML).forEach(key => { + if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { + variables[key] = { + ...varsFromYML[key], + value: varsFromURL[key], + }; + } else { + variables[key] = varsFromYML[key]; + } + }); + return variables; +}; + export default {}; diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js index 95d544bd6d4..4d2927a066e 100644 --- a/app/assets/javascripts/monitoring/utils.js +++ b/app/assets/javascripts/monitoring/utils.js @@ -170,11 +170,10 @@ export const convertVariablesForURL = variables => * begin with a constant prefix so that it doesn't collide with * other URL params. * - * @param {String} New URL + * @param {String} search URL * @returns {Object} The custom variables defined by the user in the URL */ - -export const getPromCustomVariablesFromUrl = (search = window.location.search) => { +export const templatingVariablesFromUrl = (search = window.location.search) => { const params = queryToObject(search); // pick the params with variable prefix const paramsWithVars = pickBy(params, (val, key) => key.startsWith(VARIABLE_PREFIX)); @@ -353,39 +352,4 @@ export const barChartsDataParser = (data = []) => {}, ); -/** - * Custom variables are defined in the dashboard yml file - * and their values can be passed through the URL. - * - * On component load, this method merges variables data - * from the yml file with URL data to store in the Vuex store. - * Not all params coming from the URL need to be stored. Only - * the ones that have a corresponding variable defined in the - * yml file. - * - * This ensures that there is always a single source of truth - * for variables - * - * This method can be improved further. See the below issue - * https://gitlab.com/gitlab-org/gitlab/-/issues/217713 - * - * @param {Object} varsFromYML template variables from yml file - * @returns {Object} - */ -export const mergeURLVariables = (varsFromYML = {}) => { - const varsFromURL = getPromCustomVariablesFromUrl(); - const variables = {}; - Object.keys(varsFromYML).forEach(key => { - if (Object.prototype.hasOwnProperty.call(varsFromURL, key)) { - variables[key] = { - ...varsFromYML[key], - value: varsFromURL[key], - }; - } else { - variables[key] = varsFromYML[key]; - } - }); - return variables; -}; - export default {}; diff --git a/app/controllers/repositories/git_http_client_controller.rb b/app/controllers/repositories/git_http_client_controller.rb index d03daa406cf..e02955433b2 100644 --- a/app/controllers/repositories/git_http_client_controller.rb +++ b/app/controllers/repositories/git_http_client_controller.rb @@ -18,8 +18,7 @@ module Repositories skip_around_action :set_session_storage skip_before_action :verify_authenticity_token - before_action :parse_repo_path - before_action :authenticate_user + prepend_before_action :authenticate_user, :parse_repo_path private diff --git a/app/services/authorized_project_update/periodic_recalculate_service.rb b/app/services/authorized_project_update/periodic_recalculate_service.rb new file mode 100644 index 00000000000..91c0f50e5e0 --- /dev/null +++ b/app/services/authorized_project_update/periodic_recalculate_service.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class PeriodicRecalculateService + BATCH_SIZE = 480 + DELAY_INTERVAL = 30.seconds.to_i + + def execute + # Using this approach (instead of eg. User.each_batch) keeps the arguments + # the same for AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker + # even if the user list changes, so we can deduplicate these jobs. + (1..User.maximum(:id)).each_slice(BATCH_SIZE).with_index do |batch, index| + delay = DELAY_INTERVAL * index + AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker.perform_in(delay, *batch.minmax) + end + end + end +end diff --git a/app/services/authorized_project_update/recalculate_for_user_range_service.rb b/app/services/authorized_project_update/recalculate_for_user_range_service.rb new file mode 100644 index 00000000000..14b0f5d6117 --- /dev/null +++ b/app/services/authorized_project_update/recalculate_for_user_range_service.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class RecalculateForUserRangeService + def initialize(start_user_id, end_user_id) + @start_user_id = start_user_id + @end_user_id = end_user_id + end + + def execute + User.where(id: start_user_id..end_user_id).select(:id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord + Users::RefreshAuthorizedProjectsService.new(user).execute + end + end + + private + + attr_reader :start_user_id, :end_user_id + end +end diff --git a/app/services/users/refresh_authorized_projects_service.rb b/app/services/users/refresh_authorized_projects_service.rb index 0e7a4821bdf..621266f00e1 100644 --- a/app/services/users/refresh_authorized_projects_service.rb +++ b/app/services/users/refresh_authorized_projects_service.rb @@ -85,8 +85,6 @@ module Users # remove - The IDs of the authorization rows to remove. # add - Rows to insert in the form `[user id, project id, access level]` def update_authorizations(remove = [], add = []) - return if remove.empty? && add.empty? - User.transaction do user.remove_project_authorizations(remove) unless remove.empty? ProjectAuthorization.insert_authorizations(add) unless add.empty? diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 0699be0f4cb..3baa2166812 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -11,6 +11,14 @@ :weight: 1 :idempotent: true :tags: [] +- :name: authorized_project_update:authorized_project_update_user_refresh_over_user_range + :feature_category: :authentication_and_authorization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency :feature_category: :authentication_and_authorization :has_external_dependencies: @@ -99,6 +107,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: cronjob:authorized_project_update_periodic_recalculate + :feature_category: :source_code_management + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: true + :tags: [] - :name: cronjob:ci_archive_traces_cron :feature_category: :continuous_integration :has_external_dependencies: diff --git a/app/workers/authorized_project_update/periodic_recalculate_worker.rb b/app/workers/authorized_project_update/periodic_recalculate_worker.rb new file mode 100644 index 00000000000..0d1ad67d7bb --- /dev/null +++ b/app/workers/authorized_project_update/periodic_recalculate_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class PeriodicRecalculateWorker + include ApplicationWorker + # This worker does not perform work scoped to a context + include CronjobQueue # rubocop:disable Scalability/CronWorkerContext + + feature_category :source_code_management + urgency :low + + idempotent! + + def perform + if ::Feature.enabled?(:periodic_project_authorization_recalculation, default_enabled: true) + AuthorizedProjectUpdate::PeriodicRecalculateService.new.execute + end + end + end +end diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb new file mode 100644 index 00000000000..336b1c5443e --- /dev/null +++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module AuthorizedProjectUpdate + class UserRefreshOverUserRangeWorker + include ApplicationWorker + + feature_category :authentication_and_authorization + urgency :low + queue_namespace :authorized_project_update + deduplicate :until_executing, including_scheduled: true + + idempotent! + + def perform(start_user_id, end_user_id) + if ::Feature.enabled?(:periodic_project_authorization_recalculation, default_enabled: true) + AuthorizedProjectUpdate::RecalculateForUserRangeService.new(start_user_id, end_user_id).execute + end + end + end +end diff --git a/changelogs/unreleased/205273-project_auth_periodic_recompute.yml b/changelogs/unreleased/205273-project_auth_periodic_recompute.yml new file mode 100644 index 00000000000..2548ad23367 --- /dev/null +++ b/changelogs/unreleased/205273-project_auth_periodic_recompute.yml @@ -0,0 +1,5 @@ +--- +title: Periodically recompute project authorizations +merge_request: 34071 +author: +type: added diff --git a/changelogs/unreleased/223218-re-introduce-ssh-key-comments-via-the-api.yml b/changelogs/unreleased/223218-re-introduce-ssh-key-comments-via-the-api.yml new file mode 100644 index 00000000000..f173dfc2d47 --- /dev/null +++ b/changelogs/unreleased/223218-re-introduce-ssh-key-comments-via-the-api.yml @@ -0,0 +1,5 @@ +--- +title: Do not mask key comments for DeployKeys +merge_request: 35014 +author: +type: fixed diff --git a/changelogs/unreleased/bvl-fix-git-http-user-logging.yml b/changelogs/unreleased/bvl-fix-git-http-user-logging.yml new file mode 100644 index 00000000000..70a1194b23d --- /dev/null +++ b/changelogs/unreleased/bvl-fix-git-http-user-logging.yml @@ -0,0 +1,5 @@ +--- +title: Load user before logging git http-requests +merge_request: 34923 +author: +type: fixed diff --git a/changelogs/unreleased/fix-templating-variables-set-from-url.yml b/changelogs/unreleased/fix-templating-variables-set-from-url.yml new file mode 100644 index 00000000000..d5b1c07f976 --- /dev/null +++ b/changelogs/unreleased/fix-templating-variables-set-from-url.yml @@ -0,0 +1,5 @@ +--- +title: Fix missing templating vars set from URL in metrics dashboard +merge_request: 34668 +author: +type: fixed diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 9d9f24183d5..0afd43634c3 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -499,6 +499,9 @@ Settings.cron_jobs['x509_issuer_crl_check_worker']['job_class'] = 'X509IssuerCrl Settings.cron_jobs['users_create_statistics_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['users_create_statistics_worker']['cron'] ||= '2 15 * * *' Settings.cron_jobs['users_create_statistics_worker']['job_class'] = 'Users::CreateStatisticsWorker' +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['cron'] ||= '45 1 * * 6' +Settings.cron_jobs['authorized_project_update_periodic_recalculate_worker']['job_class'] = 'AuthorizedProjectUpdate::PeriodicRecalculateWorker' Gitlab.ee do Settings.cron_jobs['adjourned_group_deletion_worker'] ||= Settingslogic.new({}) diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md index 5ee11c553af..44802214d7b 100644 --- a/doc/user/discussions/index.md +++ b/doc/user/discussions/index.md @@ -491,7 +491,10 @@ introduced by [#25381](https://gitlab.com/gitlab-org/gitlab/issues/25381). ### Batch Suggestions -> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1. +> - [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/25486) in GitLab 13.1 as an [alpha feature](https://about.gitlab.com/handbook/product/#alpha). +> - It's deployed behind a feature flag, disabled by default. +> - It's disabled on GitLab.com. +> - To use it in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-batch-suggestions). You can apply multiple suggestions at once to reduce the number of commits added to your branch to address your reviewers' requests. @@ -512,6 +515,27 @@ to your branch to address your reviewers' requests. ![A code change suggestion displayed, with the button to apply the batch of suggestions highlighted.](img/apply_batch_of_suggestions_v13_1.jpg "Apply a batch of suggestions") +#### Enable or disable Batch Suggestions + +Batch Suggestions is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../../administration/feature_flags.md) +can enable it for your instance. + +To enable it: + +```ruby +# Instance-wide +Feature.enable(:batched_suggestions) +``` + +To disable it: + +```ruby +# Instance-wide +Feature.disable(:batched_suggestions) +``` + ## Start a thread by replying to a standard comment > [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/30299) in GitLab 11.9 diff --git a/doc/user/profile/preferences.md b/doc/user/profile/preferences.md index ccaea61ae4b..a5fa3cf373f 100644 --- a/doc/user/profile/preferences.md +++ b/doc/user/profile/preferences.md @@ -80,8 +80,9 @@ The default syntax theme is White, and you can choose among 5 different themes: [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in 13.0, the theme you choose also applies to the [Web IDE](../project/web_ide/index.md)'s code editor and [Snippets](../snippets.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808), -which applies to the entire Web IDE screen. +The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and +the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), +which apply to the entire Web IDE screen. ## Behavior diff --git a/doc/user/project/issues/design_management.md b/doc/user/project/issues/design_management.md index 981c2a7c34a..ac397592a3b 100644 --- a/doc/user/project/issues/design_management.md +++ b/doc/user/project/issues/design_management.md @@ -181,17 +181,23 @@ so that everyone involved can participate in the discussion. > [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/13049) in GitLab 13.1. -Discussion threads can be resolved on Designs. You can mark a thread as resolved -or unresolved by clicking the **Resolve thread** icon at the first comment of the -discussion. +Discussion threads can be resolved on Designs. -![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png) +There are two ways to resolve/unresolve a Design thread: -Pinned comments can also be resolved or unresolved in their threads. -When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve -the thread once published. +1. You can mark a thread as resolved or unresolved by clicking the checkmark icon for **Resolve thread** in the top-right corner of the first comment of the discussion: -![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png) + ![Resolve thread icon](img/resolve_design-discussion_icon_v13_1.png) + +1. Design threads can also be resolved or unresolved in their threads by using a checkbox. + When replying to a comment, you will see a checkbox that you can click in order to resolve or unresolve + the thread once published: + + ![Resolve checkbox](img/resolve_design-discussion_checkbox_v13_1.png) + +Note that your resolved comment pins will disappear from the Design to free up space for new discussions. +However, if you need to revisit or find a resolved discussion, all of your resolved threads will be +available in the **Resolved Comment** area at the bottom of the right sidebar. ## Referring to designs in Markdown diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md index e2e498b605a..15c3bd5522e 100644 --- a/doc/user/project/static_site_editor/index.md +++ b/doc/user/project/static_site_editor/index.md @@ -7,6 +7,8 @@ description: "The static site editor enables users to edit content on static web > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10. > - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0. +> - Support for adding images through the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216640) in GitLab 13.1. +> - Markdown front matter hidden on the WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/216834) in GitLab 13.1. DANGER: **Danger:** In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282) @@ -78,6 +80,11 @@ Anyone satisfying the [requirements](#requirements) will be able to edit the content of the pages without prior knowledge of Git nor of your site's codebase. +NOTE: **Note:** +From GitLab 13.1 onwards, the YAML front matter of Markdown files is hidden on the +WYSIWYG editor to avoid unintended changes. To edit it, use the Markdown editing mode, the regular +GitLab file editor, or the Web IDE. + ### Use the Static Site Editor to edit your content For instance, suppose you are a recently hired technical writer at a large diff --git a/doc/user/project/web_ide/img/dark_theme_v13.0.png b/doc/user/project/web_ide/img/dark_theme_v13.0.png Binary files differdeleted file mode 100644 index 6034bc52c05..00000000000 --- a/doc/user/project/web_ide/img/dark_theme_v13.0.png +++ /dev/null diff --git a/doc/user/project/web_ide/img/dark_theme_v13_0.png b/doc/user/project/web_ide/img/dark_theme_v13_0.png Binary files differnew file mode 100644 index 00000000000..f1999363904 --- /dev/null +++ b/doc/user/project/web_ide/img/dark_theme_v13_0.png diff --git a/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png Binary files differnew file mode 100644 index 00000000000..ccb9cf6f126 --- /dev/null +++ b/doc/user/project/web_ide/img/solarized_dark_theme_v13_1.png diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png Binary files differdeleted file mode 100644 index f3c4aa142a4..00000000000 --- a/doc/user/project/web_ide/img/solarized_light_theme_v13.0.png +++ /dev/null diff --git a/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png b/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png Binary files differnew file mode 100644 index 00000000000..adf6d3c6b02 --- /dev/null +++ b/doc/user/project/web_ide/img/solarized_light_theme_v13_0.png diff --git a/doc/user/project/web_ide/index.md b/doc/user/project/web_ide/index.md index 0ddc9762bc5..ce20f2308e6 100644 --- a/doc/user/project/web_ide/index.md +++ b/doc/user/project/web_ide/index.md @@ -45,17 +45,19 @@ Single file editing is based on the [Ace Editor](https://ace.c9.io). ### Themes -> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab 13.0. +> - [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2389) in GitLab in 13.0. +> - Full Solarized Dark Theme [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/219228) in GitLab 13.1. All the themes GitLab supports for syntax highlighting are added to the Web IDE's code editor. You can pick a theme from your [profile preferences](../../profile/preferences.md). -The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808), -which applies to the entire Web IDE screen. +The themes are available only in the Web IDE file editor, except for the [dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/209808) and +the [solarized dark theme](https://gitlab.com/gitlab-org/gitlab/-/issues/219228), +which apply to the entire Web IDE screen. -| Solarized Light Theme | Dark Theme | -|---------------------------------------------------------------|-----------------------------------------| -| ![Solarized Light Theme](img/solarized_light_theme_v13.0.png) | ![Dark Theme](img/dark_theme_v13.0.png) | +| Solarized Light Theme | Solarized Dark Theme | Dark Theme | +|---------------------------------------------------------------|-------------------------------------------------------------|-----------------------------------------| +| ![Solarized Light Theme](img/solarized_light_theme_v13_0.png) | ![Solarized Dark Theme](img/solarized_dark_theme_v13_1.png) | ![Dark Theme](img/dark_theme_v13_0.png) | ## Configure the Web IDE diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb index 11340e91aae..3259b615369 100644 --- a/lib/api/deploy_keys.rb +++ b/lib/api/deploy_keys.rb @@ -25,8 +25,7 @@ module API get "deploy_keys" do authenticated_as_admin! - deploy_keys = DeployKey.all.preload_users - present paginate(deploy_keys), with: Entities::SSHKey + present paginate(DeployKey.all), with: Entities::DeployKey end params do @@ -43,7 +42,7 @@ module API end # rubocop: disable CodeReuse/ActiveRecord get ":id/deploy_keys" do - keys = user_project.deploy_keys_projects.preload(deploy_key: [:user]) + keys = user_project.deploy_keys_projects.preload(:deploy_key) present paginate(keys), with: Entities::DeployKeysProject end @@ -105,7 +104,7 @@ module API # rubocop: enable CodeReuse/ActiveRecord desc 'Update an existing deploy key for a project' do - success Entities::SSHKey + success Entities::DeployKey end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' @@ -140,7 +139,7 @@ module API desc 'Enable a deploy key for a project' do detail 'This feature was added in GitLab 8.11' - success Entities::SSHKey + success Entities::DeployKey end params do requires :key_id, type: Integer, desc: 'The ID of the deploy key' @@ -150,7 +149,7 @@ module API current_user, declared_params).execute if key - present key, with: Entities::SSHKey + present key, with: Entities::DeployKey else not_found!('Deploy Key') end diff --git a/lib/api/entities/deploy_key.rb b/lib/api/entities/deploy_key.rb new file mode 100644 index 00000000000..ed922c24eda --- /dev/null +++ b/lib/api/entities/deploy_key.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module API + module Entities + class DeployKey < Entities::SSHKey + expose :key + end + end +end diff --git a/lib/api/entities/deploy_key_with_user.rb b/lib/api/entities/deploy_key_with_user.rb index 31024dc3910..a8f6440b67c 100644 --- a/lib/api/entities/deploy_key_with_user.rb +++ b/lib/api/entities/deploy_key_with_user.rb @@ -2,7 +2,8 @@ module API module Entities - class DeployKeyWithUser < Entities::SSHKeyWithUser + class DeployKeyWithUser < Entities::DeployKey + expose :user, using: Entities::UserPublic expose :deploy_keys_projects end end diff --git a/lib/api/entities/deploy_keys_project.rb b/lib/api/entities/deploy_keys_project.rb index 64725459167..12a86fbdf8e 100644 --- a/lib/api/entities/deploy_keys_project.rb +++ b/lib/api/entities/deploy_keys_project.rb @@ -3,7 +3,7 @@ module API module Entities class DeployKeysProject < Grape::Entity - expose :deploy_key, merge: true, using: Entities::SSHKey + expose :deploy_key, merge: true, using: Entities::DeployKey expose :can_push end end diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb index aafb933df32..c938df8cf4e 100644 --- a/spec/controllers/repositories/git_http_controller_spec.rb +++ b/spec/controllers/repositories/git_http_controller_spec.rb @@ -60,10 +60,21 @@ RSpec.describe Repositories::GitHttpController do get :info_refs, params: params end + + include_context 'parsed logs' do + it 'adds user info to the logs' do + get :info_refs, params: params + + expect(log_data).to include('username' => user.username, + 'user_id' => user.id, + 'meta.user' => user.username) + end + end end context 'with exceptions' do before do + allow(controller).to receive(:authenticate_user).and_return(true) allow(controller).to receive(:verify_workhorse_api!).and_return(true) end diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index 3a70bda51da..2dea40585f1 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -9,6 +9,7 @@ import { convertToGrafanaTimeRange, addDashboardMetaDataToLink, } from '~/monitoring/stores/utils'; +import * as urlUtils from '~/lib/utils/url_utility'; import { annotationsData } from '../mock_data'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; @@ -398,6 +399,118 @@ describe('mapToDashboardViewModel', () => { }); }); }); + + describe('templating variables mapping', () => { + beforeEach(() => { + jest.spyOn(urlUtils, 'queryToObject'); + }); + + afterEach(() => { + urlUtils.queryToObject.mockRestore(); + }); + + it('sets variables as-is from yml file if URL has no variables', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce(); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'kubernetes', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + }, + }); + }); + + it('sets variables as-is from yml file if URL has no matching variables', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-environment': 'POD', + }); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'kubernetes', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'kubernetes-2', + }, + }, + }); + }); + + it('merges variables from URL with the ones from yml file', () => { + const response = { + dashboard: 'Dashboard Name', + links: [], + templating: { + variables: { + pod: 'kubernetes', + pod_2: 'kubernetes-2', + }, + }, + }; + + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-environment': 'POD', + 'var-pod': 'POD1', + 'var-pod_2': 'POD2', + }); + + expect(mapToDashboardViewModel(response)).toMatchObject({ + dashboard: 'Dashboard Name', + links: [], + variables: { + pod: { + label: 'pod', + type: 'text', + value: 'POD1', + }, + pod_2: { + label: 'pod_2', + type: 'text', + value: 'POD2', + }, + }, + }); + }); + }); }); describe('normalizeQueryResult', () => { diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js index c44bb957166..5164ed1b54b 100644 --- a/spec/frontend/monitoring/store/variable_mapping_spec.js +++ b/spec/frontend/monitoring/store/variable_mapping_spec.js @@ -1,4 +1,5 @@ -import { parseTemplatingVariables } from '~/monitoring/stores/variable_mapping'; +import { parseTemplatingVariables, mergeURLVariables } from '~/monitoring/stores/variable_mapping'; +import * as urlUtils from '~/lib/utils/url_utility'; import { mockTemplatingData, mockTemplatingDataResponses } from '../mock_data'; describe('parseTemplatingVariables', () => { @@ -21,3 +22,73 @@ describe('parseTemplatingVariables', () => { expect(parseTemplatingVariables(input?.dashboard?.templating)).toEqual(expected); }); }); + +describe('mergeURLVariables', () => { + beforeEach(() => { + jest.spyOn(urlUtils, 'queryToObject'); + }); + + afterEach(() => { + urlUtils.queryToObject.mockRestore(); + }); + + it('returns empty object if variables are not defined in yml or URL', () => { + urlUtils.queryToObject.mockReturnValueOnce({}); + + expect(mergeURLVariables({})).toEqual({}); + }); + + it('returns empty object if variables are defined in URL but not in yml', () => { + urlUtils.queryToObject.mockReturnValueOnce({ + 'var-env': 'one', + 'var-instance': 'localhost', + }); + + expect(mergeURLVariables({})).toEqual({}); + }); + + it('returns yml variables if variables defined in yml but not in the URL', () => { + urlUtils.queryToObject.mockReturnValueOnce({}); + + const params = { + env: 'one', + instance: 'localhost', + }; + + expect(mergeURLVariables(params)).toEqual(params); + }); + + it('returns yml variables if variables defined in URL do not match with yml variables', () => { + const urlParams = { + 'var-env': 'one', + 'var-instance': 'localhost', + }; + const ymlParams = { + pod: { value: 'one' }, + service: { value: 'database' }, + }; + urlUtils.queryToObject.mockReturnValueOnce(urlParams); + + expect(mergeURLVariables(ymlParams)).toEqual(ymlParams); + }); + + it('returns merged yml and URL variables if there is some match', () => { + const urlParams = { + 'var-env': 'one', + 'var-instance': 'localhost:8080', + }; + const ymlParams = { + instance: { value: 'localhost' }, + service: { value: 'database' }, + }; + + const merged = { + instance: { value: 'localhost:8080' }, + service: { value: 'database' }, + }; + + urlUtils.queryToObject.mockReturnValueOnce(urlParams); + + expect(mergeURLVariables(ymlParams)).toEqual(merged); + }); +}); diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index aa5a4459a72..039cf275eea 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -169,8 +169,8 @@ describe('monitoring/utils', () => { }); }); - describe('getPromCustomVariablesFromUrl', () => { - const { getPromCustomVariablesFromUrl } = monitoringUtils; + describe('templatingVariablesFromUrl', () => { + const { templatingVariablesFromUrl } = monitoringUtils; beforeEach(() => { jest.spyOn(urlUtils, 'queryToObject'); @@ -195,7 +195,7 @@ describe('monitoring/utils', () => { 'var-pod': 'POD', }); - expect(getPromCustomVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' })); + expect(templatingVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' })); }); it('returns an empty object when no custom variables are present', () => { @@ -203,7 +203,7 @@ describe('monitoring/utils', () => { dashboard: '.gitlab/dashboards/custom_dashboard.yml', }); - expect(getPromCustomVariablesFromUrl()).toStrictEqual({}); + expect(templatingVariablesFromUrl()).toStrictEqual({}); }); }); @@ -427,76 +427,6 @@ describe('monitoring/utils', () => { }); }); - describe('mergeURLVariables', () => { - beforeEach(() => { - jest.spyOn(urlUtils, 'queryToObject'); - }); - - afterEach(() => { - urlUtils.queryToObject.mockRestore(); - }); - - it('returns empty object if variables are not defined in yml or URL', () => { - urlUtils.queryToObject.mockReturnValueOnce({}); - - expect(monitoringUtils.mergeURLVariables({})).toEqual({}); - }); - - it('returns empty object if variables are defined in URL but not in yml', () => { - urlUtils.queryToObject.mockReturnValueOnce({ - 'var-env': 'one', - 'var-instance': 'localhost', - }); - - expect(monitoringUtils.mergeURLVariables({})).toEqual({}); - }); - - it('returns yml variables if variables defined in yml but not in the URL', () => { - urlUtils.queryToObject.mockReturnValueOnce({}); - - const params = { - env: 'one', - instance: 'localhost', - }; - - expect(monitoringUtils.mergeURLVariables(params)).toEqual(params); - }); - - it('returns yml variables if variables defined in URL do not match with yml variables', () => { - const urlParams = { - 'var-env': 'one', - 'var-instance': 'localhost', - }; - const ymlParams = { - pod: { value: 'one' }, - service: { value: 'database' }, - }; - urlUtils.queryToObject.mockReturnValueOnce(urlParams); - - expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(ymlParams); - }); - - it('returns merged yml and URL variables if there is some match', () => { - const urlParams = { - 'var-env': 'one', - 'var-instance': 'localhost:8080', - }; - const ymlParams = { - instance: { value: 'localhost' }, - service: { value: 'database' }, - }; - - const merged = { - instance: { value: 'localhost:8080' }, - service: { value: 'database' }, - }; - - urlUtils.queryToObject.mockReturnValueOnce(urlParams); - - expect(monitoringUtils.mergeURLVariables(ymlParams)).toEqual(merged); - }); - }); - describe('convertVariablesForURL', () => { it.each` input | expected diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index f283ac100a9..9e5eab4fc6b 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -99,6 +99,8 @@ describe 'lograge', type: :request do end context 'with a log subscriber' do + include_context 'parsed logs' + let(:subscriber) { Lograge::LogSubscribers::ActionController.new } let(:event) do @@ -119,16 +121,6 @@ describe 'lograge', type: :request do ) end - let(:log_output) { StringIO.new } - let(:logger) do - Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } - end - let(:log_data) { Gitlab::Json.parse(log_output.string) } - - before do - Lograge.logger = logger - end - describe 'with an exception' do let(:exception) { RuntimeError.new('bad request') } let(:backtrace) { caller } diff --git a/spec/lib/api/entities/deploy_key_spec.rb b/spec/lib/api/entities/deploy_key_spec.rb new file mode 100644 index 00000000000..704dabae63b --- /dev/null +++ b/spec/lib/api/entities/deploy_key_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::DeployKey do + describe '#as_json' do + subject { entity.as_json } + + let(:deploy_key) { create(:deploy_key, public: true) } + let(:entity) { described_class.new(deploy_key) } + + it 'includes basic fields', :aggregate_failures do + is_expected.to include( + id: deploy_key.id, + title: deploy_key.title, + created_at: deploy_key.created_at, + expires_at: deploy_key.expires_at, + key: deploy_key.key + ) + end + end +end diff --git a/spec/lib/api/entities/deploy_keys_project_spec.rb b/spec/lib/api/entities/deploy_keys_project_spec.rb new file mode 100644 index 00000000000..a357467d7ce --- /dev/null +++ b/spec/lib/api/entities/deploy_keys_project_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::DeployKeysProject do + describe '#as_json' do + subject { entity.as_json } + + let(:deploy_keys_project) { create(:deploy_keys_project, :write_access) } + let(:entity) { described_class.new(deploy_keys_project) } + + it 'includes basic fields', :aggregate_failures do + deploy_key = deploy_keys_project.deploy_key + + is_expected.to include( + id: deploy_key.id, + title: deploy_key.title, + created_at: deploy_key.created_at, + expires_at: deploy_key.expires_at, + key: deploy_key.key, + can_push: deploy_keys_project.can_push + ) + end + end +end diff --git a/spec/lib/api/entities/ssh_key_spec.rb b/spec/lib/api/entities/ssh_key_spec.rb new file mode 100644 index 00000000000..25a0fecfb75 --- /dev/null +++ b/spec/lib/api/entities/ssh_key_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe API::Entities::SSHKey do + describe '#as_json' do + subject { entity.as_json } + + let(:key) { create(:key, user: create(:user)) } + let(:entity) { described_class.new(key) } + + it 'includes basic fields', :aggregate_failures do + is_expected.to include( + id: key.id, + title: key.title, + created_at: key.created_at, + expires_at: key.expires_at, + key: key.publishable_key + ) + end + end +end diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb index 1baa18b53ce..e8cc6bc71ae 100644 --- a/spec/requests/api/deploy_keys_spec.rb +++ b/spec/requests/api/deploy_keys_spec.rb @@ -8,7 +8,7 @@ describe API::DeployKeys do let(:admin) { create(:admin) } let(:project) { create(:project, creator_id: user.id) } let(:project2) { create(:project, creator_id: user.id) } - let(:deploy_key) { create(:deploy_key, public: true, user: user) } + let(:deploy_key) { create(:deploy_key, public: true) } let!(:deploy_keys_project) do create(:deploy_keys_project, project: project, deploy_key: deploy_key) @@ -40,32 +40,6 @@ describe API::DeployKeys do expect(json_response).to be_an Array expect(json_response.first['id']).to eq(deploy_keys_project.deploy_key.id) end - - it 'returns all deploy keys with comments replaced with'\ - 'a simple identifier of username + hostname' do - get api('/deploy_keys', admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(response).to include_pagination_headers - expect(json_response).to be_an Array - - keys = json_response.map { |key_detail| key_detail['key'] } - expect(keys).to all(include("#{user.name} (#{Gitlab.config.gitlab.host}")) - end - - context 'N+1 queries' do - before do - get api('/deploy_keys', admin) - end - - it 'avoids N+1 queries', :request_store do - control_count = ActiveRecord::QueryRecorder.new { get api('/deploy_keys', admin) }.count - - create_list(:deploy_key, 2, public: true, user: create(:user)) - - expect { get api('/deploy_keys', admin) }.not_to exceed_query_limit(control_count) - end - end end end @@ -82,25 +56,6 @@ describe API::DeployKeys do expect(json_response).to be_an Array expect(json_response.first['title']).to eq(deploy_key.title) end - - context 'N+1 queries' do - before do - get api("/projects/#{project.id}/deploy_keys", admin) - end - - it 'avoids N+1 queries', :request_store do - control_count = ActiveRecord::QueryRecorder.new do - get api("/projects/#{project.id}/deploy_keys", admin) - end.count - - deploy_key = create(:deploy_key, user: create(:user)) - create(:deploy_keys_project, project: project, deploy_key: deploy_key) - - expect do - get api("/projects/#{project.id}/deploy_keys", admin) - end.not_to exceed_query_limit(control_count) - end - end end describe 'GET /projects/:id/deploy_keys/:key_id' do @@ -111,13 +66,6 @@ describe API::DeployKeys do expect(json_response['title']).to eq(deploy_key.title) end - it 'exposes key comment as a simple identifier of username + hostname' do - get api("/projects/#{project.id}/deploy_keys/#{deploy_key.id}", admin) - - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['key']).to include("#{deploy_key.user_name} (#{Gitlab.config.gitlab.host})") - end - it 'returns 404 Not Found with invalid ID' do get api("/projects/#{project.id}/deploy_keys/404", admin) diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb index d860179f0a7..617587e2fa6 100644 --- a/spec/requests/jwt_controller_spec.rb +++ b/spec/requests/jwt_controller_spec.rb @@ -3,19 +3,14 @@ require 'spec_helper' describe JwtController do + include_context 'parsed logs' + let(:service) { double(execute: {}) } let(:service_class) { double(new: service) } let(:service_name) { 'test' } let(:parameters) { { service: service_name } } - let(:log_output) { StringIO.new } - let(:logger) do - Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } - end - let(:log_data) { Gitlab::Json.parse(log_output.string) } before do - Lograge.logger = logger - stub_const('JwtController::SERVICES', service_name => service_class) end diff --git a/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb new file mode 100644 index 00000000000..020056da36e --- /dev/null +++ b/spec/services/authorized_project_update/periodic_recalculate_service_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::PeriodicRecalculateService do + subject(:service) { described_class.new } + + describe '#execute' do + let(:batch_size) { 2 } + + let_it_be(:users) { create_list(:user, 4) } + + before do + stub_const('AuthorizedProjectUpdate::PeriodicRecalculateService::BATCH_SIZE', batch_size) + + User.delete([users[1], users[2]]) + end + + it 'calls AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker' do + (1..User.maximum(:id)).each_slice(batch_size).with_index do |batch, index| + delay = AuthorizedProjectUpdate::PeriodicRecalculateService::DELAY_INTERVAL * index + + expect(AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker).to( + receive(:perform_in).with(delay, *batch.minmax)) + end + + service.execute + end + end +end diff --git a/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb new file mode 100644 index 00000000000..28cbda6f4fd --- /dev/null +++ b/spec/services/authorized_project_update/recalculate_for_user_range_service_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::RecalculateForUserRangeService do + describe '#execute' do + let_it_be(:users) { create_list(:user, 2) } + + it 'calls Users::RefreshAuthorizedProjectsService' do + users.each do |user| + expect(Users::RefreshAuthorizedProjectsService).to( + receive(:new).with(user).and_call_original) + end + + range = users.map(&:id).minmax + described_class.new(*range).execute + end + end +end diff --git a/spec/support/shared_contexts/controllers/logging_shared_context.rb b/spec/support/shared_contexts/controllers/logging_shared_context.rb new file mode 100644 index 00000000000..986a96f3a8d --- /dev/null +++ b/spec/support/shared_contexts/controllers/logging_shared_context.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# This context replaces the logger and exposes the `log_data` variable for +# inspection +RSpec.shared_context 'parsed logs' do + let(:logger) do + Logger.new(log_output).tap { |logger| logger.formatter = ->(_, _, _, msg) { msg } } + end + + let(:log_output) { StringIO.new } + let(:log_data) { Gitlab::Json.parse(log_output.string) } + + around do |example| + initial_logger = Lograge.logger + Lograge.logger = logger + + example.run + + Lograge.logger = initial_logger + end +end diff --git a/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb new file mode 100644 index 00000000000..fcd073953b6 --- /dev/null +++ b/spec/workers/authorized_project_update/periodic_recalculate_worker_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::PeriodicRecalculateWorker do + describe '#perform' do + it 'calls AuthorizedProjectUpdate::PeriodicRecalculateService' do + expect_next_instance_of(AuthorizedProjectUpdate::PeriodicRecalculateService) do |service| + expect(service).to receive(:execute) + end + + subject.perform + end + + context 'feature flag :periodic_project_authorization_recalculation is disabled' do + before do + stub_feature_flags(periodic_project_authorization_recalculation: false) + end + + it 'does not call AuthorizedProjectUpdate::PeriodicRecalculateService' do + expect(AuthorizedProjectUpdate::PeriodicRecalculateService).not_to receive(:new) + + subject.perform + end + end + end +end diff --git a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb new file mode 100644 index 00000000000..5d1c405dfd0 --- /dev/null +++ b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do + let(:start_user_id) { 42 } + let(:end_user_id) { 4242 } + + describe '#perform' do + it 'calls AuthorizedProjectUpdate::RecalculateForUserRangeService' do + expect_next_instance_of(AuthorizedProjectUpdate::RecalculateForUserRangeService) do |service| + expect(service).to receive(:execute) + end + + subject.perform(start_user_id, end_user_id) + end + + context 'feature flag :periodic_project_authorization_recalculation is disabled' do + before do + stub_feature_flags(periodic_project_authorization_recalculation: false) + end + + it 'does not call AuthorizedProjectUpdate::RecalculateForUserRangeService' do + expect(AuthorizedProjectUpdate::RecalculateForUserRangeService).not_to receive(:new) + + subject.perform(start_user_id, end_user_id) + end + end + end +end |