Commit 7ce7df63 authored by Sean Carroll's avatar Sean Carroll Committed by Shinya Maeda
parent c747e6ad
<script> <script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui'; import dateFormat from 'dateformat';
import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, sprintf } from '~/locale';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue'; import { getTimeago } from '~/lib/utils/datetime_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ExpandButton from '~/vue_shared/components/expand_button.vue'; import ExpandButton from '~/vue_shared/components/expand_button.vue';
...@@ -12,7 +13,7 @@ export default { ...@@ -12,7 +13,7 @@ export default {
ClipboardButton, ClipboardButton,
ExpandButton, ExpandButton,
GlLink, GlLink,
Icon, GlIcon,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -24,17 +25,33 @@ export default { ...@@ -24,17 +25,33 @@ export default {
}, },
}, },
computed: { computed: {
evidenceTitle() { evidences() {
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName }); return this.release.evidences;
}, },
evidenceUrl() {
return this.release.assets && this.release.assets.evidenceFilePath;
}, },
shortSha() { methods: {
return truncateSha(this.sha); evidenceTitle(index) {
const [tag, evidence, filename] = this.release.evidences[index].filepath.split('/').slice(-3);
return sprintf(__('%{tag}-%{evidence}-%{filename}'), { tag, evidence, filename });
}, },
sha() { evidenceUrl(index) {
return this.release.evidenceSha; return this.release.evidences[index].filepath;
},
sha(index) {
return this.release.evidences[index].sha;
},
shortSha(index) {
return truncateSha(this.release.evidences[index].sha);
},
collectedAt(index) {
return dateFormat(this.release.evidences[index].collectedAt, 'mmmm dS, yyyy, h:MM TT');
},
timeSummary(index) {
const { format } = getTimeago();
const summary = sprintf(__(' Collected %{time}'), {
time: format(this.release.evidences[index].collectedAt),
});
return summary;
}, },
}, },
}; };
...@@ -43,34 +60,45 @@ export default { ...@@ -43,34 +60,45 @@ export default {
<template> <template>
<div> <div>
<div class="card-text prepend-top-default"> <div class="card-text prepend-top-default">
<b> <b>{{ __('Evidence collection') }}</b>
{{ __('Evidence collection') }}
</b>
</div> </div>
<div class="d-flex align-items-baseline"> <div v-for="(evidence, index) in evidences" :key="evidenceTitle(index)" class="mb-2">
<div class="d-flex align-items-center">
<gl-link <gl-link
v-gl-tooltip v-gl-tooltip
class="monospace" class="d-flex align-items-center monospace"
:title="__('Download evidence JSON')" :title="__('Download evidence JSON')"
:download="evidenceTitle" :download="evidenceTitle(index)"
:href="evidenceUrl" :href="evidenceUrl(index)"
> >
<icon name="review-list" class="align-top append-right-4" /><span>{{ evidenceTitle }}</span> <gl-icon name="review-list" class="align-middle append-right-8" />
<span>{{ evidenceTitle(index) }}</span>
</gl-link> </gl-link>
<expand-button> <expand-button>
<template slot="short"> <template slot="short">
<span class="js-short monospace">{{ shortSha }}</span> <span class="js-short monospace">{{ shortSha(index) }}</span>
</template> </template>
<template slot="expanded"> <template slot="expanded">
<span class="js-expanded monospace gl-pl-1">{{ sha }}</span> <span class="js-expanded monospace gl-pl-1">{{ sha(index) }}</span>
</template> </template>
</expand-button> </expand-button>
<clipboard-button <clipboard-button
:title="__('Copy evidence SHA')" :title="__('Copy evidence SHA')"
:text="sha" :text="sha(index)"
css-class="btn-default btn-transparent btn-clipboard" css-class="btn-default btn-transparent btn-clipboard"
/> />
</div> </div>
<div class="d-flex align-items-center text-muted">
<gl-icon
v-gl-tooltip
name="clock"
class="align-middle append-right-8"
:title="collectedAt(index)"
/>
<span>{{ timeSummary(index) }}</span>
</div>
</div>
</div> </div>
</template> </template>
...@@ -44,7 +44,7 @@ export default { ...@@ -44,7 +44,7 @@ export default {
return this.release.assets || {}; return this.release.assets || {};
}, },
hasEvidence() { hasEvidence() {
return Boolean(this.release.evidenceSha); return Boolean(this.release.evidences && this.release.evidences.length);
}, },
milestones() { milestones() {
return this.release.milestones || []; return this.release.milestones || [];
......
# frozen_string_literal: true
module Projects
module Releases
class EvidencesController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :release
before_action :authorize_read_release_evidence!
def show
respond_to do |format|
format.json do
render json: evidence.summary
end
end
end
private
def authorize_read_release_evidence!
access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true)
access_denied! unless can?(current_user, :read_release_evidence, evidence)
end
def release
@release ||= project.releases.find_by_tag!(sanitized_tag_name)
end
def evidence
release.evidences.find(params[:id])
end
def sanitized_tag_name
CGI.unescape(params[:tag])
end
end
end
end
...@@ -11,7 +11,6 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -11,7 +11,6 @@ class Projects::ReleasesController < Projects::ApplicationController
push_frontend_feature_flag(:release_show_page, project, default_enabled: true) push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
end end
before_action :authorize_update_release!, only: %i[edit update] before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_read_release_evidence!, only: [:evidence]
def index def index
respond_to do |format| respond_to do |format|
...@@ -22,14 +21,6 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -22,14 +21,6 @@ class Projects::ReleasesController < Projects::ApplicationController
end end
end end
def evidence
respond_to do |format|
format.json do
render json: release.evidence_summary
end
end
end
def show def show
return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true) return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true)
...@@ -64,11 +55,6 @@ class Projects::ReleasesController < Projects::ApplicationController ...@@ -64,11 +55,6 @@ class Projects::ReleasesController < Projects::ApplicationController
access_denied! unless can?(current_user, :update_release, release) access_denied! unless can?(current_user, :update_release, release)
end end
def authorize_read_release_evidence!
access_denied! unless Feature.enabled?(:release_evidence, project, default_enabled: true)
access_denied! unless can?(current_user, :read_release_evidence, release)
end
def release def release
@release ||= project.releases.find_by_tag!(sanitized_tag_name) @release ||= project.releases.find_by_tag!(sanitized_tag_name)
end end
......
...@@ -16,7 +16,7 @@ class Release < ApplicationRecord ...@@ -16,7 +16,7 @@ class Release < ApplicationRecord
has_many :milestone_releases has_many :milestone_releases
has_many :milestones, through: :milestone_releases has_many :milestones, through: :milestone_releases
has_one :evidence has_many :evidences, inverse_of: :release, class_name: 'Releases::Evidence'
default_value_for :released_at, allows_nil: false do default_value_for :released_at, allows_nil: false do
Time.zone.now Time.zone.now
...@@ -28,7 +28,7 @@ class Release < ApplicationRecord ...@@ -28,7 +28,7 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") } validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
scope :sorted, -> { order(released_at: :desc) } scope :sorted, -> { order(released_at: :desc) }
scope :preloaded, -> { includes(project: :namespace) } scope :preloaded, -> { includes(:evidences, :milestones, project: [:project_feature, :route, { namespace: :route }]) }
scope :with_project_and_namespace, -> { includes(project: :namespace) } scope :with_project_and_namespace, -> { includes(project: :namespace) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) } scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
...@@ -66,27 +66,27 @@ class Release < ApplicationRecord ...@@ -66,27 +66,27 @@ class Release < ApplicationRecord
end end
def upcoming_release? def upcoming_release?
released_at.present? && released_at > Time.zone.now released_at.present? && released_at.to_i > Time.zone.now.to_i
end end
def historical_release? def historical_release?
released_at.present? && released_at < created_at released_at.present? && released_at.to_i < created_at.to_i
end end
def name def name
self.read_attribute(:name) || tag self.read_attribute(:name) || tag
end end
def evidence_sha def milestone_titles
evidence&.summary_sha self.milestones.map {|m| m.title }.sort.join(", ")
end end
def evidence_summary def evidence_sha
evidence&.summary || {} evidences.first&.summary_sha
end end
def milestone_titles def evidence_summary
self.milestones.map {|m| m.title }.sort.join(", ") evidences.first&.summary || {}
end end
private private
......
# frozen_string_literal: true # frozen_string_literal: true
class Evidence < ApplicationRecord class Releases::Evidence < ApplicationRecord
include ShaAttribute include ShaAttribute
include Presentable
belongs_to :release belongs_to :release, inverse_of: :evidences
before_validation :generate_summary_and_sha before_validation :generate_summary_and_sha
default_scope { order(created_at: :asc) } default_scope { order(created_at: :asc) }
sha_attribute :summary_sha sha_attribute :summary_sha
alias_attribute :collected_at, :created_at
def milestones def milestones
@milestones ||= release.milestones.includes(:issues) @milestones ||= release.milestones.includes(:issues)
......
...@@ -2,31 +2,4 @@ ...@@ -2,31 +2,4 @@
class ReleasePolicy < BasePolicy class ReleasePolicy < BasePolicy
delegate { @subject.project } delegate { @subject.project }
rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do
enable :read_release_evidence
end
##
# evidence.summary includes the following entities:
# - Release
# - git-tag (Repository)
# - Project
# - Milestones
# - Issues
condition(:allowed_to_read_evidence) do
can?(:read_release) &&
can?(:download_code) &&
can?(:read_project) &&
can?(:read_milestone) &&
can?(:read_issue)
end
##
# Currently, we don't support release evidence for the GitLab instances
# that enables external authorization services.
# See https://gitlab.com/gitlab-org/gitlab/issues/121930.
condition(:external_authorization_service_disabled) do
!Gitlab::ExternalAuthorization::Config.enabled?
end
end end
# frozen_string_literal: true
module Releases
class EvidencePolicy < BasePolicy
delegate { @subject.release.project }
rule { allowed_to_read_evidence & external_authorization_service_disabled }.policy do
enable :read_release_evidence
end
##
# evidence.summary includes the following entities:
# - Release
# - git-tag (Repository)
# - Project
# - Milestones
# - Issues
condition(:allowed_to_read_evidence) do
can?(:read_release) &&
can?(:download_code) &&
can?(:read_project) &&
can?(:read_milestone) &&
can?(:read_issue)
end
##
# Currently, we don't support release evidence for the GitLab instances
# that enables external authorization services.
# See https://gitlab.com/gitlab-org/gitlab/issues/121930.
condition(:external_authorization_service_disabled) do
!Gitlab::ExternalAuthorization::Config.enabled?
end
end
end
...@@ -44,9 +44,10 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated ...@@ -44,9 +44,10 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end end
def evidence_file_path def evidence_file_path
return unless release.evidence.present? evidence = release.evidences.first
return unless evidence
evidence_project_release_url(project, release.to_param, format: :json) project_evidence_url(project, release, evidence, format: :json)
end end
private private
......
# frozen_string_literal: true
module Releases
class EvidencePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
presents :evidence
def filepath
release = evidence.release
project = release.project
project_evidence_url(project, release, evidence, format: :json)
end
end
end
...@@ -10,6 +10,6 @@ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker ...@@ -10,6 +10,6 @@ class CreateEvidenceWorker # rubocop:disable Scalability/IdempotentWorker
release = Release.find_by_id(release_id) release = Release.find_by_id(release_id)
return unless release return unless release
Evidence.create!(release: release) Releases::Evidence.create!(release: release)
end end
end end
---
title: Support multiple Evidences for a Release
merge_request: 26509
author:
type: changed
...@@ -170,8 +170,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do ...@@ -170,8 +170,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :releases, only: [:index, :show, :edit], param: :tag, constraints: { tag: %r{[^/]+} } do resources :releases, only: [:index, :show, :edit], param: :tag, constraints: { tag: %r{[^/]+} } do
member do member do
get :evidence
get :downloads, path: 'downloads/*filepath', format: false get :downloads, path: 'downloads/*filepath', format: false
scope module: :releases do
resources :evidences, only: [:show]
end
end end
end end
......
...@@ -22,6 +22,7 @@ module API ...@@ -22,6 +22,7 @@ module API
expose :commit_path, expose_nil: false expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false expose :tag_path, expose_nil: false
expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? } expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :assets do expose :assets do
expose :assets_count, as: :count do |release, _| expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources] assets_to_exclude = can_download_code? ? [] : [:sources]
...@@ -33,6 +34,7 @@ module API ...@@ -33,6 +34,7 @@ module API
end end
expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? } expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end end
expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do expose :_links do
expose :self_url, as: :self, expose_nil: false expose :self_url, as: :self, expose_nil: false
expose :merge_requests_url, expose_nil: false expose :merge_requests_url, expose_nil: false
......
# frozen_string_literal: true
module API
module Entities
module Releases
class Evidence < Grape::Entity
include ::API::Helpers::Presentable
expose :summary_sha, as: :sha
expose :filepath
expose :collected_at
end
end
end
end
...@@ -4,7 +4,7 @@ module API ...@@ -4,7 +4,7 @@ module API
module Helpers module Helpers
## ##
# This module makes it possible to use `app/presenters` with # This module makes it possible to use `app/presenters` with
# Grape Entities. It instantiates model presenter and passes # Grape Entities. It instantiates the model presenter and passes
# options defined in the API endpoint to the presenter itself. # options defined in the API endpoint to the presenter itself.
# #
# present object, with: Entities::Something, # present object, with: Entities::Something,
...@@ -22,6 +22,7 @@ module API ...@@ -22,6 +22,7 @@ module API
extend ActiveSupport::Concern extend ActiveSupport::Concern
def initialize(object, options = {}) def initialize(object, options = {})
options = options.opts_hash if options.is_a?(Grape::Entity::Options)
super(object.present(options), options) super(object.present(options), options)
end end
end end
......
...@@ -22,6 +22,9 @@ msgstr "" ...@@ -22,6 +22,9 @@ msgstr ""
msgid " (from %{timeoutSource})" msgid " (from %{timeoutSource})"
msgstr "" msgstr ""
msgid " Collected %{time}"
msgstr ""
msgid " Please sign in." msgid " Please sign in."
msgstr "" msgstr ""
...@@ -475,7 +478,7 @@ msgstr "" ...@@ -475,7 +478,7 @@ msgstr ""
msgid "%{tags} tags per image name" msgid "%{tags} tags per image name"
msgstr "" msgstr ""
msgid "%{tag}-evidence.json" msgid "%{tag}-%{evidence}-%{filename}"
msgstr "" msgstr ""
msgid "%{template_project_id} is unknown or invalid" msgid "%{template_project_id} is unknown or invalid"
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::Releases::EvidencesController do
let!(:project) { create(:project, :repository, :public) }
let_it_be(:private_project) { create(:project, :repository, :private) }
let_it_be(:developer) { create(:user) }
let_it_be(:reporter) { create(:user) }
let(:user) { developer }
before do
project.add_developer(developer)
project.add_reporter(reporter)
end
shared_examples_for 'successful request' do
it 'renders a 200' do
subject
expect(response).to have_gitlab_http_status(:success)
end
end
shared_examples_for 'not found' do
it 'renders 404' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe 'GET #show' do
let_it_be(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, :with_evidence, project: project, tag: tag_name) }
let(:evidence) { release.evidences.first }
let(:tag) { CGI.escape(release.tag) }
let(:format) { :json }
subject do
get :show, params: {
namespace_id: project.namespace.to_param,
project_id: project,
tag: tag,
id: evidence.id,
format: format
}
end
before do
sign_in(user)
end
context 'when the user is a developer' do
it 'returns the correct evidence summary as a json' do
subject
expect(json_response).to eq(evidence.summary)
end
context 'when the release was created before evidence existed' do
before do
evidence.destroy
end
it_behaves_like 'not found'
end
end
context 'when the user is a guest for the project' do
before do
project.add_guest(user)
end
context 'when the project is private' do
let(:project) { private_project }
it_behaves_like 'not found'
end
context 'when the project is public' do
it_behaves_like 'successful request'
end
end
context 'when release is associated to a milestone which includes an issue' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:milestone) { create(:milestone, project: project, issues: [issue]) }
let_it_be(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
before do
create(:evidence, release: release)
end
shared_examples_for 'does not show the issue in evidence' do
it do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['release']['milestones']
.all? { |milestone| milestone['issues'].nil? }).to eq(true)
end
end
shared_examples_for 'evidence not found' do
it do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples_for 'safely expose evidence' do
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when the user is the author of the confidential issue' do
let(:issue) { create(:issue, :confidential, project: project, author: user) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when project is private' do
let!(:project) { create(:project, :repository, :private) }
it_behaves_like 'evidence not found'
end
context 'when project restricts the visibility of issues to project members only' do
let!(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'safely expose evidence'
end
context 'when user is auditor', if: Gitlab.ee? do
let(:user) { create(:user, :auditor) }
it_behaves_like 'safely expose evidence'
end
context 'when external authorization control is enabled' do
let(:user) { create(:user) }
before do
stub_application_setting(external_authorization_service_enabled: true)
end
it_behaves_like 'evidence not found'
end
end
end
end
...@@ -4,10 +4,10 @@ require 'spec_helper' ...@@ -4,10 +4,10 @@ require 'spec_helper'
describe Projects::ReleasesController do describe Projects::ReleasesController do
let!(:project) { create(:project, :repository, :public) } let!(:project) { create(:project, :repository, :public) }
let!(:private_project) { create(:project, :repository, :private) } let_it_be(:private_project) { create(:project, :repository, :private) }
let(:user) { developer } let_it_be(:developer) { create(:user) }
let(:developer) { create(:user) } let_it_be(:reporter) { create(:user) }
let(:reporter) { create(:user) } let_it_be(:user) { developer }
let!(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) } let!(:release_1) { create(:release, project: project, released_at: Time.zone.parse('2018-10-18')) }
let!(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) } let!(:release_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) }
...@@ -295,141 +295,6 @@ describe Projects::ReleasesController do ...@@ -295,141 +295,6 @@ describe Projects::ReleasesController do
end end
end end
describe 'GET #evidence' do
let_it_be(:tag_name) { "v1.1.0-evidence" }
let!(:release) { create(:release, :with_evidence, project: project, tag: tag_name) }
let(:tag) { CGI.escape(release.tag) }
let(:format) { :json }
subject do
get :evidence, params: {
namespace_id: project.namespace,
project_id: project,
tag: tag,
format: format
}
end
before do
sign_in(user)
end
context 'when the user is a developer' do
it 'returns the correct evidence summary as a json' do
subject
expect(json_response).to eq(release.evidence.summary)
end
context 'when the release was created before evidence existed' do
before do
release.evidence.destroy
end
it 'returns an empty json' do
subject
expect(json_response).to eq({})
end
end
end
context 'when the user is a guest for the project' do
before do
project.add_guest(user)
end
context 'when the project is private' do
let(:project) { private_project }
it_behaves_like 'not found'
end
context 'when the project is public' do
it_behaves_like 'successful request'
end
end
context 'when release is associated to a milestone which includes an issue' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:milestone) { create(:milestone, project: project, issues: [issue]) }
let_it_be(:release) { create(:release, project: project, tag: tag_name, milestones: [milestone]) }
before do
create(:evidence, release: release)
end
shared_examples_for 'does not show the issue in evidence' do
it do
subject
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['release']['milestones']
.all? { |milestone| milestone['issues'].nil? }).to eq(true)
end
end
shared_examples_for 'evidence not found' do
it do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
shared_examples_for 'safely expose evidence' do
it_behaves_like 'does not show the issue in evidence'
context 'when the issue is confidential' do
let(:issue) { create(:issue, :confidential, project: project) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when the user is the author of the confidential issue' do
let(:issue) { create(:issue, :confidential, project: project, author: user) }
it_behaves_like 'does not show the issue in evidence'
end
context 'when project is private' do
let!(:project) { create(:project, :repository, :private) }
it_behaves_like 'evidence not found'
end
context 'when project restricts the visibility of issues to project members only' do
let!(:project) { create(:project, :repository, :issues_private) }
it_behaves_like 'evidence not found'
end
end
context 'when user is non-project member' do
let(:user) { create(:user) }
it_behaves_like 'safely expose evidence'
end
context 'when user is auditor', if: Gitlab.ee? do
let(:user) { create(:user, :auditor) }
it_behaves_like 'safely expose evidence'
end
context 'when external authorization control is enabled' do
let(:user) { create(:user) }
before do
stub_application_setting(external_authorization_service_enabled: true)
end
it_behaves_like 'evidence not found'
end
end
end
private private
def get_index def get_index
......
# frozen_string_literal: true # frozen_string_literal: true
FactoryBot.define do FactoryBot.define do
factory :evidence do factory :evidence, class: 'Releases::Evidence' do
release release
end end
end end
...@@ -22,6 +22,10 @@ ...@@ -22,6 +22,10 @@
"commit_path": { "type": "string" }, "commit_path": { "type": "string" },
"tag_path": { "type": "string" }, "tag_path": { "type": "string" },
"name": { "type": "string" }, "name": { "type": "string" },
"evidences": {
"type": "array",
"items": { "$ref": "release/evidence.json" }
},
"assets": { "assets": {
"required": ["count", "links", "sources"], "required": ["count", "links", "sources"],
"properties": { "properties": {
......
{
"type": "object",
"required" : [
"sha",
"filepath",
"collected_at"
],
"properties" : {
"sha": { "type": "string" },
"filepath": { "type": "string" },
"collected_at": { "type": "date" }
},
"additionalProperties": false
}
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui'; import { GlLink, GlIcon } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility'; import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data'; import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue'; import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
...@@ -32,11 +31,11 @@ describe('Evidence Block', () => { ...@@ -32,11 +31,11 @@ describe('Evidence Block', () => {
}); });
it('renders the evidence icon', () => { it('renders the evidence icon', () => {
expect(wrapper.find(Icon).props('name')).toBe('review-list'); expect(wrapper.find(GlIcon).props('name')).toBe('review-list');
}); });
it('renders the title for the dowload link', () => { it('renders the title for the dowload link', () => {
expect(wrapper.find(GlLink).text()).toBe(`${release.tagName}-evidence.json`); expect(wrapper.find(GlLink).text()).toBe('v1.1.2-evidences-1.json');
}); });
it('renders the correct hover text for the download', () => { it('renders the correct hover text for the download', () => {
...@@ -44,19 +43,19 @@ describe('Evidence Block', () => { ...@@ -44,19 +43,19 @@ describe('Evidence Block', () => {
}); });
it('renders the correct file link for download', () => { it('renders the correct file link for download', () => {
expect(wrapper.find(GlLink).attributes().download).toBe(`${release.tagName}-evidence.json`); expect(wrapper.find(GlLink).attributes().download).toBe('v1.1.2-evidences-1.json');
}); });
describe('sha text', () => { describe('sha text', () => {
it('renders the short sha initially', () => { it('renders the short sha initially', () => {
expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidenceSha)); expect(wrapper.find('.js-short').text()).toBe(truncateSha(release.evidences[0].sha));
}); });
it('renders the long sha after expansion', () => { it('renders the long sha after expansion', () => {
wrapper.find('.js-text-expander-prepend').trigger('click'); wrapper.find('.js-text-expander-prepend').trigger('click');
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(wrapper.find('.js-expanded').text()).toBe(release.evidenceSha); expect(wrapper.find('.js-expanded').text()).toBe(release.evidences[0].sha);
}); });
}); });
}); });
...@@ -72,7 +71,7 @@ describe('Evidence Block', () => { ...@@ -72,7 +71,7 @@ describe('Evidence Block', () => {
it('copies the sha', () => { it('copies the sha', () => {
expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe( expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidenceSha, release.evidences[0].sha,
); );
}); });
}); });
......
...@@ -43,7 +43,6 @@ export const release = { ...@@ -43,7 +43,6 @@ export const release = {
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>', description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z', created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z', released_at: '2019-08-26T17:54:04.807Z',
evidence_sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
author: { author: {
id: 1, id: 1,
name: 'Administrator', name: 'Administrator',
...@@ -69,10 +68,28 @@ export const release = { ...@@ -69,10 +68,28 @@ export const release = {
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1', commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false, upcoming_release: false,
milestones, milestones,
evidences: [
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/1.json',
sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/2.json',
sha: '6ebd17a66e6a861175735416e49cf677678029805712dd71bb805c609e2d9108',
collected_at: '2018-10-19 15:43:20 +0200',
},
{
filepath:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidences/3.json',
sha: '2f65beaf275c3cb4b4e24fb01d481cc475d69c957830833f15338384816b5cba',
collected_at: '2018-10-19 15:43:20 +0200',
},
],
assets: { assets: {
count: 5, count: 5,
evidence_file_path:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidence.json',
sources: [ sources: [
{ {
format: 'zip', format: 'zip',
......
...@@ -4,26 +4,29 @@ require 'spec_helper' ...@@ -4,26 +4,29 @@ require 'spec_helper'
describe API::Entities::Release do describe API::Entities::Release do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) } let_it_be(:release) { create(:release, :with_evidence, project: project) }
let(:entity) { described_class.new(release, current_user: user) } let(:evidence) { release.evidences.first }
let(:user) { create(:user) }
describe 'evidence' do let(:entity) { described_class.new(release, current_user: user).as_json }
let(:release) { create(:release, :with_evidence, project: project) }
subject { entity.as_json }
describe 'evidences' do
context 'when the current user can download code' do context 'when the current user can download code' do
let(:entity_evidence) { entity[:evidences].first }
it 'exposes the evidence sha and the json path' do it 'exposes the evidence sha and the json path' do
allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?) allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(true) .with(user, :download_code, project).and_return(true)
expect(subject[:evidence_sha]).to eq(release.evidence_sha) expect(entity_evidence[:sha]).to eq(evidence.summary_sha)
expect(subject[:assets][:evidence_file_path]).to eq( expect(entity_evidence[:collected_at]).to eq(evidence.collected_at)
Gitlab::Routing.url_helpers.evidence_project_release_url(project, expect(entity_evidence[:filepath]).to eq(
release.tag, Gitlab::Routing.url_helpers.namespace_project_evidence_url(
format: :json) namespace_id: project.namespace,
) project_id: project,
tag: release,
id: evidence.id,
format: :json))
end end
end end
...@@ -33,8 +36,7 @@ describe API::Entities::Release do ...@@ -33,8 +36,7 @@ describe API::Entities::Release do
allow(Ability).to receive(:allowed?) allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(false) .with(user, :download_code, project).and_return(false)
expect(subject.keys).not_to include(:evidence_sha) expect(entity.keys).not_to include(:evidences)
expect(subject[:assets].keys).not_to include(:evidence_file_path)
end end
end end
end end
...@@ -45,7 +47,7 @@ describe API::Entities::Release do ...@@ -45,7 +47,7 @@ describe API::Entities::Release do
let(:issue_title) { 'title="%s"' % issue.title } let(:issue_title) { 'title="%s"' % issue.title }
let(:release) { create(:release, project: project, description: "Now shipping #{issue.to_reference}") } let(:release) { create(:release, project: project, description: "Now shipping #{issue.to_reference}") }
subject(:description_html) { entity.as_json[:description_html] } subject(:description_html) { entity.as_json['description_html'] }
it 'renders special references if current user has access' do it 'renders special references if current user has access' do
project.add_reporter(user) project.add_reporter(user)
......
...@@ -94,7 +94,7 @@ releases: ...@@ -94,7 +94,7 @@ releases:
- links - links
- milestone_releases - milestone_releases
- milestones - milestones
- evidence - evidences
links: links:
- release - release
project_members: project_members:
......
...@@ -134,7 +134,7 @@ Release: ...@@ -134,7 +134,7 @@ Release:
- created_at - created_at
- updated_at - updated_at
- released_at - released_at
Evidence: Releases::Evidence:
- id - id
- summary - summary
- created_at - created_at
......
...@@ -15,7 +15,7 @@ RSpec.describe Release do ...@@ -15,7 +15,7 @@ RSpec.describe Release do
it { is_expected.to have_many(:links).class_name('Releases::Link') } it { is_expected.to have_many(:links).class_name('Releases::Link') }
it { is_expected.to have_many(:milestones) } it { is_expected.to have_many(:milestones) }
it { is_expected.to have_many(:milestone_releases) } it { is_expected.to have_many(:milestone_releases) }
it { is_expected.to have_one(:evidence) } it { is_expected.to have_many(:evidences).class_name('Releases::Evidence') }
end end
describe 'validation' do describe 'validation' do
...@@ -97,7 +97,7 @@ RSpec.describe Release do ...@@ -97,7 +97,7 @@ RSpec.describe Release do
describe '#create_evidence!' do describe '#create_evidence!' do
context 'when a release is created' do context 'when a release is created' do
it 'creates one Evidence object too' do it 'creates one Evidence object too' do
expect { release_with_evidence }.to change(Evidence, :count).by(1) expect { release_with_evidence }.to change(Releases::Evidence, :count).by(1)
end end
end end
end end
...@@ -106,7 +106,7 @@ RSpec.describe Release do ...@@ -106,7 +106,7 @@ RSpec.describe Release do
it 'also deletes the associated evidence' do it 'also deletes the associated evidence' do
release_with_evidence release_with_evidence
expect { release_with_evidence.destroy }.to change(Evidence, :count).by(-1) expect { release_with_evidence.destroy }.to change(Releases::Evidence, :count).by(-1)
end end
end end
end end
...@@ -155,7 +155,7 @@ RSpec.describe Release do ...@@ -155,7 +155,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do context 'when a release was created with evidence collection' do
let!(:release) { create(:release, :with_evidence) } let!(:release) { create(:release, :with_evidence) }
it { is_expected.to eq(release.evidence.summary_sha) } it { is_expected.to eq(release.evidences.first.summary_sha) }
end end
end end
...@@ -171,7 +171,7 @@ RSpec.describe Release do ...@@ -171,7 +171,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do context 'when a release was created with evidence collection' do
let!(:release) { create(:release, :with_evidence) } let!(:release) { create(:release, :with_evidence) }
it { is_expected.to eq(release.evidence.summary) } it { is_expected.to eq(release.evidences.first.summary) }
end end
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Evidence do describe Releases::Evidence do
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) } let(:release) { create(:release, project: project) }
let(:schema_file) { 'evidences/evidence' } let(:schema_file) { 'evidences/evidence' }
......
...@@ -112,28 +112,4 @@ describe ReleasePresenter do ...@@ -112,28 +112,4 @@ describe ReleasePresenter do
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end
describe '#evidence_file_path' do
subject { presenter.evidence_file_path }
context 'without evidence' do
it { is_expected.to be_falsy }
end
context 'with evidence' do
let(:release) { create :release, :with_evidence, project: project }
specify do
is_expected.to match /#{evidence_project_release_url(project, release.tag, format: :json)}/
end
end
context 'when a tag contains a slash' do
let(:release) { create :release, :with_evidence, project: project, tag: 'debian/2.4.0-1' }
specify do
is_expected.to match /#{evidence_project_release_url(project, CGI.escape(release.tag), format: :json)}/
end
end
end
end end
...@@ -104,6 +104,21 @@ describe API::Releases do ...@@ -104,6 +104,21 @@ describe API::Releases do
expect(json_response.first['upcoming_release']).to eq(false) expect(json_response.first['upcoming_release']).to eq(false)
end end
it 'avoids N+1 queries' do
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
control_count = ActiveRecord::QueryRecorder.new do
get api("/projects/#{project.id}/releases", maintainer)
end.count
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
create(:release, :with_evidence, project: project, tag: 'v0.1', author: maintainer)
expect do
get api("/projects/#{project.id}/releases", maintainer)
end.not_to exceed_query_limit(control_count)
end
context 'when tag does not exist in git repository' do context 'when tag does not exist in git repository' do
let!(:release) { create(:release, project: project, tag: 'v1.1.5') } let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
...@@ -725,7 +740,7 @@ describe API::Releases do ...@@ -725,7 +740,7 @@ describe API::Releases do
end end
it 'does not create an Evidence object', :sidekiq_inline do it 'does not create an Evidence object', :sidekiq_inline do
expect { subject }.not_to change(Evidence, :count) expect { subject }.not_to change(Releases::Evidence, :count)
end end
it 'is a historical release' do it 'is a historical release' do
...@@ -755,7 +770,7 @@ describe API::Releases do ...@@ -755,7 +770,7 @@ describe API::Releases do
end end
it 'creates Evidence', :sidekiq_inline do it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1) expect { subject }.to change(Releases::Evidence, :count).by(1)
end end
it 'is not a historical release' do it 'is not a historical release' do
...@@ -785,7 +800,7 @@ describe API::Releases do ...@@ -785,7 +800,7 @@ describe API::Releases do
end end
it 'creates Evidence', :sidekiq_inline do it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1) expect { subject }.to change(Releases::Evidence, :count).by(1)
end end
it 'is not a historical release' do it 'is not a historical release' do
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe CreateEvidenceWorker do describe CreateEvidenceWorker do
let!(:release) { create(:release) } let!(:release) { create(:release) }
it 'creates a new Evidence' do it 'creates a new Evidence record' do
expect { described_class.new.perform(release.id) }.to change(Evidence, :count).by(1) expect { described_class.new.perform(release.id) }.to change(Releases::Evidence, :count).by(1)
end end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment