summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/controllers/projects/jobs_controller.rb39
-rw-r--r--app/finders/todos_finder.rb10
-rw-r--r--app/graphql/mutations/concerns/mutations/resolves_group.rb15
-rw-r--r--app/graphql/resolvers/todo_resolver.rb90
-rw-r--r--app/graphql/types/query_type.rb5
-rw-r--r--app/graphql/types/todo_action_enum.rb13
-rw-r--r--app/graphql/types/todo_state_enum.rb8
-rw-r--r--app/graphql/types/todo_target_enum.rb9
-rw-r--r--app/graphql/types/todo_type.rb53
-rw-r--r--app/graphql/types/user_type.rb3
-rw-r--r--app/models/ci/build_trace.rb41
-rw-r--r--app/models/evidence.rb27
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/release.rb7
-rw-r--r--app/models/todo.rb8
-rw-r--r--app/policies/todo_policy.rb10
-rw-r--r--app/presenters/todo_presenter.rb7
-rw-r--r--app/serializers/build_trace_entity.rb17
-rw-r--r--app/serializers/build_trace_serializer.rb5
-rw-r--r--app/serializers/evidences/author_entity.rb9
-rw-r--r--app/serializers/evidences/evidence_entity.rb7
-rw-r--r--app/serializers/evidences/evidence_serializer.rb7
-rw-r--r--app/serializers/evidences/issue_entity.rb1
-rw-r--r--app/serializers/evidences/milestone_entity.rb2
-rw-r--r--app/serializers/evidences/release_entity.rb4
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/create_evidence_worker.rb12
-rw-r--r--changelogs/unreleased/26019-evidence-collection.yml5
-rw-r--r--changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml5
-rw-r--r--changelogs/unreleased/31914-graphql-todos-query-pd.yml5
-rw-r--r--changelogs/unreleased/graphql-epic-mutate.yml5
-rw-r--r--changelogs/unreleased/use-ansi2json-for-job-logs.yml5
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20190919091300_create_evidences.rb14
-rw-r--r--db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb21
-rw-r--r--db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb22
-rw-r--r--db/schema.rb38
-rw-r--r--doc/api/graphql/reference/index.md22
-rw-r--r--lib/gitlab/ci/ansi2html.rb2
-rw-r--r--lib/gitlab/ci/ansi2json/converter.rb2
-rw-r--r--lib/gitlab/ci/trace/stream.rb4
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb12
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb125
-rw-r--r--spec/factories/evidences.rb7
-rw-r--r--spec/features/projects/jobs/permissions_spec.rb3
-rw-r--r--spec/finders/todos_finder_spec.rb41
-rw-r--r--spec/fixtures/api/schemas/evidences/author.json14
-rw-r--r--spec/fixtures/api/schemas/evidences/evidence.json11
-rw-r--r--spec/fixtures/api/schemas/evidences/issue.json5
-rw-r--r--spec/fixtures/api/schemas/evidences/milestone.json4
-rw-r--r--spec/fixtures/api/schemas/evidences/project.json2
-rw-r--r--spec/fixtures/api/schemas/evidences/release.json6
-rw-r--r--spec/fixtures/api/schemas/job/build_trace.json31
-rw-r--r--spec/fixtures/api/schemas/job/build_trace_line.json18
-rw-r--r--spec/fixtures/api/schemas/job/build_trace_line_content.json11
-rw-r--r--spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb21
-rw-r--r--spec/graphql/resolvers/todo_resolver_spec.rb113
-rw-r--r--spec/graphql/types/query_type_spec.rb2
-rw-r--r--spec/graphql/types/todo_type_spec.rb13
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb54
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb25
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml7
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml6
-rw-r--r--spec/models/ci/build_trace_spec.rb53
-rw-r--r--spec/models/evidence_spec.rb87
-rw-r--r--spec/models/group_spec.rb17
-rw-r--r--spec/models/hooks/web_hook_spec.rb11
-rw-r--r--spec/models/release_spec.rb20
-rw-r--r--spec/models/todo_spec.rb4
-rw-r--r--spec/policies/todo_policy_spec.rb47
-rw-r--r--spec/serializers/build_trace_entity_spec.rb63
-rw-r--r--spec/serializers/evidences/author_entity_spec.rb13
-rw-r--r--spec/serializers/evidences/evidence_entity_spec.rb14
-rw-r--r--spec/serializers/evidences/evidence_serializer_spec.rb9
-rw-r--r--spec/serializers/evidences/issue_entity_spec.rb2
-rw-r--r--spec/serializers/evidences/milestone_entity_spec.rb2
-rw-r--r--spec/support/shared_examples/evidence_updated_exposed_fields.rb29
-rw-r--r--spec/workers/create_evidence_worker_spec.rb11
79 files changed, 1283 insertions, 199 deletions
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 0fdd4d4f33d..050e2d1079b 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -11,7 +11,7 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
- before_action only: [:trace] do
+ before_action only: [:show] do
push_frontend_feature_flag(:job_log_json)
end
@@ -67,38 +67,27 @@ class Projects::JobsController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord
def trace
- if Feature.enabled?(:job_log_json, @project)
- json_trace
- else
- html_trace
- end
- end
-
- def html_trace
build.trace.read do |stream|
respond_to do |format|
format.json do
- result = {
- id: @build.id, status: @build.status, complete: @build.complete?
- }
-
- if stream.valid?
- stream.limit
- state = params[:state].presence
- trace = stream.html_with_state(state)
- result.merge!(trace.to_h)
- end
-
- render json: result
+ # TODO: when the feature flag is removed we should not pass
+ # content_format to serialize method.
+ content_format = Feature.enabled?(:job_log_json, @project) ? :json : :html
+
+ build_trace = Ci::BuildTrace.new(
+ build: @build,
+ stream: stream,
+ state: params[:state],
+ content_format: content_format)
+
+ render json: BuildTraceSerializer
+ .new(project: @project, current_user: @current_user)
+ .represent(build_trace)
end
end
end
end
- def json_trace
- # will be implemented with https://gitlab.com/gitlab-org/gitlab-foss/issues/66454
- end
-
def retry
return respond_422 unless @build.retryable?
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 2932e558a37..2b46e51290f 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -33,6 +33,8 @@ class TodosFinder
end
def execute
+ return Todo.none if current_user.nil?
+
items = current_user.todos
items = by_action_id(items)
items = by_action(items)
@@ -180,11 +182,9 @@ class TodosFinder
end
def by_group(items)
- if group?
- items.for_group_and_descendants(group)
- else
- items
- end
+ return items unless group?
+
+ items.for_group_ids_and_descendants(params[:group_id])
end
def by_state(items)
diff --git a/app/graphql/mutations/concerns/mutations/resolves_group.rb b/app/graphql/mutations/concerns/mutations/resolves_group.rb
new file mode 100644
index 00000000000..4306ce512f1
--- /dev/null
+++ b/app/graphql/mutations/concerns/mutations/resolves_group.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Mutations
+ module ResolvesGroup
+ extend ActiveSupport::Concern
+
+ def resolve_group(full_path:)
+ resolver.resolve(full_path: full_path)
+ end
+
+ def resolver
+ Resolvers::GroupResolver.new(object: nil, context: context)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/todo_resolver.rb b/app/graphql/resolvers/todo_resolver.rb
new file mode 100644
index 00000000000..38a4539f34a
--- /dev/null
+++ b/app/graphql/resolvers/todo_resolver.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class TodoResolver < BaseResolver
+ type Types::TodoType, null: true
+
+ alias_method :user, :object
+
+ argument :action, [Types::TodoActionEnum],
+ required: false,
+ description: 'The action to be filtered'
+
+ argument :author_id, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The ID of an author'
+
+ argument :project_id, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The ID of a project'
+
+ argument :group_id, [GraphQL::ID_TYPE],
+ required: false,
+ description: 'The ID of a group'
+
+ argument :state, [Types::TodoStateEnum],
+ required: false,
+ description: 'The state of the todo'
+
+ argument :type, [Types::TodoTargetEnum],
+ required: false,
+ description: 'The type of the todo'
+
+ def resolve(**args)
+ return Todo.none if user != context[:current_user]
+
+ TodosFinder.new(user, todo_finder_params(args)).execute
+ end
+
+ private
+
+ # TODO: Support multiple queries for e.g. state and type on TodosFinder:
+ #
+ # https://gitlab.com/gitlab-org/gitlab/merge_requests/18487
+ # https://gitlab.com/gitlab-org/gitlab/merge_requests/18518
+ #
+ # As soon as these MR's are merged, we can refactor this to query by
+ # multiple contents.
+ #
+ def todo_finder_params(args)
+ {
+ state: first_state(args),
+ type: first_type(args),
+ group_id: first_group_id(args),
+ author_id: first_author_id(args),
+ action_id: first_action(args),
+ project_id: first_project(args)
+ }
+ end
+
+ def first_project(args)
+ first_query_field(args, :project_id)
+ end
+
+ def first_action(args)
+ first_query_field(args, :action)
+ end
+
+ def first_author_id(args)
+ first_query_field(args, :author_id)
+ end
+
+ def first_group_id(args)
+ first_query_field(args, :group_id)
+ end
+
+ def first_state(args)
+ first_query_field(args, :state)
+ end
+
+ def first_type(args)
+ first_query_field(args, :type)
+ end
+
+ def first_query_field(query, field)
+ return unless query.key?(field)
+
+ query[field].first if query[field].respond_to?(:first)
+ end
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index bbf94fb92df..996bf225976 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -14,6 +14,11 @@ module Types
resolver: Resolvers::GroupResolver,
description: "Find a group"
+ field :current_user, Types::UserType,
+ null: true,
+ resolve: -> (_obj, _args, context) { context[:current_user] },
+ description: "Get information about current user"
+
field :namespace, Types::NamespaceType,
null: true,
resolver: Resolvers::NamespaceResolver,
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
new file mode 100644
index 00000000000..0e538838474
--- /dev/null
+++ b/app/graphql/types/todo_action_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ class TodoActionEnum < BaseEnum
+ value 'assigned', value: 1
+ value 'mentioned', value: 2
+ value 'build_failed', value: 3
+ value 'marked', value: 4
+ value 'approval_required', value: 5
+ value 'unmergeable', value: 6
+ value 'directly_addressed', value: 7
+ end
+end
diff --git a/app/graphql/types/todo_state_enum.rb b/app/graphql/types/todo_state_enum.rb
new file mode 100644
index 00000000000..29a28b5208d
--- /dev/null
+++ b/app/graphql/types/todo_state_enum.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Types
+ class TodoStateEnum < BaseEnum
+ value 'pending'
+ value 'done'
+ end
+end
diff --git a/app/graphql/types/todo_target_enum.rb b/app/graphql/types/todo_target_enum.rb
new file mode 100644
index 00000000000..9a7391dcd99
--- /dev/null
+++ b/app/graphql/types/todo_target_enum.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Types
+ class TodoTargetEnum < BaseEnum
+ value 'Issue'
+ value 'MergeRequest'
+ value 'Epic'
+ end
+end
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
new file mode 100644
index 00000000000..d36daaf7dec
--- /dev/null
+++ b/app/graphql/types/todo_type.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Types
+ class TodoType < BaseObject
+ graphql_name 'Todo'
+ description 'Representing a todo entry'
+
+ present_using TodoPresenter
+
+ authorize :read_todo
+
+ field :id, GraphQL::ID_TYPE,
+ description: 'Id of the todo',
+ null: false
+
+ field :project, Types::ProjectType,
+ description: 'The project this todo is associated with',
+ null: true,
+ authorize: :read_project,
+ resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, todo.project_id).find }
+
+ field :group, Types::GroupType,
+ description: 'Group this todo is associated with',
+ null: true,
+ authorize: :read_group,
+ resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(Group, todo.group_id).find }
+
+ field :author, Types::UserType,
+ description: 'The owner of this todo',
+ null: false,
+ resolve: -> (todo, args, context) { Gitlab::Graphql::Loaders::BatchModelLoader.new(User, todo.author_id).find }
+
+ field :action, Types::TodoActionEnum,
+ description: 'Action of the todo',
+ null: false
+
+ field :target_type, Types::TodoTargetEnum,
+ description: 'Target type of the todo',
+ null: false
+
+ field :body, GraphQL::STRING_TYPE,
+ description: 'Body of the todo',
+ null: false
+
+ field :state, Types::TodoStateEnum,
+ description: 'State of the todo',
+ null: false
+
+ field :created_at, Types::TimeType,
+ description: 'Timestamp this todo was created',
+ null: false
+ end
+end
diff --git a/app/graphql/types/user_type.rb b/app/graphql/types/user_type.rb
index 9f7d2a171d6..1ba37927b40 100644
--- a/app/graphql/types/user_type.rb
+++ b/app/graphql/types/user_type.rb
@@ -12,5 +12,8 @@ module Types
field :username, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :avatar_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
field :web_url, GraphQL::STRING_TYPE, null: false # rubocop:disable Graphql/Descriptions
+ field :todos, Types::TodoType.connection_type, null: false,
+ resolver: Resolvers::TodoResolver,
+ description: 'Todos of this user'
end
end
diff --git a/app/models/ci/build_trace.rb b/app/models/ci/build_trace.rb
new file mode 100644
index 00000000000..b9db1559836
--- /dev/null
+++ b/app/models/ci/build_trace.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildTrace
+ CONVERTERS = {
+ html: Gitlab::Ci::Ansi2html,
+ json: Gitlab::Ci::Ansi2json
+ }.freeze
+
+ attr_reader :trace, :build
+
+ delegate :state, :append, :truncated, :offset, :size, :total, to: :trace, allow_nil: true
+ delegate :id, :status, :complete?, to: :build, prefix: true
+
+ def initialize(build:, stream:, state:, content_format:)
+ @build = build
+ @content_format = content_format
+
+ if stream.valid?
+ stream.limit
+ @trace = CONVERTERS.fetch(content_format).convert(stream.stream, state)
+ end
+ end
+
+ def json?
+ @content_format == :json
+ end
+
+ def html?
+ @content_format == :html
+ end
+
+ def json_lines
+ @trace&.lines if json?
+ end
+
+ def html_lines
+ @trace&.html if html?
+ end
+ end
+end
diff --git a/app/models/evidence.rb b/app/models/evidence.rb
new file mode 100644
index 00000000000..69a00f1cb3f
--- /dev/null
+++ b/app/models/evidence.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+class Evidence < ApplicationRecord
+ include ShaAttribute
+
+ belongs_to :release
+
+ before_validation :generate_summary_and_sha
+
+ default_scope { order(created_at: :asc) }
+
+ sha_attribute :summary_sha
+
+ def milestones
+ @milestones ||= release.milestones.includes(:issues)
+ end
+
+ private
+
+ def generate_summary_and_sha
+ summary = Evidences::EvidenceSerializer.new.represent(self) # rubocop: disable CodeReuse/Serializer
+ return unless summary
+
+ self.summary = summary
+ self.summary_sha = Gitlab::CryptoHelper.sha256(summary)
+ end
+end
diff --git a/app/models/group.rb b/app/models/group.rb
index 8b21206fccf..042201ffa14 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -473,6 +473,12 @@ class Group < Namespace
errors.add(:visibility_level, "#{visibility} is not allowed since there are sub-groups with higher visibility.")
end
+
+ def self.groups_including_descendants_by(group_ids)
+ Gitlab::ObjectHierarchy
+ .new(Group.where(id: group_ids))
+ .base_and_descendants
+ end
end
Group.prepend_if_ee('EE::Group')
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 16fc7fdbd48..e51b1c41059 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -13,7 +13,7 @@ class WebHook < ApplicationRecord
algorithm: 'aes-256-gcm',
key: Settings.attr_encrypted_db_key_base_32
- has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :web_hook_logs
validates :url, presence: true
validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) }
diff --git a/app/models/release.rb b/app/models/release.rb
index 8759e38060c..add57367f61 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -14,6 +14,7 @@ class Release < ApplicationRecord
has_many :milestone_releases
has_many :milestones, through: :milestone_releases
+ has_one :evidence
default_value_for :released_at, allows_nil: false do
Time.zone.now
@@ -28,6 +29,8 @@ class Release < ApplicationRecord
delegate :repository, to: :project
+ after_commit :create_evidence!, on: :create
+
def commit
strong_memoize(:commit) do
repository.commit(actual_sha)
@@ -66,6 +69,10 @@ class Release < ApplicationRecord
repository.find_tag(tag)
end
end
+
+ def create_evidence!
+ CreateEvidenceWorker.perform_async(self.id)
+ end
end
Release.prepend_if_ee('EE::Release')
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 456115872d1..1927b54510e 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -75,13 +75,13 @@ class Todo < ApplicationRecord
after_save :keep_around_commit, if: :commit_id
class << self
- # Returns all todos for the given group and its descendants.
+ # Returns all todos for the given group ids and their descendants.
#
- # group - A `Group` to retrieve todos for.
+ # group_ids - Group Ids to retrieve todos for.
#
# Returns an `ActiveRecord::Relation`.
- def for_group_and_descendants(group)
- groups = group.self_and_descendants
+ def for_group_ids_and_descendants(group_ids)
+ groups = Group.groups_including_descendants_by(group_ids)
from_union([
for_project(Project.for_group(groups)),
diff --git a/app/policies/todo_policy.rb b/app/policies/todo_policy.rb
new file mode 100644
index 00000000000..f8644217f04
--- /dev/null
+++ b/app/policies/todo_policy.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class TodoPolicy < BasePolicy
+ desc 'User can only read own todos'
+ condition(:own_todo) do
+ @user && @subject.user_id == @user.id
+ end
+
+ rule { own_todo }.enable :read_todo
+end
diff --git a/app/presenters/todo_presenter.rb b/app/presenters/todo_presenter.rb
new file mode 100644
index 00000000000..b57fc712c5a
--- /dev/null
+++ b/app/presenters/todo_presenter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class TodoPresenter < Gitlab::View::Presenter::Delegated
+ include GlobalID::Identification
+
+ presents :todo
+end
diff --git a/app/serializers/build_trace_entity.rb b/app/serializers/build_trace_entity.rb
new file mode 100644
index 00000000000..b5bac8a5d64
--- /dev/null
+++ b/app/serializers/build_trace_entity.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class BuildTraceEntity < Grape::Entity
+ expose :build_id, as: :id
+ expose :build_status, as: :status
+ expose :build_complete?, as: :complete
+
+ expose :state
+ expose :append
+ expose :truncated
+ expose :offset
+ expose :size
+ expose :total
+
+ expose :json_lines, as: :lines, if: ->(*) { object.json? }
+ expose :html_lines, as: :html, if: ->(*) { object.html? }
+end
diff --git a/app/serializers/build_trace_serializer.rb b/app/serializers/build_trace_serializer.rb
new file mode 100644
index 00000000000..c95158f10a4
--- /dev/null
+++ b/app/serializers/build_trace_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class BuildTraceSerializer < BaseSerializer
+ entity BuildTraceEntity
+end
diff --git a/app/serializers/evidences/author_entity.rb b/app/serializers/evidences/author_entity.rb
deleted file mode 100644
index 9023c64dad2..00000000000
--- a/app/serializers/evidences/author_entity.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-module Evidences
- class AuthorEntity < Grape::Entity
- expose :id
- expose :name
- expose :email
- end
-end
diff --git a/app/serializers/evidences/evidence_entity.rb b/app/serializers/evidences/evidence_entity.rb
new file mode 100644
index 00000000000..9689ae10895
--- /dev/null
+++ b/app/serializers/evidences/evidence_entity.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Evidences
+ class EvidenceEntity < Grape::Entity
+ expose :release, using: Evidences::ReleaseEntity
+ end
+end
diff --git a/app/serializers/evidences/evidence_serializer.rb b/app/serializers/evidences/evidence_serializer.rb
new file mode 100644
index 00000000000..d03032bc65c
--- /dev/null
+++ b/app/serializers/evidences/evidence_serializer.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Evidences
+ class EvidenceSerializer < BaseSerializer
+ entity EvidenceEntity
+ end
+end
diff --git a/app/serializers/evidences/issue_entity.rb b/app/serializers/evidences/issue_entity.rb
index 883256bf38a..2f1f5dc3d18 100644
--- a/app/serializers/evidences/issue_entity.rb
+++ b/app/serializers/evidences/issue_entity.rb
@@ -5,7 +5,6 @@ module Evidences
expose :id
expose :title
expose :description
- expose :author, using: AuthorEntity
expose :state
expose :iid
expose :confidential
diff --git a/app/serializers/evidences/milestone_entity.rb b/app/serializers/evidences/milestone_entity.rb
index 8118cab4403..eeb3d58d4c7 100644
--- a/app/serializers/evidences/milestone_entity.rb
+++ b/app/serializers/evidences/milestone_entity.rb
@@ -9,6 +9,6 @@ module Evidences
expose :iid
expose :created_at
expose :due_date
- expose :issues, using: IssueEntity
+ expose :issues, using: Evidences::IssueEntity
end
end
diff --git a/app/serializers/evidences/release_entity.rb b/app/serializers/evidences/release_entity.rb
index 8916ce67b4c..59e379a3c08 100644
--- a/app/serializers/evidences/release_entity.rb
+++ b/app/serializers/evidences/release_entity.rb
@@ -7,7 +7,7 @@ module Evidences
expose :name
expose :description
expose :created_at
- expose :project, using: ProjectEntity
- expose :milestones, using: MilestoneEntity
+ expose :project, using: Evidences::ProjectEntity
+ expose :milestones, using: Evidences::MilestoneEntity
end
end
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index a33afd436b0..cd8d1d05d8b 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -173,3 +173,4 @@
- delete_stored_files
- import_issues_csv
- project_daily_statistics
+- create_evidence
diff --git a/app/workers/create_evidence_worker.rb b/app/workers/create_evidence_worker.rb
new file mode 100644
index 00000000000..5fc901ae514
--- /dev/null
+++ b/app/workers/create_evidence_worker.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class CreateEvidenceWorker
+ include ApplicationWorker
+
+ def perform(release_id)
+ release = Release.find_by_id(release_id)
+ return unless release
+
+ Evidence.create!(release: release)
+ end
+end
diff --git a/changelogs/unreleased/26019-evidence-collection.yml b/changelogs/unreleased/26019-evidence-collection.yml
new file mode 100644
index 00000000000..439a4b55900
--- /dev/null
+++ b/changelogs/unreleased/26019-evidence-collection.yml
@@ -0,0 +1,5 @@
+---
+title: Creation of Evidence collection of new releases.
+merge_request: 17217
+author:
+type: added
diff --git a/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml b/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml
new file mode 100644
index 00000000000..f1b82620418
--- /dev/null
+++ b/changelogs/unreleased/29215-500-error-when-deleting-group-web-hook-activerecord-statementinvali.yml
@@ -0,0 +1,5 @@
+---
+title: Use cascading deletes for deleting logs upon deleting a webhook
+merge_request: 18642
+author:
+type: performance
diff --git a/changelogs/unreleased/31914-graphql-todos-query-pd.yml b/changelogs/unreleased/31914-graphql-todos-query-pd.yml
new file mode 100644
index 00000000000..e39bcda1ff6
--- /dev/null
+++ b/changelogs/unreleased/31914-graphql-todos-query-pd.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to query todos using GraphQL
+merge_request: 18218
+author:
+type: added
diff --git a/changelogs/unreleased/graphql-epic-mutate.yml b/changelogs/unreleased/graphql-epic-mutate.yml
new file mode 100644
index 00000000000..322c069aa46
--- /dev/null
+++ b/changelogs/unreleased/graphql-epic-mutate.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for epic update through GraphQL API.
+merge_request: 18440
+author:
+type: added
diff --git a/changelogs/unreleased/use-ansi2json-for-job-logs.yml b/changelogs/unreleased/use-ansi2json-for-job-logs.yml
new file mode 100644
index 00000000000..1fce00e821c
--- /dev/null
+++ b/changelogs/unreleased/use-ansi2json-for-job-logs.yml
@@ -0,0 +1,5 @@
+---
+title: Use new Ansi2json job log converter via feature flag
+merge_request: 18134
+author:
+type: added
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index 34a8bba498f..8ca7ab4dcb1 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -96,6 +96,7 @@
- [phabricator_import_import_tasks, 1]
- [update_namespace_statistics, 1]
- [chaos, 2]
+ - [create_evidence, 2]
# EE-specific queues
- [ldap_group_sync, 2]
diff --git a/db/migrate/20190919091300_create_evidences.rb b/db/migrate/20190919091300_create_evidences.rb
new file mode 100644
index 00000000000..3f861ed26bd
--- /dev/null
+++ b/db/migrate/20190919091300_create_evidences.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class CreateEvidences < ActiveRecord::Migration[5.2]
+ DOWNTIME = false
+
+ def change
+ create_table :evidences do |t|
+ t.references :release, foreign_key: { on_delete: :cascade }, null: false
+ t.timestamps_with_timezone
+ t.binary :summary_sha
+ t.jsonb :summary, null: false, default: {}
+ end
+ end
+end
diff --git a/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
new file mode 100644
index 00000000000..94d16e921df
--- /dev/null
+++ b/db/migrate/20191003015155_add_self_managed_prometheus_alerts.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+class AddSelfManagedPrometheusAlerts < ActiveRecord::Migration[5.2]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ create_table :self_managed_prometheus_alert_events do |t|
+ t.references :project, index: false, foreign_key: { on_delete: :cascade }, null: false
+ t.references :environment, index: true, foreign_key: { on_delete: :cascade }
+ t.datetime_with_timezone :started_at, null: false
+ t.datetime_with_timezone :ended_at
+
+ t.integer :status, null: false, limit: 2
+ t.string :title, null: false, limit: 255
+ t.string :query_expression, limit: 255
+ t.string :payload_key, null: false, limit: 255
+ t.index [:project_id, :payload_key], unique: true, name: 'idx_project_id_payload_key_self_managed_prometheus_alert_events'
+ end
+ end
+end
diff --git a/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb b/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb
new file mode 100644
index 00000000000..68b448f8836
--- /dev/null
+++ b/db/migrate/20191016220135_add_join_table_for_self_managed_prometheus_alert_issues.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+class AddJoinTableForSelfManagedPrometheusAlertIssues < ActiveRecord::Migration[5.2]
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ # Join table to Issues
+ create_table :issues_self_managed_prometheus_alert_events, id: false do |t|
+ t.references :issue, null: false,
+ index: false, # Uses the index below
+ foreign_key: { on_delete: :cascade }
+ t.references :self_managed_prometheus_alert_event, null: false,
+ index: { name: 'issue_id_issues_self_managed_rometheus_alert_events_index' },
+ foreign_key: { on_delete: :cascade }
+
+ t.timestamps_with_timezone
+ t.index [:issue_id, :self_managed_prometheus_alert_event_id],
+ unique: true, name: 'issue_id_self_managed_prometheus_alert_event_id_index'
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 825b66f6dfd..cf706f8caaa 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_10_16_072826) do
+ActiveRecord::Schema.define(version: 2019_10_16_220135) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
@@ -1424,6 +1424,15 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end
+ create_table "evidences", force: :cascade do |t|
+ t.bigint "release_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.binary "summary_sha"
+ t.jsonb "summary", default: {}, null: false
+ t.index ["release_id"], name: "index_evidences_on_release_id"
+ end
+
create_table "external_pull_requests", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
@@ -1936,6 +1945,15 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.index ["prometheus_alert_event_id"], name: "issue_id_issues_prometheus_alert_events_index"
end
+ create_table "issues_self_managed_prometheus_alert_events", id: false, force: :cascade do |t|
+ t.bigint "issue_id", null: false
+ t.bigint "self_managed_prometheus_alert_event_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.index ["issue_id", "self_managed_prometheus_alert_event_id"], name: "issue_id_self_managed_prometheus_alert_event_id_index", unique: true
+ t.index ["self_managed_prometheus_alert_event_id"], name: "issue_id_issues_self_managed_rometheus_alert_events_index"
+ end
+
create_table "jira_connect_installations", force: :cascade do |t|
t.string "client_key"
t.string "encrypted_shared_secret"
@@ -3309,6 +3327,19 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
t.index ["group_id", "token_encrypted"], name: "index_scim_oauth_access_tokens_on_group_id_and_token_encrypted", unique: true
end
+ create_table "self_managed_prometheus_alert_events", force: :cascade do |t|
+ t.bigint "project_id", null: false
+ t.bigint "environment_id"
+ t.datetime_with_timezone "started_at", null: false
+ t.datetime_with_timezone "ended_at"
+ t.integer "status", limit: 2, null: false
+ t.string "title", limit: 255, null: false
+ t.string "query_expression", limit: 255
+ t.string "payload_key", limit: 255, null: false
+ t.index ["environment_id"], name: "index_self_managed_prometheus_alert_events_on_environment_id"
+ t.index ["project_id", "payload_key"], name: "idx_project_id_payload_key_self_managed_prometheus_alert_events", unique: true
+ end
+
create_table "sent_notifications", id: :serial, force: :cascade do |t|
t.integer "project_id"
t.integer "noteable_id"
@@ -4079,6 +4110,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
add_foreign_key "events", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
add_foreign_key "events", "projects", on_delete: :cascade
add_foreign_key "events", "users", column: "author_id", name: "fk_edfd187b6f", on_delete: :cascade
+ add_foreign_key "evidences", "releases", on_delete: :cascade
add_foreign_key "external_pull_requests", "projects", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", on_delete: :cascade
add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
@@ -4140,6 +4172,8 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
add_foreign_key "issues", "users", column: "updated_by_id", name: "fk_ffed080f01", on_delete: :nullify
add_foreign_key "issues_prometheus_alert_events", "issues", on_delete: :cascade
add_foreign_key "issues_prometheus_alert_events", "prometheus_alert_events", on_delete: :cascade
+ add_foreign_key "issues_self_managed_prometheus_alert_events", "issues", on_delete: :cascade
+ add_foreign_key "issues_self_managed_prometheus_alert_events", "self_managed_prometheus_alert_events", on_delete: :cascade
add_foreign_key "jira_connect_subscriptions", "jira_connect_installations", on_delete: :cascade
add_foreign_key "jira_connect_subscriptions", "namespaces", on_delete: :cascade
add_foreign_key "jira_tracker_data", "services", on_delete: :cascade
@@ -4279,6 +4313,8 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do
add_foreign_key "reviews", "users", column: "author_id", on_delete: :nullify
add_foreign_key "saml_providers", "namespaces", column: "group_id", on_delete: :cascade
add_foreign_key "scim_oauth_access_tokens", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "self_managed_prometheus_alert_events", "environments", on_delete: :cascade
+ add_foreign_key "self_managed_prometheus_alert_events", "projects", on_delete: :cascade
add_foreign_key "services", "projects", name: "fk_71cce407f9", on_delete: :cascade
add_foreign_key "slack_integrations", "services", on_delete: :cascade
add_foreign_key "smartcard_identities", "users", on_delete: :cascade
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index b21fc9bfb18..81fb3b89bcc 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -710,6 +710,20 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `count` | Int! | |
| `completedCount` | Int! | |
+### Todo
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `id` | ID! | Id of the todo |
+| `project` | Project | The project this todo is associated with |
+| `group` | Group | Group this todo is associated with |
+| `author` | User! | The owner of this todo |
+| `action` | TodoActionEnum! | Action of the todo |
+| `targetType` | TodoTargetEnum! | Target type of the todo |
+| `body` | String! | Body of the todo |
+| `state` | TodoStateEnum! | State of the todo |
+| `createdAt` | Time! | Timestamp this todo was created |
+
### ToggleAwardEmojiPayload
| Name | Type | Description |
@@ -736,6 +750,14 @@ The API can be explored interactively using the [GraphiQL IDE](../index.md#graph
| `flatPath` | String! | |
| `webUrl` | String | |
+### UpdateEpicPayload
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Reasons why the mutation failed. |
+| `epic` | Epic | The epic after mutation |
+
### UpdateNotePayload
| Name | Type | Description |
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index b7886114e9c..eb5d78ebcd4 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -178,6 +178,8 @@ module Gitlab
close_open_tags
+ # TODO: replace OpenStruct with a better type
+ # https://gitlab.com/gitlab-org/gitlab/issues/34305
OpenStruct.new(
html: @out.force_encoding(Encoding.default_external),
state: state,
diff --git a/lib/gitlab/ci/ansi2json/converter.rb b/lib/gitlab/ci/ansi2json/converter.rb
index 53adaf38b87..8d25b66af9c 100644
--- a/lib/gitlab/ci/ansi2json/converter.rb
+++ b/lib/gitlab/ci/ansi2json/converter.rb
@@ -37,6 +37,8 @@ module Gitlab
flush_current_line
+ # TODO: replace OpenStruct with a better type
+ # https://gitlab.com/gitlab-org/gitlab/issues/34305
OpenStruct.new(
lines: @lines,
state: @state.encode,
diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb
index e61fb50a303..20f5620dd64 100644
--- a/lib/gitlab/ci/trace/stream.rb
+++ b/lib/gitlab/ci/trace/stream.rb
@@ -63,10 +63,6 @@ module Gitlab
end.force_encoding(Encoding.default_external)
end
- def html_with_state(state = nil)
- ::Gitlab::Ci::Ansi2html.convert(stream, state)
- end
-
def html(last_lines: nil)
text = raw(last_lines: last_lines)
buffer = StringIO.new(text)
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 2eaf52355dd..b0559729ff3 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -298,18 +298,6 @@ module Gitlab
Gitlab::SafeRequestStore[key] = commit
end
- # rubocop: disable CodeReuse/ActiveRecord
- def patch(revision)
- request = Gitaly::CommitPatchRequest.new(
- repository: @gitaly_repo,
- revision: encode_binary(revision)
- )
- response = GitalyClient.call(@repository.storage, :diff_service, :commit_patch, request, timeout: GitalyClient.medium_timeout)
-
- response.sum(&:data)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def commit_stats(revision)
request = Gitaly::CommitStatsRequest.new(
repository: @gitaly_repo,
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 53d32665b0c..90ccb884927 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -527,6 +527,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
describe 'GET trace.json' do
before do
+ stub_feature_flags(job_log_json: true)
get_trace
end
@@ -535,8 +536,119 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
+ expect(json_response['state']).to be_present
+ expect(json_response['append']).not_to be_nil
+ expect(json_response['truncated']).not_to be_nil
+ expect(json_response['size']).to be_present
+ expect(json_response['total']).to be_present
+ expect(json_response['lines'].count).to be_positive
+ end
+ end
+
+ context 'when job has a trace' do
+ let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines']).to eq [{ 'content' => [{ 'text' => 'BUILD TRACE' }], 'offset' => 0 }]
+ end
+ end
+
+ context 'when job has no traces' do
+ let(:job) { create(:ci_build, pipeline: pipeline) }
+
+ it 'returns no traces' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines']).to be_nil
+ end
+ end
+
+ context 'when job has a trace with ANSI sequence and Unicode' do
+ let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
+
+ it 'returns a trace with Unicode' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to match_response_schema('job/build_trace')
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines'].flat_map {|l| l['content'].map { |c| c['text'] } }).to include("ヾ(´༎ຶД༎ຶ`)ノ")
+ end
+ end
+
+ context 'when trace artifact is in ObjectStorage' do
+ let(:url) { 'http://object-storage/trace' }
+ let(:file_path) { expand_fixture_path('trace/sample_trace') }
+ let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
+
+ before do
+ allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
+ allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
+ allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
+ end
+
+ context 'when there are no network issues' do
+ before do
+ stub_remote_url_206(url, file_path)
+
+ get_trace
+ end
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['lines'].count).to be_positive
+ end
+ end
+
+ context 'when there is a network issue' do
+ before do
+ stub_remote_url_500(url)
+ end
+
+ it 'returns a trace' do
+ expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
+ end
+ end
+ end
+
+ def get_trace
+ get :trace,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ },
+ format: :json
+ end
+ end
+
+ describe 'GET legacy trace.json' do
+ before do
+ get_trace
+ end
+
+ context 'when job has a trace artifact' do
+ let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
+
+ it 'returns a trace' do
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['id']).to eq job.id
+ expect(json_response['status']).to eq job.status
+ expect(json_response['state']).to be_present
+ expect(json_response['append']).not_to be_nil
+ expect(json_response['truncated']).not_to be_nil
+ expect(json_response['size']).to be_present
+ expect(json_response['total']).to be_present
expect(json_response['html']).to eq(job.trace.html)
end
end
@@ -612,12 +724,13 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
def get_trace
- get :trace, params: {
- namespace_id: project.namespace,
- project_id: project,
- id: job.id
- },
- format: :json
+ get :trace,
+ params: {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: job.id
+ },
+ format: :json
end
end
diff --git a/spec/factories/evidences.rb b/spec/factories/evidences.rb
new file mode 100644
index 00000000000..964f232a1c9
--- /dev/null
+++ b/spec/factories/evidences.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :evidence do
+ release
+ end
+end
diff --git a/spec/features/projects/jobs/permissions_spec.rb b/spec/features/projects/jobs/permissions_spec.rb
index 44309a9c4bf..ae506b66a86 100644
--- a/spec/features/projects/jobs/permissions_spec.rb
+++ b/spec/features/projects/jobs/permissions_spec.rb
@@ -10,6 +10,7 @@ describe 'Project Jobs Permissions' do
let!(:job) { create(:ci_build, :running, :coverage, :trace_artifact, pipeline: pipeline) }
before do
+ stub_feature_flags(job_log_json: true)
sign_in(user)
project.enable_ci
@@ -69,7 +70,7 @@ describe 'Project Jobs Permissions' do
it_behaves_like 'recent job page details responds with status', 200 do
it 'renders job details', :js do
expect(page).to have_content "Job ##{job.id}"
- expect(page).to have_css '.js-build-trace'
+ expect(page).to have_css '.log-line'
end
end
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 5d284f4cf17..044e135fa0b 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -16,6 +16,10 @@ describe TodosFinder do
end
describe '#execute' do
+ it 'returns no todos if user is nil' do
+ expect(described_class.new(nil, {}).execute).to be_empty
+ end
+
context 'filtering' do
let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
@@ -97,14 +101,39 @@ describe TodosFinder do
end
end
- context 'with subgroups' do
- let(:subgroup) { create(:group, parent: group) }
- let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
+ context 'by groups' do
+ context 'with subgroups' do
+ let(:subgroup) { create(:group, parent: group) }
+ let!(:todo3) { create(:todo, user: user, group: subgroup, target: issue) }
+
+ it 'returns todos from subgroups when filtered by a group' do
+ todos = finder.new(user, { group_id: group.id }).execute
+
+ expect(todos).to match_array([todo1, todo2, todo3])
+ end
+ end
+
+ context 'filtering for multiple groups' do
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:group3) { create(:group) }
+
+ let!(:todo1) { create(:todo, user: user, project: project, target: issue) }
+ let!(:todo2) { create(:todo, user: user, group: group, target: merge_request) }
+ let!(:todo3) { create(:todo, user: user, group: group2, target: merge_request) }
+
+ let(:subgroup1) { create(:group, parent: group) }
+ let!(:todo4) { create(:todo, user: user, group: subgroup1, target: issue) }
- it 'returns todos from subgroups when filtered by a group' do
- todos = finder.new(user, { group_id: group.id }).execute
+ let(:subgroup2) { create(:group, parent: group2) }
+ let!(:todo5) { create(:todo, user: user, group: subgroup2, target: issue) }
- expect(todos).to match_array([todo1, todo2, todo3])
+ let!(:todo6) { create(:todo, user: user, group: group3, target: issue) }
+
+ it 'returns the expected groups' do
+ todos = finder.new(user, { group_id: [group.id, group2.id] }).execute
+
+ expect(todos).to match_array([todo1, todo2, todo3, todo4, todo5])
+ end
end
end
end
diff --git a/spec/fixtures/api/schemas/evidences/author.json b/spec/fixtures/api/schemas/evidences/author.json
deleted file mode 100644
index 1b49446900a..00000000000
--- a/spec/fixtures/api/schemas/evidences/author.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "type": "object",
- "required": [
- "id",
- "name",
- "email"
- ],
- "properties": {
- "id": { "type": "integer" },
- "name": { "type": "string" },
- "email": { "type": "string" }
- },
- "additionalProperties": false
-}
diff --git a/spec/fixtures/api/schemas/evidences/evidence.json b/spec/fixtures/api/schemas/evidences/evidence.json
new file mode 100644
index 00000000000..ea3861258e1
--- /dev/null
+++ b/spec/fixtures/api/schemas/evidences/evidence.json
@@ -0,0 +1,11 @@
+{
+ "type": "object",
+ "required": [
+ "release"
+ ],
+ "properties": {
+ "release": { "$ref": "release.json" }
+ },
+ "additionalProperties": false
+}
+
diff --git a/spec/fixtures/api/schemas/evidences/issue.json b/spec/fixtures/api/schemas/evidences/issue.json
index 10e90dff455..fd9daf17ab8 100644
--- a/spec/fixtures/api/schemas/evidences/issue.json
+++ b/spec/fixtures/api/schemas/evidences/issue.json
@@ -14,13 +14,12 @@
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
- "description": { "type": "string" },
- "author": { "$ref": "author.json" },
+ "description": { "type": ["string", "null"] },
"state": { "type": "string" },
"iid": { "type": "integer" },
"confidential": { "type": "boolean" },
"created_at": { "type": "date" },
- "due_date": { "type": "date" }
+ "due_date": { "type": ["date", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/fixtures/api/schemas/evidences/milestone.json b/spec/fixtures/api/schemas/evidences/milestone.json
index 91f0f48bd4c..ab27fdecde2 100644
--- a/spec/fixtures/api/schemas/evidences/milestone.json
+++ b/spec/fixtures/api/schemas/evidences/milestone.json
@@ -13,11 +13,11 @@
"properties": {
"id": { "type": "integer" },
"title": { "type": "string" },
- "description": { "type": "string" },
+ "description": { "type": ["string", "null"] },
"state": { "type": "string" },
"iid": { "type": "integer" },
"created_at": { "type": "date" },
- "due_date": { "type": "date" },
+ "due_date": { "type": ["date", "null"] },
"issues": {
"type": "array",
"items": { "$ref": "issue.json" }
diff --git a/spec/fixtures/api/schemas/evidences/project.json b/spec/fixtures/api/schemas/evidences/project.json
index 542686542f8..3a094bd276f 100644
--- a/spec/fixtures/api/schemas/evidences/project.json
+++ b/spec/fixtures/api/schemas/evidences/project.json
@@ -9,7 +9,7 @@
"properties": {
"id": { "type": "integer" },
"name": { "type": "string" },
- "description": { "type": "string" },
+ "description": { "type": ["string", "null"] },
"created_at": { "type": "date" }
},
"additionalProperties": false
diff --git a/spec/fixtures/api/schemas/evidences/release.json b/spec/fixtures/api/schemas/evidences/release.json
index 68c872a9dc8..37eb9a9b5c0 100644
--- a/spec/fixtures/api/schemas/evidences/release.json
+++ b/spec/fixtures/api/schemas/evidences/release.json
@@ -2,7 +2,7 @@
"type": "object",
"required": [
"id",
- "tag",
+ "tag_name",
"name",
"description",
"created_at",
@@ -11,8 +11,8 @@
],
"properties": {
"id": { "type": "integer" },
- "tag": { "type": "string" },
- "name": { "type": "string" },
+ "tag_name": { "type": "string" },
+ "name": { "type": ["string", "null"] },
"description": { "type": "string" },
"created_at": { "type": "date" },
"project": { "$ref": "project.json" },
diff --git a/spec/fixtures/api/schemas/job/build_trace.json b/spec/fixtures/api/schemas/job/build_trace.json
new file mode 100644
index 00000000000..becd881ea57
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace.json
@@ -0,0 +1,31 @@
+{
+ "description": "Build trace",
+ "type": "object",
+ "required": [
+ "id",
+ "status",
+ "complete",
+ "state",
+ "append",
+ "truncated",
+ "offset",
+ "size",
+ "total"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "status": { "type": "string" },
+ "complete": { "type": "boolean" },
+ "state": { "type": ["string", "null"] },
+ "append": { "type": ["boolean", "null"] },
+ "truncated": { "type": ["boolean", "null"] },
+ "offset": { "type": ["integer", "null"] },
+ "size": { "type": ["integer", "null"] },
+ "total": { "type": ["integer", "null"] },
+ "html": { "type": ["string", "null"] },
+ "lines": {
+ "type": ["array", "null"],
+ "items": { "$ref": "./build_trace_line.json" }
+ }
+ }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line.json b/spec/fixtures/api/schemas/job/build_trace_line.json
new file mode 100644
index 00000000000..18726dff2bb
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line.json
@@ -0,0 +1,18 @@
+{
+ "description": "Build trace line",
+ "type": "object",
+ "required": [
+ "offset",
+ "content"
+ ],
+ "properties": {
+ "offset": { "type": "integer" },
+ "content": {
+ "type": "array",
+ "items": { "$ref": "./build_trace_line_content.json" }
+ },
+ "section": "string",
+ "section_header": "boolean",
+ "section_duration": "string"
+ }
+}
diff --git a/spec/fixtures/api/schemas/job/build_trace_line_content.json b/spec/fixtures/api/schemas/job/build_trace_line_content.json
new file mode 100644
index 00000000000..41f8124c113
--- /dev/null
+++ b/spec/fixtures/api/schemas/job/build_trace_line_content.json
@@ -0,0 +1,11 @@
+{
+ "description": "Build trace line content",
+ "type": "object",
+ "required": [
+ "text"
+ ],
+ "properties": {
+ "text": { "type": "string" },
+ "style": { "type": "string" }
+ }
+}
diff --git a/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
new file mode 100644
index 00000000000..897b8f4e9ef
--- /dev/null
+++ b/spec/graphql/mutations/concerns/mutations/resolves_group_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Mutations::ResolvesGroup do
+ let(:mutation_class) do
+ Class.new(Mutations::BaseMutation) do
+ include Mutations::ResolvesGroup
+ end
+ end
+
+ let(:context) { double }
+ subject(:mutation) { mutation_class.new(object: nil, context: context) }
+
+ it 'uses the GroupsResolver to resolve groups by path' do
+ group = create(:group)
+
+ expect(Resolvers::GroupResolver).to receive(:new).with(object: nil, context: context).and_call_original
+ expect(mutation.resolve_group(full_path: group.full_path).sync).to eq(group)
+ end
+end
diff --git a/spec/graphql/resolvers/todo_resolver_spec.rb b/spec/graphql/resolvers/todo_resolver_spec.rb
new file mode 100644
index 00000000000..fef761d7243
--- /dev/null
+++ b/spec/graphql/resolvers/todo_resolver_spec.rb
@@ -0,0 +1,113 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::TodoResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:author1) { create(:user) }
+ let_it_be(:author2) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, user: user, target_type: 'MergeRequest', state: :pending, action: Todo::MENTIONED, author: author1) }
+ let_it_be(:todo2) { create(:todo, user: user, state: :done, action: Todo::ASSIGNED, author: author2) }
+ let_it_be(:todo3) { create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author1) }
+
+ it 'calls TodosFinder' do
+ expect_next_instance_of(TodosFinder) do |finder|
+ expect(finder).to receive(:execute)
+ end
+
+ resolve_todos
+ end
+
+ context 'when using no filter' do
+ it 'returns expected todos' do
+ todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo1, todo3)
+ end
+ end
+
+ context 'when using filters' do
+ # TODO These can be removed as soon as we support filtering for multiple field contents for todos
+
+ it 'just uses the first state' do
+ todos = resolve(described_class, obj: user, args: { state: [:done, :pending] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo2)
+ end
+
+ it 'just uses the first action' do
+ todos = resolve(described_class, obj: user, args: { action: [Todo::MENTIONED, Todo::ASSIGNED] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo1)
+ end
+
+ it 'just uses the first author id' do
+ # We need a pending todo for now because of TodosFinder's state query
+ todo4 = create(:todo, user: user, state: :pending, action: Todo::ASSIGNED, author: author2)
+
+ todos = resolve(described_class, obj: user, args: { author_id: [author2.id, author1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo4)
+ end
+
+ it 'just uses the first project id' do
+ project1 = create(:project)
+ project2 = create(:project)
+
+ create(:todo, project: project1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+ todo5 = create(:todo, project: project2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+ todos = resolve(described_class, obj: user, args: { project_id: [project2.id, project1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo5)
+ end
+
+ it 'just uses the first group id' do
+ group1 = create(:group)
+ group2 = create(:group)
+
+ group1.add_developer(user)
+ group2.add_developer(user)
+
+ create(:todo, group: group1, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+ todo5 = create(:todo, group: group2, user: user, state: :pending, action: Todo::ASSIGNED, author: author1)
+
+ todos = resolve(described_class, obj: user, args: { group_id: [group2.id, group1.id] }, ctx: { current_user: user })
+
+ expect(todos).to contain_exactly(todo5)
+ end
+
+ it 'just uses the first target' do
+ todos = resolve(described_class, obj: user, args: { type: %w[Issue MergeRequest] }, ctx: { current_user: user })
+
+ # Just todo3 because todo2 is in state "done"
+ expect(todos).to contain_exactly(todo3)
+ end
+ end
+
+ context 'when no user is provided' do
+ it 'returns no todos' do
+ todos = resolve(described_class, obj: nil, args: {}, ctx: { current_user: current_user })
+
+ expect(todos).to be_empty
+ end
+ end
+
+ context 'when provided user is not current user' do
+ it 'returns no todos' do
+ todos = resolve(described_class, obj: user, args: {}, ctx: { current_user: current_user })
+
+ expect(todos).to be_empty
+ end
+ end
+ end
+
+ def resolve_todos(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: current_user, args: args, ctx: context)
+ end
+end
diff --git a/spec/graphql/types/query_type_spec.rb b/spec/graphql/types/query_type_spec.rb
index 784a4f4b4c9..1365bc0dc14 100644
--- a/spec/graphql/types/query_type_spec.rb
+++ b/spec/graphql/types/query_type_spec.rb
@@ -7,7 +7,7 @@ describe GitlabSchema.types['Query'] do
expect(described_class.graphql_name).to eq('Query')
end
- it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata) }
+ it { is_expected.to have_graphql_fields(:project, :namespace, :group, :echo, :metadata, :current_user) }
describe 'namespace field' do
subject { described_class.fields['namespace'] }
diff --git a/spec/graphql/types/todo_type_spec.rb b/spec/graphql/types/todo_type_spec.rb
new file mode 100644
index 00000000000..a5ea5bcffb0
--- /dev/null
+++ b/spec/graphql/types/todo_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['Todo'] do
+ it 'has the correct fields' do
+ expected_fields = [:id, :project, :group, :author, :action, :target_type, :body, :state, :created_at]
+
+ is_expected.to have_graphql_fields(*expected_fields)
+ end
+
+ it { expect(described_class).to require_graphql_authorizations(:read_todo) }
+end
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index dd5f2f97ac9..1baea13299b 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -248,60 +248,6 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
end
end
- describe '#html_with_state' do
- shared_examples_for 'html_with_states' do
- it 'returns html content with state' do
- result = stream.html_with_state
-
- expect(result.html).to eq("<span>1234</span>")
- end
-
- context 'follow-up state' do
- let!(:last_result) { stream.html_with_state }
-
- before do
- data_stream.seek(4, IO::SEEK_SET)
- data_stream.write("5678")
- stream.seek(0)
- end
-
- it "returns appended trace" do
- result = stream.html_with_state(last_result.state)
-
- expect(result.append).to be_truthy
- expect(result.html).to eq("<span>5678</span>")
- end
- end
- end
-
- context 'when stream is StringIO' do
- let(:data_stream) do
- StringIO.new("1234")
- end
-
- let(:stream) do
- described_class.new { data_stream }
- end
-
- it_behaves_like 'html_with_states'
- end
-
- context 'when stream is ChunkedIO' do
- let(:data_stream) do
- Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
- chunked_io.write("1234")
- chunked_io.seek(0, IO::SEEK_SET)
- end
- end
-
- let(:stream) do
- described_class.new { data_stream }
- end
-
- it_behaves_like 'html_with_states'
- end
- end
-
describe '#html' do
shared_examples_for 'htmls' do
it "returns html" do
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index ba6abba4e61..71489adb373 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -252,31 +252,6 @@ describe Gitlab::GitalyClient::CommitService do
end
end
- describe '#patch' do
- let(:request) do
- Gitaly::CommitPatchRequest.new(
- repository: repository_message, revision: revision
- )
- end
- let(:response) { [double(data: "my "), double(data: "diff")] }
-
- subject { described_class.new(repository).patch(revision) }
-
- it 'sends an RPC request' do
- expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
- .with(request, kind_of(Hash)).and_return([])
-
- subject
- end
-
- it 'concatenates the responses data' do
- allow_any_instance_of(Gitaly::DiffService::Stub).to receive(:commit_patch)
- .with(request, kind_of(Hash)).and_return(response)
-
- expect(subject).to eq("my diff")
- end
- end
-
describe '#commit_stats' do
let(:request) do
Gitaly::CommitStatsRequest.new(
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 187a8a37179..1efd7bf5c71 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -27,6 +27,7 @@ issues:
- design_versions
- prometheus_alerts
- prometheus_alert_events
+- self_managed_prometheus_alert_events
events:
- author
- project
@@ -81,6 +82,7 @@ releases:
- links
- milestone_releases
- milestones
+- evidence
links:
- release
project_members:
@@ -400,6 +402,7 @@ project:
- operations_feature_flags_client
- prometheus_alerts
- prometheus_alert_events
+- self_managed_prometheus_alert_events
- software_license_policies
- project_registry
- packages
@@ -473,6 +476,8 @@ prometheus_alerts:
- prometheus_alert_events
prometheus_alert_events:
- project
+self_managed_prometheus_alert_events:
+- project
epic_issues:
- issue
- epic
@@ -506,6 +511,8 @@ lists:
milestone_releases:
- milestone
- release
+evidences:
+- release
design: &design
- issue
- actions
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index ebc5d9d1f56..8ae571a69ef 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -127,6 +127,12 @@ Release:
- created_at
- updated_at
- released_at
+Evidence:
+- id
+- release_id
+- summary
+- created_at
+- updated_at
Releases::Link:
- id
- release_id
diff --git a/spec/models/ci/build_trace_spec.rb b/spec/models/ci/build_trace_spec.rb
new file mode 100644
index 00000000000..2471a6fa827
--- /dev/null
+++ b/spec/models/ci/build_trace_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Ci::BuildTrace do
+ let(:build) { build_stubbed(:ci_build) }
+ let(:state) { nil }
+ let(:data) { StringIO.new('the-stream') }
+
+ let(:stream) do
+ Gitlab::Ci::Trace::Stream.new { data }
+ end
+
+ subject { described_class.new(build: build, stream: stream, state: state, content_format: content_format) }
+
+ shared_examples 'delegates methods' do
+ it { is_expected.to delegate_method(:state).to(:trace) }
+ it { is_expected.to delegate_method(:append).to(:trace) }
+ it { is_expected.to delegate_method(:truncated).to(:trace) }
+ it { is_expected.to delegate_method(:offset).to(:trace) }
+ it { is_expected.to delegate_method(:size).to(:trace) }
+ it { is_expected.to delegate_method(:total).to(:trace) }
+ it { is_expected.to delegate_method(:id).to(:build).with_prefix }
+ it { is_expected.to delegate_method(:status).to(:build).with_prefix }
+ it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
+ end
+
+ context 'with :json content format' do
+ let(:content_format) { :json }
+
+ it_behaves_like 'delegates methods'
+
+ it { is_expected.to be_json }
+
+ it 'returns formatted trace' do
+ expect(subject.trace.lines).to eq([
+ { offset: 0, content: [{ text: 'the-stream' }] }
+ ])
+ end
+ end
+
+ context 'with :html content format' do
+ let(:content_format) { :html }
+
+ it_behaves_like 'delegates methods'
+
+ it { is_expected.to be_html }
+
+ it 'returns formatted trace' do
+ expect(subject.trace.html).to eq('<span>the-stream</span>')
+ end
+ end
+end
diff --git a/spec/models/evidence_spec.rb b/spec/models/evidence_spec.rb
new file mode 100644
index 00000000000..00788c2c391
--- /dev/null
+++ b/spec/models/evidence_spec.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidence do
+ let_it_be(:project) { create(:project) }
+ let(:release) { create(:release, project: project) }
+ let(:schema_file) { 'evidences/evidence' }
+ let(:summary_json) { described_class.last.summary.to_json }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:release) }
+ end
+
+ describe 'summary_sha' do
+ it 'returns nil if summary is nil' do
+ expect(build(:evidence, summary: nil).summary_sha).to be_nil
+ end
+ end
+
+ describe '#generate_summary_and_sha' do
+ before do
+ described_class.create!(release: release)
+ end
+
+ context 'when a release name is not provided' do
+ let(:release) { create(:release, project: project, name: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(release.name).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a release is associated to a milestone' do
+ let(:milestone) { create(:milestone, project: project) }
+ let(:release) { create(:release, project: project, milestones: [milestone]) }
+
+ context 'when a milestone has no issue associated with it' do
+ it 'creates a valid JSON object' do
+ expect(milestone.issues).to be_empty
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has no description' do
+ let(:milestone) { create(:milestone, project: project, description: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(milestone.description).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has no due_date' do
+ let(:milestone) { create(:milestone, project: project, due_date: nil) }
+
+ it 'creates a valid JSON object' do
+ expect(milestone.due_date).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+
+ context 'when a milestone has an issue' do
+ context 'when the issue has no description' do
+ let(:issue) { create(:issue, project: project, description: nil, state: 'closed') }
+
+ before do
+ milestone.issues << issue
+ end
+
+ it 'creates a valid JSON object' do
+ expect(milestone.issues.first.description).to be_nil
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+ end
+ end
+
+ context 'when a release is not associated to any milestone' do
+ it 'creates a valid JSON object' do
+ expect(release.milestones).to be_empty
+ expect(summary_json).to match_schema(schema_file)
+ end
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 892c31a9204..520421ac5e3 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -1042,4 +1042,21 @@ describe Group do
expect(group.access_request_approvers_to_be_notified).to eq(active_owners_in_recent_sign_in_desc_order)
end
end
+
+ describe '.groups_including_descendants_by' do
+ it 'returns the expected groups for a group and its descendants' do
+ parent_group1 = create(:group)
+ child_group1 = create(:group, parent: parent_group1)
+ child_group2 = create(:group, parent: parent_group1)
+
+ parent_group2 = create(:group)
+ child_group3 = create(:group, parent: parent_group2)
+
+ create(:group)
+
+ groups = described_class.groups_including_descendants_by([parent_group2.id, parent_group1.id])
+
+ expect(groups).to contain_exactly(parent_group1, parent_group2, child_group1, child_group2, child_group3)
+ end
+ end
end
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index fe08dc4f5e6..025c11d6407 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -6,7 +6,7 @@ describe WebHook do
let(:hook) { build(:project_hook) }
describe 'associations' do
- it { is_expected.to have_many(:web_hook_logs).dependent(:destroy) }
+ it { is_expected.to have_many(:web_hook_logs) }
end
describe 'validations' do
@@ -85,4 +85,13 @@ describe WebHook do
hook.async_execute(data, hook_name)
end
end
+
+ describe '#destroy' do
+ it 'cascades to web_hook_logs' do
+ web_hook = create(:project_hook)
+ create_list(:web_hook_log, 3, web_hook: web_hook)
+
+ expect { web_hook.destroy }.to change(web_hook.web_hook_logs, :count).by(-3)
+ end
+ end
end
diff --git a/spec/models/release_spec.rb b/spec/models/release_spec.rb
index e7a8d27a036..64799421eb6 100644
--- a/spec/models/release_spec.rb
+++ b/spec/models/release_spec.rb
@@ -15,11 +15,13 @@ RSpec.describe Release do
it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) }
+ it { is_expected.to have_one(:evidence) }
end
describe 'validation' do
it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) }
+ it { is_expected.to validate_presence_of(:tag) }
context 'when a release exists in the database without a name' do
it 'does not require name' do
@@ -89,4 +91,22 @@ RSpec.describe Release do
end
end
end
+
+ describe 'evidence' do
+ describe '#create_evidence!' do
+ context 'when a release is created' do
+ it 'creates one Evidence object too' do
+ expect { release }.to change(Evidence, :count).by(1)
+ end
+ end
+ end
+
+ context 'when a release is deleted' do
+ it 'also deletes the associated evidence' do
+ release = create(:release)
+
+ expect { release.destroy }.to change(Evidence, :count).by(-1)
+ end
+ end
+ end
end
diff --git a/spec/models/todo_spec.rb b/spec/models/todo_spec.rb
index c2566ccd047..487a1c619c6 100644
--- a/spec/models/todo_spec.rb
+++ b/spec/models/todo_spec.rb
@@ -253,14 +253,14 @@ describe Todo do
end
end
- describe '.for_group_and_descendants' do
+ describe '.for_group_ids_and_descendants' do
it 'returns the todos for a group and its descendants' do
parent_group = create(:group)
child_group = create(:group, parent: parent_group)
todo1 = create(:todo, group: parent_group)
todo2 = create(:todo, group: child_group)
- todos = described_class.for_group_and_descendants(parent_group)
+ todos = described_class.for_group_ids_and_descendants([parent_group.id])
expect(todos).to contain_exactly(todo1, todo2)
end
diff --git a/spec/policies/todo_policy_spec.rb b/spec/policies/todo_policy_spec.rb
new file mode 100644
index 00000000000..be6fecd1045
--- /dev/null
+++ b/spec/policies/todo_policy_spec.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe TodoPolicy do
+ let_it_be(:author) { create(:user) }
+
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+
+ let_it_be(:todo1) { create(:todo, author: author, user: user1) }
+ let_it_be(:todo2) { create(:todo, author: author, user: user2) }
+ let_it_be(:todo3) { create(:todo, author: author, user: user2) }
+ let_it_be(:todo4) { create(:todo, author: author, user: user3) }
+
+ def permissions(user, todo)
+ described_class.new(user, todo)
+ end
+
+ describe 'own_todo' do
+ it 'allows owners to access their own todos' do
+ [
+ [user1, todo1],
+ [user2, todo2],
+ [user2, todo3],
+ [user3, todo4]
+ ].each do |user, todo|
+ expect(permissions(user, todo)).to be_allowed(:read_todo)
+ end
+ end
+
+ it 'does not allow users to access todos of other users' do
+ [
+ [user1, todo2],
+ [user1, todo3],
+ [user2, todo1],
+ [user2, todo4],
+ [user3, todo1],
+ [user3, todo2],
+ [user3, todo3]
+ ].each do |user, todo|
+ expect(permissions(user, todo)).to be_disallowed(:read_todo)
+ end
+ end
+ end
+end
diff --git a/spec/serializers/build_trace_entity_spec.rb b/spec/serializers/build_trace_entity_spec.rb
new file mode 100644
index 00000000000..bafead04a51
--- /dev/null
+++ b/spec/serializers/build_trace_entity_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe BuildTraceEntity do
+ let(:build) { build_stubbed(:ci_build) }
+ let(:request) { double('request') }
+
+ let(:stream) do
+ Gitlab::Ci::Trace::Stream.new do
+ StringIO.new('the-trace')
+ end
+ end
+
+ let(:build_trace) do
+ Ci::BuildTrace.new(build: build, stream: stream, content_format: content_format, state: nil)
+ end
+
+ let(:entity) do
+ described_class.new(build_trace, request: request)
+ end
+
+ subject { entity.as_json }
+
+ shared_examples 'includes build and trace metadata' do
+ it 'includes build attributes' do
+ expect(subject[:id]).to eq(build.id)
+ expect(subject[:status]).to eq(build.status)
+ expect(subject[:complete]).to eq(build.complete?)
+ end
+
+ it 'includes trace metadata' do
+ expect(subject).to include(:state)
+ expect(subject).to include(:append)
+ expect(subject).to include(:truncated)
+ expect(subject).to include(:offset)
+ expect(subject).to include(:size)
+ expect(subject).to include(:total)
+ end
+ end
+
+ context 'when content format is :json' do
+ let(:content_format) { :json }
+
+ it_behaves_like 'includes build and trace metadata'
+
+ it 'includes the trace content in json' do
+ expect(subject[:lines]).to eq([
+ { offset: 0, content: [{ text: 'the-trace' }] }
+ ])
+ end
+ end
+
+ context 'when content format is :html' do
+ let(:content_format) { :html }
+
+ it_behaves_like 'includes build and trace metadata'
+
+ it 'includes the trace content in json' do
+ expect(subject[:html]).to eq('<span>the-trace</span>')
+ end
+ end
+end
diff --git a/spec/serializers/evidences/author_entity_spec.rb b/spec/serializers/evidences/author_entity_spec.rb
deleted file mode 100644
index 1d0fa95217c..00000000000
--- a/spec/serializers/evidences/author_entity_spec.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Evidences::AuthorEntity do
- let(:entity) { described_class.new(build(:author)) }
-
- subject { entity.as_json }
-
- it 'exposes the expected fields' do
- expect(subject.keys).to contain_exactly(:id, :name, :email)
- end
-end
diff --git a/spec/serializers/evidences/evidence_entity_spec.rb b/spec/serializers/evidences/evidence_entity_spec.rb
new file mode 100644
index 00000000000..531708e3be6
--- /dev/null
+++ b/spec/serializers/evidences/evidence_entity_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceEntity do
+ let(:evidence) { build(:evidence) }
+ let(:entity) { described_class.new(evidence) }
+
+ subject { entity.as_json }
+
+ it 'exposes the expected fields' do
+ expect(subject.keys).to contain_exactly(:release)
+ end
+end
diff --git a/spec/serializers/evidences/evidence_serializer_spec.rb b/spec/serializers/evidences/evidence_serializer_spec.rb
new file mode 100644
index 00000000000..5322f6a43fc
--- /dev/null
+++ b/spec/serializers/evidences/evidence_serializer_spec.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Evidences::EvidenceSerializer do
+ it 'represents an EvidenceEntity entity' do
+ expect(described_class.entity_class).to eq(Evidences::EvidenceEntity)
+ end
+end
diff --git a/spec/serializers/evidences/issue_entity_spec.rb b/spec/serializers/evidences/issue_entity_spec.rb
index a1402808757..915df986887 100644
--- a/spec/serializers/evidences/issue_entity_spec.rb
+++ b/spec/serializers/evidences/issue_entity_spec.rb
@@ -8,6 +8,6 @@ describe Evidences::IssueEntity do
subject { entity.as_json }
it 'exposes the expected fields' do
- expect(subject.keys).to contain_exactly(:id, :title, :description, :author, :state, :iid, :confidential, :created_at, :due_date)
+ expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :confidential, :created_at, :due_date)
end
end
diff --git a/spec/serializers/evidences/milestone_entity_spec.rb b/spec/serializers/evidences/milestone_entity_spec.rb
index 082e178618e..68eb12093da 100644
--- a/spec/serializers/evidences/milestone_entity_spec.rb
+++ b/spec/serializers/evidences/milestone_entity_spec.rb
@@ -12,7 +12,7 @@ describe Evidences::MilestoneEntity do
expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues)
end
- context 'when there issues linked to this milestone' do
+ context 'when there are issues linked to this milestone' do
let(:issue_1) { build(:issue) }
let(:issue_2) { build(:issue) }
let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) }
diff --git a/spec/support/shared_examples/evidence_updated_exposed_fields.rb b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
new file mode 100644
index 00000000000..2a02fdd7666
--- /dev/null
+++ b/spec/support/shared_examples/evidence_updated_exposed_fields.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+shared_examples 'updated exposed field' do
+ it 'creates another Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).to include(updated_field)
+ expect { model.save! }.to change(Evidence, :count).by(1)
+ expect(updated_json_field).to eq(updated_value)
+ end
+end
+
+shared_examples 'updated non-exposed field' do
+ it 'does not create any Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).not_to include(updated_field)
+ expect { model.save! }.not_to change(Evidence, :count)
+ end
+end
+
+shared_examples 'updated field on non-linked entity' do
+ it 'does not create any Evidence object' do
+ model.send("#{updated_field}=", updated_value)
+
+ expect(model.evidence_summary_keys).to be_empty
+ expect { model.save! }.not_to change(Evidence, :count)
+ end
+end
diff --git a/spec/workers/create_evidence_worker_spec.rb b/spec/workers/create_evidence_worker_spec.rb
new file mode 100644
index 00000000000..364b2098251
--- /dev/null
+++ b/spec/workers/create_evidence_worker_spec.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe CreateEvidenceWorker do
+ let!(:release) { create(:release) }
+
+ it 'creates a new Evidence' do
+ expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1)
+ end
+end