diff --git a/.rubocop_todo/migration/background_migration_record.yml b/.rubocop_todo/migration/background_migration_record.yml
index af93c19f11f..cb297d4fded 100644
--- a/.rubocop_todo/migration/background_migration_record.yml
+++ b/.rubocop_todo/migration/background_migration_record.yml
@@ -10,7 +10,6 @@ Migration/BackgroundMigrationRecord:
- 'lib/gitlab/background_migration/backfill_draft_status_on_merge_requests.rb'
- 'lib/gitlab/background_migration/backfill_project_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- - 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- 'lib/gitlab/background_migration/migrate_null_private_profile_to_false.rb'
- 'lib/gitlab/background_migration/populate_latest_pipeline_ids.rb'
diff --git a/.rubocop_todo/migration/batched_migration_base_class.yml b/.rubocop_todo/migration/batched_migration_base_class.yml
index 732e01ec9c0..e1e8e643d46 100644
--- a/.rubocop_todo/migration/batched_migration_base_class.yml
+++ b/.rubocop_todo/migration/batched_migration_base_class.yml
@@ -11,7 +11,6 @@ Migration/BatchedMigrationBaseClass:
- 'lib/gitlab/background_migration/backfill_snippet_repositories.rb'
- 'lib/gitlab/background_migration/backfill_topics_title.rb'
- 'lib/gitlab/background_migration/create_security_setting.rb'
- - 'lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb'
- 'lib/gitlab/background_migration/fix_projects_without_project_feature.rb'
- 'lib/gitlab/background_migration/fix_projects_without_prometheus_service.rb'
- 'lib/gitlab/background_migration/legacy_upload_mover.rb'
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_duration_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_duration_chart.vue
index 0f2f310e3de..2bb50cc9432 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_duration_chart.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_duration_chart.vue
@@ -2,6 +2,8 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
+import { stringifyTime, parseSeconds } from '~/lib/utils/datetime/date_format_utility';
+import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
export default {
components: {
@@ -35,6 +37,17 @@ export default {
return durationSeries;
},
},
+ methods: {
+ formatDate(isoDateStr) {
+ if (isoDateStr) {
+ return localeDateFormat.asDate.format(new Date(isoDateStr));
+ }
+ return '';
+ },
+ formatDuration(seconds) {
+ return stringifyTime(parseSeconds(seconds, { daysPerWeek: 7, hoursPerDay: 24 }));
+ },
+ },
lineChartOptions: {
yAxis: {
name: s__('Pipeline|Seconds'),
@@ -55,6 +68,13 @@ export default {
:data="data"
:option="$options.lineChartOptions"
:include-legend-avg-max="false"
- />
+ >
+
+ {{ formatDate(params.value) }}
+
+
+ {{ formatDuration(value) }}
+
+
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue
index 940f3b8924d..2c09558f87d 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_status_chart.vue
@@ -7,6 +7,7 @@ import {
DATA_VIZ_BLUE_500,
} from '@gitlab/ui/src/tokens/build/js/tokens';
import { s__ } from '~/locale';
+import { localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
export default {
components: {
@@ -51,6 +52,14 @@ export default {
return this.data.bars;
},
},
+ methods: {
+ formatDate(isoDateStr) {
+ if (isoDateStr) {
+ return localeDateFormat.asDate.format(new Date(isoDateStr));
+ }
+ return '';
+ },
+ },
palette: [DATA_VIZ_GREEN_500, DATA_VIZ_MAGENTA_600, DATA_VIZ_BLUE_500],
};
@@ -68,6 +77,10 @@ export default {
:group-by="groupBy"
:bars="bars"
:include-legend-avg-max="false"
- />
+ >
+
+ {{ formatDate(params.value) }}
+
+
diff --git a/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue b/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue
index 3b6df8c2e5e..87077cbf949 100644
--- a/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue
+++ b/app/assets/javascripts/token_access/components/autopopulate_allowlist_modal.vue
@@ -2,7 +2,6 @@
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__, sprintf } from '~/locale';
-import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql';
export default {
name: 'AutopopulateAllowlistModal',
@@ -33,12 +32,6 @@ export default {
},
},
apollo: {},
- data() {
- return {
- errorMessage: false,
- isAutopopulating: false,
- };
- },
computed: {
authLogExceedsLimitMessage() {
return sprintf(
@@ -56,14 +49,12 @@ export default {
text: __('Add entries'),
attributes: {
variant: 'confirm',
- loading: this.isAutopopulating,
},
},
actionSecondary: {
text: __('Cancel'),
attributes: {
variant: 'default',
- disabled: this.isAutopopulating,
},
},
};
@@ -77,48 +68,15 @@ export default {
},
},
methods: {
- async autopopulateAllowlist() {
- this.isAutopopulating = true;
- this.errorMessage = null;
-
- try {
- const {
- data: {
- ciJobTokenScopeAutopopulateAllowlist: { errors },
- },
- } = await this.$apollo.mutate({
- mutation: autopopulateAllowlistMutation,
- variables: {
- projectPath: this.fullPath,
- },
- });
-
- if (errors.length) {
- throw new Error(errors[0]);
- }
-
- this.$emit('refetch-allowlist');
- this.hideModal();
- this.$toast.show(
- s__('CICD|Authentication log entries were successfully added to the allowlist.'),
- );
- } catch (error) {
- this.errorMessage =
- error?.message ||
- s__(
- 'CICD|An error occurred while adding the authentication log entries. Please try again.',
- );
- } finally {
- this.isAutopopulating = false;
- }
+ autopopulateAllowlist() {
+ this.$emit('autopopulate-allowlist');
},
hideModal() {
- this.errorMessage = null;
this.$emit('hide');
},
},
compactionAlgorithmHelpPage: helpPagePath('ci/jobs/ci_job_token', {
- anchor: 'auto-populate-a-projects-allowlist',
+ anchor: 'allowlist-compaction',
}),
};
@@ -135,9 +93,6 @@ export default {
@canceled="hideModal"
@hidden="hideModal"
>
-
{{ content }}
+
@@ -182,7 +140,7 @@ export default {
{{ s__( - 'CICD|The process to add entries could take a moment to complete with large logs or allowlists.', + 'CICD|The process might take a moment to complete for large authentication logs or allowlists.', ) }}
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue index 3f81fbc7a7b..dbc9d0be3ce 100644 --- a/app/assets/javascripts/token_access/components/inbound_token_access.vue +++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue @@ -23,6 +23,7 @@ import inboundRemoveGroupCIJobTokenScopeMutation from '../graphql/mutations/inbo import inboundUpdateCIJobTokenScopeMutation from '../graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql'; import inboundGetCIJobTokenScopeQuery from '../graphql/queries/inbound_get_ci_job_token_scope.query.graphql'; import inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery from '../graphql/queries/inbound_get_groups_and_projects_with_ci_job_token_scope.query.graphql'; +import autopopulateAllowlistMutation from '../graphql/mutations/autopopulate_allowlist.mutation.graphql'; import getCiJobTokenScopeAllowlistQuery from '../graphql/queries/get_ci_job_token_scope_allowlist.query.graphql'; import getAuthLogCountQuery from '../graphql/queries/get_auth_log_count.query.graphql'; import removeAutopopulatedEntriesMutation from '../graphql/mutations/remove_autopopulated_entries.mutation.graphql'; @@ -71,16 +72,6 @@ export default { text: s__('CICD|Only this project and any groups and projects in the allowlist'), }, ], - crudFormActions: [ - { - text: __('Group or project'), - value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT, - }, - { - text: __('All projects in authentication log'), - value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG, - }, - ], components: { AutopopulateAllowlistModal, GlAlert, @@ -206,6 +197,23 @@ export default { anchor: 'control-job-token-access-to-your-project', }); }, + crudFormActions() { + const actions = [ + { + text: __('Group or project'), + value: JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT, + }, + ]; + + if (this.authLogCount > 0) { + actions.push({ + text: __('All projects in authentication log'), + value: JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG, + }); + } + + return actions; + }, allowlist() { const { groups, projects } = this.groupsAndProjectsWithAccess; return [...groups, ...projects]; @@ -224,6 +232,9 @@ export default { }, ]; }, + hasAutoPopulatedEntries() { + return this.allowlist.filter((entry) => entry.autopopulated).length > 0; + }, groupCount() { return this.groupsAndProjectsWithAccess.groups.length; }, @@ -317,6 +328,43 @@ export default { this.refetchGroupsAndProjects(); return Promise.resolve(); }, + async autopopulateAllowlist() { + this.hideSelectedAction(); + this.autopopulationErrorMessage = null; + this.allowlistLoadingMessage = s__( + 'CICD|Auto-populating allowlist entries. Please wait while the action completes.', + ); + + try { + const { + data: { + ciJobTokenScopeAutopopulateAllowlist: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: autopopulateAllowlistMutation, + variables: { + projectPath: this.fullPath, + }, + }); + + if (errors.length) { + this.autopopulationErrorMessage = errors[0].message; + return; + } + + this.$apollo.queries.inboundJobTokenScopeEnabled.refetch(); + this.refetchAllowlist(); + this.$toast.show( + s__('CICD|Authentication log entries were successfully added to the allowlist.'), + ); + } catch { + this.autopopulationErrorMessage = s__( + 'CICD|An error occurred while adding the authentication log entries. Please try again.', + ); + } finally { + this.allowlistLoadingMessage = ''; + } + }, async removeAutopopulatedEntries() { this.hideSelectedAction(); this.autopopulationErrorMessage = null; @@ -388,7 +436,7 @@ export default { :project-name="projectName" :show-modal="showAutopopulateModal" @hide="hideSelectedAction" - @refetch-allowlist="refetchAllowlist" + @autopopulate-allowlist="autopopulateAllowlist" />
+
-
+
Reference-style:
![alt text1][logo]
-[logo]: img/markdown_logo.png "Title Text"
+[logo]: img/markdown_logo_v17_11.png "Title Text"
-
+
@@ -1230,12 +1230,12 @@ The value must an integer with a unit of either `px` (default) or `%`.
For example
```markdown
-{width=100 height=100px}
+{width=100 height=100px}
-{width=75%}
+{width=75%}
```
-{width=100 height=100px}
+{width=100 height=100px}
You can also use the `img` HTML tag instead of Markdown and set its `height` and
`width` parameters.
diff --git a/gems/gitlab-backup-cli/.gitlab-ci.yml b/gems/gitlab-backup-cli/.gitlab-ci.yml
index 01d2a5fadc7..61c9091d582 100644
--- a/gems/gitlab-backup-cli/.gitlab-ci.yml
+++ b/gems/gitlab-backup-cli/.gitlab-ci.yml
@@ -17,13 +17,18 @@ rspec:
- name: postgres:${POSTGRES_VERSION}
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
before_script:
+ - apt update && apt install -y postgresql-client
+ - psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_test;'
+ - psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_ci_test;'
- cp gems/gitlab-backup-cli/spec/fixtures/config/database.yml config/
- "sed -i \"s/username: postgres$/username: $POSTGRES_USER/g\" config/database.yml"
- "sed -i \"s/password:\\s*$/password: $POSTGRES_PASSWORD/g\" config/database.yml"
- "sed -i \"s/host: localhost$/host: postgres/g\" config/database.yml"
- - apt update && apt install -y postgresql-client
- - psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_test;'
- - psql -h postgres -U $POSTGRES_USER -c 'create database gitlabhq_ci_test;'
+ - |
+ cd gems/gitlab-backup-cli/spec/fixtures/gitlab_fake &&
+ [ -n "$BUNDLE_GEMFILE" ] && mv Gemfile ${BUNDLE_GEMFILE} && mv Gemfile.lock ${BUNDLE_GEMFILE}.lock
+ - bundle install --retry=3
+ - cd -
- !reference [.default, before_script]
script:
- RAILS_ENV=test bundle exec rspec
diff --git a/gems/gitlab-backup-cli/.rubocop.yml b/gems/gitlab-backup-cli/.rubocop.yml
index b67751b139c..11e619c2b04 100644
--- a/gems/gitlab-backup-cli/.rubocop.yml
+++ b/gems/gitlab-backup-cli/.rubocop.yml
@@ -11,3 +11,6 @@ Rails/Exit:
RSpec/MultipleMemoizedHelpers:
Max: 25
AllowSubject: true
+
+Rails/RakeEnvironment:
+ Enabled: false
diff --git a/gems/gitlab-backup-cli/Rakefile b/gems/gitlab-backup-cli/Rakefile
index cca71754493..bc60e76f5c6 100644
--- a/gems/gitlab-backup-cli/Rakefile
+++ b/gems/gitlab-backup-cli/Rakefile
@@ -10,3 +10,7 @@ require "rubocop/rake_task"
RuboCop::RakeTask.new
task default: %i[spec rubocop]
+
+task :version do |_|
+ puts Gitlab::Backup::Cli::VERSION
+end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors.rb
index 045230a07ea..45d2f68e598 100644
--- a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors.rb
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors.rb
@@ -5,6 +5,7 @@ module Gitlab
module Cli
module Errors
autoload :DatabaseBackupError, 'gitlab/backup/cli/errors/database_backup_error'
+ autoload :DatabaseCleanupError, 'gitlab/backup/cli/errors/database_cleanup_error'
autoload :DatabaseConfigMissingError, 'gitlab/backup/cli/errors/database_config_missing_error'
autoload :DatabaseMissingConnectionError, 'gitlab/backup/cli/errors/database_missing_connection_error'
autoload :FileBackupError, 'gitlab/backup/cli/errors/file_backup_error'
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors/database_cleanup_error.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors/database_cleanup_error.rb
new file mode 100644
index 00000000000..bb4e32751e1
--- /dev/null
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/errors/database_cleanup_error.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Backup
+ module Cli
+ module Errors
+ class DatabaseCleanupError < StandardError
+ attr_reader :task, :path, :error
+
+ def initialize(task:, path:, error:)
+ @task = task
+ @path = path
+ @error = error
+
+ super(build_message)
+ end
+
+ private
+
+ def build_message
+ "Failed to cleanup GitLab databases \n" \
+ "Running the following rake task: '#{task}' (from: #{path}) failed:\n" \
+ "#{error}"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/targets/database.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/targets/database.rb
index 79b3371cca9..aaa5340911f 100644
--- a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/targets/database.rb
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/targets/database.rb
@@ -18,6 +18,10 @@ module Gitlab
].freeze
IGNORED_ERRORS_REGEXP = Regexp.union(IGNORED_ERRORS).freeze
+ # Rake task used to drop all tables from GitLab databases
+ # This task is executed before restoring data
+ DROP_TABLES_TASK = "gitlab:db:drop_tables"
+
attr_reader :errors
def initialize(context)
@@ -66,6 +70,10 @@ module Gitlab
def restore(source)
databases = Gitlab::Backup::Cli::Services::Postgres.new(context)
+ # Drop all tables Load the schema to ensure we don't have any newer tables
+ # hanging out from a failed upgrade
+ drop_tables!
+
databases.each do |db|
database_name = db.configuration.name
pg_database_name = db.configuration.database
@@ -89,10 +97,6 @@ module Gitlab
next
end
- # Drop all tables Load the schema to ensure we don't have any newer tables
- # hanging out from a failed upgrade
- drop_tables(db)
-
Gitlab::Backup::Cli::Output.info "Restoring PostgreSQL database #{pg_database_name} ... "
status = restore_tables(database: db, filepath: db_file_name)
@@ -151,18 +155,22 @@ module Gitlab
Gitlab::Backup::Cli::Output.print_tag(status ? :success : :failure)
end
- def drop_tables(database)
- pg_database_name = database.configuration.database
- Gitlab::Backup::Cli::Output.print_info "Cleaning the '#{pg_database_name}' database ... "
+ def drop_tables!
+ Gitlab::Backup::Cli::Output.print_info "Cleaning existing databases ... "
- if Rake::Task.task_defined? "gitlab:db:drop_tables:#{database.configuration.name}"
- Rake::Task["gitlab:db:drop_tables:#{database.configuration.name}"].invoke
- else
- # In single database (single or two connections)
- Rake::Task["gitlab:db:drop_tables"].invoke
+ gitlab_path = context.gitlab_basepath
+
+ # Drop existing tables from configured databases before restoring from a backup
+ rake = Utils::Rake.new(DROP_TABLES_TASK, chdir: gitlab_path).execute
+
+ unless rake.success?
+ Gitlab::Backup::Cli::Output.print_tag(:failure)
+
+ raise Errors::DatabaseCleanupError.new(task: DROP_TABLES_TASK, path: gitlab_path, error: rake.stderr)
end
Gitlab::Backup::Cli::Output.print_tag(:success)
+ Gitlab::Backup::Cli::Output.info(rake.output) unless rake.output.empty?
end
def restore_tables(database:, filepath:)
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils.rb
index 5c8293602e4..0234f17cd68 100644
--- a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils.rb
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils.rb
@@ -6,6 +6,7 @@ module Gitlab
module Utils
autoload :Compression, 'gitlab/backup/cli/utils/compression'
autoload :PgDump, 'gitlab/backup/cli/utils/pg_dump'
+ autoload :Rake, 'gitlab/backup/cli/utils/rake'
autoload :Tar, 'gitlab/backup/cli/utils/tar'
end
end
diff --git a/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils/rake.rb b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils/rake.rb
new file mode 100644
index 00000000000..3c532077d1e
--- /dev/null
+++ b/gems/gitlab-backup-cli/lib/gitlab/backup/cli/utils/rake.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Backup
+ module Cli
+ module Utils
+ class Rake
+ # @return [Array] a list of tasks to be executed
+ attr_reader :tasks
+
+ # @return [String|Pathname] a path where rake tasks are run from
+ attr_reader :chdir
+
+ # @param [Array] *tasks a list of tasks to be executed
+ # @param [String|Pathname] chdir a path where rake tasks are run from
+ def initialize(*tasks, chdir: Gitlab::Backup::Cli.root)
+ @tasks = tasks
+ @chdir = chdir
+ end
+
+ # @return [self]
+ def execute
+ Bundler.with_original_env do
+ @result = Shell::Command.new(*rake_command, chdir: chdir).capture
+ end
+
+ self
+ end
+
+ # Return whether the execution was a success or not
+ #
+ # @return [Boolean] whether the execution was a success
+ def success?
+ @result&.status&.success? || false
+ end
+
+ # Return the captured rake output
+ #
+ # @return [String] stdout content
+ def output
+ @result&.stdout || ''
+ end
+
+ # Return the captured error content
+ #
+ # @return [String] stdout content
+ def stderr
+ @result&.stderr || ''
+ end
+
+ # Return the captured execution duration
+ #
+ # @return [Float] execution duration
+ def duration
+ @result&.duration || 0.0
+ end
+
+ private
+
+ # Return a list of commands necessary to execute `rake`
+ #
+ # @return [Array] array of commands to be used by Shellout
+ def rake_command
+ %w[bundle exec rake] + tasks
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile
new file mode 100644
index 00000000000..65055a249d8
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+source "https://rubygems.org"
+
+gem 'rake', '~> 13.0'
diff --git a/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile.lock b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile.lock
new file mode 100644
index 00000000000..54a0646b769
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Gemfile.lock
@@ -0,0 +1,18 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ rake (13.2.1)
+
+PLATFORMS
+ aarch64-linux
+ arm64-darwin
+ ruby
+ x86-linux
+ x86_64-darwin
+ x86_64-linux
+
+DEPENDENCIES
+ rake (~> 13.0)
+
+BUNDLED WITH
+ 2.5.22
diff --git a/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Rakefile b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Rakefile
new file mode 100644
index 00000000000..3a0d01cd939
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/fixtures/gitlab_fake/Rakefile
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+namespace :gitlab do
+ namespace :db do
+ task :drop_tables do |_|
+ exit 0
+ end
+ end
+end
+
+task :current_pwd do |_|
+ puts Dir.getwd
+end
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/errors/database_cleanup_error_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/errors/database_cleanup_error_spec.rb
new file mode 100644
index 00000000000..6f1947f11a4
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/errors/database_cleanup_error_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+RSpec.describe Gitlab::Backup::Cli::Errors::DatabaseCleanupError do
+ let(:task) { 'gitlab:task' }
+ let(:path) { fixtures_path }
+ let(:error) { 'error message from task execution' }
+
+ subject(:database_error) { described_class.new(task: task, path: path, error: error) }
+
+ describe '#initialize' do
+ it 'sets task, path and error attributes' do
+ expect(database_error.path).to eq(path)
+ expect(database_error.task).to eq(task)
+ expect(database_error.error).to eq(error)
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/database_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/database_spec.rb
index e4e6626a703..7e34c304182 100644
--- a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/database_spec.rb
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/database_spec.rb
@@ -1,16 +1,20 @@
# frozen_string_literal: true
RSpec.describe Gitlab::Backup::Cli::Services::Database do
- let(:database_yml) { YAML.load_file(fixtures_path.join('config/database.yml'), aliases: true) }
+ let(:context) { build_test_context }
+ let(:connection) { database.send(:connection) }
let(:mocked_configuration) do
+ database_yml = YAML.load_file(fixtures_path.join('config/database.yml'), aliases: true)
ActiveRecord::DatabaseConfigurations.new(database_yml).configs_for(env_name: 'test', include_hidden: false).first
end
let(:test_configuration) do
- Gitlab::Backup::Cli::Services::Postgres.new(build_test_context).send(:database_configurations).first
+ Gitlab::Backup::Cli::Services::Postgres.new(context).send(:database_configurations).first
end
- let(:connection) { database.send(:connection) }
+ after do
+ context.cleanup!
+ end
context 'with mocked configuration' do
subject(:database) { described_class.new(mocked_configuration) }
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/postgres_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/postgres_spec.rb
index 788f0afcdb6..05cce6361d1 100644
--- a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/postgres_spec.rb
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/services/postgres_spec.rb
@@ -5,6 +5,10 @@ RSpec.describe Gitlab::Backup::Cli::Services::Postgres do
subject(:postgres) { described_class.new(context) }
+ after do
+ context.cleanup!
+ end
+
describe '#entries' do
context 'with missing database configuration' do
it 'raises an error' do
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/targets/database_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/targets/database_spec.rb
index e4a0963aa3d..821e845bb4a 100644
--- a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/targets/database_spec.rb
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/targets/database_spec.rb
@@ -7,6 +7,10 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
let(:database) { described_class.new(context) }
let(:pipeline_success) { instance_double(Gitlab::Backup::Cli::Shell::Pipeline::Result, success?: true) }
+ after do
+ context.cleanup!
+ end
+
describe '#dump', :silence_output do
let(:destination) { Pathname(Dir.mktmpdir('database-target', temp_path)) }
@@ -17,7 +21,7 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
it 'creates the destination directory' do
mock_database_dump!
- expect(FileUtils).to receive(:mkdir_p).with(destination)
+ expect(destination).to be_directory
database.dump(destination)
end
@@ -99,18 +103,16 @@ RSpec.describe Gitlab::Backup::Cli::Targets::Database do
pipeline_success
)
- mock_databases_collection('main') do |db|
+ mock_databases_collection('main') do |_|
FileUtils.touch(source.join('database.sql.gz'))
-
- expect(database).to receive(:drop_tables).with(db)
end
+ expect(database).to receive(:drop_tables!)
+
database.restore(source)
end
it 'restores the database' do
- allow(database).to receive(:drop_tables)
-
mock_databases_collection('main') do |db|
filepath = source.join('database.sql.gz')
FileUtils.touch(filepath)
diff --git a/gems/gitlab-backup-cli/spec/gitlab/backup/cli/utils/rake_spec.rb b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/utils/rake_spec.rb
new file mode 100644
index 00000000000..32b0f297fe0
--- /dev/null
+++ b/gems/gitlab-backup-cli/spec/gitlab/backup/cli/utils/rake_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+
+RSpec.describe Gitlab::Backup::Cli::Utils::Rake do
+ subject(:rake) { described_class.new('version') }
+
+ describe '#execute' do
+ it 'clears out bundler environment' do
+ expect(Bundler).to receive(:with_original_env).and_yield
+
+ rake.execute
+ end
+
+ it 'runs rake using bundle exec' do
+ expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
+ expect(shell.cmd_args).to start_with(%w[bundle exec rake])
+ end
+
+ rake.execute
+ end
+
+ it 'runs rake command with the defined tasks' do
+ expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
+ expect(shell.cmd_args).to end_with(%w[version])
+ end
+
+ rake.execute
+
+ expect(rake.success?).to eq(true)
+ end
+
+ context 'when chdir is set' do
+ let(:tmpdir) { Dir.mktmpdir }
+
+ after do
+ FileUtils.rm_rf(tmpdir)
+ end
+
+ subject(:rake) { described_class.new('current_pwd', chdir: tmpdir) }
+
+ it 'runs rake in the provided chdir directory' do
+ expect_next_instance_of(Gitlab::Backup::Cli::Shell::Command) do |shell|
+ expect(shell.chdir).to eq(tmpdir)
+ end
+
+ FileUtils.cp_r(fixtures_path.join('gitlab_fake').glob('*'), tmpdir)
+
+ rake.execute
+
+ expect(rake.success?).to eq(true)
+ expect(rake.output).to match(/#{tmpdir}/)
+ end
+ end
+ end
+
+ describe '#success?' do
+ subject(:rake) { described_class.new('--version') } # valid command that has no side-effect
+
+ context 'with a successful rake execution' do
+ it 'returns true' do
+ rake.execute
+
+ expect(rake.success?).to be_truthy
+ end
+ end
+
+ context 'with a failed rake execution', :hide_output do
+ subject(:invalid_rake) { described_class.new('--invalid') } # valid command that has no side-effect
+
+ it 'returns false when a previous execution failed' do
+ invalid_rake.execute
+
+ expect(invalid_rake.duration).to be > 0.0
+ expect(invalid_rake.success?).to be_falsey
+ end
+ end
+
+ it 'returns false when no execution was done before' do
+ expect(rake.success?).to be_falsey
+ end
+ end
+
+ describe '#output' do
+ it 'returns the output from running a rake task' do
+ rake.execute
+
+ expect(rake.output).to match(Gitlab::Backup::Cli::VERSION)
+ end
+
+ it 'returns an empty string when the task has not been run' do
+ expect(rake.output).to eq('')
+ end
+ end
+
+ describe '#stderr' do
+ subject(:invalid_rake) { described_class.new('--invalid') } # valid command that has no side-effect
+
+ it 'returns the content from stderr when available' do
+ invalid_rake.execute
+
+ expect(invalid_rake.stderr).to match('invalid option: --invalid')
+ end
+
+ it 'returns an empty string when the task has not been run' do
+ expect(invalid_rake.stderr).to eq('')
+ end
+ end
+
+ describe '#duration' do
+ it 'returns a duration time' do
+ rake.execute
+
+ expect(rake.duration).to be > 0.0
+ end
+
+ it 'returns 0.0 when the task has not been run' do
+ expect(rake.duration).to eq(0.0)
+ end
+ end
+end
diff --git a/gems/gitlab-backup-cli/spec/support/helpers.rb b/gems/gitlab-backup-cli/spec/support/helpers.rb
index a16ab4a4e8b..2be73c5bd3f 100644
--- a/gems/gitlab-backup-cli/spec/support/helpers.rb
+++ b/gems/gitlab-backup-cli/spec/support/helpers.rb
@@ -34,7 +34,16 @@ module GitlabBackupHelpers
end
def build_test_context
- TestContext.new
+ TestContext.new.tap do |context|
+ # config/database.yml
+ db = context.gitlab_original_basepath.join('config/database.yml')
+ test_db = context.gitlab_basepath.join('config/database.yml')
+ FileUtils.mkdir_p(File.dirname(test_db))
+ FileUtils.copy(db, test_db)
+
+ # Mocked Rakefile and Gemfile
+ FileUtils.cp_r(fixtures_path.join('gitlab_fake').glob('*'), context.gitlab_basepath)
+ end
end
end
diff --git a/gems/gitlab-backup-cli/spec/support/test_context.rb b/gems/gitlab-backup-cli/spec/support/test_context.rb
index c49f803bb4f..406d91fd5c9 100644
--- a/gems/gitlab-backup-cli/spec/support/test_context.rb
+++ b/gems/gitlab-backup-cli/spec/support/test_context.rb
@@ -2,11 +2,22 @@
class TestContext < Gitlab::Backup::Cli::Context::SourceContext
def gitlab_basepath
- test_helpers.spec_path.join('../../..')
+ @gitlab_basepath ||= Pathname(Dir.mktmpdir('gitlab', test_helpers.temp_path))
end
def backup_basedir
- test_helpers.temp_path.join('backups')
+ gitlab_basepath.join('backups')
+ end
+
+ def gitlab_original_basepath
+ test_helpers.spec_path.join('../../..')
+ end
+
+ # Deletes the temporary folders
+ def cleanup!
+ dir_permissions = (File.stat(gitlab_basepath).mode & 0o777).to_s(8) # retrieve permissions in octal format)
+
+ FileUtils.rm_rf(gitlab_basepath) if dir_permissions == "700" # ensure it's a temporary dir before deleting
end
private
diff --git a/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb b/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb
deleted file mode 100644
index 987decd19bc..00000000000
--- a/lib/gitlab/background_migration/fix_first_mentioned_in_commit_at.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module BackgroundMigration
- # Class that fixes the incorrectly set authored_date within
- # issue_metrics table
- class FixFirstMentionedInCommitAt
- SUB_BATCH_SIZE = 500
-
- class TmpIssueMetrics < ActiveRecord::Base
- include EachBatch
-
- self.table_name = 'issue_metrics'
-
- def self.from_2020
- where(first_mentioned_in_commit_at_condition)
- end
-
- def self.first_mentioned_in_commit_at_condition
- if columns_hash['first_mentioned_in_commit_at'].sql_type == 'timestamp without time zone'
- 'EXTRACT(YEAR FROM first_mentioned_in_commit_at) > 2019'
- else
- "EXTRACT(YEAR FROM first_mentioned_in_commit_at at time zone 'UTC') > 2019"
- end
- end
- end
-
- def perform(start_id, end_id)
- scope(start_id, end_id).each_batch(of: SUB_BATCH_SIZE, column: :issue_id) do |sub_batch|
- first, last = sub_batch.pick(Arel.sql('min(issue_id), max(issue_id)'))
-
- # The query need to be reconstructed because .each_batch modifies the default scope
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/330510
- inner_query = TmpIssueMetrics
- .unscoped
- .merge(scope(first, last))
- .from("issue_metrics, #{lateral_query}")
- .select('issue_metrics.issue_id', 'first_authored_date.authored_date')
- .where('issue_metrics.first_mentioned_in_commit_at > first_authored_date.authored_date')
-
- TmpIssueMetrics.connection.execute <<~UPDATE_METRICS
- WITH cte AS MATERIALIZED (
- #{inner_query.to_sql}
- )
- UPDATE issue_metrics
- SET
- first_mentioned_in_commit_at = cte.authored_date
- FROM
- cte
- WHERE
- cte.issue_id = issue_metrics.issue_id
- UPDATE_METRICS
- end
-
- mark_job_as_succeeded(start_id, end_id)
- end
-
- private
-
- def mark_job_as_succeeded(*arguments)
- Gitlab::Database::BackgroundMigrationJob.mark_all_as_succeeded(
- 'FixFirstMentionedInCommitAt',
- arguments
- )
- end
-
- def scope(start_id, end_id)
- TmpIssueMetrics.from_2020.where(issue_id: start_id..end_id)
- end
-
- def lateral_query
- <<~SQL
- LATERAL (
- SELECT MIN(first_authored_date.authored_date) as authored_date
- FROM merge_requests_closing_issues,
- LATERAL (
- SELECT id
- FROM merge_request_diffs
- WHERE merge_request_id = merge_requests_closing_issues.merge_request_id
- ORDER BY id DESC
- LIMIT 1
- ) last_diff_id,
- LATERAL (
- SELECT authored_date
- FROM merge_request_diff_commits
- WHERE
- merge_request_diff_id = last_diff_id.id
- ORDER BY relative_order DESC
- LIMIT 1
- ) first_authored_date
- WHERE merge_requests_closing_issues.issue_id = issue_metrics.issue_id
- ) first_authored_date
- SQL
- end
- end
- end
-end
diff --git a/lib/gitlab/github_import/markdown_text.rb b/lib/gitlab/github_import/markdown_text.rb
index 96f73f48800..029e189a59a 100644
--- a/lib/gitlab/github_import/markdown_text.rb
+++ b/lib/gitlab/github_import/markdown_text.rb
@@ -60,6 +60,8 @@ module Gitlab
end
def to_s
+ return if text.blank?
+
# Gitlab::EncodingHelper#clean remove `null` chars from the string
text = clean(format)
text = convert_ref_links(text, project) if project.present?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d852766897b..85fbdbb4d21 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -11251,6 +11251,9 @@ msgstr ""
msgid "CICD|Auto DevOps"
msgstr ""
+msgid "CICD|Auto-populating allowlist entries. Please wait while the action completes."
+msgstr ""
+
msgid "CICD|Automatic deployment to staging, manual deployment to production"
msgstr ""
@@ -11371,7 +11374,7 @@ msgstr ""
msgid "CICD|The allowlist can contain a maximum of %{projectAllowlistLimit} groups and projects."
msgstr ""
-msgid "CICD|The process to add entries could take a moment to complete with large logs or allowlists."
+msgid "CICD|The process might take a moment to complete for large authentication logs or allowlists."
msgstr ""
msgid "CICD|There are several CI/CD limits in place."
@@ -11398,7 +11401,7 @@ msgstr ""
msgid "CICD|When enabled, all projects must use their allowlist to control CI/CD job token access between projects. The option to allow access from all groups and projects is hidden. %{link_start}Learn More.%{link_end}"
msgstr ""
-msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. Duplicate entries will be ignored."
+msgid "CICD|You're about to add all entries from the authentication log to the allowlist for %{projectName}. This will also update the Job Token setting to %{codeStart}This project and any groups and projects in the allowlist%{codeEnd}, if not already set. Duplicate entries will be ignored."
msgstr ""
msgid "CICD|group enabled"
@@ -21671,6 +21674,18 @@ msgstr ""
msgid "DuoChat|Give feedback"
msgstr ""
+msgid "DuoChat|How do I change my password in GitLab?"
+msgstr ""
+
+msgid "DuoChat|How do I clone a repository?"
+msgstr ""
+
+msgid "DuoChat|How do I create a template?"
+msgstr ""
+
+msgid "DuoChat|How do I fork a project?"
+msgstr ""
+
msgid "DuoChat|How to use GitLab"
msgstr ""
@@ -29280,24 +29295,12 @@ msgstr ""
msgid "How can I make my variables more secure?"
msgstr ""
-msgid "How do I change my password in GitLab?"
-msgstr ""
-
-msgid "How do I clone a repository?"
-msgstr ""
-
msgid "How do I configure Akismet?"
msgstr ""
msgid "How do I configure this integration?"
msgstr ""
-msgid "How do I create a template?"
-msgstr ""
-
-msgid "How do I fork a project?"
-msgstr ""
-
msgid "How do I generate it?"
msgstr ""
diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb
index ba170f05fc7..973a4fae376 100644
--- a/spec/controllers/graphql_controller_spec.rb
+++ b/spec/controllers/graphql_controller_spec.rb
@@ -531,77 +531,6 @@ RSpec.describe GraphqlController, feature_category: :integrations do
end
end
- describe 'DPoP authentication' do
- context 'when :dpop_authentication FF is disabled' do
- let(:user) { create(:user, last_activity_on: last_activity_on) }
- let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
-
- it 'does not check for DPoP token' do
- stub_feature_flags(dpop_authentication: false)
-
- post :execute, params: { access_token: personal_access_token.token }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when :dpop_authentication FF is enabled' do
- before do
- stub_feature_flags(dpop_authentication: true)
- end
-
- context 'when DPoP is disabled for the user' do
- let(:user) { create(:user, last_activity_on: last_activity_on) }
- let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
-
- it 'does not check for DPoP token' do
- post :execute, params: { access_token: personal_access_token.token }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'when DPoP is enabled for the user' do
- let_it_be(:user) { create(:user, last_activity_on: last_activity_on, dpop_enabled: true) }
- let_it_be(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
- let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:api]) }
- let_it_be(:dpop_proof) { generate_dpop_proof_for(user) }
-
- context 'when API is called with an OAuth token' do
- it 'does not invoke DPoP' do
- request.headers["Authorization"] = "Bearer #{oauth_token.plaintext_token}"
- post :execute
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with a missing DPoP token' do
- it 'returns 401' do
- post :execute, params: { access_token: personal_access_token.token }
- expect(response).to have_gitlab_http_status(:unauthorized)
- expect(json_response["errors"][0]["message"]).to eq("DPoP validation error: DPoP header is missing")
- end
- end
-
- context 'with a valid DPoP token' do
- it 'returns 200' do
- request.headers["dpop"] = dpop_proof.proof
- post :execute, params: { access_token: personal_access_token.token }
- expect(response).to have_gitlab_http_status(:ok)
- end
- end
-
- context 'with a malformed DPoP token' do
- it 'returns 401' do
- request.headers["dpop"] = "invalid"
- post :execute, params: { access_token: personal_access_token.token } # -- We need the entire error message
- expect(json_response["errors"][0]["message"])
- .to eq("DPoP validation error: Malformed JWT, unable to decode. Not enough or too many segments")
- expect(response).to have_gitlab_http_status(:unauthorized)
- end
- end
- end
- end
- end
-
context 'when user is not logged in' do
it 'returns 200' do
post :execute
@@ -782,6 +711,88 @@ RSpec.describe GraphqlController, feature_category: :integrations do
end
end
end
+
+ describe 'DPoP authentication' do
+ context 'when :dpop_authentication FF is disabled' do
+ let(:user) { create(:user, last_activity_on: last_activity_on) }
+ let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
+
+ it 'does not check for DPoP token' do
+ stub_feature_flags(dpop_authentication: false)
+
+ post :execute, params: { access_token: personal_access_token.token }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when :dpop_authentication FF is enabled' do
+ before do
+ stub_feature_flags(dpop_authentication: true)
+ end
+
+ context 'when DPoP is disabled for the user' do
+ let(:user) { create(:user, last_activity_on: last_activity_on) }
+ let(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
+
+ it 'does not check for DPoP token' do
+ post :execute, params: { access_token: personal_access_token.token }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when DPoP is enabled for the user' do
+ let_it_be(:user) { create(:user, last_activity_on: last_activity_on, dpop_enabled: true) }
+ let_it_be(:personal_access_token) { create(:personal_access_token, user: user, scopes: [:api]) }
+ let_it_be(:oauth_token) { create(:oauth_access_token, user: user, scopes: [:api]) }
+ let_it_be(:dpop_proof) { generate_dpop_proof_for(user) }
+
+ context 'when cookie-based authentication is used' do
+ it 'does not invoke DPoP' do
+ sign_in(user)
+ expect(controller).not_to receive(:extract_personal_access_token)
+
+ post :execute
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when API is called with an OAuth token' do
+ it 'does not invoke DPoP' do
+ request.headers["Authorization"] = "Bearer #{oauth_token.plaintext_token}"
+ post :execute
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with a missing DPoP token' do
+ it 'returns 401' do
+ post :execute, params: { access_token: personal_access_token.token }
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ expect(json_response["errors"][0]["message"]).to eq("DPoP validation error: DPoP header is missing")
+ end
+ end
+
+ context 'with a valid DPoP token' do
+ it 'returns 200' do
+ request.headers["dpop"] = dpop_proof.proof
+ post :execute, params: { access_token: personal_access_token.token }
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'with a malformed DPoP token' do
+ it 'returns 401' do
+ request.headers["dpop"] = "invalid"
+ post :execute, params: { access_token: personal_access_token.token } # -- We need the entire error message
+ expect(json_response["errors"][0]["message"])
+ .to eq("DPoP validation error: Malformed JWT, unable to decode. Not enough or too many segments")
+ expect(response).to have_gitlab_http_status(:unauthorized)
+ end
+ end
+ end
+ end
+ end
end
describe 'Admin Mode' do
diff --git a/spec/controllers/help_controller_spec.rb b/spec/controllers/help_controller_spec.rb
index ada4e006286..66dd279309e 100644
--- a/spec/controllers/help_controller_spec.rb
+++ b/spec/controllers/help_controller_spec.rb
@@ -349,7 +349,7 @@ RSpec.describe HelpController do
context 'for image formats' do
context 'when requested file exists' do
it 'renders the raw file' do
- get :show, params: { path: 'user/img/markdown_logo' }, format: :png
+ get :show, params: { path: 'user/img/markdown_logo_v17_11' }, format: :png
aggregate_failures do
expect(response).to be_successful
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_duration_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_duration_chart_spec.js
index 8cf45b256ec..ba556c6f458 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_duration_chart_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_duration_chart_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { GlLineChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import PipelineDurationChart from '~/projects/pipelines/charts/components/pipeline_duration_chart.vue';
+import { stubComponent } from 'helpers/stub_component';
describe('PipelineDurationChart', () => {
let wrapper;
@@ -9,11 +10,12 @@ describe('PipelineDurationChart', () => {
const findLineChart = () => wrapper.findComponent(GlLineChart);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const createComponent = ({ props } = {}) => {
+ const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(PipelineDurationChart, {
propsData: {
...props,
},
+ ...options,
});
};
@@ -75,4 +77,31 @@ describe('PipelineDurationChart', () => {
},
]);
});
+
+ describe('formats tooltip', () => {
+ const oneMinute = 60;
+ const oneHour = 3600;
+ const oneDay = oneHour * 24;
+
+ it.each`
+ date | value | expectedTooltip
+ ${'2021-12-01'} | ${oneMinute} | ${'Dec 1, 2021 - 1m'}
+ ${'2022-12-15'} | ${oneHour + oneMinute} | ${'Dec 15, 2022 - 1h 1m'}
+ ${'2023-12-31'} | ${oneDay + oneHour + oneMinute} | ${'Dec 31, 2023 - 1d 1h 1m'}
+ `('$expectedTooltip', ({ date, value, expectedTooltip }) => {
+ createComponent({
+ stubs: {
+ GlLineChart: stubComponent(GlLineChart, {
+ template: `
+
+ -
+
+ `,
+ }),
+ },
+ });
+
+ expect(findLineChart().text()).toMatchInterpolatedText(expectedTooltip);
+ });
+ });
});
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js
index e762374cd3c..c649aaa60aa 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_status_chart_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { GlStackedColumnChart } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import PipelineStatusChart from '~/projects/pipelines/charts/components/pipeline_status_chart.vue';
+import { stubComponent } from 'helpers/stub_component';
describe('PipelineStatusChart', () => {
let wrapper;
@@ -9,11 +10,12 @@ describe('PipelineStatusChart', () => {
const findStackedColumnChart = () => wrapper.findComponent(GlStackedColumnChart);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const createComponent = ({ props } = {}) => {
+ const createComponent = ({ props, ...options } = {}) => {
wrapper = shallowMount(PipelineStatusChart, {
propsData: {
...props,
},
+ ...options,
});
};
@@ -62,4 +64,25 @@ describe('PipelineStatusChart', () => {
{ data: [30, 31], name: 'Other' },
]);
});
+
+ describe('formats tooltip', () => {
+ it.each`
+ date | expectedTooltip
+ ${'2021-12-01'} | ${'Dec 1, 2021'}
+ ${'2022-12-15'} | ${'Dec 15, 2022'}
+ ${'2023-12-31'} | ${'Dec 31, 2023'}
+ `('$expectedTooltip', ({ date, expectedTooltip }) => {
+ createComponent({
+ stubs: {
+ GlStackedColumnChart: stubComponent(GlStackedColumnChart, {
+ template: `
+
+ `,
+ }),
+ },
+ });
+
+ expect(findStackedColumnChart().text()).toMatchInterpolatedText(expectedTooltip);
+ });
+ });
});
diff --git a/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js
index e0eac4f7236..6e460c96ccb 100644
--- a/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js
+++ b/spec/frontend/token_access/autopopulate_allowlist_modal_spec.js
@@ -1,44 +1,22 @@
import { GlAlert, GlLink, GlModal, GlSprintf } from '@gitlab/ui';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import { createMockDirective } from 'helpers/vue_mock_directive';
-import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import AutopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
-import { mockAutopopulateAllowlistResponse, mockAutopopulateAllowlistError } from './mock_data';
const projectName = 'My project';
const fullPath = 'root/my-repo';
-Vue.use(VueApollo);
-const mockToastShow = jest.fn();
-
describe('AutopopulateAllowlistModal component', () => {
let wrapper;
- let mockApollo;
- let mockAutopopulateMutation;
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const findLink = () => wrapper.findComponent(GlLink);
const createComponent = ({ props } = {}) => {
- const handlers = [[AutopopulateAllowlistMutation, mockAutopopulateMutation]];
- mockApollo = createMockApollo(handlers);
-
wrapper = shallowMountExtended(AutopopulateAllowlistModal, {
- apolloProvider: mockApollo,
provide: {
fullPath,
},
- mocks: {
- $toast: { show: mockToastShow },
- },
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
propsData: {
authLogExceedsLimit: false,
projectAllowlistLimit: 4,
@@ -52,10 +30,6 @@ describe('AutopopulateAllowlistModal component', () => {
});
};
- beforeEach(() => {
- mockAutopopulateMutation = jest.fn();
- });
-
describe('template', () => {
beforeEach(() => {
createComponent();
@@ -90,7 +64,7 @@ describe('AutopopulateAllowlistModal component', () => {
it('renders help link', () => {
expect(findLink().text()).toBe('What is the compaction algorithm?');
expect(findLink().attributes('href')).toBe(
- '/help/ci/jobs/ci_job_token#auto-populate-a-projects-allowlist',
+ '/help/ci/jobs/ci_job_token#allowlist-compaction',
);
});
});
@@ -112,66 +86,15 @@ describe('AutopopulateAllowlistModal component', () => {
);
});
- describe('when mutation is running', () => {
- beforeEach(() => {
- mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
+ describe('when clicking on the primary button', () => {
+ it('emits the remove-entries event', () => {
createComponent();
- });
- it('shows loading state for confirm button and disables cancel button', async () => {
- expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: false });
- expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: false });
+ expect(wrapper.emitted('autopopulate-allowlist')).toBeUndefined();
findModal().vm.$emit('primary', { preventDefault: jest.fn() });
- await nextTick();
- expect(findModal().props('actionPrimary').attributes).toMatchObject({ loading: true });
- expect(findModal().props('actionSecondary').attributes).toMatchObject({ disabled: true });
- });
- });
-
- describe('when mutation is successful', () => {
- beforeEach(async () => {
- mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistResponse);
-
- createComponent();
- findModal().vm.$emit('primary', { preventDefault: jest.fn() });
- await waitForPromises();
- });
-
- it('calls the mutation', () => {
- expect(mockAutopopulateMutation).toHaveBeenCalledWith({ projectPath: fullPath });
- });
-
- it('shows toast message', () => {
- expect(mockToastShow).toHaveBeenCalledWith(
- 'Authentication log entries were successfully added to the allowlist.',
- );
- });
-
- it('emits events for refetching data and hiding modal', () => {
- expect(wrapper.emitted('refetch-allowlist')).toHaveLength(1);
- expect(wrapper.emitted('hide')).toHaveLength(1);
- });
- });
-
- describe('when mutation fails', () => {
- beforeEach(async () => {
- createComponent();
- findModal().vm.$emit('primary', { preventDefault: jest.fn() });
- await waitForPromises();
-
- mockAutopopulateMutation.mockResolvedValue(mockAutopopulateAllowlistError);
- });
-
- it('renders alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('does not render toast message or emit events', () => {
- expect(mockToastShow).not.toHaveBeenCalledWith();
- expect(wrapper.emitted('refetch-allowlist')).toBeUndefined();
- expect(wrapper.emitted('hide')).toBeUndefined();
+ expect(wrapper.emitted('autopopulate-allowlist')).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
index b3c9aabcc8c..bf9207b3611 100644
--- a/spec/frontend/token_access/inbound_token_access_spec.js
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -18,6 +18,7 @@ import {
import AutopopulateAllowlistModal from '~/token_access/components/autopopulate_allowlist_modal.vue';
import NamespaceForm from '~/token_access/components/namespace_form.vue';
import RemoveAutopopulatedEntriesModal from '~/token_access/components/remove_autopopulated_entries_modal.vue';
+import autopopulateAllowlistMutation from '~/token_access/graphql/mutations/autopopulate_allowlist.mutation.graphql';
import inboundRemoveGroupCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_group_ci_job_token_scope.mutation.graphql';
import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
@@ -38,6 +39,7 @@ import {
inboundRemoveNamespaceSuccess,
inboundUpdateScopeSuccessResponse,
mockAuthLogsCountResponse,
+ mockAutopopulateAllowlistResponse,
mockRemoveAutopopulatedEntriesResponse,
} from './mock_data';
@@ -53,6 +55,13 @@ describe('TokenAccess component', () => {
let wrapper;
const authLogCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(4));
+ const authLogZeroCountResponseHandler = jest.fn().mockResolvedValue(mockAuthLogsCountResponse(0));
+ const autopopulateAllowlistResponseHandler = jest
+ .fn()
+ .mockResolvedValue(mockAutopopulateAllowlistResponse());
+ const autopopulateAllowlistResponseErrorHandler = jest
+ .fn()
+ .mockResolvedValue(mockAutopopulateAllowlistResponse({ errorMessage: message }));
const inboundJobTokenScopeEnabledResponseHandler = jest
.fn()
.mockResolvedValue(inboundJobTokenScopeEnabledResponse);
@@ -61,7 +70,10 @@ describe('TokenAccess component', () => {
.mockResolvedValue(inboundJobTokenScopeDisabledResponse);
const inboundGroupsAndProjectsWithScopeResponseHandler = jest
.fn()
- .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse);
+ .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(true));
+ const inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundGroupsAndProjectsWithScopeResponse(false));
const inboundRemoveGroupSuccessHandler = jest
.fn()
.mockResolvedValue(inboundRemoveNamespaceSuccess);
@@ -474,7 +486,9 @@ describe('TokenAccess component', () => {
inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
inboundGroupsAndProjectsWithScopeResponseHandler,
],
+ [autopopulateAllowlistMutation, autopopulateAllowlistResponseHandler],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationHandler],
+ [getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@@ -515,15 +529,112 @@ describe('TokenAccess component', () => {
expect(findFormSelector().props('selected')).toBe(null);
});
- it('refetches allowlist when autopopulate mutation is successful', async () => {
- expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
+ it('shows loading state while autopopulating entries', async () => {
+ expect(findCountLoadingIcon().exists()).toBe(false);
+ expect(findTokenAccessTable().props('loading')).toBe(false);
findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
- findAutopopulateAllowlistModal().vm.$emit('refetch-allowlist');
+ findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
+
await nextTick();
+ expect(findCountLoadingIcon().exists()).toBe(true);
+ expect(findTokenAccessTable().props('loading')).toBe(true);
+ expect(findTokenAccessTable().props('loadingMessage')).toBe(
+ 'Auto-populating allowlist entries. Please wait while the action completes.',
+ );
+ });
+
+ it('resets loading state after autopopulating entries', async () => {
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
+
+ await nextTick();
+
+ expect(findTokenAccessTable().props('loadingMessage')).toBe(
+ 'Auto-populating allowlist entries. Please wait while the action completes.',
+ );
+
+ await waitForPromises();
+
+ expect(findCountLoadingIcon().exists()).toBe(false);
+ expect(findTokenAccessTable().props('loading')).toBe(false);
+ expect(findTokenAccessTable().props('loadingMessage')).toBe('');
+ });
+
+ it('calls the autopopulate allowlist mutation and refetches allowlist and job token setting', async () => {
+ expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(0);
+ expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
+ expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(1);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
+ await waitForPromises();
+ await nextTick();
+
+ expect(autopopulateAllowlistResponseHandler).toHaveBeenCalledTimes(1);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(2);
- expect(findFormSelector().props('selected')).toBe(null);
+ expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows error alert when mutation returns an error', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [autopopulateAllowlistMutation, autopopulateAllowlistResponseErrorHandler],
+ [getAuthLogCountQuery, authLogCountResponseHandler],
+ ],
+ {
+ authenticationLogsMigrationForAllowlist: true,
+ stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
+ },
+ );
+
+ await waitForPromises();
+
+ expect(findAutopopulationAlert().exists()).toBe(false);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
+ await waitForPromises();
+ await nextTick();
+
+ expect(findAutopopulationAlert().text()).toBe('An error occurred');
+ });
+
+ it('shows error alert when mutation fails', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithScopeResponseHandler,
+ ],
+ [autopopulateAllowlistMutation, failureHandler],
+ [getAuthLogCountQuery, authLogCountResponseHandler],
+ ],
+ {
+ authenticationLogsMigrationForAllowlist: true,
+ stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
+ },
+ );
+
+ await waitForPromises();
+
+ expect(findAutopopulationAlert().exists()).toBe(false);
+
+ findFormSelector().vm.$emit('select', JOB_TOKEN_FORM_AUTOPOPULATE_AUTH_LOG);
+ findAutopopulateAllowlistModal().vm.$emit('autopopulate-allowlist');
+ await waitForPromises();
+ await nextTick();
+
+ expect(findAutopopulationAlert().text()).toBe(
+ 'An error occurred while adding the authentication log entries. Please try again.',
+ );
});
});
@@ -561,6 +672,21 @@ describe('TokenAccess component', () => {
);
});
+ it('resets loading state after removing autopopulated entries', async () => {
+ triggerRemoveEntries();
+ await nextTick();
+
+ expect(findTokenAccessTable().props('loadingMessage')).toBe(
+ 'Removing auto-added allowlist entries. Please wait while the action completes.',
+ );
+
+ await waitForPromises();
+
+ expect(findCountLoadingIcon().exists()).toBe(false);
+ expect(findTokenAccessTable().props('loading')).toBe(false);
+ expect(findTokenAccessTable().props('loadingMessage')).toBe('');
+ });
+
it('calls the remove autopopulated entries mutation and refetches allowlist', async () => {
expect(removeAutopopulatedEntriesMutationHandler).toHaveBeenCalledTimes(0);
expect(inboundGroupsAndProjectsWithScopeResponseHandler).toHaveBeenCalledTimes(1);
@@ -592,6 +718,7 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, removeAutopopulatedEntriesMutationErrorHandler],
+ [getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@@ -599,6 +726,8 @@ describe('TokenAccess component', () => {
},
);
+ await waitForPromises();
+
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
@@ -617,6 +746,7 @@ describe('TokenAccess component', () => {
inboundGroupsAndProjectsWithScopeResponseHandler,
],
[removeAutopopulatedEntriesMutation, failureHandler],
+ [getAuthLogCountQuery, authLogCountResponseHandler],
],
{
authenticationLogsMigrationForAllowlist: true,
@@ -624,6 +754,8 @@ describe('TokenAccess component', () => {
},
);
+ await waitForPromises();
+
expect(findAutopopulationAlert().exists()).toBe(false);
triggerRemoveEntries();
@@ -652,6 +784,39 @@ describe('TokenAccess component', () => {
expect(findRemoveAutopopulatedEntriesModal().props('showModal')).toBe(true);
});
});
+
+ describe('allowlist actions', () => {
+ beforeEach(async () => {
+ await createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [
+ inboundGetGroupsAndProjectsWithCIJobTokenScopeQuery,
+ inboundGroupsAndProjectsWithoutAutopopulatedEntriesResponseHandler,
+ ],
+ [getAuthLogCountQuery, authLogZeroCountResponseHandler],
+ ],
+ {
+ authenticationLogsMigrationForAllowlist: true,
+ stubs: { CrudComponent, GlDisclosureDropdown, GlDisclosureDropdownItem },
+ },
+ );
+ await nextTick();
+ });
+
+ it('hides add auth log entries option if auth log count is zero', () => {
+ expect(findFormSelector().props('items')).toMatchObject([
+ {
+ text: 'Group or project',
+ value: 'JOB_TOKEN_FORM_ADD_GROUP_OR_PROJECT',
+ },
+ ]);
+ });
+
+ it('hides remove auth log entries option if there are no autopopulated entries', () => {
+ expect(findAllowlistOptions().exists()).toBe(false);
+ });
+ });
});
describe.each`
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index b7fd38afb6a..d1e107b2265 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -164,7 +164,7 @@ export const inboundJobTokenScopeDisabledResponse = {
},
};
-export const inboundGroupsAndProjectsWithScopeResponse = {
+export const inboundGroupsAndProjectsWithScopeResponse = (hasAutopopulatedEntries = true) => ({
data: {
project: {
__typename: 'Project',
@@ -197,12 +197,14 @@ export const inboundGroupsAndProjectsWithScopeResponse = {
},
],
},
- groupAllowlistAutopopulatedIds: ['gid://gitlab/Group/45'],
- inboundAllowlistAutopopulatedIds: ['gid://gitlab/Project/23'],
+ groupAllowlistAutopopulatedIds: hasAutopopulatedEntries ? ['gid://gitlab/Group/45'] : [],
+ inboundAllowlistAutopopulatedIds: hasAutopopulatedEntries
+ ? ['gid://gitlab/Project/23']
+ : [],
},
},
},
-};
+});
export const getSaveNamespaceHandler = (error) =>
jest.fn().mockResolvedValue({
@@ -301,28 +303,15 @@ export const mockAuthLogsResponse = (hasNextPage = false) => ({
},
});
-export const mockAutopopulateAllowlistResponse = {
+export const mockAutopopulateAllowlistResponse = ({ errorMessage } = {}) => ({
data: {
ciJobTokenScopeAutopopulateAllowlist: {
status: 'complete',
- errors: [],
+ errors: errorMessage ? [{ message: errorMessage }] : [],
__typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
},
},
-};
-
-export const mockAutopopulateAllowlistError = {
- data: {
- ciJobTokenScopeAutopopulateAllowlist: {
- errors: [
- {
- message: 'An error occurred',
- },
- ],
- __typename: 'CiJobTokenScopeAutopopulateAllowlistPayload',
- },
- },
-};
+});
export const mockRemoveAutopopulatedEntriesResponse = ({ errorMessage } = {}) => ({
data: {
diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb
index aabb67c4dae..308f7b02951 100644
--- a/spec/helpers/ci/runners_helper_spec.rb
+++ b/spec/helpers/ci/runners_helper_spec.rb
@@ -78,6 +78,39 @@ RSpec.describe Ci::RunnersHelper, feature_category: :fleet_visibility do
end
end
+ describe '#admin_runners_fleet_dashboard_data', :enable_admin_mode do
+ let_it_be(:user) { admin_user }
+
+ subject(:data) { helper.admin_runners_fleet_dashboard_data }
+
+ it 'returns correct data' do
+ expect(data).to include(
+ admin_runners_path: '/admin/runners',
+ new_runner_path: '/admin/runners/new',
+ clickhouse_ci_analytics_available: 'false',
+ can_admin_runners: 'true'
+ )
+ end
+
+ context 'when ClickHouse is configured' do
+ before do
+ allow(Gitlab::ClickHouse).to receive(:configured?).and_return(true)
+ end
+
+ it 'returns the correct data' do
+ expect(data).to include(clickhouse_ci_analytics_available: 'true')
+ end
+ end
+
+ context 'when current user is not an admin' do
+ let_it_be(:user) { non_admin_user }
+
+ it 'returns the correct data' do
+ expect(data).to include(can_admin_runners: 'false')
+ end
+ end
+ end
+
describe '#group_shared_runners_settings_data' do
let_it_be(:parent) { create(:group) }
let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) }
diff --git a/spec/lib/gitlab/github_import/markdown_text_spec.rb b/spec/lib/gitlab/github_import/markdown_text_spec.rb
index dfa6276ebe2..cd7fbe240ad 100644
--- a/spec/lib/gitlab/github_import/markdown_text_spec.rb
+++ b/spec/lib/gitlab/github_import/markdown_text_spec.rb
@@ -124,13 +124,6 @@ RSpec.describe Gitlab::GithubImport::MarkdownText, feature_category: :importers
expect(text.to_s).to eq('Hello')
end
- it 'returns empty text when it receives nil' do
- author = double(:author, login: nil)
- text = described_class.new(nil, author, true)
-
- expect(text.to_s).to eq('')
- end
-
it 'returns the text with an extra header when the author was not found' do
author = double(:author, login: 'Alice')
text = described_class.new('Hello', author)
@@ -150,11 +143,28 @@ RSpec.describe Gitlab::GithubImport::MarkdownText, feature_category: :importers
let(:text) { "I said to @sam_allen\0 the code" }
let(:instance) { described_class.new(text, project:) }
+ subject(:format) { instance.to_s }
+
it 'calls wrap_mentions_in_backticks and convert_ref_links method as a cleaning step' do
expect(instance).to receive(:wrap_mentions_in_backticks)
expect(instance).to receive(:convert_ref_links)
- instance.to_s
+ format
+ end
+
+ context "when the text is blank?" do
+ let(:text) { nil }
+
+ it "skips formatting" do
+ expect(instance).not_to receive(:wrap_mentions_in_backticks)
+ expect(instance).not_to receive(:convert_ref_links)
+
+ format
+ end
+
+ it "returns nil as response" do
+ expect(format).to be_nil
+ end
end
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 181e2e9c311..f24ec315ad3 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -115,8 +115,8 @@ RSpec.describe HelpController, "routing" do
path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show', path: 'user/markdown', format: 'md')
- path = '/help/user/markdown/markdown_logo.png'
- expect(get(path)).to route_to('help#show', path: 'user/markdown/markdown_logo', format: 'png')
+ path = '/help/user/markdown/markdown_logo_v17_11.png'
+ expect(get(path)).to route_to('help#show', path: 'user/markdown/markdown_logo_v17_11', format: 'png')
end
end
diff --git a/workhorse/_support/lint_last_known_acceptable.txt b/workhorse/_support/lint_last_known_acceptable.txt
index 473c5e2f176..27be97b2e4e 100644
--- a/workhorse/_support/lint_last_known_acceptable.txt
+++ b/workhorse/_support/lint_last_known_acceptable.txt
@@ -9,12 +9,7 @@ internal/config/config.go:247:18: G204: Subprocess launched with variable (gosec
internal/config/config.go:339:8: G101: Potential hardcoded credentials (gosec)
internal/dependencyproxy/dependencyproxy.go:121: Function 'Inject' is too long (61 > 60) (funlen)
internal/dependencyproxy/dependencyproxy_test.go:514: internal/dependencyproxy/dependencyproxy_test.go:514: Line contains TODO/BUG/FIXME/NOTE/OPTIMIZE/HACK: "note that the timeout duration here is s..." (godox)
-internal/git/archive.go:39:2: var-naming: struct field CommitId should be CommitID (revive)
-internal/git/archive.go:49:2: exported: exported var SendArchive should have comment or be unexported (revive)
-internal/git/archive.go:66: Function 'Inject' has too many statements (49 > 40) (funlen)
-internal/git/archive.go:90:29: Error return value of `cachedArchive.Close` is not checked (errcheck)
-internal/git/archive.go:116:23: Error return value of `tempFile.Close` is not checked (errcheck)
-internal/git/archive.go:117:18: Error return value of `os.Remove` is not checked (errcheck)
+internal/git/archive.go:67: Function 'Inject' has too many statements (55 > 40) (funlen)
internal/git/blob.go:21:5: exported: exported var SendBlob should have comment or be unexported (revive)
internal/git/diff.go:1: 1-47 lines are duplicate of `internal/git/format-patch.go:1-48` (dupl)
internal/git/diff.go:22:5: exported: exported var SendDiff should have comment or be unexported (revive)
diff --git a/workhorse/internal/git/archive.go b/workhorse/internal/git/archive.go
index 0a7ed8a0e0c..9ac362ae2c2 100644
--- a/workhorse/internal/git/archive.go
+++ b/workhorse/internal/git/archive.go
@@ -36,7 +36,7 @@ type archive struct {
type archiveParams struct {
ArchivePath string
ArchivePrefix string
- CommitId string
+ CommitID string
GitalyServer api.GitalyServer
GitalyRepository gitalypb.Repository
DisableCache bool
@@ -46,6 +46,7 @@ type archiveParams struct {
}
var (
+ // SendArchive sends a Git archive to the client, retrieving from the local disk cache if available.
SendArchive = newArchive("git-archive:")
gitArchiveCache = promauto.NewCounterVec(
prometheus.CounterOpts{
@@ -87,7 +88,12 @@ func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string
cachedArchive, err := os.Open(params.ArchivePath)
if err == nil {
- defer cachedArchive.Close()
+ defer func() {
+ err = cachedArchive.Close()
+ if err != nil {
+ log.WithError(err).Error("SendArchive: failed to close cached archive")
+ }
+ }()
gitArchiveCache.WithLabelValues("hit").Inc()
setArchiveHeaders(w, format, archiveFilename)
// Even if somebody deleted the cachedArchive from disk since we opened
@@ -113,8 +119,15 @@ func (a *archive) Inject(w http.ResponseWriter, r *http.Request, sendData string
fail.Request(w, r, fmt.Errorf("SendArchive: create tempfile: %v", err))
return
}
- defer tempFile.Close()
- defer os.Remove(tempFile.Name())
+ defer func() {
+ // Ignore error, this may have already been closed with finalizeCachedArchive
+ _ = tempFile.Close()
+
+ err = os.Remove(tempFile.Name())
+ if err != nil {
+ log.WithError(err).Error("SendArchive: failed to remove tempfile")
+ }
+ }()
}
var archiveReader io.Reader
@@ -175,7 +188,7 @@ func handleArchiveWithGitaly(r *http.Request, params *archiveParams, format gita
} else {
request = &gitalypb.GetArchiveRequest{
Repository: ¶ms.GitalyRepository,
- CommitId: params.CommitId,
+ CommitId: params.CommitID,
Prefix: params.ArchivePrefix,
Format: format,
}