Commit 7cc68724 authored by GitLab Bot's avatar GitLab Bot

Add latest changes from gitlab-org/gitlab@master

parent 46b10c0f
This diff is collapsed.
...@@ -10,6 +10,7 @@ import { ...@@ -10,6 +10,7 @@ import {
GlLoadingIcon, GlLoadingIcon,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale'; import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import { import {
NAME_REGEX_LENGTH, NAME_REGEX_LENGTH,
UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_ERROR_MESSAGE,
...@@ -27,10 +28,18 @@ export default { ...@@ -27,10 +28,18 @@ export default {
GlCard, GlCard,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [Tracking.mixin()],
labelsConfig: { labelsConfig: {
cols: 3, cols: 3,
align: 'right', align: 'right',
}, },
data() {
return {
tracking: {
label: 'docker_container_retention_and_expiration_policies',
},
};
},
computed: { computed: {
...mapState(['formOptions', 'isLoading']), ...mapState(['formOptions', 'isLoading']),
...mapComputed( ...mapComputed(
...@@ -86,7 +95,12 @@ export default { ...@@ -86,7 +95,12 @@ export default {
}, },
methods: { methods: {
...mapActions(['resetSettings', 'saveSettings']), ...mapActions(['resetSettings', 'saveSettings']),
reset() {
this.track('reset_form');
this.resetSettings();
},
submit() { submit() {
this.track('submit_form');
this.saveSettings() this.saveSettings()
.then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' })) .then(() => this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' }))
.catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' })); .catch(() => this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' }));
...@@ -96,7 +110,7 @@ export default { ...@@ -96,7 +110,7 @@ export default {
</script> </script>
<template> <template>
<form ref="form-element" @submit.prevent="submit" @reset.prevent="resetSettings"> <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
<gl-card> <gl-card>
<template #header> <template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }} {{ s__('ContainerRegistry|Tag expiration policy') }}
......
...@@ -3,7 +3,13 @@ ...@@ -3,7 +3,13 @@
class Admin::ApplicationSettingsController < Admin::ApplicationController class Admin::ApplicationSettingsController < Admin::ApplicationController
include InternalRedirect include InternalRedirect
# NOTE: Use @application_setting in this controller when you need to access
# application_settings after it has been modified. This is because the
# ApplicationSetting model uses Gitlab::ThreadMemoryCache for caching and the
# cache might be stale immediately after an update.
# https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/30233
before_action :set_application_setting before_action :set_application_setting
before_action :whitelist_query_limiting, only: [:usage_data] before_action :whitelist_query_limiting, only: [:usage_data]
before_action :validate_self_monitoring_feature_flag_enabled, only: [ before_action :validate_self_monitoring_feature_flag_enabled, only: [
:create_self_monitoring_project, :create_self_monitoring_project,
...@@ -79,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -79,6 +85,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
end end
# Specs are in spec/requests/self_monitoring_project_spec.rb
def create_self_monitoring_project def create_self_monitoring_project
job_id = SelfMonitoringProjectCreateWorker.perform_async job_id = SelfMonitoringProjectCreateWorker.perform_async
...@@ -88,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -88,6 +95,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
# Specs are in spec/requests/self_monitoring_project_spec.rb
def status_create_self_monitoring_project def status_create_self_monitoring_project
job_id = params[:job_id].to_s job_id = params[:job_id].to_s
...@@ -98,10 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -98,10 +106,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
if Gitlab::CurrentSettings.self_monitoring_project_id.present? if SelfMonitoringProjectCreateWorker.in_progress?(job_id)
return render status: :ok, json: self_monitoring_data
elsif SelfMonitoringProjectCreateWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
return render status: :accepted, json: { return render status: :accepted, json: {
...@@ -109,12 +114,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -109,12 +114,17 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
if @application_setting.self_monitoring_project_id.present?
return render status: :ok, json: self_monitoring_data
end
render status: :bad_request, json: { render status: :bad_request, json: {
message: _('Self-monitoring project does not exist. Please check logs ' \ message: _('Self-monitoring project does not exist. Please check logs ' \
'for any error messages') 'for any error messages')
} }
end end
# Specs are in spec/requests/self_monitoring_project_spec.rb
def delete_self_monitoring_project def delete_self_monitoring_project
job_id = SelfMonitoringProjectDeleteWorker.perform_async job_id = SelfMonitoringProjectDeleteWorker.perform_async
...@@ -124,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -124,6 +134,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
# Specs are in spec/requests/self_monitoring_project_spec.rb
def status_delete_self_monitoring_project def status_delete_self_monitoring_project
job_id = params[:job_id].to_s job_id = params[:job_id].to_s
...@@ -134,12 +145,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -134,12 +145,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
if Gitlab::CurrentSettings.self_monitoring_project_id.nil? if SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
return render status: :ok, json: {
message: _('Self-monitoring project has been successfully deleted')
}
elsif SelfMonitoringProjectDeleteWorker.in_progress?(job_id)
::Gitlab::PollingInterval.set_header(response, interval: 3_000) ::Gitlab::PollingInterval.set_header(response, interval: 3_000)
return render status: :accepted, json: { return render status: :accepted, json: {
...@@ -147,6 +153,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -147,6 +153,12 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
} }
end end
if @application_setting.self_monitoring_project_id.nil?
return render status: :ok, json: {
message: _('Self-monitoring project has been successfully deleted')
}
end
render status: :bad_request, json: { render status: :bad_request, json: {
message: _('Self-monitoring project was not deleted. Please check logs ' \ message: _('Self-monitoring project was not deleted. Please check logs ' \
'for any error messages') 'for any error messages')
...@@ -161,8 +173,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -161,8 +173,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
def self_monitoring_data def self_monitoring_data
{ {
project_id: Gitlab::CurrentSettings.self_monitoring_project_id, project_id: @application_setting.self_monitoring_project_id,
project_full_path: Gitlab::CurrentSettings.self_monitoring_project&.full_path project_full_path: @application_setting.self_monitoring_project&.full_path
} }
end end
......
...@@ -24,7 +24,7 @@ class Admin::SpamLogsController < Admin::ApplicationController ...@@ -24,7 +24,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
def mark_as_ham def mark_as_ham
spam_log = SpamLog.find(params[:id]) spam_log = SpamLog.find(params[:id])
if HamService.new(spam_log).mark_as_ham! if Spam::HamService.new(spam_log).mark_as_ham!
redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.') redirect_to admin_spam_logs_path, notice: _('Spam log successfully submitted as ham.')
else else
redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.') redirect_to admin_spam_logs_path, alert: _('Error with Akismet. Please check the logs for more info.')
......
...@@ -8,7 +8,6 @@ module Resolvers ...@@ -8,7 +8,6 @@ module Resolvers
description: 'ID of the Sentry issue' description: 'ID of the Sentry issue'
def resolve(**args) def resolve(**args)
project = object
current_user = context[:current_user] current_user = context[:current_user]
issue_id = GlobalID.parse(args[:id]).model_id issue_id = GlobalID.parse(args[:id]).model_id
...@@ -23,6 +22,14 @@ module Resolvers ...@@ -23,6 +22,14 @@ module Resolvers
issue issue
end end
private
def project
return object.gitlab_project if object.respond_to?(:gitlab_project)
object
end
end end
end end
end end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorCollectionResolver < BaseResolver
def resolve(**args)
project = object
service = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user]
)
Gitlab::ErrorTracking::ErrorCollection.new(
external_url: service.external_url,
project: project
)
end
end
end
end
# frozen_string_literal: true
module Resolvers
module ErrorTracking
class SentryErrorsResolver < BaseResolver
def resolve(**args)
args[:cursor] = args.delete(:after)
project = object.project
result = ::ErrorTracking::ListIssuesService.new(
project,
context[:current_user],
args
).execute
next_cursor = result[:pagination]&.dig('next', 'cursor')
previous_cursor = result[:pagination]&.dig('previous', 'cursor')
issues = result[:issues]
# ReactiveCache is still fetching data
return if issues.nil?
Gitlab::Graphql::ExternallyPaginatedArray.new(previous_cursor, next_cursor, *issues)
end
end
end
end
...@@ -4,8 +4,9 @@ module Types ...@@ -4,8 +4,9 @@ module Types
module ErrorTracking module ErrorTracking
class SentryDetailedErrorType < ::Types::BaseObject class SentryDetailedErrorType < ::Types::BaseObject
graphql_name 'SentryDetailedError' graphql_name 'SentryDetailedError'
description 'A Sentry error.'
present_using SentryDetailedErrorPresenter present_using SentryErrorPresenter
authorize :read_sentry_issue authorize :read_sentry_issue
...@@ -92,18 +93,6 @@ module Types ...@@ -92,18 +93,6 @@ module Types
field :tags, Types::ErrorTracking::SentryErrorTagsType, field :tags, Types::ErrorTracking::SentryErrorTagsType,
null: false, null: false,
description: 'Tags associated with the Sentry Error' description: 'Tags associated with the Sentry Error'
def first_seen
DateTime.parse(object.first_seen)
end
def last_seen
DateTime.parse(object.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: object.project_id).to_s
end
end end
end end
end end
# frozen_string_literal: true
module Types
module ErrorTracking
class SentryErrorCollectionType < ::Types::BaseObject
graphql_name 'SentryErrorCollection'
description 'An object containing a collection of Sentry errors, and a detailed error.'
authorize :read_sentry_issue
field :errors,
Types::ErrorTracking::SentryErrorType.connection_type,
connection: false,
null: true,
description: "Collection of Sentry Errors",
extensions: [Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension],
resolver: Resolvers::ErrorTracking::SentryErrorsResolver do
argument :search_term,
String,
description: 'Search term for the Sentry error.',
required: false
argument :sort,
String,
description: 'Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.',
required: false
end
field :detailed_error, Types::ErrorTracking::SentryDetailedErrorType,
null: true,
description: 'Detailed version of a Sentry error on the project',
resolver: Resolvers::ErrorTracking::SentryDetailedErrorResolver
field :external_url,
GraphQL::STRING_TYPE,
null: true,
description: "External URL for Sentry"
end
end
end
# frozen_string_literal: true
module Types
module ErrorTracking
# rubocop: disable Graphql/AuthorizeTypes
class SentryErrorType < ::Types::BaseObject
graphql_name 'SentryError'
description 'A Sentry error. A simplified version of SentryDetailedError.'
present_using SentryErrorPresenter
field :id, GraphQL::ID_TYPE,
null: false,
description: 'ID (global ID) of the error'
field :sentry_id, GraphQL::STRING_TYPE,
method: :id,
null: false,
description: 'ID (Sentry ID) of the error'
field :first_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was first seen'
field :last_seen, Types::TimeType,
null: false,
description: 'Timestamp when the error was last seen'
field :title, GraphQL::STRING_TYPE,
null: false,
description: 'Title of the error'
field :type, GraphQL::STRING_TYPE,
null: false,
description: 'Type of the error'
field :user_count, GraphQL::INT_TYPE,
null: false,
description: 'Count of users affected by the error'
field :count, GraphQL::INT_TYPE,
null: false,
description: 'Count of occurrences'
field :message, GraphQL::STRING_TYPE,
null: true,
description: 'Sentry metadata message of the error'
field :culprit, GraphQL::STRING_TYPE,
null: false,
description: 'Culprit of the error'
field :external_url, GraphQL::STRING_TYPE,
null: false,
description: 'External URL of the error'
field :short_id, GraphQL::STRING_TYPE,
null: false,
description: 'Short ID (Sentry ID) of the error'
field :status, Types::ErrorTracking::SentryErrorStatusEnum,
null: false,
description: 'Status of the error'
field :frequency, [Types::ErrorTracking::SentryErrorFrequencyType],
null: false,
description: 'Last 24hr stats of the error'
field :sentry_project_id, GraphQL::ID_TYPE,
method: :project_id,
null: false,
description: 'ID of the project (Sentry project)'
field :sentry_project_name, GraphQL::STRING_TYPE,
method: :project_name,
null: false,
description: 'Name of the project affected by the error'
field :sentry_project_slug, GraphQL::STRING_TYPE,
method: :project_slug,
null: false,
description: 'Slug of the project affected by the error'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
...@@ -173,6 +173,12 @@ module Types ...@@ -173,6 +173,12 @@ module Types
null: true, null: true,
description: 'Snippets of the project', description: 'Snippets of the project',
resolver: Resolvers::Projects::SnippetsResolver resolver: Resolvers::Projects::SnippetsResolver
field :sentry_errors,
Types::ErrorTracking::SentryErrorCollectionType,
null: true,
description: 'Paginated collection of Sentry errors on the project',
resolver: Resolvers::ErrorTracking::SentryErrorCollectionResolver
end end
end end
......
...@@ -484,10 +484,10 @@ class Commit ...@@ -484,10 +484,10 @@ class Commit
end end
def commit_reference(from, referable_commit_id, full: false) def commit_reference(from, referable_commit_id, full: false)
reference = project.to_reference(from, full: full) base = project.to_reference_base(from, full: full)
if reference.present? if base.present?
"#{reference}#{self.class.reference_prefix}#{referable_commit_id}" "#{base}#{self.class.reference_prefix}#{referable_commit_id}"
else else
referable_commit_id referable_commit_id
end end
......
...@@ -92,7 +92,7 @@ class CommitRange ...@@ -92,7 +92,7 @@ class CommitRange
alias_method :id, :to_s alias_method :id, :to_s
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
project_reference = project.to_reference(from, full: full) project_reference = project.to_reference_base(from, full: full)
if project_reference.present? if project_reference.present?
project_reference + self.class.reference_prefix + self.id project_reference + self.class.reference_prefix + self.id
...@@ -102,7 +102,7 @@ class CommitRange ...@@ -102,7 +102,7 @@ class CommitRange
end end
def reference_link_text(from = nil) def reference_link_text(from = nil)
project_reference = project.to_reference(from) project_reference = project.to_reference_base(from)
reference = ref_from + notation + ref_to reference = ref_from + notation + ref_to
if project_reference.present? if project_reference.present?
......
...@@ -23,6 +23,14 @@ module Referable ...@@ -23,6 +23,14 @@ module Referable
'' ''
end end
# If this referable object can serve as the base for the
# reference of child objects (e.g. projects are the base of
# issues), but it is formatted differently, then you may wish
# to override this method.
def to_reference_base(from = nil, full:)
to_reference(from, full: full)
end
def reference_link_text(from = nil) def reference_link_text(from = nil)
to_reference(from) to_reference(from)
end end
......
...@@ -173,7 +173,7 @@ class Issue < ApplicationRecord ...@@ -173,7 +173,7 @@ class Issue < ApplicationRecord
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
end end
def suggested_branch_name def suggested_branch_name
......
...@@ -225,7 +225,7 @@ class Label < ApplicationRecord ...@@ -225,7 +225,7 @@ class Label < ApplicationRecord
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if from if from
"#{from.to_reference(target_project, full: full)}#{reference}" "#{from.to_reference_base(target_project, full: full)}#{reference}"
else else
reference reference
end end
......
...@@ -396,7 +396,7 @@ class MergeRequest < ApplicationRecord ...@@ -396,7 +396,7 @@ class MergeRequest < ApplicationRecord
def to_reference(from = nil, full: false) def to_reference(from = nil, full: false)
reference = "#{self.class.reference_prefix}#{iid}" reference = "#{self.class.reference_prefix}#{iid}"
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
end end
def commits(limit: nil) def commits(limit: nil)
......
...@@ -228,7 +228,7 @@ class Milestone < ApplicationRecord ...@@ -228,7 +228,7 @@ class Milestone < ApplicationRecord
reference = "#{self.class.reference_prefix}#{format_reference}" reference = "#{self.class.reference_prefix}#{format_reference}"
if project if project
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
else else
reference reference
end end
......
...@@ -1068,12 +1068,19 @@ class Project < ApplicationRecord ...@@ -1068,12 +1068,19 @@ class Project < ApplicationRecord
end end
end end
def to_reference_with_postfix # Produce a valid reference (see Referable#to_reference)
"#{to_reference(full: true)}#{self.class.reference_postfix}" #
# NB: For projects, all references are 'full' - i.e. they all include the
# full_path, rather than just the project name. For this reason, we ignore
# the value of `full:` passed to this method, which is part of the Referable
# interface.
def to_reference(from = nil, full: false)
base = to_reference_base(from, full: true)
"#{base}#{self.class.reference_postfix}"
end end
# `from` argument can be a Namespace or Project. # `from` argument can be a Namespace or Project.
def to_reference(from = nil, full: false) def to_reference_base(from = nil, full: false)
if full || cross_namespace_reference?(from) if full || cross_namespace_reference?(from)
full_path full_path
elsif cross_project_reference?(from) elsif cross_project_reference?(from)
......
...@@ -180,7 +180,7 @@ class Snippet < ApplicationRecord ...@@ -180,7 +180,7 @@ class Snippet < ApplicationRecord
reference = "#{self.class.reference_prefix}#{id}" reference = "#{self.class.reference_prefix}#{id}"
if project.present? if project.present?
"#{project.to_reference(from, full: full)}#{reference}" "#{project.to_reference_base(from, full: full)}#{reference}"
else else
reference reference
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module ErrorTracking module ErrorTracking
class DetailedErrorPolicy < BasePolicy class BasePolicy < ::BasePolicy
delegate { @subject.gitlab_project } delegate { @subject.gitlab_project }
end end
end end
# frozen_string_literal: true # frozen_string_literal: true
class SentryDetailedErrorPresenter < Gitlab::View::Presenter::Delegated class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
presents :error presents :error
FrequencyStruct = Struct.new(:time, :count, keyword_init: true) FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
def first_seen
DateTime.parse(error.first_seen)
end
def last_seen
DateTime.parse(error.last_seen)
end
def project_id
Gitlab::GlobalId.build(model_name: 'Project', id: error.project_id).to_s
end
def frequency def frequency
utc_offset = Time.zone_offset('UTC') utc_offset = Time.zone_offset('UTC')
......
# frozen_string_literal: true
class HamService
attr_accessor :spam_log
def initialize(spam_log)
@spam_log = spam_log
end
def mark_as_ham!
if akismet.submit_ham
spam_log.update_attribute(:submitted_as_ham, true)
else
false
end
end
private
def akismet
user = spam_log.user
@akismet ||= AkismetService.new(
user.name,
user.email,
spam_log.text,
ip_address: spam_log.source_ip,
user_agent: spam_log.user_agent
)
end
end
# frozen_string_literal: true
module Spam
class HamService
attr_accessor :spam_log
def initialize(spam_log)
@spam_log = spam_log
end
def mark_as_ham!
if akismet.submit_ham
spam_log.update_attribute(:submitted_as_ham, true)
else
false
end
end
private
def akismet
user = spam_log.user
@akismet ||= AkismetService.new(
user.name,
user.email,
spam_log.text,
ip_address: spam_log.source_ip,
user_agent: spam_log.user_agent
)
end
end
end
---
title: Add querying of Sentry errors to Graphql
merge_request: 21802
author:
type: added
---
title: refactoring gl_dropdown.js to use ES6 classes instead of constructor functions
merge_request: 20488
author: nuwe1
type: other
---
title: Add license FAQ link to license expired message
merge_request:
author:
type: added
...@@ -342,16 +342,28 @@ pages: ...@@ -342,16 +342,28 @@ pages:
1. [Reconfigure GitLab][reconfigure] for the changes to take effect. 1. [Reconfigure GitLab][reconfigure] for the changes to take effect.
### Using a custom Certificate Authority (CA) with Access Control ### Using a custom Certificate Authority (CA)
When using certificates issued by a custom CA, Access Control on GitLab Pages may fail to work if the custom CA is not recognized. When using certificates issued by a custom CA, [Access Control](../../user/project/pages/pages_access_control.md#gitlab-pages-access-control) and
the [online view of HTML job artifacts](../../user/project/pipelines/job_artifacts.md#browsing-artifacts)
will fail to work if the custom CA is not recognized.
This usually results in this error: This usually results in this error:
`Post /oauth/token: x509: certificate signed by unknown authority`. `Post /oauth/token: x509: certificate signed by unknown authority`.
For GitLab Pages Access Control with TLS/SSL certs issued by an internal or custom CA: For installation from source this can be fixed by installing the custom Certificate
Authority (CA) in the system certificate store.
1. Copy the certificate bundle to `/opt/gitlab/embedded/ssl/certs/` in `.pem` format. For Omnibus, normally this would be fixed by [installing a custom CA in GitLab Omnibus](https://docs.gitlab.com/omnibus/settings/ssl.html#install-custom-public-certificates)
but a [bug](https://gitlab.com/gitlab-org/gitlab/issues/25411) is currently preventing
that method from working. Use the following workaround:
1. Append your GitLab server TLS/SSL certficate to `/opt/gitlab/embedded/ssl/certs/cacert.pem` where `gitlab-domain-example.com` is your GitLab application URL
```bash
printf "\ngitlab-domain-example.com\n===========================\n" | sudo tee --append /opt/gitlab/embedded/ssl/certs/cacert.pem
echo -n | openssl s_client -connect gitlab-domain-example.com:443 | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' | sudo tee --append /opt/gitlab/embedded/ssl/certs/cacert.pem
```
1. [Restart](../restart_gitlab.md) the GitLab Pages Daemon. For GitLab Omnibus instances: 1. [Restart](../restart_gitlab.md) the GitLab Pages Daemon. For GitLab Omnibus instances:
...@@ -359,6 +371,9 @@ For GitLab Pages Access Control with TLS/SSL certs issued by an internal or cust ...@@ -359,6 +371,9 @@ For GitLab Pages Access Control with TLS/SSL certs issued by an internal or cust
sudo gitlab-ctl restart gitlab-pages sudo gitlab-ctl restart gitlab-pages
``` ```
CAUTION: **Caution:**
Some GitLab Omnibus upgrades will revert this workaround and you'll need to apply it again.
## Activate verbose logging for daemon ## Activate verbose logging for daemon
Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in Verbose logging was [introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/2533) in
......
...@@ -5453,6 +5453,11 @@ type Project { ...@@ -5453,6 +5453,11 @@ type Project {
id: ID! id: ID!
): SentryDetailedError ): SentryDetailedError
"""
Paginated collection of Sentry errors on the project
"""
sentryErrors: SentryErrorCollection
""" """
E-mail address of the service desk. E-mail address of the service desk.
""" """
...@@ -6054,6 +6059,9 @@ type RootStorageStatistics { ...@@ -6054,6 +6059,9 @@ type RootStorageStatistics {
wikiSize: Int! wikiSize: Int!
} }
"""
A Sentry error.
"""
type SentryDetailedError { type SentryDetailedError {
""" """
Count of occurrences Count of occurrences
...@@ -6186,6 +6194,186 @@ type SentryDetailedError { ...@@ -6186,6 +6194,186 @@ type SentryDetailedError {
userCount: Int! userCount: Int!
} }
"""
A Sentry error. A simplified version of SentryDetailedError.
"""
type SentryError {
"""
Count of occurrences
"""
count: Int!
"""
Culprit of the error
"""
culprit: String!
"""
External URL of the error
"""
externalUrl: String!
"""
Timestamp when the error was first seen
"""
firstSeen: Time!
"""
Last 24hr stats of the error
"""
frequency: [SentryErrorFrequency!]!
"""
ID (global ID) of the error
"""
id: ID!
"""
Timestamp when the error was last seen
"""
lastSeen: Time!
"""
Sentry metadata message of the error
"""
message: String
"""
ID (Sentry ID) of the error
"""
sentryId: String!
"""
ID of the project (Sentry project)
"""
sentryProjectId: ID!
"""
Name of the project affected by the error
"""
sentryProjectName: String!
"""
Slug of the project affected by the error
"""
sentryProjectSlug: String!
"""
Short ID (Sentry ID) of the error
"""
shortId: String!
"""
Status of the error
"""
status: SentryErrorStatus!
"""
Title of the error
"""
title: String!
"""
Type of the error
"""
type: String!
"""
Count of users affected by the error
"""
userCount: Int!
}
"""
An object containing a collection of Sentry errors, and a detailed error.
"""
type SentryErrorCollection {
"""
Detailed version of a Sentry error on the project
"""
detailedError(
"""
ID of the Sentry issue
"""
id: ID!
): SentryDetailedError
"""
Collection of Sentry Errors
"""
errors(
"""
Returns the elements in the list that come after the specified cursor.
"""
after: String
"""
Returns the elements in the list that come before the specified cursor.
"""
before: String
"""
Returns the first _n_ elements from the list.
"""
first: Int
"""
Returns the last _n_ elements from the list.
"""
last: Int
"""
Search term for the Sentry error.
"""
searchTerm: String
"""
Attribute to sort on. Options are frequency, first_seen, last_seen. last_seen is default.
"""
sort: String
): SentryErrorConnection
"""
External URL for Sentry
"""
externalUrl: String
}
"""
The connection type for SentryError.
"""
type SentryErrorConnection {
"""
A list of edges.
"""
edges: [SentryErrorEdge]
"""
A list of nodes.
"""
nodes: [SentryError]
"""
Information to aid in pagination.
"""
pageInfo: PageInfo!
}
"""
An edge in a connection.
"""
type SentryErrorEdge {
"""
A cursor for use in pagination.
"""
cursor: String!
"""
The item at the end of the edge.
"""
node: SentryError
}
type SentryErrorFrequency { type SentryErrorFrequency {
""" """
Count of errors received since the previously recorded time Count of errors received since the previously recorded time
......
...@@ -815,6 +815,7 @@ Information about pagination in a connection. ...@@ -815,6 +815,7 @@ Information about pagination in a connection.
| `repository` | Repository | Git repository of the project | | `repository` | Repository | Git repository of the project |
| `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project | | `requestAccessEnabled` | Boolean | Indicates if users can request member access to the project |
| `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project | | `sentryDetailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `sentryErrors` | SentryErrorCollection | Paginated collection of Sentry errors on the project |
| `serviceDeskAddress` | String | E-mail address of the service desk. | | `serviceDeskAddress` | String | E-mail address of the service desk. |
| `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. | | `serviceDeskEnabled` | Boolean | Indicates if the project has service desk enabled. |
| `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled on the project | | `sharedRunnersEnabled` | Boolean | Indicates if shared runners are enabled on the project |
...@@ -919,6 +920,8 @@ Autogenerated return type of RemoveAwardEmoji ...@@ -919,6 +920,8 @@ Autogenerated return type of RemoveAwardEmoji
## SentryDetailedError ## SentryDetailedError
A Sentry error.
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `count` | Int! | Count of occurrences | | `count` | Int! | Count of occurrences |
...@@ -948,6 +951,40 @@ Autogenerated return type of RemoveAwardEmoji ...@@ -948,6 +951,40 @@ Autogenerated return type of RemoveAwardEmoji
| `type` | String! | Type of the error | | `type` | String! | Type of the error |
| `userCount` | Int! | Count of users affected by the error | | `userCount` | Int! | Count of users affected by the error |
## SentryError
A Sentry error. A simplified version of SentryDetailedError.
| Name | Type | Description |
| --- | ---- | ---------- |
| `count` | Int! | Count of occurrences |
| `culprit` | String! | Culprit of the error |
| `externalUrl` | String! | External URL of the error |
| `firstSeen` | Time! | Timestamp when the error was first seen |
| `frequency` | SentryErrorFrequency! => Array | Last 24hr stats of the error |
| `id` | ID! | ID (global ID) of the error |
| `lastSeen` | Time! | Timestamp when the error was last seen |
| `message` | String | Sentry metadata message of the error |
| `sentryId` | String! | ID (Sentry ID) of the error |
| `sentryProjectId` | ID! | ID of the project (Sentry project) |
| `sentryProjectName` | String! | Name of the project affected by the error |
| `sentryProjectSlug` | String! | Slug of the project affected by the error |
| `shortId` | String! | Short ID (Sentry ID) of the error |
| `status` | SentryErrorStatus! | Status of the error |
| `title` | String! | Title of the error |
| `type` | String! | Type of the error |
| `userCount` | Int! | Count of users affected by the error |
## SentryErrorCollection
An object containing a collection of Sentry errors, and a detailed error.
| Name | Type | Description |
| --- | ---- | ---------- |
| `detailedError` | SentryDetailedError | Detailed version of a Sentry error on the project |
| `errors` | SentryErrorConnection | Collection of Sentry Errors |
| `externalUrl` | String | External URL for Sentry |
## SentryErrorFrequency ## SentryErrorFrequency
| Name | Type | Description | | Name | Type | Description |
......
...@@ -385,6 +385,21 @@ NOTE: **Note:** ...@@ -385,6 +385,21 @@ NOTE: **Note:**
The usage of `perform_enqueued_jobs` is currently useless since our The usage of `perform_enqueued_jobs` is currently useless since our
workers aren't inheriting from `ApplicationJob` / `ActiveJob::Base`. workers aren't inheriting from `ApplicationJob` / `ActiveJob::Base`.
#### DNS
DNS requests are stubbed universally in the test suite
(as of [!22368](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22368)), as DNS can
cause issues depending on the developer's local network. There are RSpec labels
available in `spec/support/dns.rb` which you can apply to tests if you need to
bypass the DNS stubbing, e.g.:
```
it "really connects to Prometheus", :permit_dns do
```
And if you need more specific control, the DNS blocking is implemented in
`spec/support/helpers/dns_helpers.rb` and these methods can be called elsewhere.
#### Filesystem #### Filesystem
Filesystem data can be roughly split into "repositories", and "everything else". Filesystem data can be roughly split into "repositories", and "everything else".
......
...@@ -121,7 +121,7 @@ module Banzai ...@@ -121,7 +121,7 @@ module Banzai
def object_link_text(object, matches) def object_link_text(object, matches)
milestone_link = escape_once(super) milestone_link = escape_once(super)
reference = object.project&.to_reference(project) reference = object.project&.to_reference_base(project)
if reference.present? if reference.present?
"#{milestone_link} <i>in #{reference}</i>".html_safe "#{milestone_link} <i>in #{reference}</i>".html_safe
......
...@@ -104,7 +104,7 @@ module Banzai ...@@ -104,7 +104,7 @@ module Banzai
def link_to_project(project, link_content: nil) def link_to_project(project, link_content: nil)
url = urls.project_url(project, only_path: context[:only_path]) url = urls.project_url(project, only_path: context[:only_path])
data = data_attribute(project: project.id) data = data_attribute(project: project.id)
content = link_content || project.to_reference_with_postfix content = link_content || project.to_reference
link_tag(url, data, content, project.name) link_tag(url, data, content, project.name)
end end
......
...@@ -35,7 +35,7 @@ module Gitlab ...@@ -35,7 +35,7 @@ module Gitlab
:user_count :user_count
def self.declarative_policy_class def self.declarative_policy_class
'ErrorTracking::DetailedErrorPolicy' 'ErrorTracking::BasePolicy'
end end
end end
end end
......
...@@ -4,11 +4,16 @@ module Gitlab ...@@ -4,11 +4,16 @@ module Gitlab
module ErrorTracking module ErrorTracking
class Error class Error
include ActiveModel::Model include ActiveModel::Model
include GlobalID::Identification
attr_accessor :id, :title, :type, :user_count, :count, attr_accessor :id, :title, :type, :user_count, :count,
:first_seen, :last_seen, :message, :culprit, :first_seen, :last_seen, :message, :culprit,
:external_url, :project_id, :project_name, :project_slug, :external_url, :project_id, :project_name, :project_slug,
:short_id, :status, :frequency :short_id, :status, :frequency
def self.declarative_policy_class
'ErrorTracking::BasePolicy'
end
end end
end end
end end
# frozen_string_literal: true
module Gitlab
module ErrorTracking
class ErrorCollection
include GlobalID::Identification
attr_accessor :issues, :external_url, :project
alias_attribute :gitlab_project, :project
def initialize(project:, external_url: nil, issues: [])
@project = project
@external_url = external_url
@issues = issues
end
def self.declarative_policy_class
'ErrorTracking::BasePolicy'
end
end
end
end
# frozen_string_literal: true
module Gitlab
module Graphql
module Extensions
class ExternallyPaginatedArrayExtension < GraphQL::Schema::Field::ConnectionExtension
def resolve(object:, arguments:, context:)
yield(object, arguments)
end
end
end
end
end
# frozen_string_literal: true
module Gitlab
module ImportExport
class ProjectTreeLoader
def load(path, dedup_entries: false)
tree_hash = ActiveSupport::JSON.decode(IO.read(path))
if dedup_entries
dedup_tree(tree_hash)
else
tree_hash
end
end
private
# This function removes duplicate entries from the given tree recursively
# by caching nodes it encounters repeatedly. We only consider nodes for
# which there can actually be multiple equivalent instances (e.g. strings,
# hashes and arrays, but not `nil`s, numbers or booleans.)
#
# The algorithm uses a recursive depth-first descent with 3 cases, starting
# with a root node (the tree/hash itself):
# - a node has already been cached; in this case we return it from the cache
# - a node has not been cached yet but should be; descend into its children
# - a node is neither cached nor qualifies for caching; this is a no-op
def dedup_tree(node, nodes_seen = {})
if nodes_seen.key?(node) && distinguishable?(node)
yield nodes_seen[node]
elsif should_dedup?(node)
nodes_seen[node] = node
case node
when Array
node.each_index do |idx|
dedup_tree(node[idx], nodes_seen) do |cached_node|
node[idx] = cached_node
end
end
when Hash
node.each do |k, v|
dedup_tree(v, nodes_seen) do |cached_node|
node[k] = cached_node
end
end
end
else
node
end
end
# We do not need to consider nodes for which there cannot be multiple instances
def should_dedup?(node)
node && !(node.is_a?(Numeric) || node.is_a?(TrueClass) || node.is_a?(FalseClass))
end
# We can only safely de-dup values that are distinguishable. True value objects
# are always distinguishable by nature. Hashes however can represent entities,
# which are identified by ID, not value. We therefore disallow de-duping hashes
# that do not have an `id` field, since we might risk dropping entities that
# have equal attributes yet different identities.
def distinguishable?(node)
if node.is_a?(Hash)
node.key?('id')
else
true
end
end
end
end
end
...@@ -3,15 +3,17 @@ ...@@ -3,15 +3,17 @@
module Gitlab module Gitlab
module ImportExport module ImportExport
class ProjectTreeRestorer class ProjectTreeRestorer
LARGE_PROJECT_FILE_SIZE_BYTES = 500.megabyte
attr_reader :user attr_reader :user
attr_reader :shared attr_reader :shared
attr_reader :project attr_reader :project
def initialize(user:, shared:, project:) def initialize(user:, shared:, project:)
@path = File.join(shared.export_path, 'project.json')
@user = user @user = user
@shared = shared @shared = shared
@project = project @project = project
@tree_loader = ProjectTreeLoader.new
end end
def restore def restore
...@@ -36,9 +38,16 @@ module Gitlab ...@@ -36,9 +38,16 @@ module Gitlab
private private
def large_project?(path)
File.size(path) >= LARGE_PROJECT_FILE_SIZE_BYTES
end
def read_tree_hash def read_tree_hash
json = IO.read(@path) path = File.join(@shared.export_path, 'project.json')
ActiveSupport::JSON.decode(json) dedup_entries = large_project?(path) &&
Feature.enabled?(:dedup_project_import_metadata, project.group)
@tree_loader.load(path, dedup_entries: dedup_entries)
rescue => e rescue => e
Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger Rails.logger.error("Import/Export error: #{e.message}") # rubocop:disable Gitlab/RailsLogger
raise Gitlab::ImportExport::Error.new('Incorrect JSON format') raise Gitlab::ImportExport::Error.new('Incorrect JSON format')
......
...@@ -159,7 +159,7 @@ module Gitlab ...@@ -159,7 +159,7 @@ module Gitlab
def build_relation(relation_key, relation_definition, data_hash) def build_relation(relation_key, relation_definition, data_hash)
# TODO: This is hack to not create relation for the author # TODO: This is hack to not create relation for the author
# Rather make `RelationFactory#set_note_author` to take care of that # Rather make `RelationFactory#set_note_author` to take care of that
return data_hash if relation_key == 'author' return data_hash if relation_key == 'author' || already_restored?(data_hash)
# create relation objects recursively for all sub-objects # create relation objects recursively for all sub-objects
relation_definition.each do |sub_relation_key, sub_relation_definition| relation_definition.each do |sub_relation_key, sub_relation_definition|
...@@ -169,6 +169,13 @@ module Gitlab ...@@ -169,6 +169,13 @@ module Gitlab
@relation_factory.create(relation_factory_params(relation_key, data_hash)) @relation_factory.create(relation_factory_params(relation_key, data_hash))
end end
# Since we update the data hash in place as we restore relation items,
# and since we also de-duplicate items, we might encounter items that
# have already been restored in a previous iteration.
def already_restored?(relation_item)
!relation_item.is_a?(Hash)
end
def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition) def transform_sub_relations!(data_hash, sub_relation_key, sub_relation_definition)
sub_data_hash = data_hash[sub_relation_key] sub_data_hash = data_hash[sub_relation_key]
return unless sub_data_hash return unless sub_data_hash
......
...@@ -8450,6 +8450,9 @@ msgstr "" ...@@ -8450,6 +8450,9 @@ msgstr ""
msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)" msgid "For public projects, anyone can view pipelines and access job details (output logs and artifacts)"
msgstr "" msgstr ""
msgid "For renewal instructions %{link_start}view our Licensing FAQ.%{link_end}"
msgstr ""
msgid "Forgot your password?" msgid "Forgot your password?"
msgstr "" msgstr ""
......
# frozen_string_literal: true # frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :detailed_error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do factory :detailed_error_tracking_error, parent: :error_tracking_error, class: 'Gitlab::ErrorTracking::DetailedError' do
id { '1' } gitlab_issue { 'http://gitlab.example.com/issues/1' }
title { 'title' }
type { 'error' }
user_count { 1 }
count { 2 }
first_seen { Time.now.iso8601 }
last_seen { Time.now.iso8601 }
message { 'message' }
culprit { 'culprit' }
external_url { 'http://example.com/id' }
external_base_url { 'http://example.com' } external_base_url { 'http://example.com' }
project_id { 'project1' } first_release_last_commit { '68c914da9' }
project_name { 'project name' } last_release_last_commit { '9ad419c86' }
project_slug { 'project_name' } first_release_short_version { 'abc123' }
short_id { 'ID' } last_release_short_version { 'abc123' }
status { 'unresolved' } first_release_version { '12345678' }
tags do tags do
{ {
level: 'error', level: 'error',
logger: 'rails' logger: 'rails'
} }
end end
frequency do
[
[Time.now.to_i, 10]
]
end
gitlab_issue { 'http://gitlab.example.com/issues/1' }
first_release_last_commit { '68c914da9' }
last_release_last_commit { '9ad419c86' }
first_release_short_version { 'abc123' }
last_release_short_version { 'abc123' }
first_release_version { '12345678' }
skip_create skip_create
end end
end end
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
FactoryBot.define do FactoryBot.define do
factory :error_tracking_error, class: 'Gitlab::ErrorTracking::Error' do factory :error_tracking_error, class: 'Gitlab::ErrorTracking::Error' do
id { 'id' } id { '1' }
title { 'title' } title { 'title' }
type { 'error' } type { 'error' }
user_count { 1 } user_count { 1 }
count { 2 } count { 2 }
first_seen { Time.now } first_seen { Time.now.iso8601 }
last_seen { Time.now } last_seen { Time.now.iso8601 }
message { 'message' } message { 'message' }
culprit { 'culprit' } culprit { 'culprit' }
external_url { 'http://example.com/id' } external_url { 'http://example.com/id' }
...@@ -17,7 +17,11 @@ FactoryBot.define do ...@@ -17,7 +17,11 @@ FactoryBot.define do
project_slug { 'project_name' } project_slug { 'project_name' }
short_id { 'ID' } short_id { 'ID' }
status { 'unresolved' } status { 'unresolved' }
frequency { [] } frequency do
[
[Time.now.to_i, 10]
]
end
skip_create skip_create
end end
......
...@@ -32,7 +32,7 @@ describe 'issue move to another project' do ...@@ -32,7 +32,7 @@ describe 'issue move to another project' do
let(:new_project) { create(:project) } let(:new_project) { create(:project) }
let(:new_project_search) { create(:project) } let(:new_project_search) { create(:project) }
let(:text) { "Text with #{mr.to_reference}" } let(:text) { "Text with #{mr.to_reference}" }
let(:cross_reference) { old_project.to_reference(new_project) } let(:cross_reference) { old_project.to_reference_base(new_project) }
before do before do
old_project.add_reporter(user) old_project.add_reporter(user)
......
{
"simple": 42,
"duped_hash_with_id": {
"id": 0,
"v1": 1
},
"duped_hash_no_id": {
"v1": 1
},
"duped_array": [
"v2"
],
"array": [
{
"duped_hash_with_id": {
"id": 0,
"v1": 1
}
},
{
"duped_array": [
"v2"
]
},
{
"duped_hash_no_id": {
"v1": 1
}
}
],
"nested": {
"duped_hash_with_id": {
"id": 0,
"v1": 1
},
"duped_array": [
"v2"
],
"array": [
"don't touch"
]
}
}
\ No newline at end of file
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/settings/components/settings_form.vue'; import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/'; import { createStore } from '~/registry/settings/store/';
...@@ -15,6 +16,9 @@ describe('Settings Form', () => { ...@@ -15,6 +16,9 @@ describe('Settings Form', () => {
let dispatchSpy; let dispatchSpy;
const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy'; const FORM_ELEMENTS_ID_PREFIX = '#expiration-policy';
const trackingPayload = {
label: 'docker_container_retention_and_expiration_policies',
};
const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' }; const GlLoadingIcon = { name: 'gl-loading-icon-stub', template: '<svg></svg>' };
...@@ -48,6 +52,7 @@ describe('Settings Form', () => { ...@@ -48,6 +52,7 @@ describe('Settings Form', () => {
store.dispatch('setInitialState', stringifiedFormOptions); store.dispatch('setInitialState', stringifiedFormOptions);
dispatchSpy = jest.spyOn(store, 'dispatch'); dispatchSpy = jest.spyOn(store, 'dispatch');
mountComponent(); mountComponent();
jest.spyOn(Tracking, 'event');
}); });
afterEach(() => { afterEach(() => {
...@@ -118,15 +123,23 @@ describe('Settings Form', () => { ...@@ -118,15 +123,23 @@ describe('Settings Form', () => {
beforeEach(() => { beforeEach(() => {
form = findForm(); form = findForm();
}); });
it('cancel has type reset', () => {
describe('form cancel event', () => {
it('has type reset', () => {
expect(findCancelButton().attributes('type')).toBe('reset'); expect(findCancelButton().attributes('type')).toBe('reset');
}); });
it('form reset event call the appropriate function', () => { it('calls the appropriate function', () => {
dispatchSpy.mockReturnValue();
form.trigger('reset');
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings');
});
it('tracks the reset event', () => {
dispatchSpy.mockReturnValue(); dispatchSpy.mockReturnValue();
form.trigger('reset'); form.trigger('reset');
// expect.any(Object) is necessary because the event payload is passed to the function expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload);
expect(dispatchSpy).toHaveBeenCalledWith('resetSettings', expect.any(Object)); });
}); });
it('save has type submit', () => { it('save has type submit', () => {
...@@ -177,6 +190,12 @@ describe('Settings Form', () => { ...@@ -177,6 +190,12 @@ describe('Settings Form', () => {
expect(dispatchSpy).toHaveBeenCalledWith('saveSettings'); expect(dispatchSpy).toHaveBeenCalledWith('saveSettings');
}); });
it('tracks the submit event', () => {
dispatchSpy.mockResolvedValue();
form.trigger('submit');
expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload);
});
it('show a success toast when submit succeed', () => { it('show a success toast when submit succeed', () => {
dispatchSpy.mockResolvedValue(); dispatchSpy.mockResolvedValue();
form.trigger('submit'); form.trigger('submit');
......
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ErrorTracking::SentryErrorCollectionResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
before do
project.add_developer(current_user)
allow(ErrorTracking::ListIssuesService)
.to receive(:new)
.and_return list_issues_service
end
describe '#resolve' do
it 'returns an error collection object' do
expect(resolve_error_collection).to be_a Gitlab::ErrorTracking::ErrorCollection
end
it 'provides the service url' do
fake_url = 'http://test.com'
expect(list_issues_service)
.to receive(:external_url)
.and_return(fake_url)
result = resolve_error_collection
expect(result.external_url).to eq fake_url
end
it 'provides the project' do
expect(resolve_error_collection.project).to eq project
end
end
private
def resolve_error_collection(context = { current_user: current_user })
resolve(described_class, obj: project, args: {}, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Resolvers::ErrorTracking::SentryErrorsResolver do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:error_collection) { Gitlab::ErrorTracking::ErrorCollection.new(project: project) }
let(:list_issues_service) { spy('ErrorTracking::ListIssuesService') }
let(:issues) { nil }
let(:pagination) { nil }
describe '#resolve' do
context 'insufficient user permission' do
let(:user) { create(:user) }
it 'returns nil' do
context = { current_user: user }
expect(resolve_errors({}, context)).to eq nil
end
end
context 'user with permission' do
before do
project.add_developer(current_user)
allow(ErrorTracking::ListIssuesService)
.to receive(:new)
.and_return list_issues_service
end
context 'when after arg given' do
let(:after) { "1576029072000:0:0" }
it 'gives the cursor arg' do
expect(ErrorTracking::ListIssuesService)
.to receive(:new)
.with(project, current_user, { cursor: after })
.and_return list_issues_service
resolve_errors({ after: after })
end
end
context 'when no issues fetched' do
before do
allow(list_issues_service)
.to receive(:execute)
.and_return(
issues: nil
)
end
it 'returns nil' do
expect(resolve_errors).to eq nil
end
end
context 'when issues returned' do
let(:issues) { [:issue_1, :issue_2] }
let(:pagination) do
{
'next' => { 'cursor' => 'next' },
'previous' => { 'cursor' => 'prev' }
}
end
before do
allow(list_issues_service)
.to receive(:execute)
.and_return(
issues: issues,
pagination: pagination
)
end
it 'sets the issues' do
expect(resolve_errors).to contain_exactly(*issues)
end
it 'sets the pagination variables' do
result = resolve_errors
expect(result.next_cursor).to eq 'next'
expect(result.previous_cursor).to eq 'prev'
end
it 'returns an externally paginated array' do
expect(resolve_errors).to be_a Gitlab::Graphql::ExternallyPaginatedArray
end
end
end
end
private
def resolve_errors(args = {}, context = { current_user: current_user })
resolve(described_class, obj: error_collection, args: args, ctx: context)
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryErrorCollection'] do
it { expect(described_class.graphql_name).to eq('SentryErrorCollection') }
it { expect(described_class).to require_graphql_authorizations(:read_sentry_issue) }
it 'exposes the expected fields' do
expected_fields = %i[
errors
detailed_error
external_url
]
is_expected.to have_graphql_fields(*expected_fields)
end
describe 'errors field' do
subject { described_class.fields['errors'] }
it 'returns errors' do
aggregate_failures 'testing the correct types are returned' do
is_expected.to have_graphql_type(Types::ErrorTracking::SentryErrorType.connection_type)
is_expected.to have_graphql_extension(Gitlab::Graphql::Extensions::ExternallyPaginatedArrayExtension)
is_expected.to have_graphql_resolver(Resolvers::ErrorTracking::SentryErrorsResolver)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe GitlabSchema.types['SentryError'] do
it { expect(described_class.graphql_name).to eq('SentryError') }
it 'exposes the expected fields' do
expected_fields = %i[
id
sentryId
title
type
userCount
count
firstSeen
lastSeen
message
culprit
externalUrl
sentryProjectId
sentryProjectName
sentryProjectSlug
shortId
status
frequency
]
is_expected.to have_graphql_fields(*expected_fields)
end
end
...@@ -229,10 +229,10 @@ describe Banzai::Filter::CommitRangeReferenceFilter do ...@@ -229,10 +229,10 @@ describe Banzai::Filter::CommitRangeReferenceFilter do
end end
it 'ignores invalid commit IDs on the referenced project' do it 'ignores invalid commit IDs on the referenced project' do
exp = act = "Fixed #{project2.to_reference}@#{commit1.id.reverse}...#{commit2.id}" exp = act = "Fixed #{project2.to_reference_base}@#{commit1.id.reverse}...#{commit2.id}"
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
exp = act = "Fixed #{project2.to_reference}@#{commit1.id}...#{commit2.id.reverse}" exp = act = "Fixed #{project2.to_reference_base}@#{commit1.id}...#{commit2.id.reverse}"
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
end end
......
...@@ -369,7 +369,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -369,7 +369,7 @@ describe Banzai::Filter::LabelReferenceFilter do
end end
context 'with project reference' do context 'with project reference' do
let(:reference) { "#{project.to_reference}#{group_label.to_reference(format: :name)}" } let(:reference) { "#{project.to_reference_base}#{group_label.to_reference(format: :name)}" }
it 'links to a valid reference' do it 'links to a valid reference' do
doc = reference_filter("See #{reference}", project: project) doc = reference_filter("See #{reference}", project: project)
...@@ -385,7 +385,7 @@ describe Banzai::Filter::LabelReferenceFilter do ...@@ -385,7 +385,7 @@ describe Banzai::Filter::LabelReferenceFilter do
end end
it 'ignores invalid label names' do it 'ignores invalid label names' do
exp = act = %(Label #{project.to_reference}#{Label.reference_prefix}"#{group_label.name.reverse}") exp = act = %(Label #{project.to_reference_base}#{Label.reference_prefix}"#{group_label.name.reverse}")
expect(reference_filter(act).to_html).to eq exp expect(reference_filter(act).to_html).to eq exp
end end
......
...@@ -367,15 +367,17 @@ describe Banzai::Filter::MilestoneReferenceFilter do ...@@ -367,15 +367,17 @@ describe Banzai::Filter::MilestoneReferenceFilter do
expect(doc.css('a').first.text).to eq(urls.milestone_url(milestone)) expect(doc.css('a').first.text).to eq(urls.milestone_url(milestone))
end end
it 'does not support cross-project references' do it 'does not support cross-project references', :aggregate_failures do
another_group = create(:group) another_group = create(:group)
another_project = create(:project, :public, group: group) another_project = create(:project, :public, group: group)
project_reference = another_project.to_reference(project) project_reference = another_project.to_reference_base(project)
input_text = "See #{project_reference}#{reference}"
milestone.update!(group: another_group) milestone.update!(group: another_group)
doc = reference_filter("See #{project_reference}#{reference}") doc = reference_filter(input_text)
expect(input_text).to match(Milestone.reference_pattern)
expect(doc.css('a')).to be_empty expect(doc.css('a')).to be_empty
end end
......
...@@ -10,7 +10,7 @@ describe Banzai::Filter::ProjectReferenceFilter do ...@@ -10,7 +10,7 @@ describe Banzai::Filter::ProjectReferenceFilter do
end end
def get_reference(project) def get_reference(project)
project.to_reference_with_postfix project.to_reference
end end
let(:project) { create(:project, :public) } let(:project) { create(:project, :public) }
......
...@@ -8,7 +8,7 @@ describe Gitlab::Gfm::ReferenceRewriter do ...@@ -8,7 +8,7 @@ describe Gitlab::Gfm::ReferenceRewriter do
let(:new_project) { create(:project, name: 'new-project', group: group) } let(:new_project) { create(:project, name: 'new-project', group: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:old_project_ref) { old_project.to_reference(new_project) } let(:old_project_ref) { old_project.to_reference_base(new_project) }
let(:text) { 'some text' } let(:text) { 'some text' }
before do before do
......
# frozen_string_literal: true
require 'spec_helper'
describe Gitlab::ImportExport::ProjectTreeLoader do
let(:fixture) { 'spec/fixtures/lib/gitlab/import_export/with_duplicates.json' }
let(:project_tree) { JSON.parse(File.read(fixture)) }
context 'without de-duplicating entries' do
let(:parsed_tree) do
subject.load(fixture)
end
it 'parses the JSON into the expected tree' do
expect(parsed_tree).to eq(project_tree)
end
it 'does not de-duplicate entries' do
expect(parsed_tree['duped_hash_with_id']).not_to be(parsed_tree['array'][0]['duped_hash_with_id'])
end
end
context 'with de-duplicating entries' do
let(:parsed_tree) do
subject.load(fixture, dedup_entries: true)
end
it 'parses the JSON into the expected tree' do
expect(parsed_tree).to eq(project_tree)
end
it 'de-duplicates equal values' do
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['array'][0]['duped_hash_with_id'])
expect(parsed_tree['duped_hash_with_id']).to be(parsed_tree['nested']['duped_hash_with_id'])
expect(parsed_tree['duped_array']).to be(parsed_tree['array'][1]['duped_array'])
expect(parsed_tree['duped_array']).to be(parsed_tree['nested']['duped_array'])
end
it 'does not de-duplicate hashes without IDs' do
expect(parsed_tree['duped_hash_no_id']).to eq(parsed_tree['array'][2]['duped_hash_no_id'])
expect(parsed_tree['duped_hash_no_id']).not_to be(parsed_tree['array'][2]['duped_hash_no_id'])
end
it 'keeps single entries intact' do
expect(parsed_tree['simple']).to eq(42)
expect(parsed_tree['nested']['array']).to eq(["don't touch"])
end
end
end
...@@ -450,7 +450,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -450,7 +450,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
context 'project.json file access check' do context 'project.json file access check' do
let(:user) { create(:user) } let(:user) { create(:user) }
let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') } let!(:project) { create(:project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) } let(:project_tree_restorer) do
described_class.new(user: user, shared: shared, project: project)
end
let(:restored_project_json) { project_tree_restorer.restore } let(:restored_project_json) { project_tree_restorer.restore }
it 'does not read a symlink' do it 'does not read a symlink' do
...@@ -725,7 +727,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do ...@@ -725,7 +727,9 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:tree_hash) { { 'visibility_level' => visibility } } let(:tree_hash) { { 'visibility_level' => visibility } }
let(:restorer) { described_class.new(user: user, shared: shared, project: project) } let(:restorer) do
described_class.new(user: user, shared: shared, project: project)
end
before do before do
expect(restorer).to receive(:read_tree_hash) { tree_hash } expect(restorer).to receive(:read_tree_hash) { tree_hash }
......
...@@ -131,23 +131,19 @@ describe Project do ...@@ -131,23 +131,19 @@ describe Project do
end end
context 'when creating a new project' do context 'when creating a new project' do
it 'automatically creates a CI/CD settings row' do let_it_be(:project) { create(:project) }
project = create(:project)
it 'automatically creates a CI/CD settings row' do
expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting) expect(project.ci_cd_settings).to be_an_instance_of(ProjectCiCdSetting)
expect(project.ci_cd_settings).to be_persisted expect(project.ci_cd_settings).to be_persisted
end end
it 'automatically creates a container expiration policy row' do it 'automatically creates a container expiration policy row' do
project = create(:project)
expect(project.container_expiration_policy).to be_an_instance_of(ContainerExpirationPolicy) expect(project.container_expiration_policy).to be_an_instance_of(ContainerExpirationPolicy)
expect(project.container_expiration_policy).to be_persisted expect(project.container_expiration_policy).to be_persisted
end end
it 'automatically creates a Pages metadata row' do it 'automatically creates a Pages metadata row' do
project = create(:project)
expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum) expect(project.pages_metadatum).to be_an_instance_of(ProjectPagesMetadatum)
expect(project.pages_metadatum).to be_persisted expect(project.pages_metadatum).to be_persisted
end end
...@@ -532,86 +528,92 @@ describe Project do ...@@ -532,86 +528,92 @@ describe Project do
it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) } it { is_expected.to delegate_method(:last_pipeline).to(:commit).with_arguments(allow_nil: true) }
end end
describe '#to_reference_with_postfix' do describe 'reference methods' do
it 'returns the full path with reference_postfix' do let_it_be(:owner) { create(:user, name: 'Gitlab') }
namespace = create(:namespace, path: 'sample-namespace') let_it_be(:namespace) { create(:namespace, name: 'Sample namespace', path: 'sample-namespace', owner: owner) }
project = create(:project, path: 'sample-project', namespace: namespace) let_it_be(:project) { create(:project, name: 'Sample project', path: 'sample-project', namespace: namespace) }
let_it_be(:group) { create(:group, name: 'Group', path: 'sample-group') }
let_it_be(:another_project) { create(:project, namespace: namespace) }
let_it_be(:another_namespace_project) { create(:project, name: 'another-project') }
expect(project.to_reference_with_postfix).to eq 'sample-namespace/sample-project>' describe '#to_reference' do
it 'returns the path with reference_postfix' do
expect(project.to_reference).to eq("#{project.full_path}>")
end end
it 'returns the path with reference_postfix when arg is self' do
expect(project.to_reference(project)).to eq("#{project.full_path}>")
end end
describe '#to_reference' do it 'returns the full_path with reference_postfix when full' do
let(:owner) { create(:user, name: 'Gitlab') } expect(project.to_reference(full: true)).to eq("#{project.full_path}>")
let(:namespace) { create(:namespace, path: 'sample-namespace', owner: owner) } end
let(:project) { create(:project, path: 'sample-project', namespace: namespace) }
let(:group) { create(:group, name: 'Group', path: 'sample-group') }
it 'returns the full_path with reference_postfix when cross-project' do
expect(project.to_reference(build_stubbed(:project))).to eq("#{project.full_path}>")
end
end
describe '#to_reference_base' do
context 'when nil argument' do context 'when nil argument' do
it 'returns nil' do it 'returns nil' do
expect(project.to_reference).to be_nil expect(project.to_reference_base).to be_nil
end end
end end
context 'when full is true' do context 'when full is true' do
it 'returns complete path to the project' do it 'returns complete path to the project', :aggregate_failures do
expect(project.to_reference(full: true)).to eq 'sample-namespace/sample-project' be_full_path = eq('sample-namespace/sample-project')
expect(project.to_reference(project, full: true)).to eq 'sample-namespace/sample-project'
expect(project.to_reference(group, full: true)).to eq 'sample-namespace/sample-project' expect(project.to_reference_base(full: true)).to be_full_path
expect(project.to_reference_base(project, full: true)).to be_full_path
expect(project.to_reference_base(group, full: true)).to be_full_path
end end
end end
context 'when same project argument' do context 'when same project argument' do
it 'returns nil' do it 'returns nil' do
expect(project.to_reference(project)).to be_nil expect(project.to_reference_base(project)).to be_nil
end end
end end
context 'when cross namespace project argument' do context 'when cross namespace project argument' do
let(:another_namespace_project) { create(:project, name: 'another-project') }
it 'returns complete path to the project' do it 'returns complete path to the project' do
expect(project.to_reference(another_namespace_project)).to eq 'sample-namespace/sample-project' expect(project.to_reference_base(another_namespace_project)).to eq 'sample-namespace/sample-project'
end end
end end
context 'when same namespace / cross-project argument' do context 'when same namespace / cross-project argument' do
let(:another_project) { create(:project, namespace: namespace) }
it 'returns path to the project' do it 'returns path to the project' do
expect(project.to_reference(another_project)).to eq 'sample-project' expect(project.to_reference_base(another_project)).to eq 'sample-project'
end end
end end
context 'when different namespace / cross-project argument' do context 'when different namespace / cross-project argument with same owner' do
let(:another_namespace) { create(:namespace, path: 'another-namespace', owner: owner) } let(:another_namespace_same_owner) { create(:namespace, path: 'another-namespace', owner: owner) }
let(:another_project) { create(:project, path: 'another-project', namespace: another_namespace) } let(:another_project_same_owner) { create(:project, path: 'another-project', namespace: another_namespace_same_owner) }
it 'returns full path to the project' do it 'returns full path to the project' do
expect(project.to_reference(another_project)).to eq 'sample-namespace/sample-project' expect(project.to_reference_base(another_project_same_owner)).to eq 'sample-namespace/sample-project'
end end
end end
context 'when argument is a namespace' do context 'when argument is a namespace' do
context 'with same project path' do context 'with same project path' do
it 'returns path to the project' do it 'returns path to the project' do
expect(project.to_reference(namespace)).to eq 'sample-project' expect(project.to_reference_base(namespace)).to eq 'sample-project'
end end
end end
context 'with different project path' do context 'with different project path' do
it 'returns full path to the project' do it 'returns full path to the project' do
expect(project.to_reference(group)).to eq 'sample-namespace/sample-project' expect(project.to_reference_base(group)).to eq 'sample-namespace/sample-project'
end end
end end
end end
end end
describe '#to_human_reference' do describe '#to_human_reference' do
let(:owner) { create(:user, name: 'Gitlab') }
let(:namespace) { create(:namespace, name: 'Sample namespace', owner: owner) }
let(:project) { create(:project, name: 'Sample project', namespace: namespace) }
context 'when nil argument' do context 'when nil argument' do
it 'returns nil' do it 'returns nil' do
expect(project.to_human_reference).to be_nil expect(project.to_human_reference).to be_nil
...@@ -625,21 +627,18 @@ describe Project do ...@@ -625,21 +627,18 @@ describe Project do
end end
context 'when cross namespace project argument' do context 'when cross namespace project argument' do
let(:another_namespace_project) { create(:project, name: 'another-project') }
it 'returns complete name with namespace of the project' do it 'returns complete name with namespace of the project' do
expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project' expect(project.to_human_reference(another_namespace_project)).to eq 'Gitlab / Sample project'
end end
end end
context 'when same namespace / cross-project argument' do context 'when same namespace / cross-project argument' do
let(:another_project) { create(:project, namespace: namespace) }
it 'returns name of the project' do it 'returns name of the project' do
expect(project.to_human_reference(another_project)).to eq 'Sample project' expect(project.to_human_reference(another_project)).to eq 'Sample project'
end end
end end
end end
end
describe '#merge_method' do describe '#merge_method' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe SentryDetailedErrorPresenter do describe SentryErrorPresenter do
let(:error) { build(:detailed_error_tracking_error) } let(:error) { build(:detailed_error_tracking_error) }
let(:presenter) { described_class.new(error) } let(:presenter) { described_class.new(error) }
...@@ -10,7 +10,7 @@ describe SentryDetailedErrorPresenter do ...@@ -10,7 +10,7 @@ describe SentryDetailedErrorPresenter do
subject { presenter.frequency } subject { presenter.frequency }
it 'returns an array of frequency structs' do it 'returns an array of frequency structs' do
expect(subject).to include(a_kind_of(SentryDetailedErrorPresenter::FrequencyStruct)) expect(subject).to include(a_kind_of(SentryErrorPresenter::FrequencyStruct))
end end
it 'converts the times into UTC time objects' do it 'converts the times into UTC time objects' do
......
# frozen_string_literal: true
require 'spec_helper'
describe 'sentry errors requests' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:project_setting) { create(:project_error_tracking_setting, project: project) }
let_it_be(:current_user) { project.owner }
let(:query) do
graphql_query_for(
'project',
{ 'fullPath' => project.full_path },
query_graphql_field('sentryErrors', {}, fields)
)
end
describe 'getting a detailed sentry error' do
let_it_be(:sentry_detailed_error) { build(:detailed_error_tracking_error) }
let(:sentry_gid) { sentry_detailed_error.to_global_id.to_s }
let(:detailed_fields) do
all_graphql_fields_for('SentryDetailedError'.classify)
end
let(:fields) do
query_graphql_field('detailedError', { id: sentry_gid }, detailed_fields)
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'detailedError') }
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when data is loading via reactive cache' do
before do
post_graphql(query, current_user: current_user)
end
it "is expected to return an empty error" do
expect(error_data).to eq nil
end
end
context 'reactive cache returns data' do
before do
allow_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ issue: sentry_detailed_error })
post_graphql(query, current_user: current_user)
end
let(:sentry_error) { sentry_detailed_error }
let(:error) { error_data }
it_behaves_like 'setting sentry error data'
it 'is expected to return the frequency correctly' do
aggregate_failures 'it returns the frequency correctly' do
expect(error_data['frequency'].count).to eql sentry_detailed_error.frequency.count
first_frequency = error_data['frequency'].first
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_detailed_error.frequency[0][0], in: 0)
expect(first_frequency['count']).to eql sentry_detailed_error.frequency[0][1]
end
end
context 'user does not have permission' do
let(:current_user) { create(:user) }
it "is expected to return an empty error" do
expect(error_data).to eq nil
end
end
end
context 'sentry api returns an error' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:issue_details)
.and_return({ error: 'error message' })
post_graphql(query, current_user: current_user)
end
it 'is expected to handle the error and return nil' do
expect(error_data).to eq nil
end
end
end
describe 'getting an errors list' do
let_it_be(:sentry_error) { build(:error_tracking_error) }
let_it_be(:pagination) do
{
'next' => { 'cursor' => '2222' },
'previous' => { 'cursor' => '1111' }
}
end
let(:fields) do
<<~QUERY
errors {
nodes {
#{all_graphql_fields_for('SentryError'.classify)}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
QUERY
end
let(:error_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'nodes') }
let(:pagination_data) { graphql_data.dig('project', 'sentryErrors', 'errors', 'pageInfo') }
it_behaves_like 'a working graphql query' do
before do
post_graphql(query, current_user: current_user)
end
end
context 'when data is loading via reactive cache' do
before do
post_graphql(query, current_user: current_user)
end
it "is expected to return nil" do
expect(error_data).to eq nil
end
end
context 'reactive cache returns data' do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues)
.and_return({ issues: [sentry_error], pagination: pagination })
post_graphql(query, current_user: current_user)
end
let(:error) { error_data.first }
it 'is expected to return an array of data' do
expect(error_data).to be_a Array
expect(error_data.count).to eq 1
end
it_behaves_like 'setting sentry error data'
it 'sets the pagination correctly' do
expect(pagination_data['startCursor']).to eq(pagination['previous']['cursor'])
expect(pagination_data['endCursor']).to eq(pagination['next']['cursor'])
end
it 'is expected to return the frequency correctly' do
aggregate_failures 'it returns the frequency correctly' do
error = error_data.first
expect(error['frequency'].count).to eql sentry_error.frequency.count
first_frequency = error['frequency'].first
expect(Time.parse(first_frequency['time'])).to eql Time.at(sentry_error.frequency[0][0], in: 0)
expect(first_frequency['count']).to eql sentry_error.frequency[0][1]
end
end
end
context "sentry api itself errors out" do
before do
expect_any_instance_of(ErrorTracking::ProjectErrorTrackingSetting)
.to receive(:list_sentry_issues)
.and_return({ error: 'error message' })
post_graphql(query, current_user: current_user)
end
it 'is expected to handle the error and return nil' do
expect(error_data).to eq nil
end
end
end
end
...@@ -68,6 +68,8 @@ describe 'Self-Monitoring project requests' do ...@@ -68,6 +68,8 @@ describe 'Self-Monitoring project requests' do
let(:job_id) { nil } let(:job_id) { nil }
it 'returns bad_request' do it 'returns bad_request' do
create(:application_setting)
subject subject
aggregate_failures do aggregate_failures do
...@@ -81,11 +83,10 @@ describe 'Self-Monitoring project requests' do ...@@ -81,11 +83,10 @@ describe 'Self-Monitoring project requests' do
end end
context 'when self-monitoring project exists' do context 'when self-monitoring project exists' do
let(:project) { build(:project) } let(:project) { create(:project) }
before do before do
stub_application_setting(self_monitoring_project_id: 1) create(:application_setting, self_monitoring_project_id: project.id)
stub_application_setting(self_monitoring_project: project)
end end
it 'does not need job_id' do it 'does not need job_id' do
...@@ -94,7 +95,7 @@ describe 'Self-Monitoring project requests' do ...@@ -94,7 +95,7 @@ describe 'Self-Monitoring project requests' do
aggregate_failures do aggregate_failures do
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq( expect(json_response).to eq(
'project_id' => 1, 'project_id' => project.id,
'project_full_path' => project.full_path 'project_full_path' => project.full_path
) )
end end
...@@ -106,7 +107,7 @@ describe 'Self-Monitoring project requests' do ...@@ -106,7 +107,7 @@ describe 'Self-Monitoring project requests' do
aggregate_failures do aggregate_failures do
expect(response).to have_gitlab_http_status(:success) expect(response).to have_gitlab_http_status(:success)
expect(json_response).to eq( expect(json_response).to eq(
'project_id' => 1, 'project_id' => project.id,
'project_full_path' => project.full_path 'project_full_path' => project.full_path
) )
end end
...@@ -179,7 +180,7 @@ describe 'Self-Monitoring project requests' do ...@@ -179,7 +180,7 @@ describe 'Self-Monitoring project requests' do
context 'when self-monitoring project exists and job does not exist' do context 'when self-monitoring project exists and job does not exist' do
before do before do
stub_application_setting(self_monitoring_project_id: 1) create(:application_setting, self_monitoring_project_id: create(:project).id)
end end
it 'returns bad_request' do it 'returns bad_request' do
...@@ -196,6 +197,10 @@ describe 'Self-Monitoring project requests' do ...@@ -196,6 +197,10 @@ describe 'Self-Monitoring project requests' do
end end
context 'when self-monitoring project does not exist' do context 'when self-monitoring project does not exist' do
before do
create(:application_setting)
end
it 'does not need job_id' do it 'does not need job_id' do
get status_delete_self_monitoring_project_admin_application_settings_path get status_delete_self_monitoring_project_admin_application_settings_path
......
...@@ -108,6 +108,12 @@ RSpec::Matchers.define :have_graphql_resolver do |expected| ...@@ -108,6 +108,12 @@ RSpec::Matchers.define :have_graphql_resolver do |expected|
end end
end end
RSpec::Matchers.define :have_graphql_extension do |expected|
match do |field|
expect(field.metadata[:type_class].extensions).to include(expected)
end
end
RSpec::Matchers.define :expose_permissions_using do |expected| RSpec::Matchers.define :expose_permissions_using do |expected|
match do |type| match do |type|
permission_field = type.fields['userPermissions'] permission_field = type.fields['userPermissions']
......
# frozen_string_literal: true
RSpec.shared_examples 'setting sentry error data' do
it 'sets the sentry error data correctly' do
aggregate_failures 'testing the sentry error is correct' do
expect(error['id']).to eql sentry_error.to_global_id.to_s
expect(error['sentryId']).to eql sentry_error.id.to_s
expect(error['status']).to eql sentry_error.status.upcase
expect(error['firstSeen']).to eql sentry_error.first_seen
expect(error['lastSeen']).to eql sentry_error.last_seen
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment