diff options
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 |