Commit be70aa0e authored by Etienne Baqué's avatar Etienne Baqué Committed by Nick Thomas

Added Evidence migration, model and spec

Added Evidence model and migration file.
Schema file was updated accordingly.
Added related spec file.
Updated release model and spec files.
parent 72e21af5
# 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
...@@ -14,6 +14,7 @@ class Release < ApplicationRecord ...@@ -14,6 +14,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
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,6 +29,8 @@ class Release < ApplicationRecord ...@@ -28,6 +29,8 @@ class Release < ApplicationRecord
delegate :repository, to: :project delegate :repository, to: :project
after_commit :create_evidence!, on: :create
def commit def commit
strong_memoize(:commit) do strong_memoize(:commit) do
repository.commit(actual_sha) repository.commit(actual_sha)
...@@ -66,6 +69,10 @@ class Release < ApplicationRecord ...@@ -66,6 +69,10 @@ class Release < ApplicationRecord
repository.find_tag(tag) repository.find_tag(tag)
end end
end end
def create_evidence!
CreateEvidenceWorker.perform_async(self.id)
end
end end
Release.prepend_if_ee('EE::Release') Release.prepend_if_ee('EE::Release')
# frozen_string_literal: true
module Evidences
class EvidenceEntity < Grape::Entity
expose :release, using: Evidences::ReleaseEntity
end
end
# frozen_string_literal: true # frozen_string_literal: true
module Evidences module Evidences
class AuthorEntity < Grape::Entity class EvidenceSerializer < BaseSerializer
expose :id entity EvidenceEntity
expose :name
expose :email
end end
end end
...@@ -5,7 +5,6 @@ module Evidences ...@@ -5,7 +5,6 @@ module Evidences
expose :id expose :id
expose :title expose :title
expose :description expose :description
expose :author, using: AuthorEntity
expose :state expose :state
expose :iid expose :iid
expose :confidential expose :confidential
......
...@@ -9,6 +9,6 @@ module Evidences ...@@ -9,6 +9,6 @@ module Evidences
expose :iid expose :iid
expose :created_at expose :created_at
expose :due_date expose :due_date
expose :issues, using: IssueEntity expose :issues, using: Evidences::IssueEntity
end end
end end
...@@ -7,7 +7,7 @@ module Evidences ...@@ -7,7 +7,7 @@ module Evidences
expose :name expose :name
expose :description expose :description
expose :created_at expose :created_at
expose :project, using: ProjectEntity expose :project, using: Evidences::ProjectEntity
expose :milestones, using: MilestoneEntity expose :milestones, using: Evidences::MilestoneEntity
end end
end end
...@@ -173,3 +173,4 @@ ...@@ -173,3 +173,4 @@
- delete_stored_files - delete_stored_files
- import_issues_csv - import_issues_csv
- project_daily_statistics - project_daily_statistics
- create_evidence
# 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
---
title: Creation of Evidence collection of new releases.
merge_request: 17217
author:
type: added
...@@ -96,6 +96,7 @@ ...@@ -96,6 +96,7 @@
- [phabricator_import_import_tasks, 1] - [phabricator_import_import_tasks, 1]
- [update_namespace_statistics, 1] - [update_namespace_statistics, 1]
- [chaos, 2] - [chaos, 2]
- [create_evidence, 2]
# EE-specific queues # EE-specific queues
- [ldap_group_sync, 2] - [ldap_group_sync, 2]
......
# 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
...@@ -1424,6 +1424,15 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do ...@@ -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" t.index ["target_type", "target_id"], name: "index_events_on_target_type_and_target_id"
end 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| create_table "external_pull_requests", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime_with_timezone "updated_at", null: false
...@@ -4079,6 +4088,7 @@ ActiveRecord::Schema.define(version: 2019_10_16_072826) do ...@@ -4079,6 +4088,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", "namespaces", column: "group_id", name: "fk_61fbf6ca48", on_delete: :cascade
add_foreign_key "events", "projects", 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 "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 "external_pull_requests", "projects", on_delete: :cascade
add_foreign_key "fork_network_members", "fork_networks", 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 add_foreign_key "fork_network_members", "projects", column: "forked_from_project_id", name: "fk_b01280dae4", on_delete: :nullify
......
# frozen_string_literal: true
FactoryBot.define do
factory :evidence do
release
end
end
{ {
"type": "object", "type": "object",
"required": [ "required": [
"id", "release"
"name",
"email"
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "release": { "$ref": "release.json" }
"name": { "type": "string" },
"email": { "type": "string" }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -14,13 +14,12 @@ ...@@ -14,13 +14,12 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"author": { "$ref": "author.json" },
"state": { "type": "string" }, "state": { "type": "string" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"confidential": { "type": "boolean" }, "confidential": { "type": "boolean" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"due_date": { "type": "date" } "due_date": { "type": ["date", "null"] }
}, },
"additionalProperties": false "additionalProperties": false
} }
...@@ -13,11 +13,11 @@ ...@@ -13,11 +13,11 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"title": { "type": "string" }, "title": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"state": { "type": "string" }, "state": { "type": "string" },
"iid": { "type": "integer" }, "iid": { "type": "integer" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"due_date": { "type": "date" }, "due_date": { "type": ["date", "null"] },
"issues": { "issues": {
"type": "array", "type": "array",
"items": { "$ref": "issue.json" } "items": { "$ref": "issue.json" }
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"name": { "type": "string" }, "name": { "type": "string" },
"description": { "type": "string" }, "description": { "type": ["string", "null"] },
"created_at": { "type": "date" } "created_at": { "type": "date" }
}, },
"additionalProperties": false "additionalProperties": false
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
"type": "object", "type": "object",
"required": [ "required": [
"id", "id",
"tag", "tag_name",
"name", "name",
"description", "description",
"created_at", "created_at",
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
], ],
"properties": { "properties": {
"id": { "type": "integer" }, "id": { "type": "integer" },
"tag": { "type": "string" }, "tag_name": { "type": "string" },
"name": { "type": "string" }, "name": { "type": ["string", "null"] },
"description": { "type": "string" }, "description": { "type": "string" },
"created_at": { "type": "date" }, "created_at": { "type": "date" },
"project": { "$ref": "project.json" }, "project": { "$ref": "project.json" },
......
...@@ -81,6 +81,7 @@ releases: ...@@ -81,6 +81,7 @@ releases:
- links - links
- milestone_releases - milestone_releases
- milestones - milestones
- evidence
links: links:
- release - release
project_members: project_members:
...@@ -506,6 +507,8 @@ lists: ...@@ -506,6 +507,8 @@ lists:
milestone_releases: milestone_releases:
- milestone - milestone
- release - release
evidences:
- release
design: &design design: &design
- issue - issue
- actions - actions
......
...@@ -127,6 +127,12 @@ Release: ...@@ -127,6 +127,12 @@ Release:
- created_at - created_at
- updated_at - updated_at
- released_at - released_at
Evidence:
- id
- release_id
- summary
- created_at
- updated_at
Releases::Link: Releases::Link:
- id - id
- release_id - release_id
......
# 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
...@@ -15,11 +15,13 @@ RSpec.describe Release do ...@@ -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(: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) }
end end
describe 'validation' do describe 'validation' do
it { is_expected.to validate_presence_of(:project) } it { is_expected.to validate_presence_of(:project) }
it { is_expected.to validate_presence_of(:description) } 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 context 'when a release exists in the database without a name' do
it 'does not require name' do it 'does not require name' do
...@@ -89,4 +91,22 @@ RSpec.describe Release do ...@@ -89,4 +91,22 @@ RSpec.describe Release do
end end
end 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 end
...@@ -2,12 +2,13 @@ ...@@ -2,12 +2,13 @@
require 'spec_helper' require 'spec_helper'
describe Evidences::AuthorEntity do describe Evidences::EvidenceEntity do
let(:entity) { described_class.new(build(:author)) } let(:evidence) { build(:evidence) }
let(:entity) { described_class.new(evidence) }
subject { entity.as_json } subject { entity.as_json }
it 'exposes the expected fields' do it 'exposes the expected fields' do
expect(subject.keys).to contain_exactly(:id, :name, :email) expect(subject.keys).to contain_exactly(:release)
end end
end end
# 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
...@@ -8,6 +8,6 @@ describe Evidences::IssueEntity do ...@@ -8,6 +8,6 @@ describe Evidences::IssueEntity do
subject { entity.as_json } subject { entity.as_json }
it 'exposes the expected fields' do 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
end end
...@@ -12,7 +12,7 @@ describe Evidences::MilestoneEntity do ...@@ -12,7 +12,7 @@ describe Evidences::MilestoneEntity do
expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues) expect(subject.keys).to contain_exactly(:id, :title, :description, :state, :iid, :created_at, :due_date, :issues)
end 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_1) { build(:issue) }
let(:issue_2) { build(:issue) } let(:issue_2) { build(:issue) }
let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) } let(:milestone) { build(:milestone, issues: [issue_1, issue_2]) }
......
# 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
# 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
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