Commit fa582374 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch '199065-support-on-demand-release-evidence' into 'master'

Multiple Evidences for a Release

See merge request gitlab-org/gitlab!26509
parents 6b1c2eb3 7ce7df63
<script>
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import dateFormat from 'dateformat';
import { GlLink, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
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 ExpandButton from '~/vue_shared/components/expand_button.vue';
......@@ -12,7 +13,7 @@ export default {
ClipboardButton,
ExpandButton,
GlLink,
Icon,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -24,17 +25,33 @@ export default {
},
},
computed: {
evidenceTitle() {
return sprintf(__('%{tag}-evidence.json'), { tag: this.release.tagName });
evidences() {
return this.release.evidences;
},
evidenceUrl() {
return this.release.assets && this.release.assets.evidenceFilePath;
},
shortSha() {
return truncateSha(this.sha);
methods: {
evidenceTitle(index) {
const [tag, evidence, filename] = this.release.evidences[index].filepath.split('/').slice(-3);
return sprintf(__('%{tag}-%{evidence}-%{filename}'), { tag, evidence, filename });
},
sha() {
return this.release.evidenceSha;
evidenceUrl(index) {
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 {
<template>
<div>
<div class="card-text prepend-top-default">
<b>
{{ __('Evidence collection') }}
</b>
<b>{{ __('Evidence collection') }}</b>
</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
v-gl-tooltip
class="monospace"
class="d-flex align-items-center monospace"
:title="__('Download evidence JSON')"
:download="evidenceTitle"
:href="evidenceUrl"
:download="evidenceTitle(index)"
: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>
<expand-button>
<template slot="short">
<span class="js-short monospace">{{ shortSha }}</span>
<span class="js-short monospace">{{ shortSha(index) }}</span>
</template>
<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>
</expand-button>
<clipboard-button
:title="__('Copy evidence SHA')"
:text="sha"
:text="sha(index)"
css-class="btn-default btn-transparent btn-clipboard"
/>
</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>
</template>
......@@ -44,7 +44,7 @@ export default {
return this.release.assets || {};
},
hasEvidence() {
return Boolean(this.release.evidenceSha);
return Boolean(this.release.evidences && this.release.evidences.length);
},
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
push_frontend_feature_flag(:release_show_page, project, default_enabled: true)
end
before_action :authorize_update_release!, only: %i[edit update]
before_action :authorize_read_release_evidence!, only: [:evidence]
def index
respond_to do |format|
......@@ -22,14 +21,6 @@ class Projects::ReleasesController < Projects::ApplicationController
end
end
def evidence
respond_to do |format|
format.json do
render json: release.evidence_summary
end
end
end
def show
return render_404 unless Feature.enabled?(:release_show_page, project, default_enabled: true)
......@@ -64,11 +55,6 @@ class Projects::ReleasesController < Projects::ApplicationController
access_denied! unless can?(current_user, :update_release, release)
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
@release ||= project.releases.find_by_tag!(sanitized_tag_name)
end
......
......@@ -16,7 +16,7 @@ class Release < ApplicationRecord
has_many :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
Time.zone.now
......@@ -28,7 +28,7 @@ class Release < ApplicationRecord
validates_associated :milestone_releases, message: -> (_, obj) { obj[:value].map(&:errors).map(&:full_messages).join(",") }
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 :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
......@@ -66,27 +66,27 @@ class Release < ApplicationRecord
end
def upcoming_release?
released_at.present? && released_at > Time.zone.now
released_at.present? && released_at.to_i > Time.zone.now.to_i
end
def historical_release?
released_at.present? && released_at < created_at
released_at.present? && released_at.to_i < created_at.to_i
end
def name
self.read_attribute(:name) || tag
end
def evidence_sha
evidence&.summary_sha
def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ")
end
def evidence_summary
evidence&.summary || {}
def evidence_sha
evidences.first&.summary_sha
end
def milestone_titles
self.milestones.map {|m| m.title }.sort.join(", ")
def evidence_summary
evidences.first&.summary || {}
end
private
......
# frozen_string_literal: true
class Evidence < ApplicationRecord
class Releases::Evidence < ApplicationRecord
include ShaAttribute
include Presentable
belongs_to :release
belongs_to :release, inverse_of: :evidences
before_validation :generate_summary_and_sha
default_scope { order(created_at: :asc) }
sha_attribute :summary_sha
alias_attribute :collected_at, :created_at
def milestones
@milestones ||= release.milestones.includes(:issues)
......
......@@ -2,31 +2,4 @@
class ReleasePolicy < BasePolicy
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
# 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
end
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
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
release = Release.find_by_id(release_id)
return unless release
Evidence.create!(release: release)
Releases::Evidence.create!(release: release)
end
end
---
title: Support multiple Evidences for a Release
merge_request: 26509
author:
type: changed
......@@ -170,8 +170,10 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :releases, only: [:index, :show, :edit], param: :tag, constraints: { tag: %r{[^/]+} } do
member do
get :evidence
get :downloads, path: 'downloads/*filepath', format: false
scope module: :releases do
resources :evidences, only: [:show]
end
end
end
......
......@@ -22,6 +22,7 @@ module API
expose :commit_path, expose_nil: false
expose :tag_path, expose_nil: false
expose :evidence_sha, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :assets do
expose :assets_count, as: :count do |release, _|
assets_to_exclude = can_download_code? ? [] : [:sources]
......@@ -33,6 +34,7 @@ module API
end
expose :evidence_file_path, expose_nil: false, if: ->(_, _) { can_download_code? }
end
expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
expose :_links do
expose :self_url, as: :self, 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
module Helpers
##
# 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.
#
# present object, with: Entities::Something,
......@@ -22,6 +22,7 @@ module API
extend ActiveSupport::Concern
def initialize(object, options = {})
options = options.opts_hash if options.is_a?(Grape::Entity::Options)
super(object.present(options), options)
end
end
......
......@@ -22,6 +22,9 @@ msgstr ""
msgid " (from %{timeoutSource})"
msgstr ""
msgid " Collected %{time}"
msgstr ""
msgid " Please sign in."
msgstr ""
......@@ -475,7 +478,7 @@ msgstr ""
msgid "%{tags} tags per image name"
msgstr ""
msgid "%{tag}-evidence.json"
msgid "%{tag}-%{evidence}-%{filename}"
msgstr ""
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'
describe Projects::ReleasesController do
let!(:project) { create(:project, :repository, :public) }
let!(:private_project) { create(:project, :repository, :private) }
let(:user) { developer }
let(:developer) { create(:user) }
let(:reporter) { create(:user) }
let_it_be(:private_project) { create(:project, :repository, :private) }
let_it_be(:developer) { create(:user) }
let_it_be(: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_2) { create(:release, project: project, released_at: Time.zone.parse('2019-10-19')) }
......@@ -295,141 +295,6 @@ describe Projects::ReleasesController do
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
def get_index
......
# frozen_string_literal: true
FactoryBot.define do
factory :evidence do
factory :evidence, class: 'Releases::Evidence' do
release
end
end
......@@ -22,6 +22,10 @@
"commit_path": { "type": "string" },
"tag_path": { "type": "string" },
"name": { "type": "string" },
"evidences": {
"type": "array",
"items": { "$ref": "release/evidence.json" }
},
"assets": {
"required": ["count", "links", "sources"],
"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 { GlLink } from '@gitlab/ui';
import { GlLink, GlIcon } from '@gitlab/ui';
import { truncateSha } from '~/lib/utils/text_utility';
import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data';
import EvidenceBlock from '~/releases/components/evidence_block.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
......@@ -32,11 +31,11 @@ describe('Evidence Block', () => {
});
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', () => {
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', () => {
......@@ -44,19 +43,19 @@ describe('Evidence Block', () => {
});
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', () => {
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', () => {
wrapper.find('.js-text-expander-prepend').trigger('click');
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', () => {
it('copies the sha', () => {
expect(wrapper.find(ClipboardButton).attributes('data-clipboard-text')).toBe(
release.evidenceSha,
release.evidences[0].sha,
);
});
});
......
......@@ -43,7 +43,6 @@ export const release = {
description_html: '<p data-sourcepos="1:1-1:21" dir="auto">A super nice release!</p>',
created_at: '2019-08-26T17:54:04.952Z',
released_at: '2019-08-26T17:54:04.807Z',
evidence_sha: 'fb3a125fd69a0e5048ebfb0ba43eb32ce4911520dd8d',
author: {
id: 1,
name: 'Administrator',
......@@ -69,10 +68,28 @@ export const release = {
commit_path: '/root/release-test/commit/c22b0728d1b465f82898c884d32b01aa642f96c1',
upcoming_release: false,
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: {
count: 5,
evidence_file_path:
'https://20592.qa-tunnel.gitlab.info/root/test-deployments/-/releases/v1.1.2/evidence.json',
sources: [
{
format: 'zip',
......
......@@ -4,26 +4,29 @@ require 'spec_helper'
describe API::Entities::Release do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let(:entity) { described_class.new(release, current_user: user) }
describe 'evidence' do
let(:release) { create(:release, :with_evidence, project: project) }
subject { entity.as_json }
let_it_be(:release) { create(:release, :with_evidence, project: project) }
let(:evidence) { release.evidences.first }
let(:user) { create(:user) }
let(:entity) { described_class.new(release, current_user: user).as_json }
describe 'evidences' 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
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(true)
expect(subject[:evidence_sha]).to eq(release.evidence_sha)
expect(subject[:assets][:evidence_file_path]).to eq(
Gitlab::Routing.url_helpers.evidence_project_release_url(project,
release.tag,
format: :json)
)
expect(entity_evidence[:sha]).to eq(evidence.summary_sha)
expect(entity_evidence[:collected_at]).to eq(evidence.collected_at)
expect(entity_evidence[:filepath]).to eq(
Gitlab::Routing.url_helpers.namespace_project_evidence_url(
namespace_id: project.namespace,
project_id: project,
tag: release,
id: evidence.id,
format: :json))
end
end
......@@ -33,8 +36,7 @@ describe API::Entities::Release do
allow(Ability).to receive(:allowed?)
.with(user, :download_code, project).and_return(false)
expect(subject.keys).not_to include(:evidence_sha)
expect(subject[:assets].keys).not_to include(:evidence_file_path)
expect(entity.keys).not_to include(:evidences)
end
end
end
......@@ -45,7 +47,7 @@ describe API::Entities::Release do
let(:issue_title) { 'title="%s"' % issue.title }
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
project.add_reporter(user)
......
......@@ -94,7 +94,7 @@ releases:
- links
- milestone_releases
- milestones
- evidence
- evidences
links:
- release
project_members:
......
......@@ -134,7 +134,7 @@ Release:
- created_at
- updated_at
- released_at
Evidence:
Releases::Evidence:
- id
- summary
- created_at
......
......@@ -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(:milestones) }
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
describe 'validation' do
......@@ -97,7 +97,7 @@ RSpec.describe Release do
describe '#create_evidence!' do
context 'when a release is created' 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
......@@ -106,7 +106,7 @@ RSpec.describe Release do
it 'also deletes the associated evidence' do
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
......@@ -155,7 +155,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do
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
......@@ -171,7 +171,7 @@ RSpec.describe Release do
context 'when a release was created with evidence collection' do
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
......
......@@ -2,7 +2,7 @@
require 'spec_helper'
describe Evidence do
describe Releases::Evidence do
let_it_be(:project) { create(:project) }
let(:release) { create(:release, project: project) }
let(:schema_file) { 'evidences/evidence' }
......
......@@ -112,28 +112,4 @@ describe ReleasePresenter do
it { is_expected.to be_nil }
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
......@@ -104,6 +104,21 @@ describe API::Releases do
expect(json_response.first['upcoming_release']).to eq(false)
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
let!(:release) { create(:release, project: project, tag: 'v1.1.5') }
......@@ -725,7 +740,7 @@ describe API::Releases do
end
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
it 'is a historical release' do
......@@ -755,7 +770,7 @@ describe API::Releases do
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1)
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
......@@ -785,7 +800,7 @@ describe API::Releases do
end
it 'creates Evidence', :sidekiq_inline do
expect { subject }.to change(Evidence, :count).by(1)
expect { subject }.to change(Releases::Evidence, :count).by(1)
end
it 'is not a historical release' do
......
......@@ -5,7 +5,7 @@ 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)
it 'creates a new Evidence record' do
expect { described_class.new.perform(release.id) }.to change(Releases::Evidence, :count).by(1)
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