diff --git a/db/migrations/20210215_create_deployed_versions_table.rb b/db/migrations/20210215_create_deployed_versions_table.rb new file mode 100644 index 000000000..28c75107b --- /dev/null +++ b/db/migrations/20210215_create_deployed_versions_table.rb @@ -0,0 +1,18 @@ +Sequel.migration do + change do + create_table(:deployed_versions, charset: 'utf8') do + primary_key :id + String :uuid, null: false + foreign_key :version_id, :versions, null: false + Integer :pacticipant_id, null: false + foreign_key :environment_id, :environments, null: false + Boolean :currently_deployed, null: false + Boolean :replaced_previous_deployed_version, null: false + DateTime :created_at, nullable: false + DateTime :updated_at, nullable: false + DateTime :undeployed_at + index [:uuid], unique: true, name: "deployed_versions_uuid_index" + index [:pacticipant_id, :currently_deployed], name: "deployed_versions_pacticipant_id_currently_deployed_index" + end + end +end diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index cbe158c30..c2382175a 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -1,5 +1,6 @@ require 'webmachine/adapters/rack_mapped' require 'webmachine/rack_adapter_monkey_patch' +require 'pact_broker/db/models' require 'pact_broker/api/resources' require 'pact_broker/api/decorators' require 'pact_broker/application_context' @@ -108,6 +109,7 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ if PactBroker.feature_enabled?(:environments) add ['environments'], Api::Resources::Environments, { resource_name: "environments" } add ['environments', :environment_uuid], Api::Resources::Environment, { resource_name: "environment" } + add ['pacticipants', :pacticipant_name, 'versions', :pacticipant_version_number, 'deployed-versions', 'environment', :environment_uuid], Api::Resources::DeployedVersionsForVersion, { resource_name: "deployed_versions_for_version" } end add ['integrations'], Api::Resources::Integrations, {resource_name: "integrations"} diff --git a/lib/pact_broker/api/decorators/deployed_version_decorator.rb b/lib/pact_broker/api/decorators/deployed_version_decorator.rb new file mode 100644 index 000000000..417a424df --- /dev/null +++ b/lib/pact_broker/api/decorators/deployed_version_decorator.rb @@ -0,0 +1,18 @@ +require 'pact_broker/api/decorators/base_decorator' +require 'pact_broker/api/decorators/embedded_version_decorator' +require 'pact_broker/api/decorators/environment_decorator' + +module PactBroker + module Api + module Decorators + class DeployedVersionDecorator < BaseDecorator + property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true + property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true + property :currently_deployed, camelize: true + property :replaced_previous_deployed_version, camelize: true + include Timestamps + property :undeployedAt, getter: lambda { |_| undeployed_at ? FormatDateTime.call(undeployed_at) : nil }, writeable: false + end + end + end +end diff --git a/lib/pact_broker/api/decorators/embedded_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_version_decorator.rb index 9bbba9397..4fa93589b 100644 --- a/lib/pact_broker/api/decorators/embedded_version_decorator.rb +++ b/lib/pact_broker/api/decorators/embedded_version_decorator.rb @@ -6,6 +6,7 @@ module Decorators class EmbeddedVersionDecorator < BaseDecorator property :number + property :branch link :self do | options | { diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index 8d4c455c6..40a822317 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -55,6 +55,16 @@ class VersionDecorator < BaseDecorator end end + links :'pb:record-deployment' do | context | + context.fetch(:environments, []).collect do | environment | + { + title: "Record deployment to #{environment.display_name}", + name: environment.name, + href: deployed_versions_for_environment_url(represented, environment, context.fetch(:base_url)) + } + end + end + curies do | options | [{ name: :pb, diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 21e37e648..d0642fc80 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -315,6 +315,14 @@ def environment_url(environment, base_url = '') "#{environments_url(base_url)}/#{environment.uuid}" end + def deployed_versions_for_environment_url(version, environment, base_url = '') + "#{version_url(base_url, version)}/deployed-versions/environment/#{environment.uuid}" + end + + def deployed_version_url(deployed_version, base_url = '') + "/deployed-versions/#{deployed_version.uuid}" + end + def hal_browser_url target_url, base_url = '' "#{base_url}/hal-browser/browser.html#" + target_url end diff --git a/lib/pact_broker/api/resources/deployed_versions_for_version.rb b/lib/pact_broker/api/resources/deployed_versions_for_version.rb new file mode 100644 index 000000000..0180f68f1 --- /dev/null +++ b/lib/pact_broker/api/resources/deployed_versions_for_version.rb @@ -0,0 +1,72 @@ +require 'pact_broker/api/resources/base_resource' +require 'pact_broker/configuration' +require 'pact_broker/api/decorators/versions_decorator' + +module PactBroker + module Api + module Resources + class DeployedVersionsForVersion < BaseResource + def content_types_accepted + [["application/json", :from_json]] + end + + def content_types_provided + [["application/hal+json"]] + end + + def allowed_methods + ["POST", "OPTIONS"] + end + + def resource_exists? + !!version && !!environment + end + + def post_is_create? + true + end + + def create_path + deployed_version_url(OpenStruct.new(uuid: deployed_version_uuid), base_url) + end + + def from_json + @deployed_version = deployed_version_service.create(deployed_version_uuid, version, environment, replaced_previous_deployed_version) + response.body = to_json + end + + def to_json + decorator_class(:deployed_version_decorator).new(deployed_version).to_json(decorator_options) + end + + def policy_name + :'versions::versions' + end + + private + + attr_reader :deployed_version + + def version + @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path) + end + + def environment + @environment ||= environment_service.find(environment_uuid) + end + + def environment_uuid + identifier_from_path[:environment_uuid] + end + + def deployed_version_uuid + @deployed_version_uuid ||= deployed_version_service.next_uuid + end + + def replaced_previous_deployed_version + params[:replacedPreviousDeployedVersion] == true + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/matrix.rb b/lib/pact_broker/api/resources/matrix.rb index 1a44876ab..7f398da1a 100644 --- a/lib/pact_broker/api/resources/matrix.rb +++ b/lib/pact_broker/api/resources/matrix.rb @@ -26,7 +26,7 @@ def allowed_methods end def malformed_request? - error_messages = matrix_service.validate_selectors(selectors) + error_messages = matrix_service.validate_selectors(selectors, options) if error_messages.any? set_json_validation_error_messages error_messages true diff --git a/lib/pact_broker/api/resources/version.rb b/lib/pact_broker/api/resources/version.rb index d6a3e392f..b5a272f47 100644 --- a/lib/pact_broker/api/resources/version.rb +++ b/lib/pact_broker/api/resources/version.rb @@ -31,7 +31,7 @@ def from_json end def to_json - decorator_class(:version_decorator).new(version).to_json(decorator_options) + decorator_class(:version_decorator).new(version).to_json(decorator_options(environments: environments)) end def delete_resource @@ -45,6 +45,10 @@ def policy_name private + def environments + @environments ||= environment_service.find_for_pacticipant(version.pacticipant) + end + def version @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path) end diff --git a/lib/pact_broker/db/models.rb b/lib/pact_broker/db/models.rb index 39c0e6d67..01441ae40 100644 --- a/lib/pact_broker/db/models.rb +++ b/lib/pact_broker/db/models.rb @@ -10,6 +10,8 @@ require 'pact_broker/domain/version' require 'pact_broker/domain/label' require 'pact_broker/domain/pacticipant' +require 'pact_broker/deployments/environment' +require 'pact_broker/deployments/deployed_version' module PactBroker INTEGRATIONS_TABLES = [ @@ -22,6 +24,7 @@ module PactBroker PactBroker::Pacts::PactPublication, PactBroker::Pacts::PactVersion, PactBroker::Domain::Tag, + PactBroker::Deployments::DeployedVersion, PactBroker::Domain::Version, PactBroker::Domain::Label, PactBroker::Domain::Pacticipant diff --git a/lib/pact_broker/db/seed_example_data.rb b/lib/pact_broker/db/seed_example_data.rb index 5ca2ec07a..2fc03f755 100644 --- a/lib/pact_broker/db/seed_example_data.rb +++ b/lib/pact_broker/db/seed_example_data.rb @@ -14,15 +14,18 @@ def self.call def call(consumer_name: CONSUMER_NAME, provider_name: PROVIDER_NAME) return unless database_empty? PactBroker::Test::TestDataBuilder.new + .create_environment("test", display_name: "Test", production: false) + .create_environment("production", display_name: "Production", production: true) .create_consumer(consumer_name, created_at: days_ago(16)) .create_provider(provider_name, created_at: days_ago(16)) .create_consumer_version("e15da45d3943bf10793a6d04cfb9f5dabe430fe2", branch: "main", created_at: days_ago(16)) .create_consumer_version_tag("prod", created_at: days_ago(16)) .create_consumer_version_tag("main", created_at: days_ago(16)) + .create_deployed_version_for_consumer_version(environment_name: "production", currently_deployed: false, created_at: days_ago(16)) .create_pact(json_content: pact_1, created_at: days_ago(16)) .create_verification(provider_version: "1315e0b1924cb6f42751f977789be3559373033a", branch: "main", execution_date: days_ago(15)) - .create_provider_version_tag("main", created_at: days_ago(14)) - .create_provider_version_tag("prod", created_at: days_ago(14)) + .create_deployed_version_for_provider_version(environment_name: "production", currently_deployed: true, created_at: days_ago(15)) + .create_deployed_version_for_consumer_version(environment_name: "production", currently_deployed: true, created_at: days_ago(14)) .create_verification(provider_version: "480e5aeb30467856ca995d0024d2c1800b0719e5", branch: "main", success: false, number: 2, execution_date: days_ago(14)) .create_provider_version_tag("main", created_at: days_ago(14)) .create_consumer_version("725c6ccb7cf7efc51b4394f9828585eea9c379d9", branch: "feat/new-thing", created_at: days_ago(7)) @@ -32,9 +35,11 @@ def call(consumer_name: CONSUMER_NAME, provider_name: PROVIDER_NAME) .create_consumer_version_tag("main", created_at: days_ago(1)) .create_pact(json_content: pact_3, created_at: days_ago(1)) .create_verification(provider_version: "4fdf20082263d4c5038355a3b734be1c0054d1e1", branch: "main", execution_date: days_ago(1)) + .create_deployed_version_for_provider_version(environment_name: "test", created_at: days_ago(1)) .create_provider_version_tag("main", created_at: days_ago(1)) .create_consumer_version("5556b8149bf8bac76bc30f50a8a2dd4c22c85f30", branch: "main", created_at: days_ago(0.5)) .create_consumer_version_tag("main", created_at: days_ago(0.5)) + .create_deployed_version_for_consumer_version(environment_name: "test", created_at: days_ago(0.5)) .republish_same_pact(created_at: days_ago(0.5)) end diff --git a/lib/pact_broker/deployments/deployed_version.rb b/lib/pact_broker/deployments/deployed_version.rb new file mode 100644 index 000000000..d0bef5151 --- /dev/null +++ b/lib/pact_broker/deployments/deployed_version.rb @@ -0,0 +1,38 @@ +require 'pact_broker/repositories/helpers' + +module PactBroker + module Deployments + class DeployedVersion < Sequel::Model + many_to_one :version, :class => "PactBroker::Domain::Version", :key => :version_id, :primary_key => :id + many_to_one :environment, :class => "PactBroker::Deployments::Environment", :key => :environment_id, :primary_key => :id + + dataset_module do + include PactBroker::Repositories::Helpers + + def last_deployed_version(pacticipant, environment) + currently_deployed + .where(pacticipant_id: pacticipant.id) + .where(environment: environment) + .order(Sequel.desc(:created_at), Sequel.desc(:id)) + .first + end + + def currently_deployed + where(currently_deployed: true) + end + + def for_environment_name(environment_name) + where(environment_id: db[:environments].select(:id).where(name: environment_name)) + end + + def for_pacticipant_name(pacticipant_name) + where(pacticipant_id: db[:pacticipants].select(:id).where(name_like(:name, pacticipant_name))) + end + end + + def record_undeployed + update(currently_deployed: false, undeployed_at: Sequel.datetime_class.now) + end + end + end +end diff --git a/lib/pact_broker/deployments/deployed_version_service.rb b/lib/pact_broker/deployments/deployed_version_service.rb new file mode 100644 index 000000000..09a72f5be --- /dev/null +++ b/lib/pact_broker/deployments/deployed_version_service.rb @@ -0,0 +1,29 @@ +require 'pact_broker/deployments/deployed_version' + +module PactBroker + module Deployments + class DeployedVersionService + def self.next_uuid + SecureRandom.uuid + end + + def self.create(uuid, version, environment, replaced_previous_deployed_version) + if replaced_previous_deployed_version + record_previous_version_undeployed(version.pacticipant, environment) + end + DeployedVersion.create( + uuid: uuid, + version: version, + pacticipant_id: version.pacticipant_id, + environment: environment, + currently_deployed: true, + replaced_previous_deployed_version: replaced_previous_deployed_version + ) + end + + def self.record_previous_version_undeployed(pacticipant, environment) + DeployedVersion.last_deployed_version(pacticipant, environment)&.record_undeployed + end + end + end +end diff --git a/lib/pact_broker/deployments/environment_service.rb b/lib/pact_broker/deployments/environment_service.rb index b1a3a7631..4bbe4d58f 100644 --- a/lib/pact_broker/deployments/environment_service.rb +++ b/lib/pact_broker/deployments/environment_service.rb @@ -4,7 +4,6 @@ module PactBroker module Deployments module EnvironmentService - def self.next_uuid SecureRandom.uuid end @@ -34,6 +33,10 @@ def self.find_by_name(name) def self.delete(uuid) PactBroker::Deployments::Environment.where(uuid: uuid).delete end + + def self.find_for_pacticipant(pacticipant) + find_all + end end end end diff --git a/lib/pact_broker/domain/label.rb b/lib/pact_broker/domain/label.rb index 3515da96a..fb5f7a991 100644 --- a/lib/pact_broker/domain/label.rb +++ b/lib/pact_broker/domain/label.rb @@ -3,7 +3,6 @@ module PactBroker module Domain class Label < Sequel::Model - unrestrict_primary_key associate(:many_to_one, :pacticipant, :class => "PactBroker::Domain::Pacticipant", :key => :pacticipant_id, :primary_key => :id) diff --git a/lib/pact_broker/domain/version.rb b/lib/pact_broker/domain/version.rb index 8ec41d359..0d9b71de7 100644 --- a/lib/pact_broker/domain/version.rb +++ b/lib/pact_broker/domain/version.rb @@ -30,6 +30,9 @@ class Version < Sequel::Model one_to_many :pact_publications, order: :revision_number, class: "PactBroker::Pacts::PactPublication", key: :consumer_version_id associate(:many_to_one, :pacticipant, :class => "PactBroker::Domain::Pacticipant", :key => :pacticipant_id, :primary_key => :id) one_to_many :tags, :reciprocal => :version, order: :created_at + one_to_many :current_deployed_versions, class: "PactBroker::Deployments::DeployedVersion", key: :version_id, primary_key: :id do | ds | + ds.currently_deployed + end many_to_one :latest_version_for_pacticipant, read_only: true, key: :id, class: Version, @@ -79,6 +82,12 @@ def where_pacticipant_name(pacticipant_name) # end end + def currently_deployed_to_environment(environment_name, pacticipant_name) + deployed_version_query = PactBroker::Deployments::DeployedVersion.currently_deployed.for_environment_name(environment_name) + deployed_version_query = deployed_version_query.for_pacticipant_name(pacticipant_name) if pacticipant_name + where(id: deployed_version_query.select(:version_id)) + end + def where_tag(tag) if tag == true join(:tags, Sequel[:tags][:version_id] => Sequel[first_source_alias][:id]) @@ -115,6 +124,7 @@ def delete def for_selector(selector) query = self query = query.where_pacticipant_name(selector.pacticipant_name) if selector.pacticipant_name + query = query.currently_deployed_to_environment(selector.environment_name, selector.pacticipant_name) if selector.environment_name query = query.where_tag(selector.tag) if selector.tag query = query.where_number(selector.pacticipant_version_number) if selector.pacticipant_version_number query = query.where_age_less_than(selector.max_age) if selector.max_age diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index 7d7cc9678..b37389b66 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -48,6 +48,8 @@ en: invalid_webhook_uuid: The UUID can only contain the characters A-Z, a-z, 0-9, _ and -, and must be 16 or more characters. pacticipant_not_found: No pacticipant with name '%{name}' found environment_name_must_be_unique: Another environment with name '%{name}' already exists. + cannot_specify_tag_and_environment: Cannot specify both a 'to' tag and an environment. + cannot_specify_latest_and_environment: Cannot specify both latest=true and an environment. duplicate_pacticipant: | This is the first time a pact has been published for "%{new_name}". The name "%{new_name}" is very similar to the following existing consumers/providers: diff --git a/lib/pact_broker/matrix/can_i_deploy_query_schema.rb b/lib/pact_broker/matrix/can_i_deploy_query_schema.rb index 130924ca7..d7f14a23d 100644 --- a/lib/pact_broker/matrix/can_i_deploy_query_schema.rb +++ b/lib/pact_broker/matrix/can_i_deploy_query_schema.rb @@ -1,17 +1,26 @@ require 'dry-validation' +require 'pact_broker/messages' module PactBroker module Api module Contracts class CanIDeployQuerySchema + extend PactBroker::Messages + SCHEMA = Dry::Validation.Schema do required(:pacticipant).filled(:str?) required(:version).filled(:str?) optional(:to).filled(:str?) + optional(:environment).filled(:str?) end def self.call(params) - select_first_message(SCHEMA.call(params).messages(full: true)) + result = select_first_message(SCHEMA.call(params).messages(full: true)) + if params[:to] && params[:environment] + result[:to] ||= [] + result[:to] << message("errors.validation.cannot_specify_tag_and_environment") + end + result end def self.select_first_message(messages) diff --git a/lib/pact_broker/matrix/parse_can_i_deploy_query.rb b/lib/pact_broker/matrix/parse_can_i_deploy_query.rb index a5be910c1..192db8140 100644 --- a/lib/pact_broker/matrix/parse_can_i_deploy_query.rb +++ b/lib/pact_broker/matrix/parse_can_i_deploy_query.rb @@ -23,6 +23,10 @@ def self.call params options[:tag] = params[:to] end + if params[:environment].is_a?(String) + options[:environment_name] = params[:environment] + end + return [selector], options end end diff --git a/lib/pact_broker/matrix/parse_query.rb b/lib/pact_broker/matrix/parse_query.rb index 2ab126d1c..eb68e932f 100644 --- a/lib/pact_broker/matrix/parse_query.rb +++ b/lib/pact_broker/matrix/parse_query.rb @@ -11,6 +11,7 @@ def self.call query p[:pacticipant_name] = i['pacticipant'] if i['pacticipant'] && i['pacticipant'] != '' p[:pacticipant_version_number] = i['version'] if i['version'] && i['version'] != '' p[:latest] = true if i['latest'] == 'true' + p[:branch] = i['branch'] if i['branch'] && i['branch'] != '' p[:tag] = i['tag'] if i['tag'] && i['tag'] != '' p end @@ -20,9 +21,11 @@ def self.call query value == '' ? nil : value == 'true' end end + if params.key?('success') && params['success'].is_a?(String) options[:success] = [params['success'] == '' ? nil : params['success'] == 'true'] end + if params.key?('latestby') && params['latestby'] != '' options[:latestby] = params['latestby'] end @@ -40,9 +43,15 @@ def self.call query if params.key?('latest') && params['latest'] != '' options[:latest] = params['latest'] == 'true' end + if params.key?('tag') && params['tag'] != '' options[:tag] = params['tag'] end + + if params.key?('environment') && params['environment'] != '' + options[:environment_name] = params['environment'] + end + return selectors, options end end diff --git a/lib/pact_broker/matrix/repository.rb b/lib/pact_broker/matrix/repository.rb index 225977cb2..5a4974ba8 100644 --- a/lib/pact_broker/matrix/repository.rb +++ b/lib/pact_broker/matrix/repository.rb @@ -148,8 +148,7 @@ def resolve_versions_and_add_ids(selectors, selector_type) def find_versions_for_selector(selector) # For selectors that just set the pacticipant name, there's no need to resolve the version - # only the pacticipant ID will be used in the query - return nil unless (selector.tag || selector.latest || selector.pacticipant_version_number) - + return nil if selector.all_for_pacticipant? versions = version_repository.find_versions_for_selector(selector) if selector.latest @@ -163,9 +162,10 @@ def find_versions_for_selector(selector) # the single selector into one selector for each version. def build_resolved_selectors(pacticipant, versions, original_selector, selector_type) if versions + one_of_many = versions.compact.size > 1 versions.collect do | version | if version - selector_for_version(pacticipant, version, original_selector, selector_type) + selector_for_version(pacticipant, version, original_selector, selector_type, one_of_many) else selector_for_non_existing_version(pacticipant, original_selector, selector_type) end @@ -176,7 +176,7 @@ def build_resolved_selectors(pacticipant, versions, original_selector, selector_ end def infer_selectors_for_integrations?(options) - options[:latest] || options[:tag] + options[:latest] || options[:tag] || options[:environment_name] end # When only one selector is specified, (eg. checking to see if Foo version 2 can be deployed to prod), @@ -193,6 +193,7 @@ def build_inferred_selectors(inferred_pacticipant_names, options) selector = UnresolvedSelector.new(pacticipant_name: pacticipant_name) selector.tag = options[:tag] if options[:tag] selector.latest = options[:latest] if options[:latest] + selector.environment_name = options[:environment_name] if options[:environment_name] selector end resolve_versions_and_add_ids(selectors, :inferred) @@ -202,8 +203,8 @@ def selector_for_non_existing_version(pacticipant, original_selector, selector_t ResolvedSelector.for_pacticipant_and_non_existing_version(pacticipant, original_selector, selector_type) end - def selector_for_version(pacticipant, version, original_selector, selector_type) - ResolvedSelector.for_pacticipant_and_version(pacticipant, version, original_selector, selector_type) + def selector_for_version(pacticipant, version, original_selector, selector_type, one_of_many) + ResolvedSelector.for_pacticipant_and_version(pacticipant, version, original_selector, selector_type, one_of_many) end def selector_without_version(pacticipant, selector_type) diff --git a/lib/pact_broker/matrix/resolved_selector.rb b/lib/pact_broker/matrix/resolved_selector.rb index 19d2813f3..fdb219353 100644 --- a/lib/pact_broker/matrix/resolved_selector.rb +++ b/lib/pact_broker/matrix/resolved_selector.rb @@ -23,7 +23,7 @@ def self.for_pacticipant(pacticipant, type) ) end - def self.for_pacticipant_and_version(pacticipant, version, original_selector, type) + def self.for_pacticipant_and_version(pacticipant, version, original_selector, type, one_of_many = false) ResolvedSelector.new( pacticipant_id: pacticipant.id, pacticipant_name: pacticipant.name, @@ -31,7 +31,9 @@ def self.for_pacticipant_and_version(pacticipant, version, original_selector, ty pacticipant_version_number: version.number, latest: original_selector[:latest], tag: original_selector[:tag], - type: type + environment_name: original_selector[:environment_name], + type: type, + one_of_many: one_of_many ) end @@ -43,6 +45,7 @@ def self.for_pacticipant_and_non_existing_version(pacticipant, original_selector pacticipant_version_number: original_selector[:pacticipant_version_number], latest: original_selector[:latest], tag: original_selector[:tag], + environment_name: original_selector[:environment_name], type: type ) end @@ -71,6 +74,10 @@ def tag self[:tag] end + def environment_name + self[:environment_name] + end + def most_specific_criterion if pacticipant_version_id { pacticipant_version_id: pacticipant_version_id } @@ -114,6 +121,10 @@ def inferred? self[:type] == :inferred end + def one_of_many? + self[:one_of_many] + end + def description if latest_tagged? && pacticipant_version_number "the latest version of #{pacticipant_name} with tag #{tag} (#{pacticipant_version_number})" @@ -127,6 +138,11 @@ def description "a version of #{pacticipant_name} with tag #{tag} (#{pacticipant_version_number})" elsif tag "a version of #{pacticipant_name} with tag #{tag} (no such version exists)" + elsif environment_name && pacticipant_version_number + prefix = one_of_many? ? "one of the versions" : "the version" + "#{prefix} of #{pacticipant_name} currently deployed to #{environment_name} (#{pacticipant_version_number})" + elsif environment_name + "the version of #{pacticipant_name} currently deployed to #{environment_name} (no such version exists)" elsif pacticipant_version_number "version #{pacticipant_version_number} of #{pacticipant_name}" else diff --git a/lib/pact_broker/matrix/service.rb b/lib/pact_broker/matrix/service.rb index 8ab100388..7c18602e8 100644 --- a/lib/pact_broker/matrix/service.rb +++ b/lib/pact_broker/matrix/service.rb @@ -2,6 +2,8 @@ require 'pact_broker/repositories' require 'pact_broker/matrix/row' require 'pact_broker/matrix/deployment_status_summary' +require 'pact_broker/messages' +require 'pact_broker/string_refinements' module PactBroker module Matrix @@ -11,6 +13,8 @@ module Service extend PactBroker::Repositories extend PactBroker::Services include PactBroker::Logging + extend PactBroker::Messages + using PactBroker::StringRefinements def find selectors, options = {} logger.info "Querying matrix", selectors: selectors, options: options @@ -48,7 +52,7 @@ def find_compatible_pacticipant_versions criteria matrix_repository.find_compatible_pacticipant_versions criteria end - def validate_selectors selectors + def validate_selectors selectors, options = {} error_messages = [] selectors.each do | s | @@ -71,6 +75,14 @@ def validate_selectors selectors error_messages << "Please provide 1 or more version selectors." end + if options[:tag]&.not_blank? && options[:environment_name]&.not_blank? + error_messages << message("errors.validation.cannot_specify_tag_and_environment") + end + + if options[:latest] && options[:environment_name]&.not_blank? + error_messages << message("errors.validation.cannot_specify_latest_and_environment") + end + error_messages end end diff --git a/lib/pact_broker/matrix/unresolved_selector.rb b/lib/pact_broker/matrix/unresolved_selector.rb index 34912d788..1f68d406e 100644 --- a/lib/pact_broker/matrix/unresolved_selector.rb +++ b/lib/pact_broker/matrix/unresolved_selector.rb @@ -10,7 +10,7 @@ def initialize(params = {}) end def self.from_hash(hash) - new(hash.symbolize_keys.snakecase_keys.slice(:pacticipant_name, :pacticipant_version_number, :latest, :tag, :max_age)) + new(hash.symbolize_keys.snakecase_keys.slice(:pacticipant_name, :pacticipant_version_number, :latest, :tag, :environment_name, :max_age)) end def pacticipant_name @@ -37,6 +37,10 @@ def tag self[:tag] end + def environment_name + self[:environment_name] + end + def latest= latest self[:latest] = latest end @@ -45,6 +49,10 @@ def tag= tag self[:tag] = tag end + def environment_name= environment_name + self[:environment_name] = environment_name + end + def pacticipant_name= pacticipant_name self[:pacticipant_name] = pacticipant_name end @@ -61,6 +69,10 @@ def max_age self[:max_age] end + def all_for_pacticipant? + !!pacticipant_name && !pacticipant_version_number && !tag && !latest && !environment_name && !max_age + end + def latest_for_pacticipant_and_tag? !!(pacticipant_name && tag && latest) end diff --git a/lib/pact_broker/services.rb b/lib/pact_broker/services.rb index 5204a46a8..e99723d03 100644 --- a/lib/pact_broker/services.rb +++ b/lib/pact_broker/services.rb @@ -77,6 +77,10 @@ def environment_service get(:environment_service) end + def deployed_version_service + get(:deployed_version_service) + end + def register_default_services register_service(:index_service) do require 'pact_broker/index/service' @@ -157,6 +161,11 @@ def register_default_services require 'pact_broker/deployments/environment_service' Deployments::EnvironmentService end + + register_service(:deployed_version_service) do + require 'pact_broker/deployments/deployed_version_service' + PactBroker::Deployments::DeployedVersionService + end end end end diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index bfa719851..c86be3b1b 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -1,4 +1,5 @@ require 'json' +require 'pact_broker/string_refinements' require 'pact_broker/repositories' require 'pact_broker/services' require 'pact_broker/webhooks/repository' @@ -25,14 +26,16 @@ require 'pact_broker/certificates/certificate' require 'pact_broker/matrix/row' require 'pact_broker/deployments/environment_service' +require 'pact_broker/deployments/deployed_version_service' require 'ostruct' module PactBroker module Test class TestDataBuilder - include PactBroker::Repositories include PactBroker::Services + using PactBroker::StringRefinements + attr_reader :pacticipant attr_reader :consumer @@ -47,6 +50,7 @@ class TestDataBuilder attr_reader :webhook_execution attr_reader :triggered_webhook attr_reader :environment + attr_reader :deployed_version def initialize(params = {}) @now = DateTime.now @@ -93,9 +97,9 @@ def create_pact_with_consumer_version_tag consumer_name, consumer_version_number self end - def create_pact_with_verification consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", provider_name = "Provider", provider_version = "1.0.#{model_counter}" + def create_pact_with_verification consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", provider_name = "Provider", provider_version = "1.0.#{model_counter}", success = true create_pact_with_hierarchy(consumer_name, consumer_version, provider_name) - create_verification(number: model_counter, provider_version: provider_version) + create_verification(number: model_counter, provider_version: provider_version, success: success) self end @@ -355,7 +359,7 @@ def create_verification parameters = {} branch = parameters.delete(:branch) tag_names = [parameters.delete(:tag_names), parameters.delete(:tag_name)].flatten.compact provider_version_number = parameters[:provider_version] || '4.5.6' - default_parameters = { success: true, number: 1, test_results: {some: 'results'}, wip: false } + default_parameters = { success: true, number: 1, test_results: { some: 'results' }, wip: false } default_parameters[:execution_date] = @now if @now parameters = default_parameters.merge(parameters) parameters.delete(:provider_version) @@ -386,12 +390,20 @@ def create_certificate options = {path: 'spec/fixtures/single-certificate.pem'} def create_environment(name, params = {}) uuid = params[:uuid] || PactBroker::Deployments::EnvironmentService.next_uuid production = params[:production] || false - @environment = PactBroker::Deployments::EnvironmentService.create(uuid, PactBroker::Deployments::Environment.new(params.merge(name: name, production: production))) + display_name = params[:display_name] || name.camelcase(true) + the_params = params.merge(name: name, production: production, display_name: display_name) + @environment = PactBroker::Deployments::EnvironmentService.create(uuid, PactBroker::Deployments::Environment.new(the_params)) set_created_at_if_set(params[:created_at], :environments, id: environment.id) self end - def create_deployment(_) + def create_deployed_version_for_consumer_version(uuid: SecureRandom.uuid, currently_deployed: true, environment_name: environment&.name, created_at: nil) + create_deployed_version(uuid: uuid, currently_deployed: currently_deployed, version: consumer_version, environment_name: environment_name, created_at: created_at) + self + end + + def create_deployed_version_for_provider_version(uuid: SecureRandom.uuid, currently_deployed: true, environment_name: environment&.name, created_at: nil) + create_deployed_version(uuid: uuid, currently_deployed: currently_deployed, version: provider_version, environment_name: environment_name, created_at: created_at) self end @@ -425,6 +437,10 @@ def find_pact_publication(consumer_name, consumer_version_number, provider_name) .single_record end + def find_environment(environment_name) + PactBroker::Deployments::EnvironmentService.find_by_name(environment_name) + end + def model_counter @@model_counter ||= 0 @@model_counter += 1 @@ -502,6 +518,13 @@ def random_json_content(consumer_name, provider_name) private + def create_deployed_version(uuid: , currently_deployed: , version:, environment_name: , created_at: nil) + env = find_environment(environment_name) + @deployed_version = PactBroker::Deployments::DeployedVersionService.create(uuid, version, env, false) + @deployed_version.update(currently_deployed: false) unless currently_deployed + set_created_at_if_set(created_at, :deployed_versions, id: deployed_version.id) + end + def pact_version_id PactBroker::Pacts::PactPublication.find(id: @pact.id).pact_version_id end diff --git a/lib/pact_broker/ui/controllers/matrix.rb b/lib/pact_broker/ui/controllers/matrix.rb index cd68e01e2..4fda22812 100644 --- a/lib/pact_broker/ui/controllers/matrix.rb +++ b/lib/pact_broker/ui/controllers/matrix.rb @@ -29,7 +29,7 @@ class Matrix < Base selectors, options = PactBroker::Matrix::ParseQuery.call(request.env['QUERY_STRING']) locals[:selectors] = create_selector_objects(selectors) locals[:options] = create_options_model(options) - errors = matrix_service.validate_selectors(selectors) + errors = matrix_service.validate_selectors(selectors, options) if errors.empty? lines = matrix_service.find(selectors, options) locals[:lines] = PactBroker::UI::ViewDomain::MatrixLines.new(lines) diff --git a/lib/pact_broker/ui/helpers/matrix_helper.rb b/lib/pact_broker/ui/helpers/matrix_helper.rb index 8390bb015..af84b3608 100644 --- a/lib/pact_broker/ui/helpers/matrix_helper.rb +++ b/lib/pact_broker/ui/helpers/matrix_helper.rb @@ -9,10 +9,11 @@ def create_selector_objects(selector_hashes) selector_hashes.collect do | selector_hash | o = OpenStruct.new(selector_hash) o.specify_latest_tag = o.tag && o.latest ? 'checked' : nil + o.specify_latest_branch = o.branch && o.latest ? 'checked' : nil o.specify_all_tagged = o.tag && !o.latest ? 'checked' : nil o.specify_latest = o.latest ? 'checked' : nil o.specify_version = o.pacticipant_version_number ? 'checked' : nil - o.specify_all_versions = !(o.tag || o.pacticipant_version_number) ? 'checked' : nil + o.specify_all_versions = !(o.tag || o.pacticipant_version_number || o.branch) ? 'checked' : nil o end end diff --git a/lib/pact_broker/ui/views/matrix/show.haml b/lib/pact_broker/ui/views/matrix/show.haml index 451147cad..47b745d0f 100644 --- a/lib/pact_broker/ui/views/matrix/show.haml +++ b/lib/pact_broker/ui/views/matrix/show.haml @@ -40,6 +40,8 @@ Latest version %option{ value: 'specify-version', selected: selector.specify_version } Version number ... + %option{ value: 'specify-latest-branch', selected: selector.specify_latest_branch } + Latest version from branch ... %option{ value: 'specify-latest-tag', selected: selector.specify_latest_tag } Latest version with tag ... %option{ value: 'specify-all-tagged', selected: selector.specify_all_tagged } @@ -47,6 +49,8 @@ %input{name: 'q[]version', type: 'text', id: "pacticipant#{index}_version", class: 'version', value: selector.pacticipant_version_number} + %input{name: 'q[]branch', type: 'text', id: "pacticipant#{index}_branch", class: 'branch', value: selector.branch} + %input{name: 'q[]tag', type: 'text', id: "pacticipant#{index}_tag", class: 'tag', value: selector.tag} %input{name: 'q[]latest', value: 'true', hidden: true, class: 'latest-flag'} @@ -55,10 +59,15 @@ - if options.latest || options.tag .selector %label{for: 'to'} - = options.latest ? 'To' : 'With all' + = options.latest ? 'To (tag)' : 'With all (tagged)' %input{name: 'tag', id: 'to', value: options.tag } %input{name: 'latest', value: options.latest.to_s, hidden: true} + - if options.environment_name + .selector + %label{for: 'environment'} + To environment + %input{name: 'environment', id: 'environment', value: options.environment_name } %div.top-of-group.form-check %input{type: 'radio', name: "latestby", class: 'form-check-input', value: '', id: 'all_rows', checked: options.all_rows_checked} diff --git a/public/javascripts/matrix.js b/public/javascripts/matrix.js index 0b46ef6a8..0b80bb8fe 100644 --- a/public/javascripts/matrix.js +++ b/public/javascripts/matrix.js @@ -22,18 +22,26 @@ function showApplicableTextBoxes(selectorizor) { var selectorizorType = selectorizor.val(); if( selectorizorType === 'specify-version') { setTextboxVisibility(selectorizor, '.version', true); + setTextboxVisibility(selectorizor, '.branch', false); setTextboxVisibility(selectorizor, '.tag', false); } else if( selectorizorType === 'specify-latest-tag' || selectorizorType === 'specify-all-tagged') { setTextboxVisibility(selectorizor, '.version', false); + setTextboxVisibility(selectorizor, '.branch', false); setTextboxVisibility(selectorizor, '.tag', true); } + else if( selectorizorType === 'specify-latest-branch') { + setTextboxVisibility(selectorizor, '.version', false); + setTextboxVisibility(selectorizor, '.branch', true); + setTextboxVisibility(selectorizor, '.tag', false); + } else if ( selectorizorType === 'specify-all-versions' || selectorizorType === 'specify-latest') { setTextboxVisibility(selectorizor, '.version', false); + setTextboxVisibility(selectorizor, '.branch', false); setTextboxVisibility(selectorizor, '.tag', false); } - if (selectorizorType === 'specify-latest' || selectorizorType === 'specify-latest-tag') { + if (selectorizorType === 'specify-latest' || selectorizorType === 'specify-latest-tag' || selectorizorType === 'specify-latest-branch') { toggleLatestFlag(selectorizor, true); } else { toggleLatestFlag(selectorizor, false); diff --git a/spec/features/record_deployment_spec.rb b/spec/features/record_deployment_spec.rb index 7f8f03212..48dc00f95 100644 --- a/spec/features/record_deployment_spec.rb +++ b/spec/features/record_deployment_spec.rb @@ -1,28 +1,46 @@ # -# pact-broker record-deployment --pacticipant Foo --version 1 --environment test --end-previous-deployment +# pact-broker record-deployment --pacticipant Foo --version 1 --environment test --replace-previous-deployed-version # -describe "Record deployment", skip: "Not yet implemented" do +describe "Record deployment" do before do - td.create_environment("test") - .create_pacticipant("Foo") - .create_pacticipant_version("1") + td.create_environment("test", uuid: "1234") + .create_consumer("Foo") + .create_consumer_version("1") + .create_deployed_version_for_consumer_version end - let(:path) { "/pacticipants/Foo/versions/1/deployments/test" } let(:headers) { {"CONTENT_TYPE" => "application/json"} } - let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)} + let(:response_body) { JSON.parse(subject.body, symbolize_names: true) } + let(:version_path) { "/pacticipants/Foo/versions/1" } + let(:version_response) { get(version_path, nil, { "HTTP_ACCEPT" => "application/hal+json" } ) } + let(:replaced_previous) { true } + let(:path) do + JSON.parse(version_response.body)["_links"]["pb:record-deployment"] + .find{ |relation| relation["name"] == "test" } + .fetch("href") + end - subject { post(path, nil, headers) } + subject { post(path, { replacedPreviousDeployedVersion: replaced_previous }.to_json, headers) } it { is_expected.to be_a_hal_json_created_response } it "returns the Location header" do - subject - expect(last_response.headers["Location"]).to eq "http://example.org/deployments/123456" + expect(subject.headers["Location"]).to start_with "http://example.org/deployed-versions/" end it "returns the newly created deployment" do - subject - expect(response_body).to include_key(:createdAt) + expect(response_body[:currentlyDeployed]).to be true + end + + it "marks the previous deployment as not currently deployed" do + expect { subject }.to_not change { PactBroker::Deployments::DeployedVersion.currently_deployed.count } + end + + context "when the deployment does not replace the previous deployed version" do + let(:replaced_previous) { false } + + it "leaves the previous deployed version as currently deployed" do + expect { subject }.to change { PactBroker::Deployments::DeployedVersion.currently_deployed.count }.by(1) + end end end diff --git a/spec/fixtures/approvals/modifiable_resources.approved.json b/spec/fixtures/approvals/modifiable_resources.approved.json index ca0af5450..e923f5022 100644 --- a/spec/fixtures/approvals/modifiable_resources.approved.json +++ b/spec/fixtures/approvals/modifiable_resources.approved.json @@ -6,6 +6,9 @@ { "resource_class_name": "PactBroker::Api::Resources::Clean" }, + { + "resource_class_name": "PactBroker::Api::Resources::DeployedVersionsForVersion" + }, { "resource_class_name": "PactBroker::Api::Resources::Environment" }, diff --git a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb index 8a207a5fa..07d050ed7 100644 --- a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb @@ -27,7 +27,7 @@ module Decorators name: 'pact_name')} let(:consumer) { instance_double(PactBroker::Domain::Pacticipant, name: 'Consumer')} let(:provider) { instance_double(PactBroker::Domain::Pacticipant, name: 'Provider')} - let(:consumer_version) { instance_double(PactBroker::Domain::Version, number: '1234', pacticipant: consumer)} + let(:consumer_version) { instance_double(PactBroker::Domain::Version, number: '1234', branch: 'main', pacticipant: consumer)} let(:decorator_context) { DecoratorContext.new(base_url, '', {}) } let(:json) { PactVersionDecorator.new(pact).to_json(user_options: decorator_context) } diff --git a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb index d1f49fac7..078c2e26d 100644 --- a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb @@ -5,7 +5,6 @@ module PactBroker module Api module Decorators describe VersionDecorator do - describe "from_json" do let(:hash) do { @@ -24,65 +23,93 @@ module Decorators end end - let(:version) do - TestDataBuilder.new - .create_consumer("Consumer") - .create_provider("providerA") - .create_consumer_version("1.2.3") - .create_consumer_version_tag("prod") - .create_pact - .create_provider("ProviderB") - .create_pact - .and_return(:consumer_version) - end + describe "to_json" do + before do + allow(decorator).to receive(:deployed_versions_for_environment_url).and_return("http://deployed-versions") + end - let(:options) { { user_options: { base_url: 'http://example.org' } } } + let(:version) do + TestDataBuilder.new + .create_consumer("Consumer") + .create_provider("providerA") + .create_consumer_version("1.2.3") + .create_consumer_version_tag("prod") + .create_pact + .create_provider("ProviderB") + .create_pact + .and_return(:consumer_version) + end - subject { JSON.parse VersionDecorator.new(version).to_json(options), symbolize_names: true } + let(:environments) do + [ + instance_double("PactBroker::Deployments::Environment", + uuid: "1234", + name: "test", + display_name: "Test" + ) + ] + end - it "includes a link to itself" do - expect(subject[:_links][:self][:href]).to eq "http://example.org/pacticipants/Consumer/versions/1.2.3" - end + let(:base_url) { 'http://example.org' } + let(:options) { { user_options: { base_url: base_url, environments: environments } } } + let(:decorator) { VersionDecorator.new(version) } - it "includes the version number in the link" do - expect(subject[:_links][:self][:name]).to eq "1.2.3" - end + subject { JSON.parse(decorator.to_json(options), symbolize_names: true) } - it "includes its title in the link" do - expect(subject[:_links][:self][:title]).to eq "Version" - end + it "includes a link to itself" do + expect(subject[:_links][:self][:href]).to eq "http://example.org/pacticipants/Consumer/versions/1.2.3" + end - it "includes the version number" do - expect(subject[:number]).to eq "1.2.3" - end + it "includes the version number in the link" do + expect(subject[:_links][:self][:name]).to eq "1.2.3" + end - it "includes a link to the pacticipant" do - expect(subject[:_links][:'pb:pacticipant']).to eq title: "Pacticipant", name: "Consumer", href: "http://example.org/pacticipants/Consumer" - end + it "includes its title in the link" do + expect(subject[:_links][:self][:title]).to eq "Version" + end - it "includes a link to get, create or delete a tag" do - expect(subject[:_links][:'pb:tag']).to include href: "http://example.org/pacticipants/Consumer/versions/1.2.3/tags/{tag}", templated: true - end + it "includes the version number" do + expect(subject[:number]).to eq "1.2.3" + end - it "includes a list of the tags" do - expect(subject[:_embedded][:tags]).to be_instance_of(Array) - expect(subject[:_embedded][:tags].first[:name]).to eq "prod" - end + it "includes a link to the pacticipant" do + expect(subject[:_links][:'pb:pacticipant']).to eq title: "Pacticipant", name: "Consumer", href: "http://example.org/pacticipants/Consumer" + end - it "includes the timestamps" do - expect(subject[:createdAt]).to_not be nil - end + it "includes a link to get, create or delete a tag" do + expect(subject[:_links][:'pb:tag']).to include href: "http://example.org/pacticipants/Consumer/versions/1.2.3/tags/{tag}", templated: true + end - it "includes a list of sorted pacts" do - expect(subject[:_links][:'pb:pact-versions']).to be_instance_of(Array) - expect(subject[:_links][:'pb:pact-versions'].first[:href]).to include ("1.2.3") - expect(subject[:_links][:'pb:pact-versions'].first[:name]).to include ("Pact between") - expect(subject[:_links][:'pb:pact-versions'].first[:name]).to include ("providerA") - expect(subject[:_links][:'pb:pact-versions'].last[:name]).to include ("ProviderB") - end + it "includes a list of the tags" do + expect(subject[:_embedded][:tags]).to be_instance_of(Array) + expect(subject[:_embedded][:tags].first[:name]).to eq "prod" + end - it "includes a link to the latest verification results for the pacts for this version" do - expect(subject[:_links][:'pb:latest-verification-results-where-pacticipant-is-consumer'][:href]).to match(%r{http://.*/verification-results/.*/latest}) + it "includes the timestamps" do + expect(subject[:createdAt]).to_not be nil + end + + it "includes a list of sorted pacts" do + expect(subject[:_links][:'pb:pact-versions']).to be_instance_of(Array) + expect(subject[:_links][:'pb:pact-versions'].first[:href]).to include ("1.2.3") + expect(subject[:_links][:'pb:pact-versions'].first[:name]).to include ("Pact between") + expect(subject[:_links][:'pb:pact-versions'].first[:name]).to include ("providerA") + expect(subject[:_links][:'pb:pact-versions'].last[:name]).to include ("ProviderB") + end + + it "includes a link to the latest verification results for the pacts for this version" do + expect(subject[:_links][:'pb:latest-verification-results-where-pacticipant-is-consumer'][:href]).to match(%r{http://.*/verification-results/.*/latest}) + end + + it "includes a list of environments that this version can be deployed to" do + expect(decorator).to receive(:deployed_versions_for_environment_url).with(version, environments.first, base_url) + expect(subject[:_links][:'pb:record-deployment']).to be_instance_of(Array) + expect(subject[:_links][:'pb:record-deployment'].first).to eq( + name: "test", + title: "Record deployment to Test", + href: "http://deployed-versions" + ) + end end end end diff --git a/spec/lib/pact_broker/api/resources/matrix_spec.rb b/spec/lib/pact_broker/api/resources/matrix_spec.rb index c776f8b70..abf746e4b 100644 --- a/spec/lib/pact_broker/api/resources/matrix_spec.rb +++ b/spec/lib/pact_broker/api/resources/matrix_spec.rb @@ -22,7 +22,7 @@ module Resources subject { get path, params, {'Content-Type' => 'application/hal+json'}; last_response } it "validates the selectors" do - expect(PactBroker::Matrix::Service).to receive(:validate_selectors).with(selectors) + expect(PactBroker::Matrix::Service).to receive(:validate_selectors).with(selectors, options) subject end diff --git a/spec/lib/pact_broker/domain/version_spec.rb b/spec/lib/pact_broker/domain/version_spec.rb index 9ef11ca9c..fbc7222a9 100644 --- a/spec/lib/pact_broker/domain/version_spec.rb +++ b/spec/lib/pact_broker/domain/version_spec.rb @@ -140,6 +140,57 @@ def version_numbers expect(version_numbers).to eq %w{2 3} end end + + context "when selecting all versions of a pacticipant currently deployed to an environment" do + let(:selector) { PactBroker::Matrix::UnresolvedSelector.new(environment_name: "prod", pacticipant_name: "Foo") } + + before do + td.create_environment("test") + .create_consumer("Foo") + .create_consumer_version("1") + .create_deployed_version_for_consumer_version + .create_consumer_version("2") + .create_environment("prod") + .create_deployed_version_for_consumer_version + .create_consumer_version("3") + .create_deployed_version_for_consumer_version + .create_consumer_version("4") + .create_deployed_version_for_consumer_version(currently_deployed: false) + .create_consumer_version("5") + .create_consumer("Bar") + .create_consumer_version("10") + .create_consumer_version("11") + end + + it "returns the versions of that pacticipant currently deployed to the environment" do + expect(version_numbers).to eq %w{2 3} + end + end + + context "when selecting all versions currently deployed to an environment" do + let(:selector) { PactBroker::Matrix::UnresolvedSelector.new(environment_name: "prod") } + + before do + td.create_environment("test") + .create_consumer("Foo") + .create_consumer_version("1") + .create_deployed_version_for_consumer_version + .create_consumer_version("2") + .create_environment("prod") + .create_deployed_version_for_consumer_version + .create_consumer_version("3") + .create_consumer_version("5") + .create_consumer("Bar") + .create_consumer_version("10") + .create_deployed_version_for_consumer_version + .create_consumer_version("11") + .create_deployed_version_for_consumer_version(currently_deployed: false) + end + + it "returns the versions of that pacticipant currently deployed to the environment" do + expect(version_numbers).to eq %w{2 10} + end + end end describe "latest_for_pacticipant?" do @@ -283,6 +334,30 @@ def version_numbers it { is_expected.to be nil } end end + + describe "current_deployed_versions" do + before do + td.create_environment("test") + .create_environment("prod") + .create_consumer("Foo") + .create_consumer_version("1") + .create_deployed_version_for_consumer_version(currently_deployed: false, environment_name: "test") + .create_deployed_version_for_consumer_version(currently_deployed: true, environment_name: "prod") + .create_consumer_version("2") + .create_deployed_version_for_consumer_version(currently_deployed: true, environment_name: "prod") + end + + it "returns the currently active deployed versions" do + expect(td.find_version("Foo", "1").current_deployed_versions.size).to eq 1 + expect(td.find_version("Foo", "1").current_deployed_versions.first.environment.name).to eq "prod" + end + + it "eager loads" do + all = PactBroker::Domain::Version.where(number: "2").eager(:current_deployed_versions).all + expect(all.first.associations[:current_deployed_versions].size).to eq 1 + expect(all.first.associations[:current_deployed_versions].first.environment.name).to eq "prod" + end + end end end end diff --git a/spec/lib/pact_broker/matrix/can_i_deploy_query_schema_spec.rb b/spec/lib/pact_broker/matrix/can_i_deploy_query_schema_spec.rb new file mode 100644 index 000000000..6d55607c9 --- /dev/null +++ b/spec/lib/pact_broker/matrix/can_i_deploy_query_schema_spec.rb @@ -0,0 +1,35 @@ +require 'pact_broker/matrix/can_i_deploy_query_schema' + +module PactBroker + module Api + module Contracts + describe CanIDeployQuerySchema do + subject { CanIDeployQuerySchema.call(params) } + + context "with valid params" do + let(:params) do + { + pacticipant: "foo", + version: "1", + to: "prod" + } + end + + it { is_expected.to be_empty } + end + + context "with a to tag and an environment specified" do + let(:params) do + { + pacticipant: "foo", + version: "1", + environment: "prod", + to: "prod" + } + end + it { is_expected.to_not be_empty } + end + end + end + end +end diff --git a/spec/lib/pact_broker/matrix/deployment_status_summary_spec.rb b/spec/lib/pact_broker/matrix/deployment_status_summary_spec.rb index df00e3911..c6cc78814 100644 --- a/spec/lib/pact_broker/matrix/deployment_status_summary_spec.rb +++ b/spec/lib/pact_broker/matrix/deployment_status_summary_spec.rb @@ -201,7 +201,9 @@ module Matrix pacticipant_version_number: bar_version.number, latest: nil, tag: nil, - type: :inferred + environment_name: nil, + type: :inferred, + one_of_many: false ) end diff --git a/spec/lib/pact_broker/matrix/integration_environment_spec.rb b/spec/lib/pact_broker/matrix/integration_environment_spec.rb new file mode 100644 index 000000000..3db8f09af --- /dev/null +++ b/spec/lib/pact_broker/matrix/integration_environment_spec.rb @@ -0,0 +1,175 @@ +require 'pact_broker/matrix/service' + +module PactBroker + module Matrix + describe Service do + describe "find with environments" do + subject { Service.find(selectors, options) } + + # Useful for eyeballing the messages to make sure they read nicely + # after do + # require 'pact_broker/api/decorators/reason_decorator' + # subject.deployment_status_summary.reasons.each do | reason | + # puts reason + # puts PactBroker::Api::Decorators::ReasonDecorator.new(reason).to_s + # end + # end + + context "when there is a successful verification between the provider in test environment and the consumer to be deployed" do + before do + td.create_environment("test") + .create_pact_with_verification("Foo", "1", "Bar", "2") + .create_deployed_version_for_provider_version + .create_verification(provider_version: "3", number: 2, success: false) + end + + let(:selectors) do + [ + UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1"), + ] + end + + let(:options) { { latestby: "cvp", environment_name: "test" } } + + it "allows the consumer to be deployed" do + expect(subject.deployment_status_summary).to be_deployable + end + end + + context "when there is an unsuccessful verification between the provider in test environment and the consumer to be deployed" do + before do + td.create_environment("test") + .create_pact_with_verification("Foo", "1", "Bar", "2", false) + .create_deployed_version_for_provider_version + .create_verification(provider_version: "3", number: 3, success: true) + end + + let(:selectors) do + [ + UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1"), + ] + end + + let(:options) { { latestby: "cvp", environment_name: "test" } } + + it "does not allow the consumer to be deployed" do + expect(subject.deployment_status_summary).to_not be_deployable + end + end + + context "when the provider has not been deployed to the given environment" do + before do + td.create_environment("test") + .create_pact_with_verification("Foo", "1", "Bar", "2") + end + + let(:selectors) do + [ + UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "1"), + ] + end + + let(:options) { { latestby: "cvp", environment_name: "test" } } + + it "does not allow the consumer to be deployed" do + expect(subject.deployment_status_summary).to_not be_deployable + end + end + + context "when the consumer has not been deployed to the given environment" do + before do + td.create_environment("test") + .create_pact_with_verification("Foo", "1", "Bar", "2") + end + + let(:selectors) do + [ + UnresolvedSelector.new(pacticipant_name: "Bar", pacticipant_version_number: "2"), + ] + end + + let(:options) { { latestby: "cvp", environment_name: "test" } } + + it "allows the provider to be deployed" do + expect(subject.deployment_status_summary).to be_deployable + end + end + + describe "when deploying a version of a provider with multiple versions of a consumer in production" do + before do + td.create_environment("prod") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_deployed_version_for_consumer_version(environment_name: "prod") + .create_verification(provider_version: "10") + .create_consumer_version("2") + .create_pact + .create_deployed_version_for_consumer_version(environment_name: "prod") + end + + let(:selectors) { [ UnresolvedSelector.new(pacticipant_name: "Bar", pacticipant_version_number: "10") ]} + let(:options) { { environment_name: "prod" } } + + it "knows that there are multiple versions of the consumer in production" do + subject + expect(subject.resolved_selectors.select { |s| s.pacticipant_name == "Bar" }.collect(&:one_of_many?)).to eq [false] + expect(subject.resolved_selectors.select { |s| s.pacticipant_name == "Foo" }.collect(&:one_of_many?)).to eq [true, true] + end + + context "when a verification for the latest prod version is missing" do + it "does not allow the provider to be deployed" do + expect(subject.deployment_status_summary).to_not be_deployable + end + end + + context "when there is a successful verification for every prod version of the consumer" do + before do + td.create_verification(provider_version: "10") + end + + it "does allow the provider to be deployed" do + expect(subject.deployment_status_summary).to be_deployable + end + end + end + + describe "when deploying a version of a consumer with multiple versions of a provider in production" do + before do + td.create_environment("prod") + .create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "10") + .create_deployed_version_for_provider_version(environment_name: "prod") + .create_consumer_version("2") + .create_pact + .create_provider_version("11") + .create_deployed_version_for_provider_version(environment_name: "prod") + end + + let(:selectors) { [ UnresolvedSelector.new(pacticipant_name: "Foo", pacticipant_version_number: "2") ]} + let(:options) { { environment_name: "prod" } } + + it "knows that there are multiple versions of the provider in production" do + subject + expect(subject.resolved_selectors.select { |s| s.pacticipant_name == "Foo" }.collect(&:one_of_many?)).to eq [false] + expect(subject.resolved_selectors.select { |s| s.pacticipant_name == "Bar" }.collect(&:one_of_many?)).to eq [true, true] + end + + context "when a verification for the latest prod version is missing" do + it "does not allow the consumer to be deployed" do + expect(subject.deployment_status_summary).to_not be_deployable + end + end + + context "when there is a successful verification for every prod version of the consumer" do + before do + td.create_verification(provider_version: "11") + end + + it "does allow the consumer to be deployed" do + expect(subject.deployment_status_summary).to be_deployable + end + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/matrix/integration_spec.rb b/spec/lib/pact_broker/matrix/integration_spec.rb index 5510277db..b43c278fb 100644 --- a/spec/lib/pact_broker/matrix/integration_spec.rb +++ b/spec/lib/pact_broker/matrix/integration_spec.rb @@ -3,8 +3,6 @@ module PactBroker module Matrix describe Service do - let(:td) { TestDataBuilder.new } - describe "find" do subject { Service.find(selectors, options) } @@ -56,7 +54,7 @@ module Matrix let(:selectors) { [ UnresolvedSelector.new(pacticipant_name: "Bar", latest: true, tag: "test") ]} let(:options) { { tag: "prod", latestby: "cvp" } } - it "does not allow the consumer to be deployed" do + it "does not allow the provider to be deployed" do expect(subject.deployment_status_summary).to_not be_deployable end end diff --git a/spec/lib/pact_broker/matrix/service_spec.rb b/spec/lib/pact_broker/matrix/service_spec.rb index fa6a3f3bf..52a8ee14d 100644 --- a/spec/lib/pact_broker/matrix/service_spec.rb +++ b/spec/lib/pact_broker/matrix/service_spec.rb @@ -4,11 +4,10 @@ module PactBroker module Matrix describe Service do - let(:td) { TestDataBuilder.new } - describe "validate_selectors" do - subject { Service.validate_selectors(selectors) } + subject { Service.validate_selectors(selectors, options) } + let(:options) { {} } context "when there are no selectors" do let(:selectors) { [] } @@ -82,6 +81,36 @@ module Matrix expect(subject).to eq ["A version number and latest flag cannot both be specified for Foo"] end end + + context "when both a to tag and an environment are specified" do + let(:selectors) { [] } + + let(:options) do + { + tag: "prod", + environment_name: "prod" + } + end + + it "returns an error message" do + expect(subject.last).to include "Cannot specify both" + end + end + + context "when both latest=true and an environment are specified" do + let(:selectors) { [] } + + let(:options) do + { + latest: true, + environment_name: "prod" + } + end + + it "returns an error message" do + expect(subject.last).to include "Cannot specify both latest" + end + end end describe "find_for_consumer_and_provider_with_tags integration test" do