diff --git a/db/migrations/20210117_add_branch_to_version.rb b/db/migrations/20210117_add_branch_to_version.rb new file mode 100644 index 000000000..39537269c --- /dev/null +++ b/db/migrations/20210117_add_branch_to_version.rb @@ -0,0 +1,9 @@ +Sequel.migration do + change do + alter_table(:versions) do + add_column(:branch, String) + add_column(:build_url, String) + add_index([:pacticipant_id, :branch, :order], name: "versions_pacticipant_id_branch_order_index") + end + end +end diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 033a3d3e8..715325efd 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -71,10 +71,10 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ['pacticipants', :pacticipant_name], Api::Resources::Pacticipant, {resource_name: "pacticipant"} add ['pacticipants', :pacticipant_name, 'versions'], Api::Resources::Versions, {resource_name: "pacticipant_versions"} add ['pacticipants', :pacticipant_name, 'versions', :pacticipant_version_number], Api::Resources::Version, {resource_name: "pacticipant_version"} - add ['pacticipants', :pacticipant_name, 'latest-version', :tag], Api::Resources::Version, {resource_name: "latest_tagged_pacticipant_version"} + add ['pacticipants', :pacticipant_name, 'latest-version', :tag], Api::Resources::LatestVersion, {resource_name: "latest_tagged_pacticipant_version"} add ['pacticipants', :pacticipant_name, 'latest-version', :tag, 'can-i-deploy', 'to', :to], Api::Resources::CanIDeployPacticipantVersion, { resource_name: "can_i_deploy_latest_tagged_version" } add ['pacticipants', :pacticipant_name, 'latest-version', :tag, 'can-i-deploy', 'to', :to, 'badge'], Api::Resources::CanIDeployBadge, { resource_name: "can_i_deploy_badge" } - add ['pacticipants', :pacticipant_name, 'latest-version'], Api::Resources::Version, {resource_name: "latest_pacticipant_version"} + add ['pacticipants', :pacticipant_name, 'latest-version'], Api::Resources::LatestVersion, {resource_name: "latest_pacticipant_version"} add ['pacticipants', :pacticipant_name, 'versions', :pacticipant_version_number, 'tags', :tag_name], Api::Resources::Tag, {resource_name: "pacticipant_version_tag"} add ['pacticipants', :pacticipant_name, 'labels', :label_name], Api::Resources::Label, {resource_name: "pacticipant_label"} diff --git a/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb index 5b617cdef..a57349610 100644 --- a/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb +++ b/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator.rb @@ -12,9 +12,11 @@ class VerifiablePactsQueryDecorator < BaseDecorator using PactBroker::HashRefinements collection :provider_version_tags, default: [] + property :provider_version_branch collection :consumer_version_selectors, default: PactBroker::Pacts::Selectors.new, class: PactBroker::Pacts::Selector do property :tag + property :branch property :latest, setter: ->(fragment:, represented:, **) { represented.latest = (fragment == 'true' || fragment == true) diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index ad08e229f..583df424e 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -6,9 +6,11 @@ module Api module Decorators class VersionDecorator < BaseDecorator - property :number + property :number, writeable: false + property :branch + property :build_url, as: :buildUrl - collection :tags, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedTagDecorator + collection :tags, embedded: true, writeable: false, :extend => PactBroker::Api::Decorators::EmbeddedTagDecorator include Timestamps diff --git a/lib/pact_broker/api/resources/default_base_resource.rb b/lib/pact_broker/api/resources/default_base_resource.rb index 93625d0f5..4de40d10d 100644 --- a/lib/pact_broker/api/resources/default_base_resource.rb +++ b/lib/pact_broker/api/resources/default_base_resource.rb @@ -149,6 +149,10 @@ def consumer_version_number identifier_from_path[:consumer_version_number] end + def pacticipant_version_number + identifier_from_path[:pacticipant_version_number] + end + def consumer_specified? identifier_from_path.key?(:consumer_name) end diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb index f65271e9a..5acb82483 100644 --- a/lib/pact_broker/api/resources/index.rb +++ b/lib/pact_broker/api/resources/index.rb @@ -110,6 +110,12 @@ def links title: "Get, create or delete a tag for a pacticipant version", templated: true }, + 'pb:pacticipant-version' => + { + href: base_url + '/pacticipants/{pacticipant}/versions/{version}', + title: "Get, create or delete a pacticipant version", + templated: true + }, 'pb:metrics' => { href: base_url + '/metrics', diff --git a/lib/pact_broker/api/resources/latest_version.rb b/lib/pact_broker/api/resources/latest_version.rb new file mode 100644 index 000000000..d4b44184f --- /dev/null +++ b/lib/pact_broker/api/resources/latest_version.rb @@ -0,0 +1,27 @@ +require 'pact_broker/api/resources/version' + +module PactBroker + module Api + module Resources + class LatestVersion < Version + def content_types_accepted + [] + end + + def allowed_methods + ["GET", "OPTIONS"] + end + + private + + def version + if identifier_from_path[:tag] + @version ||= version_service.find_by_pacticipant_name_and_latest_tag(identifier_from_path[:pacticipant_name], identifier_from_path[:tag]) + else + @version ||= version_service.find_latest_by_pacticpant_name(identifier_from_path) + end + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb index 60f633bfd..6737ce9a3 100644 --- a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +++ b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb @@ -38,6 +38,7 @@ def process_post def pacts @pacts ||= pact_service.find_for_verification( provider_name, + parsed_query_params.provider_version_branch, parsed_query_params.provider_version_tags, parsed_query_params.consumer_version_selectors, { diff --git a/lib/pact_broker/api/resources/version.rb b/lib/pact_broker/api/resources/version.rb index cfd28aba4..d04d762c0 100644 --- a/lib/pact_broker/api/resources/version.rb +++ b/lib/pact_broker/api/resources/version.rb @@ -10,20 +10,32 @@ def content_types_provided [["application/hal+json", :to_json]] end + def content_types_accepted + [["application/json", :from_json]] + end + def allowed_methods - ["GET", "DELETE", "OPTIONS"] + ["GET", "PUT", "DELETE", "OPTIONS"] end def resource_exists? !!version end + def from_json + response_code = version ? 200 : 201 + parsed_version = Decorators::VersionDecorator.new(PactBroker::Domain::Version.new).from_json(request_body) + @version = version_service.create_or_update(pacticipant_name, pacticipant_version_number, parsed_version) + response.body = to_json + response_code + end + def to_json decorator_class(:version_decorator).new(version).to_json(decorator_options) end def delete_resource - version_service.delete version + version_service.delete(version) true end @@ -34,13 +46,7 @@ def policy_name private def version - if identifier_from_path[:tag] - @version ||= version_service.find_by_pacticipant_name_and_latest_tag(identifier_from_path[:pacticipant_name], identifier_from_path[:tag]) - elsif identifier_from_path[:pacticipant_version_number] - @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path) - else - @version ||= version_service.find_latest_by_pacticpant_name(identifier_from_path) - end + @version ||= version_service.find_by_pacticipant_name_and_number(identifier_from_path) end end end diff --git a/lib/pact_broker/doc/views/index/pacticipant-version.markdown b/lib/pact_broker/doc/views/index/pacticipant-version.markdown new file mode 100644 index 000000000..d9cbe5dee --- /dev/null +++ b/lib/pact_broker/doc/views/index/pacticipant-version.markdown @@ -0,0 +1,13 @@ +# Pacticipant version + +Allowed methods: `GET`, `PUT`, `DELETE` + +Path: `/pacticipants/{pacticipant}/versions/{version}` + +## Example + + PUT http://broker/pacticipants/Bar/versions/1e70030c6579915e5ff56b107a0fd25cf5df7464 + { + "branch": "main", + "buildUrl": "http://ci.mydomain/my-job/1" + } diff --git a/lib/pact_broker/domain/pacticipant.rb b/lib/pact_broker/domain/pacticipant.rb index 8cb454a18..1be083036 100644 --- a/lib/pact_broker/domain/pacticipant.rb +++ b/lib/pact_broker/domain/pacticipant.rb @@ -27,6 +27,10 @@ def label label_name filter = name_like(Sequel[:labels][:name], label_name) join(:labels, {pacticipant_id: :id}).where(filter) end + + def find_by_name(name) + where(name_like(:name, name)) + end end def before_destroy @@ -51,6 +55,10 @@ def validate messages << message('errors.validation.attribute_missing', attribute: 'name') unless name messages end + + def any_versions? + PactBroker::Domain::Version.where(pacticipant: self).any? + end end end end diff --git a/lib/pact_broker/domain/version.rb b/lib/pact_broker/domain/version.rb index dda352ced..f56193758 100644 --- a/lib/pact_broker/domain/version.rb +++ b/lib/pact_broker/domain/version.rb @@ -22,7 +22,7 @@ def latest? class Version < Sequel::Model plugin :timestamps, update_on_create: true - plugin :insert_ignore, identifying_columns: [:pacticipant_id, :number] + plugin :upsert, identifying_columns: [:pacticipant_id, :number] set_primary_key :id one_to_many :pact_publications, order: :revision_number, class: "PactBroker::Pacts::PactPublication", key: :consumer_version_id diff --git a/lib/pact_broker/pacts/pact_publication.rb b/lib/pact_broker/pacts/pact_publication.rb index 2b54d2d70..bb54f9319 100644 --- a/lib/pact_broker/pacts/pact_publication.rb +++ b/lib/pact_broker/pacts/pact_publication.rb @@ -4,6 +4,7 @@ require 'pact_broker/repositories/helpers' require 'pact_broker/integrations/integration' require 'pact_broker/tags/head_pact_tags' +require 'pact_broker/pacts/pact_publication_dataset_module' module PactBroker module Pacts @@ -27,86 +28,8 @@ class PactPublication < Sequel::Model(:pact_publications) dataset_module do include PactBroker::Repositories::Helpers + include PactPublicationDatasetModule - def remove_overridden_revisions - join(:latest_pact_publication_ids_for_consumer_versions, { Sequel[:lp][:pact_publication_id] => Sequel[:pact_publications][:id] }, { table_alias: :lp}) - end - - def join_consumer_versions(table_alias = :cv) - join(:versions, { Sequel[:pact_publications][:consumer_version_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) - end - - def join_consumer_version_tags(table_alias = :ct) - join(:tags, { Sequel[table_alias][:version_id] => Sequel[:pact_publications][:consumer_version_id]}, { table_alias: table_alias }) - end - - def join_consumer_version_tags_with_names(consumer_version_tag_names) - join(:tags, { - Sequel[:ct][:version_id] => Sequel[:pact_publications][:consumer_version_id], - Sequel[:ct][:name] => consumer_version_tag_names - }, { - table_alias: :ct - }) - end - - def join_providers(table_alias = :providers) - join(:pacticipants, { Sequel[:pact_publications][:provider_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) - end - - def join_consumers(table_alias = :consumers) - join(:pacticipants, { Sequel[:pact_publications][:consumer_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) - end - - def join_pact_versions - join(:pact_versions, { Sequel[:pact_publications][:pact_version_id] => Sequel[:pact_versions][:id] }) - end - - def eager_load_pact_versions - eager(:pact_versions) - end - - def tag tag_name - filter = name_like(Sequel.qualify(:tags, :name), tag_name) - join(:tags, {version_id: :consumer_version_id}).where(filter) - end - - def provider_name_like(name) - where(name_like(Sequel[:providers][:name], name)) - end - - def consumer_name_like(name) - where(name_like(Sequel[:consumers][:name], name)) - end - - def consumer_version_number_like(number) - where(name_like(Sequel[:cv][:number], number)) - end - - def consumer_version_tag(tag) - where(Sequel[:ct][:name] => tag) - end - - def order_by_consumer_name - order_append_ignore_case(Sequel[:consumers][:name]) - end - - def order_by_consumer_version_order - order_append(Sequel[:cv][:order]) - end - - def where_consumer_if_set(consumer) - if consumer - where(consumer: consumer) - else - self - end - end - - def delete - require 'pact_broker/webhooks/triggered_webhook' - PactBroker::Webhooks::TriggeredWebhook.where(pact_publication: self).delete - super - end end def before_create diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb new file mode 100644 index 000000000..a952bd42c --- /dev/null +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -0,0 +1,263 @@ +module PactBroker + module Pacts + module PactPublicationDatasetModule + def for_provider provider + where(provider: provider) + end + + def for_consumer consumer + where(consumer: consumer) + end + + def for_provider_and_consumer_version_selector provider, selector + # Does not yet support "all pacts for specified tag" - that code is still in the Pact::Repository + query = for_provider(provider) + query = query.for_consumer(PactBroker::Domain::Pacticipant.find_by_name(selector.consumer)) if selector.consumer + # Do this last so that the provider/consumer criteria get included in the "latest" query before the join, rather than after + query = query.latest_for_consumer_branch(selector.branch) if selector.latest_for_branch? + query = query.latest_for_consumer_tag(selector.tag) if selector.latest_for_tag? + query = query.overall_latest if selector.overall_latest? + query + end + + def latest_by_consumer_branch + versions_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:cv][:id] + } + + base_query = select_all_qualified + .select_append(Sequel[:cv][:branch], Sequel[:cv][:order]) + .remove_overridden_revisions + .join(:versions, versions_join, { table_alias: :cv }) do + Sequel.lit("cv.branch is not null") + end + + self_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:pp2][:consumer_id], + Sequel[:cv][:branch] => Sequel[:pp2][:branch] + } + + base_query.left_join(base_query, self_join, { table_alias: :pp2 } ) do + Sequel[:pp2][:order] > Sequel[:cv][:order] + end + .where(Sequel[:pp2][:order] => nil) + end + + def overall_latest + base_query = select_all_qualified + .select_append(Sequel[:cv][:order]) + .remove_overridden_revisions + .join_consumer_versions # won't need to do this when we add the order to latest_pact_publication_ids_for_consumer_versions + + self_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:pp2][:consumer_id], + Sequel[:pact_publications][:provider_id] => Sequel[:pp2][:provider_id] + } + + base_query.left_join(base_query, self_join, { table_alias: :pp2 } ) do + Sequel[:pp2][:order] > Sequel[:cv][:order] + end + .where(Sequel[:pp2][:order] => nil) + end + + def latest_for_consumer_branch(branch) + versions_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:cv][:id], + Sequel[:cv][:branch] => branch + } + + base_query = select_all_qualified + .select_append(Sequel[:cv][:branch], Sequel[:cv][:order]) + .remove_overridden_revisions + .join(:versions, versions_join, { table_alias: :cv }) do + Sequel.lit("cv.branch is not null") + end + + self_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:pp2][:consumer_id], + Sequel[:cv][:branch] => Sequel[:pp2][:branch] + } + + base_query.left_join(base_query, self_join, { table_alias: :pp2 } ) do + Sequel[:pp2][:order] > Sequel[:cv][:order] + end + .where(Sequel[:pp2][:order] => nil) + end + + def latest_by_consumer_tag + versions_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:cv][:id] + } + + tags_join = { + Sequel[:cv][:id] => Sequel[:tags][:version_id] + } + + base_query = select_all_qualified + .select_append(Sequel[:cv][:order], Sequel[:tags][:name].as(:tag_name)) + .remove_overridden_revisions + .join(:versions, versions_join, { table_alias: :cv }) + .join(:tags, tags_join) + + self_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:pp2][:consumer_id], + Sequel[:tags][:name] => Sequel[:pp2][:tag_name] + } + base_query.left_join(base_query, self_join, { table_alias: :pp2 } ) do + Sequel[:pp2][:order] > Sequel[:cv][:order] + end + .where(Sequel[:pp2][:order] => nil) + end + + def latest_for_consumer_tag(tag_name) + versions_join = { + Sequel[:pact_publications][:consumer_version_id] => Sequel[:cv][:id] + } + + tags_join = { + Sequel[:cv][:id] => Sequel[:tags][:version_id], + Sequel[:tags][:name] => tag_name + } + + base_query = select_all_qualified + .select_append(Sequel[:cv][:order], Sequel[:tags][:name].as(:tag_name)) + .remove_overridden_revisions + .join(:versions, versions_join, { table_alias: :cv }) + .join(:tags, tags_join) + .where(Sequel[:tags][:name] => tag_name) + + self_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:pp2][:consumer_id], + Sequel[:tags][:name] => Sequel[:pp2][:tag_name] + } + base_query.left_join(base_query, self_join, { table_alias: :pp2 } ) do + Sequel[:pp2][:order] > Sequel[:cv][:order] + end + .where(Sequel[:pp2][:order] => nil) + end + + def successfully_verified_by_provider_branch(provider_id, provider_version_branch) + verifications_join = { + pact_version_id: :pact_version_id, + Sequel[:verifications][:success] => true, + Sequel[:verifications][:wip] => false, + Sequel[:verifications][:provider_id] => provider_id + } + versions_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:provider_versions][:id], + Sequel[:provider_versions][:branch] => provider_version_branch, + Sequel[:provider_versions][:pacticipant_id] => provider_id + } + + from_self(alias: :pp).select(Sequel[:pp].*) + .join(:verifications, verifications_join) + .join(:versions, versions_join, { table_alias: :provider_versions } ) + .where(Sequel[:pp][:provider_id] => provider_id) + .distinct + end + + def successfully_verified_by_provider_tag(provider_id, provider_tag) + verifications_join = { + pact_version_id: :pact_version_id, + Sequel[:verifications][:success] => true, + Sequel[:verifications][:wip] => false, + Sequel[:verifications][:provider_id] => provider_id + } + tags_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], + Sequel[:provider_tags][:name] => provider_tag + } + + from_self(alias: :pp).select(Sequel[:pp].*) + .join(:verifications, verifications_join) + .join(:tags, tags_join, { table_alias: :provider_tags } ) + .where(Sequel[:pp][:provider_id] => provider_id) + .distinct + end + + def created_after date + where(Sequel.lit("#{first_source_alias}.created_at > ?", date)) + end + + def remove_overridden_revisions + join(:latest_pact_publication_ids_for_consumer_versions, { Sequel[:lp][:pact_publication_id] => Sequel[:pact_publications][:id] }, { table_alias: :lp}) + end + + def join_consumer_versions(table_alias = :cv) + join(:versions, { Sequel[:pact_publications][:consumer_version_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_consumer_version_tags(table_alias = :ct) + join(:tags, { Sequel[table_alias][:version_id] => Sequel[:pact_publications][:consumer_version_id]}, { table_alias: table_alias }) + end + + def join_consumer_version_tags_with_names(consumer_version_tag_names) + join(:tags, { + Sequel[:ct][:version_id] => Sequel[:pact_publications][:consumer_version_id], + Sequel[:ct][:name] => consumer_version_tag_names + }, { + table_alias: :ct + }) + end + + def join_providers(table_alias = :providers) + join(:pacticipants, { Sequel[:pact_publications][:provider_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_consumers(table_alias = :consumers) + join(:pacticipants, { Sequel[:pact_publications][:consumer_id] => Sequel[table_alias][:id] }, { table_alias: table_alias }) + end + + def join_pact_versions + join(:pact_versions, { Sequel[:pact_publications][:pact_version_id] => Sequel[:pact_versions][:id] }) + end + + def eager_load_pact_versions + eager(:pact_versions) + end + + def tag tag_name + filter = name_like(Sequel.qualify(:tags, :name), tag_name) + join(:tags, {version_id: :consumer_version_id}).where(filter) + end + + def provider_name_like(name) + where(name_like(Sequel[:providers][:name], name)) + end + + def consumer_name_like(name) + where(name_like(Sequel[:consumers][:name], name)) + end + + def consumer_version_number_like(number) + where(name_like(Sequel[:cv][:number], number)) + end + + def consumer_version_tag(tag) + where(Sequel[:ct][:name] => tag) + end + + def order_by_consumer_name + order_append_ignore_case(Sequel[:consumers][:name]) + end + + def order_by_consumer_version_order + order_append(Sequel[:cv][:order]) + end + + def where_consumer_if_set(consumer) + if consumer + where(consumer: consumer) + else + self + end + end + + def delete + require 'pact_broker/webhooks/triggered_webhook' + PactBroker::Webhooks::TriggeredWebhook.where(pact_publication: self).delete + super + end + end + end +end diff --git a/lib/pact_broker/pacts/pacts_for_verification_repository.rb b/lib/pact_broker/pacts/pacts_for_verification_repository.rb new file mode 100644 index 000000000..50e44b9ad --- /dev/null +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -0,0 +1,287 @@ +require 'sequel' +require 'pact_broker/logging' +require 'pact_broker/pacts/pact_publication' +require 'pact_broker/domain' +require 'pact_broker/pacts/verifiable_pact' +require 'pact_broker/repositories/helpers' +require 'pact_broker/pacts/selected_pact' +require 'pact_broker/pacts/selector' +require 'pact_broker/pacts/selectors' +require 'pact_broker/feature_toggle' + +module PactBroker + module Pacts + class PactsForVerificationRepository + include PactBroker::Logging + include PactBroker::Repositories + include PactBroker::Repositories::Helpers + + def find(provider_name, consumer_version_selectors) + selected_pacts = find_pacts_for_which_the_latest_version_of_something_is_required(provider_name, consumer_version_selectors) + + find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) + selected_pacts = selected_pacts + find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) + merge_selected_pacts(selected_pacts) + end + + # To find the work in progress pacts for this verification execution: + # For each provider tag that will be applied to this verification result (usually there will just be one, but + # we have to allow for multiple tags), + # find the head pacts (the pacts that are the latest for their tag) that have been successfully + # verified against the provider tag. + # Then, find all the head pacts, and remove the ones that have been successfully verified by ALL + # of the provider tags supplied, and the ones that were published before the include_wip_pacts_since date. + # Then, for all of the head pacts that are remaining (these are the WIP ones) work out which + # provider tags they are pending for. + # Don't include pact publications that were created before the provider tag was first used + # (that is, before the provider's git branch was created). + def find_wip provider_name, provider_version_branch, provider_tags_names = [], options = {} + # TODO not sure about this + return [] if provider_tags_names.empty? && provider_version_branch == nil + + if provider_version_branch + return find_wip_pact_versions_for_provider_by_provider_branch(provider_name, provider_version_branch, options) + end + + provider = pacticipant_repository.find_by_name(provider_name) + wip_start_date = options.fetch(:include_wip_pacts_since) + provider_tags = provider_tag_objects_for(provider, provider_tags_names) + + wip_by_consumer_tags = find_wip_pact_versions_for_provider_by_provider_tags( + provider, + provider_tags_names, + provider_tags, + wip_start_date, + :latest_by_consumer_tag) + + wip_by_consumer_branches = find_wip_pact_versions_for_provider_by_provider_tags( + provider, + provider_tags_names, + provider_tags, + wip_start_date, + :latest_by_consumer_branch) + + deduplicate_verifiable_pacts(wip_by_consumer_tags + wip_by_consumer_branches).sort + end + + private + + def scope_for(scope) + PactBroker.policy_scope!(scope) + end + + # For the times when it doesn't make sense to use the scoped class, this is a way to + # indicate that it is an intentional use of the PactVersion class directly. + def unscoped(scope) + scope + end + + # Note: created_at is coming back as a string for sqlite + # Can't work out how to to tell Sequel that this should be a date + def to_datetime string_or_datetime + if string_or_datetime.is_a?(String) + Sequel.string_to_datetime(string_or_datetime) + else + string_or_datetime + end + end + + def find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) + # TODO at the moment, the validation doesn't stop fallback being used with 'all' selectors + selectors_with_fallback_tags = consumer_version_selectors.select(&:fallback_tag?) + selectors_missing_a_pact = selectors_with_fallback_tags.reject do | selector | + selected_pacts.any? do | selected_pact | + selected_pact.latest_for_tag?(selector.tag) + end + end + + if selectors_missing_a_pact.any? + find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors_missing_a_pact) + else + [] + end + end + + def find_pacts_for_which_the_latest_version_of_something_is_required(provider_name, consumer_version_selectors) + provider = pacticipant_repository.find_by_name(provider_name) + + selectors = if consumer_version_selectors.empty? + Selectors.create_for_overall_latest + else + consumer_version_selectors.select(&:latest_for_tag?) + + consumer_version_selectors.select(&:latest_for_branch?) + + consumer_version_selectors.select(&:overall_latest?) + end + + selectors.flat_map do | selector | + query = scope_for(PactPublication).for_provider_and_consumer_version_selector(provider, selector) + query.all.collect do | pact_publication | + SelectedPact.new( + pact_publication.to_domain, + Selectors.new(selector.resolve(pact_publication.consumer_version)) + ) + end + end + end + + def find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors) + selectors.collect do | selector | + query = scope_for(LatestTaggedPactPublications).provider(provider_name).where(tag_name: selector.fallback_tag) + query = query.consumer(selector.consumer) if selector.consumer + query.all + .collect do | latest_tagged_pact_publication | + pact_publication = unscoped(PactPublication).find(id: latest_tagged_pact_publication.id) + SelectedPact.new( + pact_publication.to_domain, + Selectors.new(selector.resolve_for_fallback(pact_publication.consumer_version)) + ) + end + end.flatten + end + + + def find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) + # The tags for which all versions are specified + selectors = consumer_version_selectors.select(&:all_for_tag?) + + selectors.flat_map do | selector | + find_all_pact_versions_for_provider_with_consumer_version_tags(provider_name, selector) + end + end + + def find_provider_tags_for_which_pact_publication_id_is_pending(pact_publication, successfully_verified_head_pact_publication_ids_for_each_provider_tag) + successfully_verified_head_pact_publication_ids_for_each_provider_tag + .select do | _, pact_publication_ids | + !pact_publication_ids.include?(pact_publication.id) + end.keys + end + + def find_provider_tag_names_that_were_first_used_before_pact_published(pact_publication, provider_tag_collection) + provider_tag_collection.select { | tag| to_datetime(tag.created_at) < pact_publication.created_at }.collect(&:name) + end + + def deduplicate_verifiable_pacts(verifiable_pacts) + VerifiablePact.deduplicate(verifiable_pacts) + end + + def merge_selected_pacts(selected_pacts) + selected_pacts + .group_by{ |p| [p.consumer_name, p.pact_version_sha] } + .values + .collect do | selected_pacts_for_pact_version_id | + SelectedPact.merge(selected_pacts_for_pact_version_id) + end + end + + def provider_tag_objects_for(provider, provider_tags_names) + PactBroker::Domain::Tag + .select_group(Sequel[:tags][:name], Sequel[:pacticipant_id]) + .select_append(Sequel.function(:min, Sequel[:tags][:created_at]).as(:created_at)) + .distinct + .join(:versions, { Sequel[:tags][:version_id] => Sequel[:versions][:id] } ) + .where(pacticipant_id: provider.id) + .where(name: provider_tags_names) + .all + end + + def find_wip_pact_versions_for_provider_by_provider_tags(provider, provider_tags_names, provider_tags, wip_start_date, pact_publication_scope) + potential_wip_pacts_by_consumer_tag_query = PactPublication.for_provider(provider).created_after(wip_start_date).send(pact_publication_scope) + potential_wip_pacts_by_consumer_tag = potential_wip_pacts_by_consumer_tag_query.all + + tag_to_pact_publications = provider_tags_names.each_with_object({}) do | provider_tag_name, tag_to_pact_publications | + tag_to_pact_publications[provider_tag_name] = remove_already_verified_by_tag( + potential_wip_pacts_by_consumer_tag, + potential_wip_pacts_by_consumer_tag_query, + provider, + provider_tag_name + ) + end + + provider_has_no_versions = !provider.any_versions? + + tag_to_pact_publications.flat_map do | provider_tag_name, pact_publications | + pact_publications.collect do | pact_publication | + force_include = PactBroker.feature_enabled?(:experimental_no_provider_versions_makes_all_head_pacts_wip) && provider_has_no_versions + + pending_tag_names_to_use = if force_include + [provider_tag_name] + else + pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact_publication, provider_tags) + [provider_tag_name] & pre_existing_tag_names + end + + if pending_tag_names_to_use.any? + selectors = create_selectors_for_wip_pact(pact_publication) + VerifiablePact.create_for_wip_for_provider_tags(pact_publication.to_domain, selectors, pending_tag_names_to_use) + end + end + end.compact + end + + def create_selectors_for_wip_pact(pact_publication) + if pact_publication.values[:tag_name] + Selectors.create_for_latest_for_tag(pact_publication.values[:tag_name]) + else + Selectors.create_for_latest_for_branch(pact_publication.consumer_version.branch) + end + end + + def find_wip_pact_versions_for_provider_by_provider_branch(provider_name, provider_version_branch, options) + provider = pacticipant_repository.find_by_name(provider_name) + wip_start_date = options.fetch(:include_wip_pacts_since) + + wip_pact_publications_by_branch = remove_already_verified_by_branch( + PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_branch, + provider, + provider_version_branch + ) + + wip_pact_publications_by_tag = remove_already_verified_by_branch( + PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_tag, + provider, + provider_version_branch + ) + + verifiable_pacts = (wip_pact_publications_by_branch + wip_pact_publications_by_tag).collect do | pact_publication | + selectors = create_selectors_for_wip_pact(pact_publication) + VerifiablePact.create_for_wip_for_provider_branch(pact_publication.to_domain, selectors, provider_version_branch) + end + + deduplicate_verifiable_pacts(verifiable_pacts).sort + end + + def find_all_pact_versions_for_provider_with_consumer_version_tags provider_name, selector + provider = pacticipant_repository.find_by_name(provider_name) + consumer = selector.consumer ? pacticipant_repository.find_by_name(selector.consumer) : nil + + scope_for(PactPublication) + .select_all_qualified + .select_append(Sequel[:cv][:order].as(:consumer_version_order)) + .select_append(Sequel[:ct][:name].as(:consumer_version_tag_name)) + .remove_overridden_revisions + .join_consumer_versions(:cv) + .join_consumer_version_tags_with_names(selector.tag) + .where(provider: provider) + .where_consumer_if_set(consumer) + .eager(:consumer) + .eager(:consumer_version) + .eager(:provider) + .eager(:pact_version) + .all + .group_by(&:pact_version_id) + .values + .collect do | pact_publications | + latest_pact_publication = pact_publications.sort_by{ |p| p.values.fetch(:consumer_version_order) }.last + SelectedPact.new(latest_pact_publication.to_domain, Selectors.new(selector).resolve(latest_pact_publication.consumer_version)) + end + end + + def remove_already_verified_by_branch(pact_publications, provider, provider_version_branch) + pact_publications.all - pact_publications.successfully_verified_by_provider_branch(provider.id, provider_version_branch).all + end + + def remove_already_verified_by_tag(pact_publications, query, provider, tag) + pact_publications - query.successfully_verified_by_provider_tag(provider.id, tag).all + end + end + end +end diff --git a/lib/pact_broker/pacts/repository.rb b/lib/pact_broker/pacts/repository.rb index c74416c58..b3b24d273 100644 --- a/lib/pact_broker/pacts/repository.rb +++ b/lib/pact_broker/pacts/repository.rb @@ -19,11 +19,11 @@ require 'pact_broker/pacts/selector' require 'pact_broker/pacts/selectors' require 'pact_broker/feature_toggle' +require 'pact_broker/pacts/pacts_for_verification_repository' module PactBroker module Pacts class Repository - include PactBroker::Logging include PactBroker::Repositories include PactBroker::Repositories::Helpers @@ -146,91 +146,12 @@ def find_latest_pact_versions_for_provider provider_name, tag = nil end end - def find_all_pact_versions_for_provider_with_consumer_version_tags provider_name, selector - provider = pacticipant_repository.find_by_name(provider_name) - consumer = selector.consumer ? pacticipant_repository.find_by_name(selector.consumer) : nil - - scope_for(PactPublication) - .select_all_qualified - .select_append(Sequel[:cv][:order].as(:consumer_version_order)) - .select_append(Sequel[:ct][:name].as(:consumer_version_tag_name)) - .remove_overridden_revisions - .join_consumer_versions(:cv) - .join_consumer_version_tags_with_names(selector.tag) - .where(provider: provider) - .where_consumer_if_set(consumer) - .eager(:consumer) - .eager(:consumer_version) - .eager(:provider) - .eager(:pact_version) - .all - .group_by(&:pact_version_id) - .values - .collect do | pact_publications | - latest_pact_publication = pact_publications.sort_by{ |p| p.values.fetch(:consumer_version_order) }.last - SelectedPact.new(latest_pact_publication.to_domain, Selectors.new(selector).resolve(latest_pact_publication.consumer_version)) - end + def find_for_verification(provider_name, consumer_version_selectors) + PactsForVerificationRepository.new.find(provider_name, consumer_version_selectors) end - # To find the work in progress pacts for this verification execution: - # For each provider tag that will be applied to this verification result (usually there will just be one, but - # we have to allow for multiple tags), - # find the head pacts (the pacts that are the latest for their tag) that have been successfully - # verified against the provider tag. - # Then, find all the head pacts, and remove the ones that have been successfully verified by ALL - # of the provider tags supplied, and the ones that were published before the include_wip_pacts_since date. - # Then, for all of the head pacts that are remaining (these are the WIP ones) work out which - # provider tags they are pending for. - # Don't include pact publications that were created before the provider tag was first used - # (that is, before the provider's git branch was created). - def find_wip_pact_versions_for_provider provider_name, provider_tags_names = [], options = {} - # TODO not sure about this - return [] if provider_tags_names.empty? - - provider = pacticipant_repository.find_by_name(provider_name) - - # Hash of provider tag name => list of head pacts that have been successfully verified by that tag - successfully_verified_head_pacts_for_provider_tags = find_successfully_verified_head_pacts_by_provider_tag(provider.id, provider_tags_names, options) - # Create hash of provider tag name => list of pact publication ids - successfully_verified_head_pact_publication_ids_for_each_provider_tag = successfully_verified_head_pacts_for_provider_tags.each_with_object({}) do | (provider_tag_name, head_pacts), hash | - hash[provider_tag_name] = head_pacts.collect(&:id).uniq - end - - # list of head pact_publication_ids that are NOT work in progress because they've been verified by all of the provider version tags supplied - non_wip_pact_publication_ids = successfully_verified_head_pacts_for_provider_tags.values.collect{ |head_pacts| head_pacts.collect(&:id) }.reduce(:&) - - wip_pact_publication_ids = find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags( - provider.id, - non_wip_pact_publication_ids, - options) - - wip_pacts = scope_for(PactPublication).where(id: wip_pact_publication_ids) - - # The first instance (by date) of each provider tag with that name - provider_tag_collection = PactBroker::Domain::Tag - .select_group(Sequel[:tags][:name], Sequel[:pacticipant_id]) - .select_append(Sequel.function(:min, Sequel[:tags][:created_at]).as(:created_at)) - .distinct - .join(:versions, { Sequel[:tags][:version_id] => Sequel[:versions][:id] } ) - .where(pacticipant_id: provider.id) - .where(name: provider_tags_names) - .all - - provider_version_count = scope_for(PactBroker::Domain::Version).where(pacticipant: provider).count - - verifiable_pacts = wip_pacts.collect do | pact| - pending_tag_names = find_provider_tags_for_which_pact_publication_id_is_pending(pact, successfully_verified_head_pact_publication_ids_for_each_provider_tag) - pre_existing_tag_names = find_provider_tag_names_that_were_first_used_before_pact_published(pact, provider_tag_collection) - - pre_existing_pending_tags = pending_tag_names & pre_existing_tag_names - - if pre_existing_pending_tags.any? || (PactBroker.feature_enabled?(:experimental_no_provider_versions_makes_all_head_pacts_wip) && provider_version_count == 0) - selectors = Selectors.create_for_latest_of_each_tag(pact.head_tag_names) - VerifiablePact.new(pact.to_domain, selectors, true, pre_existing_pending_tags, [], true) - end - end.compact.sort - - deduplicate_verifiable_pacts(verifiable_pacts) + def find_wip_pact_versions_for_provider provider_name, provider_version_branch, provider_tags_names = [], options = {} + PactsForVerificationRepository.new.find_wip(provider_name, provider_version_branch, provider_tags_names, options) end def find_pact_versions_for_provider provider_name, tag = nil @@ -389,110 +310,8 @@ def find_previous_pacts pact end end - # Returns a list of Domain::Pact objects the represent pact publications - def find_for_verification(provider_name, consumer_version_selectors) - selected_pacts = find_pacts_for_which_the_latest_version_is_required(provider_name, consumer_version_selectors) + - find_pacts_for_which_the_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) + - find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) - - selected_pacts = selected_pacts + find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) - - selected_pacts - .group_by{ |p| [p.consumer_name, p.pact_version_sha] } - .values - .collect do | selected_pacts_for_pact_version_id | - SelectedPact.merge(selected_pacts_for_pact_version_id) - end - end - private - def find_pacts_for_fallback_tags(selected_pacts, provider_name, consumer_version_selectors) - # TODO at the moment, the validation doesn't stop fallback being used with 'all' selectors - selectors_with_fallback_tags = consumer_version_selectors.select(&:fallback_tag?) - selectors_missing_a_pact = selectors_with_fallback_tags.reject do | selector | - selected_pacts.any? do | selected_pact | - selected_pact.latest_for_tag?(selector.tag) - end - end - - if selectors_missing_a_pact.any? - find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors_missing_a_pact) - else - [] - end - end - - def find_pacts_for_which_the_latest_version_is_required(provider_name, consumer_version_selectors) - if consumer_version_selectors.empty? - scope_for(LatestPactPublications) - .provider(provider_name) - .order_ignore_case(:consumer_name) - .collect do | latest_pact_publication | - pact_publication = PactPublication.find(id: latest_pact_publication.id) - SelectedPact.new(pact_publication.to_domain, Selectors.create_for_overall_latest.resolve(pact_publication.consumer_version)) - end - else - selectors_for_overall_latest = consumer_version_selectors.select(&:overall_latest?) - selectors_for_overall_latest.flat_map do | selector | - query = scope_for(LatestPactPublications).provider(provider_name) - query = query.consumer(selector.consumer) if selector.consumer - query.collect do | latest_pact_publication | - pact_publication = PactPublication.find(id: latest_pact_publication.id) - resolved_selector = selector.consumer ? Selector.latest_for_consumer(selector.consumer) : Selector.overall_latest - SelectedPact.new(pact_publication.to_domain, Selectors.new(resolved_selector).resolve(pact_publication.consumer_version)) - end - end - end - end - - def find_pacts_for_which_the_latest_version_for_the_tag_is_required(provider_name, consumer_version_selectors) - # The tags for which only the latest version is specified - selectors = consumer_version_selectors.select(&:latest_for_tag?) - - selectors.flat_map do | selector | - query = scope_for(LatestTaggedPactPublications).where(tag_name: selector.tag).provider(provider_name) - query = query.consumer(selector.consumer) if selector.consumer - query.all.collect do | latest_tagged_pact_publication | - pact_publication = PactPublication.find(id: latest_tagged_pact_publication.id) - resolved_selector = if selector.consumer - Selector.latest_for_tag_and_consumer(selector.tag, selector.consumer).resolve(pact_publication.consumer_version) - else - Selector.latest_for_tag(selector.tag).resolve(pact_publication.consumer_version) - end - SelectedPact.new( - pact_publication.to_domain, - Selectors.new(resolved_selector) - ) - end - end - end - - def find_pacts_for_which_the_latest_version_for_the_fallback_tag_is_required(provider_name, selectors) - selectors.collect do | selector | - query = scope_for(LatestTaggedPactPublications).provider(provider_name).where(tag_name: selector.fallback_tag) - query = query.consumer(selector.consumer) if selector.consumer - query.all - .collect do | latest_tagged_pact_publication | - pact_publication = unscoped(PactPublication).find(id: latest_tagged_pact_publication.id) - SelectedPact.new( - pact_publication.to_domain, - Selectors.new(selector.resolve(pact_publication.consumer_version)) - ) - end - end.flatten - end - - - def find_pacts_for_which_all_versions_for_the_tag_are_required(provider_name, consumer_version_selectors) - # The tags for which all versions are specified - selectors = consumer_version_selectors.select(&:all_for_tag?) - - selectors.flat_map do | selector | - find_all_pact_versions_for_provider_with_consumer_version_tags(provider_name, selector) - end - end - def find_previous_distinct_pact_by_sha pact current_pact_content_sha = scope_for(LatestPactPublicationsByConsumerVersion).select(:pact_version_sha) @@ -541,17 +360,6 @@ def find_all_database_versions_between(consumer_name, options, base_class = Late query end - def find_provider_tags_for_which_pact_publication_id_is_pending(pact_publication, successfully_verified_head_pact_publication_ids_for_each_provider_tag) - successfully_verified_head_pact_publication_ids_for_each_provider_tag - .select do | _, pact_publication_ids | - !pact_publication_ids.include?(pact_publication.id) - end.keys - end - - def find_provider_tag_names_that_were_first_used_before_pact_published(pact_publication, provider_tag_collection) - provider_tag_collection.select { | tag| to_datetime(tag.created_at) < pact_publication.created_at }.collect(&:name) - end - # Note: created_at is coming back as a string for sqlite # Can't work out how to to tell Sequel that this should be a date def to_datetime string_or_datetime @@ -561,49 +369,6 @@ def to_datetime string_or_datetime string_or_datetime end end - - def find_head_pacts_that_have_not_been_successfully_verified_by_all_provider_tags(provider_id, pact_publication_ids_successfully_verified_by_all_provider_tags, options) - # Exclude the head pacts that have been successfully verified by all the specified provider tags - scope_for(LatestTaggedPactPublications) - .where(provider_id: provider_id) - .where(Sequel.lit('latest_tagged_pact_publications.created_at > ?', options.fetch(:include_wip_pacts_since))) - .exclude(id: pact_publication_ids_successfully_verified_by_all_provider_tags) - .select_for_subquery(:id) - end - - # Find the head pacts that have been successfully verified by a provider version with the specified - # provider version tags. - # Returns a Hash of provider_tag => LatestTaggedPactPublications with only pact publication id and tag_name populated - # This is the list of pacts we are EXCLUDING from the WIP list because they have already been verified successfully - def find_successfully_verified_head_pacts_by_provider_tag(provider_id, provider_tags, options) - provider_tags.compact.each_with_object({}) do | provider_tag, hash | - verifications_join = { - pact_version_id: :pact_version_id, - Sequel[:verifications][:success] => true, - Sequel[:verifications][:wip] => false, - Sequel[:verifications][:provider_id] => provider_id - } - tags_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], - Sequel[:provider_tags][:name] => provider_tag - } - head_pacts = scope_for(LatestTaggedPactPublications) - .select(Sequel[:latest_tagged_pact_publications][:id].as(:id)) - .join(:verifications, verifications_join) - .join(:tags, tags_join, { table_alias: :provider_tags } ) - .where(Sequel[:latest_tagged_pact_publications][:provider_id] => provider_id) - .distinct - .all - hash[provider_tag] = head_pacts - end - end - - def deduplicate_verifiable_pacts(verifiable_pacts) - verifiable_pacts - .group_by { | verifiable_pact | verifiable_pact.pact_version_sha } - .values - .collect { | verifiable_pacts | verifiable_pacts.reduce(&:+) } - end end end end diff --git a/lib/pact_broker/pacts/selected_pact.rb b/lib/pact_broker/pacts/selected_pact.rb index 74fc71dde..78fddac5a 100644 --- a/lib/pact_broker/pacts/selected_pact.rb +++ b/lib/pact_broker/pacts/selected_pact.rb @@ -21,6 +21,10 @@ def tag_names_of_selectors_for_latest_pacts selectors.tag_names_of_selectors_for_latest_pacts end + def branches_of_selectors_for_latest_pacts + selectors.branches_of_selectors_for_latest_pacts + end + def overall_latest? selectors.overall_latest? end diff --git a/lib/pact_broker/pacts/selector.rb b/lib/pact_broker/pacts/selector.rb index feb8a7af0..640498d51 100644 --- a/lib/pact_broker/pacts/selector.rb +++ b/lib/pact_broker/pacts/selector.rb @@ -1,11 +1,19 @@ +require 'pact_broker/hash_refinements' + module PactBroker module Pacts class Selector < Hash + using PactBroker::HashRefinements + def initialize(options = {}) merge!(options) end def resolve(consumer_version) + ResolvedSelector.new(self.to_h.without(:fallback_tag, :fallback_branch), consumer_version) + end + + def resolve_for_fallback(consumer_version) ResolvedSelector.new(self.to_h, consumer_version) end @@ -13,6 +21,10 @@ def tag= tag self[:tag] = tag end + def branch= branch + self[:branch] = branch + end + def latest= latest self[:latest] = latest end @@ -25,10 +37,18 @@ def fallback_tag= fallback_tag self[:fallback_tag] = fallback_tag end + def fallback_branch= fallback_branch + self[:fallback_branch] = fallback_branch + end + def fallback_tag self[:fallback_tag] end + def fallback_branch + self[:fallback_branch] + end + def consumer= consumer self[:consumer] = consumer end @@ -45,10 +65,18 @@ def self.latest_for_tag(tag) Selector.new(latest: true, tag: tag) end + def self.latest_for_branch(branch) + Selector.new(latest: true, branch: branch) + end + def self.latest_for_tag_with_fallback(tag, fallback_tag) Selector.new(latest: true, tag: tag, fallback_tag: fallback_tag) end + def self.latest_for_branch_with_fallback(branch, fallback_branch) + Selector.new(latest: true, branch: branch, fallback_branch: fallback_branch) + end + def self.all_for_tag(tag) Selector.new(tag: tag) end @@ -61,6 +89,10 @@ def self.latest_for_tag_and_consumer(tag, consumer) Selector.new(latest: true, tag: tag, consumer: consumer) end + def self.latest_for_branch_and_consumer(branch, consumer) + Selector.new(latest: true, branch: branch, consumer: consumer) + end + def self.latest_for_consumer(consumer) Selector.new(latest: true, consumer: consumer) end @@ -73,12 +105,20 @@ def fallback_tag? !!fallback_tag end + def fallback_branch? + !!fallback_branch + end + def tag self[:tag] end + def branch + self[:branch] + end + def overall_latest? - !!(latest? && !tag) + !!(latest? && !tag && !branch) end # Not sure if the fallback_tag logic is needed @@ -90,6 +130,15 @@ def latest_for_tag? potential_tag = nil end end + # Not sure if the fallback_tag logic is needed + def latest_for_branch? potential_branch = nil + if potential_branch + !!(latest && branch == potential_branch) + else + !!(latest && !!branch) + end + end + def all_for_tag_and_consumer? !!(tag && !latest? && consumer) end @@ -109,6 +158,12 @@ def <=> other else overall_latest? ? -1 : 1 end + elsif latest_for_branch? || other.latest_for_branch? + if latest_for_branch? == other.latest_for_branch? + branch <=> other.branch + else + latest_for_branch? ? -1 : 1 + end elsif latest_for_tag? || other.latest_for_tag? if latest_for_tag? == other.latest_for_tag? tag <=> other.tag diff --git a/lib/pact_broker/pacts/selectors.rb b/lib/pact_broker/pacts/selectors.rb index 592131842..d40e400c1 100644 --- a/lib/pact_broker/pacts/selectors.rb +++ b/lib/pact_broker/pacts/selectors.rb @@ -15,6 +15,18 @@ def self.create_for_latest_of_each_tag(tag_names) Selectors.new(tag_names.collect{ | tag_name | Selector.latest_for_tag(tag_name) }) end + def self.create_for_latest_for_tag(tag_name) + Selectors.new([Selector.latest_for_tag(tag_name)]) + end + + def self.create_for_latest_of_each_branch(branches) + Selectors.new(branches.collect{ | branch | Selector.latest_for_branch(branch) }) + end + + def self.create_for_latest_for_branch(branch) + Selectors.new([Selector.latest_for_branch(branch)]) + end + def self.create_for_overall_latest Selectors.new([Selector.overall_latest]) end @@ -42,6 +54,10 @@ def tag_names_of_selectors_for_all_pacts def tag_names_of_selectors_for_latest_pacts select(&:latest_for_tag?).collect(&:tag).uniq end + + def branches_of_selectors_for_latest_pacts + select(&:latest_for_branch?).collect(&:branch).uniq + end end end end diff --git a/lib/pact_broker/pacts/service.rb b/lib/pact_broker/pacts/service.rb index b6456b7f2..8abb6fa12 100644 --- a/lib/pact_broker/pacts/service.rb +++ b/lib/pact_broker/pacts/service.rb @@ -85,10 +85,6 @@ def find_latest_pact_versions_for_provider provider_name, options = {} pact_repository.find_latest_pact_versions_for_provider provider_name, options[:tag] end - def find_wip_pact_versions_for_provider provider_name - pact_repository.find_wip_pact_versions_for_provider provider_name - end - def find_pact_versions_for_provider provider_name, options = {} pact_repository.find_pact_versions_for_provider provider_name, options[:tag] end @@ -115,7 +111,7 @@ def find_distinct_pacts_between consumer, options distinct end - def find_for_verification(provider_name, provider_version_tags, consumer_version_selectors, options) + def find_for_verification(provider_name, provider_version_branch, provider_version_tags, consumer_version_selectors, options) verifiable_pacts_specified_in_request = pact_repository .find_for_verification(provider_name, consumer_version_selectors) .collect do | selected_pact | @@ -124,7 +120,7 @@ def find_for_verification(provider_name, provider_version_tags, consumer_version verifiable_wip_pacts = if options[:include_wip_pacts_since] exclude_specified_pacts( - pact_repository.find_wip_pact_versions_for_provider(provider_name, provider_version_tags, options), + pact_repository.find_wip_pact_versions_for_provider(provider_name, provider_version_branch, provider_version_tags, options), verifiable_pacts_specified_in_request) else [] diff --git a/lib/pact_broker/pacts/squash_pacts_for_verification.rb b/lib/pact_broker/pacts/squash_pacts_for_verification.rb index 6c82f63f3..43b617297 100644 --- a/lib/pact_broker/pacts/squash_pacts_for_verification.rb +++ b/lib/pact_broker/pacts/squash_pacts_for_verification.rb @@ -29,10 +29,7 @@ def self.call(provider_version_tags, selected_pact, include_pending_status = fal else VerifiablePact.new( domain_pact, - selected_pact.selectors, - nil, - [], - [] + selected_pact.selectors ) end end diff --git a/lib/pact_broker/pacts/verifiable_pact.rb b/lib/pact_broker/pacts/verifiable_pact.rb index 2a9e74004..92e8f2a6b 100644 --- a/lib/pact_broker/pacts/verifiable_pact.rb +++ b/lib/pact_broker/pacts/verifiable_pact.rb @@ -4,18 +4,34 @@ module PactBroker module Pacts class VerifiablePact < SimpleDelegator - attr_reader :selectors, :pending, :pending_provider_tags, :non_pending_provider_tags, :wip + attr_reader :selectors, :pending, :pending_provider_tags, :non_pending_provider_tags, :provider_branch, :wip # TODO refactor this constructor - def initialize(pact, selectors, pending, pending_provider_tags = [], non_pending_provider_tags = [], wip = false) + def initialize(pact, selectors, pending = nil, pending_provider_tags = [], non_pending_provider_tags = [], provider_branch = nil, wip = false) super(pact) @pending = pending @selectors = selectors @pending_provider_tags = pending_provider_tags @non_pending_provider_tags = non_pending_provider_tags + @provider_branch = provider_branch @wip = wip end + def self.create_for_wip_for_provider_branch(pact, selectors, provider_branch) + new(pact, selectors, true, [], [], provider_branch, true) + end + + def self.create_for_wip_for_provider_tags(pact, selectors, pending_provider_tags) + new(pact, selectors, true, pending_provider_tags, [], nil, true) + end + + def self.deduplicate(verifiable_pacts) + verifiable_pacts + .group_by { | verifiable_pact | verifiable_pact.pact_version_sha } + .values + .collect { | verifiable_pacts | verifiable_pacts.reduce(&:+) } + end + def pending? pending end @@ -29,6 +45,10 @@ def + other raise PactBroker::Error.new("Can't merge two verifiable pacts with different pact content") end + if provider_branch != other.provider_branch + raise PactBroker::Error.new("Can't merge two verifiable pacts with different provider_branch") + end + latest_pact = [self, other].sort_by(&:consumer_version_order).last.__getobj__() VerifiablePact.new( @@ -37,6 +57,7 @@ def + other pending || other.pending, pending_provider_tags + other.pending_provider_tags, non_pending_provider_tags + other.non_pending_provider_tags, + provider_branch, wip || other.wip ) end diff --git a/lib/pact_broker/pacts/verifiable_pact_messages.rb b/lib/pact_broker/pacts/verifiable_pact_messages.rb index 6b29b40da..634693aff 100644 --- a/lib/pact_broker/pacts/verifiable_pact_messages.rb +++ b/lib/pact_broker/pacts/verifiable_pact_messages.rb @@ -8,7 +8,7 @@ class VerifiablePactMessages READ_MORE_PENDING = "Read more at https://pact.io/pending" READ_MORE_WIP = "Read more at https://pact.io/wip" - delegate [:consumer_name, :provider_name, :consumer_version_number, :pending_provider_tags, :non_pending_provider_tags, :pending?, :wip?] => :verifiable_pact + delegate [:consumer_name, :provider_name, :consumer_version_number, :pending_provider_tags, :non_pending_provider_tags, :provider_branch, :pending?, :wip?] => :verifiable_pact def initialize(verifiable_pact, pact_version_url) @verifiable_pact = verifiable_pact @@ -16,20 +16,20 @@ def initialize(verifiable_pact, pact_version_url) end def pact_description - position_descs = if head_consumer_tags.empty? + position_descs = if head_consumer_tags.empty? && branches.empty? ["latest"] else - head_consumer_tags.collect { | tag | "latest #{tag}"} + head_consumer_tags.collect { |tag| "latest with tag #{tag}" } + branches.collect{ |branch| "latest from branch #{branch}" } end - "Pact between #{consumer_name} and #{provider_name}, consumer version #{consumer_version_number}, #{position_descs.join(",")}" + "Pact between #{consumer_name} and #{provider_name}, consumer version #{consumer_version_number}, #{position_descs.join(", ")}" end def inclusion_reason - version_text = head_consumer_tags.size == 1 ? "version" : "versions" + version_text = head_consumer_tags.size == 1 || branches.size == 1 ? "version" : "versions" if wip? # WIP pacts will always have tags, because it is part of the definition of being a WIP pact - "The pact at #{pact_version_url} is being verified because it is a 'work in progress' pact (ie. it is the pact for the latest #{version_text} of #{consumer_name} tagged with #{joined_head_consumer_tags} and is still in pending state). #{READ_MORE_WIP}" + "The pact at #{pact_version_url} is being verified because it is a 'work in progress' pact (ie. it is the pact for the latest #{version_text} of #{consumer_name} #{joined_head_consumer_tags_and_branches} and is still in pending state). #{READ_MORE_WIP}" else criteria_or_criterion = selectors.size > 1 ? "criteria" : "criterion" "The pact at #{pact_version_url} is being verified because it matches the following configured selection #{criteria_or_criterion}: #{selector_descriptions}#{same_content_note}" @@ -38,27 +38,27 @@ def inclusion_reason def pending_reason if pending? - "This pact is in pending state for this version of #{provider_name} because a successful verification result for #{pending_provider_tags_description("a")} has not yet been published. If this verification fails, it will not cause the overall build to fail. #{READ_MORE_PENDING}" + "This pact is in pending state for this version of #{provider_name} because a successful verification result for #{pending_provider_branch_or_tags_description("a")} has not yet been published. If this verification fails, it will not cause the overall build to fail. #{READ_MORE_PENDING}" else - "This pact has previously been successfully verified by #{non_pending_provider_tags_description}. If this verification fails, it will fail the build. #{READ_MORE_PENDING}" + "This pact has previously been successfully verified by #{non_pending_provider_branch_or_tags_description}. If this verification fails, it will fail the build. #{READ_MORE_PENDING}" end end def verification_success_true_published_false if pending? - "This pact is still in pending state for #{pending_provider_tags_description} as the successful verification results #{with_these_tags}have not yet been published." + "This pact is still in pending state for #{pending_provider_branch_or_tags_description} as the successful verification results #{with_these_tags}have not yet been published." end end def verification_success_true_published_true if pending? - "This pact is no longer in pending state for #{pending_provider_tags_description}, as a successful verification result #{with_these_tags}has been published. If a verification for a version of #{provider_name} #{with_these_tags}fails in the future, it will fail the build. #{READ_MORE_PENDING}" + "This pact is no longer in pending state for #{pending_provider_branch_or_tags_description}, as a successful verification result #{with_these_tags}has been published. If a verification for a version of #{provider_name} #{with_these_tags}fails in the future, it will fail the build. #{READ_MORE_PENDING}" end end def verification_success_false_published_false if pending? - "This pact is still in pending state for #{pending_provider_tags_description} as a successful verification result #{with_these_tags}has not yet been published" + "This pact is still in pending state for #{pending_provider_branch_or_tags_description} as a successful verification result #{with_these_tags}has not yet been published" end end @@ -85,8 +85,11 @@ def join(list, last_joiner = " and ") end end - def joined_head_consumer_tags - join(head_consumer_tags) + same_content_note + def joined_head_consumer_tags_and_branches + parts = [] + parts << "from #{branches.size == 1 ? "branch" : "branches"} #{join(branches)}" if branches.any? + parts << "tagged with #{join(head_consumer_tags)}" if head_consumer_tags.any? + parts.join(" and ") + same_content_note end def same_content_note @@ -97,7 +100,19 @@ def same_content_note end end - def pending_provider_tags_description(any_or_a = "any") + def pending_provider_branch_or_tags_description(any_or_a = "any") + if provider_branch + pending_provider_branch_description(any_or_a) + else + pending_provider_tags_description(any_or_a) + end + end + + def pending_provider_branch_description(any_or_a) + "#{any_or_a} version of #{provider_name} from branch '#{provider_branch}'" + end + + def pending_provider_tags_description(any_or_a) case pending_provider_tags.size when 0 then provider_name when 1 then "#{any_or_a} version of #{provider_name} with tag '#{pending_provider_tags.first}'" @@ -105,6 +120,14 @@ def pending_provider_tags_description(any_or_a = "any") end end + def non_pending_provider_branch_or_tags_description + if provider_branch + "a version of #{provider_name} from branch '#{provider_branch}'" + else + non_pending_provider_tags_description + end + end + def non_pending_provider_tags_description case non_pending_provider_tags.size when 0 then provider_name @@ -125,6 +148,10 @@ def head_consumer_tags selectors.tag_names_of_selectors_for_latest_pacts end + def branches + selectors.branches_of_selectors_for_latest_pacts + end + def selector_descriptions selectors.sort.collect do | selector | selector_description(selector) @@ -142,6 +169,13 @@ def selector_description selector else "latest pact for a #{version_label} tagged '#{selector.tag}'" end + elsif selector.latest_for_branch? + version_label = selector.consumer ? "version of #{selector.consumer}" : "consumer version" + if selector.fallback_branch? + "latest pact for a #{version_label} from branch '#{selector.fallback_branch}' (fallback branch used as no pact was found from branch '#{selector.branch}')" + else + "latest pact for a #{version_label} from branch '#{selector.branch}'" + end elsif selector.all_for_tag_and_consumer? "pacts for all #{selector.consumer} versions tagged '#{selector.tag}'" elsif selector.all_for_tag? @@ -161,9 +195,15 @@ def short_selector_description selector "latest" elsif selector.latest_for_tag? if selector.fallback_tag? - "latest #{selector.fallback_tag}" + "latest with tag #{selector.fallback_tag}" + else + "latest with tag #{selector.tag}" + end + elsif selector.latest_for_branch? + if selector.fallback_branch? + "latest from branch #{selector.fallback_branch}" else - "latest #{selector.tag}" + "latest from branch #{selector.branch}" end elsif selector.all_for_tag_and_consumer? "one of #{selector.consumer} #{selector.tag}" diff --git a/lib/pact_broker/test/http_test_data_builder.rb b/lib/pact_broker/test/http_test_data_builder.rb index 51516036d..a116d6c0d 100644 --- a/lib/pact_broker/test/http_test_data_builder.rb +++ b/lib/pact_broker/test/http_test_data_builder.rb @@ -8,7 +8,7 @@ module PactBroker module Test class HttpTestDataBuilder - attr_reader :client, :last_consumer_name, :last_provider_name, :last_consumer_version_number, :last_provider_version_number + attr_reader :client, :last_consumer_name, :last_provider_name, :last_consumer_version_number, :last_provider_version_number, :last_provider_version_tag, :last_provider_version_branch def initialize(pact_broker_base_url, auth = {}) @client = Faraday.new(url: pact_broker_base_url) do |faraday| @@ -46,6 +46,13 @@ def create_tag(pacticipant:, version:, tag:) self end + def create_version(pacticipant:, version:, branch:) + request_body = { + branch: branch + } + client.put("pacticipants/#{encode(pacticipant)}/versions/#{encode(version)}", request_body).tap { |response| check_for_error(response) } + end + def deploy_to_prod(pacticipant:, version:) puts "Deploying #{pacticipant} version #{version} to prod" create_tag(pacticipant: pacticipant, version: version, tag: "prod") @@ -60,11 +67,13 @@ def create_pacticipant(name) self end - def publish_pact(consumer: last_consumer_name, consumer_version:, provider: last_provider_name, content_id:, tag:) + def publish_pact(consumer: last_consumer_name, consumer_version:, provider: last_provider_name, content_id:, tag: nil, branch: nil) @last_consumer_name = consumer @last_provider_name = provider @last_consumer_version_number = consumer_version + create_version(pacticipant: consumer, version: consumer_version, branch: branch) if branch + [*tag].each do | tag | create_tag(pacticipant: consumer, version: consumer_version, tag: tag) end @@ -78,18 +87,21 @@ def publish_pact(consumer: last_consumer_name, consumer_version:, provider: last self end - def get_pacts_for_verification(provider: last_provider_name, provider_version_tag: , consumer_version_selectors:, enable_pending: false, include_wip_pacts_since: nil) + def get_pacts_for_verification(provider: last_provider_name, provider_version_tag: nil, provider_version_branch: nil, consumer_version_selectors:, enable_pending: false, include_wip_pacts_since: nil) @last_provider_name = provider + @last_provider_version_tag = provider_version_tag + @last_provder_version_branch = provider_version_branch puts "Fetching pacts for verification for #{provider}" - body = { + request_body = { providerVersionTags: [*provider_version_tag], + providerVersionBranch: provider_version_branch, consumerVersionSelectors: consumer_version_selectors, includePendingStatus: enable_pending, includeWipPactsSince: include_wip_pacts_since }.compact - puts body.to_yaml + puts request_body.to_yaml puts "" - @pacts_for_verification_response = client.post("pacts/provider/#{encode(provider)}/for-verification", body).tap { |response| check_for_error(response) } + @pacts_for_verification_response = client.post("pacts/provider/#{encode(provider)}/for-verification", request_body).tap { |response| check_for_error(response) } print_pacts_for_verification separate @@ -114,14 +126,18 @@ def print_pacts_for_verification def verify_latest_pact_for_tag(success: true, provider: last_provider_name, consumer: last_consumer_name, consumer_version_tag: , provider_version:, provider_version_tag: nil) @last_provider_name = provider @last_consumer_name = consumer + url_of_pact_to_verify = "pacts/provider/#{encode(provider)}/consumer/#{encode(consumer)}/latest/#{encode(consumer_version_tag)}" publish_verification_results(url_of_pact_to_verify, provider, provider_version, provider_version_tag, success) separate self end - def verify_pact(index: 0, success:, provider: last_provider_name, provider_version_tag: , provider_version: ) + def verify_pact(index: 0, success:, provider: last_provider_name, provider_version_tag: last_provider_version_tag, provider_version_branch: last_provider_version_branch, provider_version: ) @last_provider_name = provider + @last_provider_version_tag = provider_version_tag + @last_provider_version_branch = provider_version_branch + pact_to_verify = @pacts_for_verification_response.body["_embedded"]["pacts"][index] raise "No pact found to verify at index #{index}" unless pact_to_verify url_of_pact_to_verify = pact_to_verify["_links"]["self"]["href"] diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 512e59bd5..2ae13eddc 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -159,7 +159,12 @@ def create_version version_number = "1.0.#{model_counter}", params = {} def create_consumer_version version_number = "1.0.#{model_counter}", params = {} params.delete(:comment) tag_names = [params.delete(:tag_names), params.delete(:tag_name)].flatten.compact - @consumer_version = PactBroker::Domain::Version.create(:number => version_number, :pacticipant => @consumer) + @consumer_version = PactBroker::Domain::Version.create( + number: version_number, + pacticipant: @consumer, + branch: params[:branch], + build_url: params[:build_url] + ) set_created_at_if_set params[:created_at], :versions, { id: @consumer_version.id } tag_names.each do | tag_name | PactBroker::Domain::Tag.create(name: tag_name, version: @consumer_version) @@ -169,8 +174,12 @@ def create_consumer_version version_number = "1.0.#{model_counter}", params = {} def create_provider_version version_number = "1.0.#{model_counter}", params = {} params.delete(:comment) + tag_names = [params.delete(:tag_names), params.delete(:tag_name)].flatten.compact @version = PactBroker::Domain::Version.create(:number => version_number, :pacticipant => @provider) @provider_version = @version + tag_names.each do | tag_name | + PactBroker::Domain::Tag.create(name: tag_name, version: @provider_version) + end self end @@ -339,6 +348,7 @@ def create_webhook_execution params = {} def create_verification parameters = {} parameters.delete(:comment) + 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 } @@ -348,6 +358,7 @@ def create_verification parameters = {} verification = PactBroker::Domain::Verification.new(parameters) @verification = PactBroker::Verifications::Repository.new.create(verification, provider_version_number, @pact) @provider_version = PactBroker::Domain::Version.where(pacticipant_id: @provider.id, number: provider_version_number).single_record + @provider_version.update(branch: branch) if branch set_created_at_if_set(parameters[:created_at], :verifications, id: @verification.id) set_created_at_if_set(parameters[:created_at], :versions, id: @provider_version.id) @@ -378,6 +389,10 @@ def create_everything_for_an_integration .create_webhook_execution end + def find_pacticipant(name) + PactBroker::Domain::Pacticipant.where(name: name).single_record + end + def model_counter @@model_counter ||= 0 @@model_counter += 1 diff --git a/lib/pact_broker/versions/repository.rb b/lib/pact_broker/versions/repository.rb index 46f3a8861..789f8ce8d 100644 --- a/lib/pact_broker/versions/repository.rb +++ b/lib/pact_broker/versions/repository.rb @@ -57,7 +57,16 @@ def create args updated_at: Sequel.datetime_class.now } - PactBroker::Domain::Version.new(version_params).insert_ignore + PactBroker::Domain::Version.new(version_params).upsert + end + + def create_or_update(pacticipant, version_number, version) + PactBroker::Domain::Version.new( + number: version_number, + pacticipant: pacticipant, + build_url: version.build_url, + branch: version.branch + ).upsert end def find_by_pacticipant_id_and_number_or_create pacticipant_id, number diff --git a/lib/pact_broker/versions/service.rb b/lib/pact_broker/versions/service.rb index e615d2f5d..b251264c5 100644 --- a/lib/pact_broker/versions/service.rb +++ b/lib/pact_broker/versions/service.rb @@ -1,7 +1,6 @@ require 'pact_broker/repositories' module PactBroker - module Versions class Service @@ -19,6 +18,11 @@ def self.find_by_pacticipant_name_and_latest_tag(pacticipant_name, tag) version_repository.find_by_pacticipant_name_and_latest_tag(pacticipant_name, tag) end + def self.create_or_update(pacticipant_name, version_number, version) + pacticipant = pacticipant_repository.find_by_name_or_create(pacticipant_name) + version_repository.create_or_update(pacticipant, version_number, version) + end + def self.delete version tag_repository.delete_by_version_id version.id webhook_repository.delete_triggered_webhooks_by_version_id version.id diff --git a/lib/sequel/plugins/upsert.rb b/lib/sequel/plugins/upsert.rb index 602e78a5d..0277425bf 100644 --- a/lib/sequel/plugins/upsert.rb +++ b/lib/sequel/plugins/upsert.rb @@ -67,7 +67,8 @@ def manual_upsert(opts) query = values.select{ |k, _| self.class.upsert_plugin_identifying_columns.include?(k) } existing_record = model.where(query).single_record if existing_record - existing_record.update(values) + existing_record.set(values_to_update) + existing_record.save(opts) else save(opts) end @@ -78,9 +79,9 @@ def manual_upsert(opts) def _insert_dataset if upsert_plugin_upserting if postgres? - super.insert_conflict(update: values, target: self.class.upsert_plugin_identifying_columns) + super.insert_conflict(update: values_to_update, target: self.class.upsert_plugin_identifying_columns) elsif mysql? - columns_to_update = values.keys - self.class.upsert_plugin_identifying_columns + columns_to_update = values_to_update.keys - self.class.upsert_plugin_identifying_columns super.on_duplicate_key_update(*columns_to_update) else super @@ -97,6 +98,18 @@ def mysql? def postgres? model.db.adapter_scheme.to_s == "postgres" end + + def values_to_update + columns_with_nil_values.merge(values).reject{ |k, _| columns_to_not_update.include?(k) } + end + + def columns_with_nil_values + self.class.columns.each_with_object({}) {|key, hash| hash[key] = nil } + end + + def columns_to_not_update + @columns_to_not_update ||= (self.class.upsert_plugin_identifying_columns + [:created_at, self.class.primary_key]).flatten + end end end end diff --git a/script/demonstrate-version-branches.rb b/script/demonstrate-version-branches.rb new file mode 100755 index 000000000..03bb728bf --- /dev/null +++ b/script/demonstrate-version-branches.rb @@ -0,0 +1,33 @@ +#!/usr/bin/env ruby +begin + + $LOAD_PATH << "#{Dir.pwd}/lib" + require 'pact_broker/test/http_test_data_builder' + base_url = ENV['PACT_BROKER_BASE_URL'] || 'http://localhost:9292' + + td = PactBroker::Test::HttpTestDataBuilder.new(base_url) + td.delete_integration(consumer: "Foo", provider: "Bar") + .delete_integration(consumer: "foo-consumer", provider: "bar-provider") + .publish_pact(consumer: "foo-consumer", consumer_version: "1", provider: "bar-provider", content_id: "111", branch: "main") + .publish_pact(consumer: "foo-consumer", consumer_version: "2", provider: "bar-provider", content_id: "222", tag: "feat/x") + .get_pacts_for_verification( + enable_pending: true, + provider_version_branch: "main", + provider_version_tag: "dev", + include_wip_pacts_since: "2020-01-01", + consumer_version_selectors: [{ branch: "main", latest: true }]) + .verify_pact( + index: 0, + provider_version: "1", + success: true + ) + .can_i_deploy(pacticipant: "bar-provider", version: "1", to: "prod") + .deploy_to_prod(pacticipant: "bar-provider", version: "1") + .can_i_deploy(pacticipant: "foo-consumer", version: "1", to: "prod") + .deploy_to_prod(pacticipant: "foo-consumer", version: "1") + +rescue StandardError => e + puts "#{e.class} #{e.message}" + puts e.backtrace + exit 1 +end diff --git a/spec/features/create_version_spec.rb b/spec/features/create_version_spec.rb new file mode 100644 index 000000000..79f9fdde4 --- /dev/null +++ b/spec/features/create_version_spec.rb @@ -0,0 +1,44 @@ +describe "Creating a pacticipant version" do + let(:path) { "/pacticipants/Foo/versions/1234" } + let(:headers) { { 'CONTENT_TYPE' => 'application/json' } } + let(:response_body) { JSON.parse(subject.body, symbolize_names: true)} + let(:version_hash) { { branch: "main", buildUrl: "http://build" } } + + subject { put(path, version_hash.to_json, headers) } + + it "returns a 201 response" do + expect(subject.status).to be 201 + end + + it "returns a HAL JSON Content Type" do + expect(subject.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8' + end + + it "returns the newly created version" do + expect(response_body).to include version_hash + end + + context "when the version already exists" do + before do + td.subtract_day + .create_consumer("Foo") + .create_consumer_version("1234", branch: "original-branch", build_url: "original-build-url") + .create_consumer_version_tag("dev") + end + + let(:version_hash) { { branch: "new-branch" } } + + it "overwrites the direct properties" do + expect(response_body[:branch]).to eq "new-branch" + expect(response_body).to_not have_key(:buildUrl) + end + + it "does not change the tags" do + expect { subject }.to_not change { PactBroker::Domain::Version.for("Foo", "1234").tags } + end + + it "does not change the created date" do + expect { subject }.to_not change { PactBroker::Domain::Version.for("Foo", "1234").created_at } + end + end +end diff --git a/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb index 0366afb3d..a881ac2fa 100644 --- a/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/verifiable_pacts_query_decorator_spec.rb @@ -6,6 +6,7 @@ module Decorators describe VerifiablePactsQueryDecorator do let(:provider_version_tags) { %w[dev] } + let(:provider_version_branch) { "main" } subject { VerifiablePactsQueryDecorator.new(OpenStruct.new).from_hash(params) } @@ -13,6 +14,7 @@ module Decorators let(:params) do { "providerVersionTags" => provider_version_tags, + "providerVersionBranch" => provider_version_branch, "consumerVersionSelectors" => consumer_version_selectors } end @@ -25,6 +27,10 @@ module Decorators expect(subject.consumer_version_selectors).to be_a(PactBroker::Pacts::Selectors) end + it "parses the provider version branch" do + expect(subject.provider_version_branch).to eq "main" + end + context "when latest is not specified" do let(:consumer_version_selectors) do [{ "tag" => "dev" }] diff --git a/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb b/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb index b5b35a882..7250a8703 100644 --- a/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb +++ b/spec/lib/pact_broker/api/resources/provider_pacts_for_verification_spec.rb @@ -16,6 +16,7 @@ module Resources let(:decorator) { instance_double('PactBroker::Api::Decorators::VerifiablePactsDecorator') } let(:query) do { + provider_version_branch: "main", provider_version_tags: ['master'], consumer_version_selectors: [ { tag: 'dev', latest: 'true' }], include_pending_status: 'true', @@ -30,6 +31,7 @@ module Resources # Naughty not mocking out the query parsing... expect(PactBroker::Pacts::Service).to receive(:find_for_verification).with( "Bar", + "main", ["master"], PactBroker::Pacts::Selectors.new([PactBroker::Pacts::Selector.latest_for_tag("dev")]), { @@ -56,6 +58,7 @@ module Resources describe "POST" do let(:request_body) do { + providerVersionBranch: "main", providerVersionTags: ['master'], consumerVersionSelectors: [ { tag: 'dev', latest: true }], includePendingStatus: true, @@ -76,6 +79,7 @@ module Resources # Naughty not mocking out the query parsing... expect(PactBroker::Pacts::Service).to receive(:find_for_verification).with( "Bar", + "main", ["master"], PactBroker::Pacts::Selectors.new([PactBroker::Pacts::Selector.latest_for_tag("dev")]), { diff --git a/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb b/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb new file mode 100644 index 000000000..498f1bd69 --- /dev/null +++ b/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb @@ -0,0 +1,343 @@ +require 'pact_broker/pacts/pact_publication' + +module PactBroker + module Pacts + describe PactPublication do + describe "latest_by_consumer_branch" do + before do + td.set_now(Date.new(2020, 1, 1)) + .create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .set_now(Date.new(2020, 1, 2)) + .create_consumer_version("2", branch: "feat/x") + .create_pact + .set_now(Date.new(2020, 1, 3)) + .create_consumer_version("3", branch: "main", comment: "latest") + .create_pact + .set_now(Date.new(2020, 1, 4)) + .create_consumer_version("4", branch: "feat/x", comment: "latest") + .create_pact + .set_now(Date.new(2020, 1, 5)) + .create_consumer("FooZ") + .create_consumer_version("6", branch: "main", comment: "latest, different consumer") + .create_pact + .set_now(Date.new(2020, 1, 6)) + .create_consumer_version("7", comment: "No branch") + .create_pact + .set_now(Date.new(2020, 1, 7)) + .create_consumer_version("8", branch: "main", comment: "No pact") + end + + subject { PactPublication.latest_by_consumer_branch.all } + + let(:foo) { PactBroker::Domain::Pacticipant.where(name: "Foo").single_record } + let(:bar) { PactBroker::Domain::Pacticipant.where(name: "Bar").single_record } + let(:foo_z) { PactBroker::Domain::Pacticipant.where(name: "FooZ").single_record } + + it "returns the latest pact publications for each consumer/branch" do + expect(subject.size).to eq 3 + + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "main" }.consumer_version.number).to eq "3" + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:branch] == "main" }.consumer_version.number).to eq "6" + end + + context "chained with created after" do + subject { PactPublication.created_after(DateTime.new(2020, 1, 3)).latest_by_consumer_branch.all } + + its(:size) { is_expected.to eq 2 } + + it "returns the right versions" do + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:branch] == "main" }.consumer_version.number).to eq "6" + end + end + end + + describe "latest_for_consumer_branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .create_consumer_version("2", branch: "main") + .create_pact + .create_consumer_version("3", branch: "feat-x") + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", branch: "main") + .create_pact + .create_consumer_version("11", branch: "main") + .create_pact + end + + subject { PactPublication.latest_for_consumer_branch("main") } + + it "returns the latest pacts for the branches with the specified name (for any consumer/provider)" do + all = subject.all.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 2 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + expect(all.first.consumer_version.number).to eq "2" + + expect(all.last.consumer.name).to eq "Foo2" + expect(all.last.provider.name).to eq "Bar2" + expect(all.last.consumer_version.number).to eq "11" + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_branch("main").all + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "latest_by_consumer_tag" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["feat/x"]) + .create_pact + .create_consumer_version("3", tag_names: ["main"], comment: "latest") + .create_pact + .create_consumer_version("4", tag_names: ["feat/x"], comment: "latest") + .create_pact + .create_consumer("FooZ") + .create_consumer_version("6", tag_names: ["main"], comment: "Different consumer") + .create_pact + .create_consumer_version("7", comment: "No branch") + .create_pact + .create_consumer_version("8", tag_names: ["main"], comment: "No pact") + end + + subject { PactPublication.latest_by_consumer_tag.all } + + let(:foo) { PactBroker::Domain::Pacticipant.where(name: "Foo").single_record } + let(:bar) { PactBroker::Domain::Pacticipant.where(name: "Bar").single_record } + let(:foo_z) { PactBroker::Domain::Pacticipant.where(name: "FooZ").single_record } + + it "returns the latest pact publications for each consumer/branch" do + expect(subject.size).to eq 3 + hashes = subject.collect(&:values) + + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "main" }.consumer_version.number).to eq "3" + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:tag_name] == "main" }.consumer_version.number).to eq "6" + end + end + + describe "overall_latest" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["main"]) + .create_pact + .create_consumer_version("3", tag_names: ["feat/x"]) + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", tag_names: ["main"]) + .create_pact + .create_consumer_version("11", tag_names: ["main"]) + .create_pact + end + + subject { PactPublication.overall_latest } + + it "returns the latest by consumer/provider" do + all = subject.all.sort_by{ | pact_publication | pact_publication.consumer_version.order } + expect(all.size).to eq 2 + end + + context "when chained" do + it "works with a consumer" do + expect(PactPublication.for_consumer(td.find_pacticipant("Foo")).overall_latest.all.first.consumer.name).to eq "Foo" + end + + it "works with a consumer and provider" do + td.create_pact_with_hierarchy("Foo", "666", "Nope") + all = PactPublication + .for_consumer(td.find_pacticipant("Foo")) + .for_provider(td.find_pacticipant("Bar")) + .overall_latest.all + expect(all.size).to eq 1 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "latest_for_consumer_tag" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["main"]) + .create_pact + .create_consumer_version("3", tag_names: ["feat/x"]) + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", tag_names: ["main"]) + .create_pact + .create_consumer_version("11", tag_names: ["main"]) + .create_pact + end + + subject { PactPublication.latest_for_consumer_tag("main") } + + it "returns the latest pacts for the tags with the specified name (for any consumer/provider)" do + all = subject.all.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 2 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + expect(all.first.consumer_version.number).to eq "2" + + expect(all.last.consumer.name).to eq "Foo2" + expect(all.last.provider.name).to eq "Bar2" + expect(all.last.consumer_version.number).to eq "11" + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_tag("main").all + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "#successfully_verified_by_provider_branch" do + let(:bar) { td.find_pacticipant("Bar") } + + subject { PactPublication.successfully_verified_by_provider_branch(bar.id, "main").all } + + context "PactPublication" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "2", branch: "main", success: false) + .create_pact_with_hierarchy("Foo", "2", "Bar") + .create_verification(provider_version: "2", branch: "main", success: true) + .create_pact_with_hierarchy("Foo", "3", "Bar") + .create_verification(provider_version: "3", branch: "not-main", success: true) + end + + it "returns the pact publications that have been succesfully verified by the given provider id and branch" do + expect(subject.size).to eq 1 + expect(subject.first.consumer_version.number).to eq "2" + end + end + + + context "with chained scopes" do + subject { PactPublication.latest_by_consumer_branch.successfully_verified_by_provider_branch(bar.id, "provider-main").all } + + context "when there are no latest branch pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main", comment: "successful but not latest") + .create_consumer_version("2", branch: "main", comment: "latest") + .create_pact + .create_verification(provider_version: "2", success: false, branch: "provider-main", comment: "latest but not successful") + .create_consumer_version("3", branch: "feat/x", comment: "latest") + .create_pact + .create_verification(provider_version: "3", success: true, branch: "not-provider-main", comment: "latest, successful, but wrong branch") + end + + it { is_expected.to eq [] } + end + + context "when there are latest branch pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_consumer_version("2", branch: "main", comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + end + + its(:size) { is_expected.to eq 1 } + + it "returns them" do + expect(subject.first.consumer_version.number).to eq "2" + end + end + + context "when there are latest tagged pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"], comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, tag_names: ["provider-main"]) + .create_consumer_version("2", tag_names: ["main"], comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + end + + subject { PactPublication.latest_by_consumer_tag.successfully_verified_by_provider_branch(bar.id, "provider-main").all } + + its(:size) { is_expected.to eq 1 } + + it "returns them" do + expect(subject.first.consumer_version.number).to eq "2" + end + end + + context "subtracting Pact Publications" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", tag_names: ["main"], comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_consumer_version("2", branch: "main", tag_names: ["main"], comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + .create_consumer_version("3", branch: "feat/x", tag_names: ["feat/x"], ) + .create_pact + end + + let(:bar) { td.find_pacticipant("Bar") } + + it "with branches" do + potential = PactPublication.for_provider(bar).latest_by_consumer_branch + already_verified = potential.successfully_verified_by_provider_branch(bar.id, "provider-main") + not_verified = potential.all - already_verified.all + + expect(not_verified.size).to eq 1 + expect(not_verified.first.consumer_version_number).to eq "3" + end + + it "with tags" do + potential = PactPublication.for_provider(bar).latest_by_consumer_tag + already_verified = potential.successfully_verified_by_provider_branch(bar.id, "provider-main") + not_verified = potential.all - already_verified.all + + expect(not_verified.size).to eq 1 + expect(not_verified.first.consumer_version_number).to eq "3" + end + end + end + end + end + end +end + diff --git a/spec/lib/pact_broker/pacts/pact_publication_spec.rb b/spec/lib/pact_broker/pacts/pact_publication_spec.rb index b72858448..b7a81723b 100644 --- a/spec/lib/pact_broker/pacts/pact_publication_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_publication_spec.rb @@ -134,6 +134,363 @@ module Pacts end end end + + describe "created_after" do + before do + td.set_now(Date.new(2020, 1, 1)) + .create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .set_now(Date.new(2020, 1, 3)) + .create_consumer_version("2", branch: "feat/x") + .create_pact + end + + subject { PactPublication.created_after(Date.new(2020, 1, 2)).all } + + its(:size) { is_expected.to eq 1 } + + it "returns the pact publications created after the specified date" do + expect(subject.first.consumer_version.number).to eq "2" + end + end + + describe "latest_by_consumer_branch" do + before do + td.set_now(Date.new(2020, 1, 1)) + .create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .set_now(Date.new(2020, 1, 2)) + .create_consumer_version("2", branch: "feat/x") + .create_pact + .set_now(Date.new(2020, 1, 3)) + .create_consumer_version("3", branch: "main", comment: "latest") + .create_pact + .set_now(Date.new(2020, 1, 4)) + .create_consumer_version("4", branch: "feat/x", comment: "latest") + .create_pact + .set_now(Date.new(2020, 1, 5)) + .create_consumer("FooZ") + .create_consumer_version("6", branch: "main", comment: "latest, different consumer") + .create_pact + .set_now(Date.new(2020, 1, 6)) + .create_consumer_version("7", comment: "No branch") + .create_pact + .set_now(Date.new(2020, 1, 7)) + .create_consumer_version("8", branch: "main", comment: "No pact") + end + + subject { PactPublication.latest_by_consumer_branch.all } + + let(:foo) { PactBroker::Domain::Pacticipant.where(name: "Foo").single_record } + let(:bar) { PactBroker::Domain::Pacticipant.where(name: "Bar").single_record } + let(:foo_z) { PactBroker::Domain::Pacticipant.where(name: "FooZ").single_record } + + it "returns the latest pact publications for each consumer/branch" do + expect(subject.size).to eq 3 + hashes = subject.collect(&:values) + + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "main" }.consumer_version.number).to eq "3" + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:branch] == "main" }.consumer_version.number).to eq "6" + end + + context "chained with created after" do + subject { PactPublication.created_after(DateTime.new(2020, 1, 3)).latest_by_consumer_branch.all } + + its(:size) { is_expected.to eq 2 } + + it "returns the right versions" do + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:branch] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:branch] == "main" }.consumer_version.number).to eq "6" + end + end + end + + describe "latest_for_consumer_branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main") + .create_pact + .create_consumer_version("2", branch: "main") + .create_pact + .create_consumer_version("3", branch: "feat-x") + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", branch: "main") + .create_pact + .create_consumer_version("11", branch: "main") + .create_pact + end + + subject { PactPublication.latest_for_consumer_branch("main") } + + it "returns the latest pacts for the branches with the specified name (for any consumer/provider)" do + all = subject.all.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 2 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + expect(all.first.consumer_version.number).to eq "2" + + expect(all.last.consumer.name).to eq "Foo2" + expect(all.last.provider.name).to eq "Bar2" + expect(all.last.consumer_version.number).to eq "11" + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_branch("main").all + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "latest_by_consumer_tag" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["feat/x"]) + .create_pact + .create_consumer_version("3", tag_names: ["main"], comment: "latest") + .create_pact + .create_consumer_version("4", tag_names: ["feat/x"], comment: "latest") + .create_pact + .create_consumer("FooZ") + .create_consumer_version("6", tag_names: ["main"], comment: "Different consumer") + .create_pact + .create_consumer_version("7", comment: "No branch") + .create_pact + .create_consumer_version("8", tag_names: ["main"], comment: "No pact") + end + + subject { PactPublication.latest_by_consumer_tag.all } + + let(:foo) { PactBroker::Domain::Pacticipant.where(name: "Foo").single_record } + let(:bar) { PactBroker::Domain::Pacticipant.where(name: "Bar").single_record } + let(:foo_z) { PactBroker::Domain::Pacticipant.where(name: "FooZ").single_record } + + it "returns the latest pact publications for each consumer/branch" do + expect(subject.size).to eq 3 + hashes = subject.collect(&:values) + + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "main" }.consumer_version.number).to eq "3" + expect(subject.find { |pp| pp.consumer_id == foo.id && pp[:tag_name] == "feat/x" }.consumer_version.number).to eq "4" + expect(subject.find { |pp| pp.consumer_id == foo_z.id && pp[:tag_name] == "main" }.consumer_version.number).to eq "6" + end + end + + describe "overall_latest" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["main"]) + .create_pact + .create_consumer_version("3", tag_names: ["feat/x"]) + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", tag_names: ["main"]) + .create_pact + .create_consumer_version("11", tag_names: ["main"]) + .create_pact + end + + subject { PactPublication.overall_latest } + + it "returns the latest by consumer/provider" do + all = subject.all.sort_by{ | pact_publication | pact_publication.consumer_version.order } + expect(all.size).to eq 2 + end + + context "when chained" do + it "works with a consumer" do + expect(PactPublication.for_consumer(td.find_pacticipant("Foo")).overall_latest.all.first.consumer.name).to eq "Foo" + end + + it "works with a consumer and provider" do + td.create_pact_with_hierarchy("Foo", "666", "Nope") + all = PactPublication + .for_consumer(td.find_pacticipant("Foo")) + .for_provider(td.find_pacticipant("Bar")) + .overall_latest.all + expect(all.size).to eq 1 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "latest_for_consumer_tag" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"]) + .create_pact + .create_consumer_version("2", tag_names: ["main"]) + .create_pact + .create_consumer_version("3", tag_names: ["feat/x"]) + .create_pact + .create_consumer("Foo2") + .create_provider("Bar2") + .create_consumer_version("10", tag_names: ["main"]) + .create_pact + .create_consumer_version("11", tag_names: ["main"]) + .create_pact + end + + subject { PactPublication.latest_for_consumer_tag("main") } + + it "returns the latest pacts for the tags with the specified name (for any consumer/provider)" do + all = subject.all.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 2 + expect(all.first.consumer.name).to eq "Foo" + expect(all.first.provider.name).to eq "Bar" + expect(all.first.consumer_version.number).to eq "2" + + expect(all.last.consumer.name).to eq "Foo2" + expect(all.last.provider.name).to eq "Bar2" + expect(all.last.consumer_version.number).to eq "11" + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_tag("main").all + expect(all.first.provider.name).to eq "Bar" + end + end + end + + describe "#successfully_verified_by_provider_branch" do + let(:bar) { td.find_pacticipant("Bar") } + + subject { PactPublication.successfully_verified_by_provider_branch(bar.id, "main").all } + + context "PactPublication" do + before do + td.create_pact_with_hierarchy("Foo", "1", "Bar") + .create_verification(provider_version: "2", branch: "main", success: false) + .create_pact_with_hierarchy("Foo", "2", "Bar") + .create_verification(provider_version: "2", branch: "main", success: true) + .create_pact_with_hierarchy("Foo", "3", "Bar") + .create_verification(provider_version: "3", branch: "not-main", success: true) + end + + it "returns the pact publications that have been succesfully verified by the given provider id and branch" do + expect(subject.size).to eq 1 + expect(subject.first.consumer_version.number).to eq "2" + end + end + + + context "with chained scopes" do + subject { PactPublication.latest_by_consumer_branch.successfully_verified_by_provider_branch(bar.id, "provider-main").all } + + context "when there are no latest branch pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main", comment: "successful but not latest") + .create_consumer_version("2", branch: "main", comment: "latest") + .create_pact + .create_verification(provider_version: "2", success: false, branch: "provider-main", comment: "latest but not successful") + .create_consumer_version("3", branch: "feat/x", comment: "latest") + .create_pact + .create_verification(provider_version: "3", success: true, branch: "not-provider-main", comment: "latest, successful, but wrong branch") + end + + it { is_expected.to eq [] } + end + + context "when there are latest branch pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_consumer_version("2", branch: "main", comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + end + + its(:size) { is_expected.to eq 1 } + + it "returns them" do + expect(subject.first.consumer_version.number).to eq "2" + end + end + + context "when there are latest tagged pacts that have been successfully verified by the specified provider branch" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", tag_names: ["main"], comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, tag_names: ["provider-main"]) + .create_consumer_version("2", tag_names: ["main"], comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + end + + subject { PactPublication.latest_by_consumer_tag.successfully_verified_by_provider_branch(bar.id, "provider-main").all } + + its(:size) { is_expected.to eq 1 } + + it "returns them" do + expect(subject.first.consumer_version.number).to eq "2" + end + end + + context "subtracting Pact Publications" do + before do + td.create_consumer("Foo") + .create_provider("Bar") + .create_consumer_version("1", branch: "main", tag_names: ["main"], comment: "not latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_consumer_version("2", branch: "main", tag_names: ["main"], comment: "latest") + .create_pact + .create_verification(provider_version: "1", success: true, branch: "provider-main") + .create_verification(provider_version: "2", success: true, branch: "provider-main", number: "2") + .create_consumer_version("3", branch: "feat/x", tag_names: ["feat/x"], ) + .create_pact + end + + let(:bar) { td.find_pacticipant("Bar") } + + it "with branches" do + potential = PactPublication.for_provider(bar).latest_by_consumer_branch + already_verified = potential.successfully_verified_by_provider_branch(bar.id, "provider-main") + not_verified = potential.all - already_verified.all + + expect(not_verified.size).to eq 1 + expect(not_verified.first.consumer_version_number).to eq "3" + end + + it "with tags" do + potential = PactPublication.for_provider(bar).latest_by_consumer_tag + already_verified = potential.successfully_verified_by_provider_branch(bar.id, "provider-main") + not_verified = potential.all - already_verified.all + + expect(not_verified.size).to eq 1 + expect(not_verified.first.consumer_version_number).to eq "3" + end + end + end + end end end end diff --git a/spec/lib/pact_broker/pacts/repository_find_for_verification_fallback_spec.rb b/spec/lib/pact_broker/pacts/repository_find_for_verification_fallback_spec.rb index b3a9966f6..5886aae63 100644 --- a/spec/lib/pact_broker/pacts/repository_find_for_verification_fallback_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_find_for_verification_fallback_spec.rb @@ -67,7 +67,7 @@ def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_ve it "only returns the pacts for the consumer" do expect(subject.size).to eq 1 expect(subject.first.consumer.name).to eq "Foo" - expect(subject.first.selectors.first).to eq selector.resolve(PactBroker::Domain::Version.for("Foo", "1")) + expect(subject.first.selectors.first).to eq selector.resolve_for_fallback(PactBroker::Domain::Version.for("Foo", "1")) end end end diff --git a/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb b/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb index 7b66e2fc1..8e3b037cc 100644 --- a/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_find_for_verification_spec.rb @@ -74,7 +74,7 @@ def find_by_consumer_name_and_consumer_version_number(consumer_name, consumer_ve it "returns the latest pact for each consumer" do expect(subject.size).to eq 1 - expect(find_by_consumer_name_and_consumer_version_number("Foo1", "2").selectors).to eq [pact_selector_1.resolve(PactBroker::Domain::Version.find(number: "2"))] + expect(find_by_consumer_name_and_consumer_version_number("Foo1", "2").selectors).to eq [pact_selector_1.resolve(PactBroker::Domain::Version.for("Foo1", "2"))] end end diff --git a/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_branch_spec.rb b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_branch_spec.rb new file mode 100644 index 000000000..0f54e4c17 --- /dev/null +++ b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_branch_spec.rb @@ -0,0 +1,224 @@ +require 'pact_broker/pacts/repository' + +module PactBroker + module Pacts + describe Repository do + describe "find_wip_pact_versions_for_provider by branch" do + # let(:provider_tags) { %w[dev] } + let(:provider_tags) { [] } + let(:provider_version_branch) { "dev" } + let(:options) { { include_wip_pacts_since: include_wip_pacts_since } } + let(:include_wip_pacts_since) { (Date.today - 1).to_datetime } + + subject { Repository.new.find_wip_pact_versions_for_provider("bar", provider_version_branch, provider_tags, options) } + + context "when there are no tags or branch" do + let(:provider_tags) { [] } + let(:provider_version_branch) { nil } + + it "returns an empty list" do + expect(subject).to eq [] + end + end + + context "when there are multiple wip pacts" do + before do + td.create_provider("bar") + .create_provider_version("333", branch: provider_version_branch) + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("feat-1") + .add_day + .create_consumer_version("2", branch: "branch-1") + .create_pact + .create_pact_with_hierarchy("meep", "2", "bar") + .create_consumer_version_tag("feat-2") + .add_day + .create_pact_with_hierarchy("foo", "3", "bar") + .create_consumer_version_tag("feat-2") + .add_day + .create_pact_with_hierarchy("meep", "1", "bar") + .create_consumer_version_tag("feat-1") + end + + it "sorts them" do + expect(subject[0].consumer_name).to eq "foo" + expect(subject[0].consumer_version_number).to eq "1" + + expect(subject[1].consumer_name).to eq "foo" + expect(subject[1].consumer_version_number).to eq "2" + + expect(subject[2].consumer_name).to eq "foo" + expect(subject[2].consumer_version_number).to eq "3" + + expect(subject[3].consumer_name).to eq "meep" + expect(subject[3].consumer_version_number).to eq "2" + + expect(subject[4].consumer_name).to eq "meep" + expect(subject[4].consumer_version_number).to eq "1" + end + + it "sets the selectors" do + expect(subject[0].selectors).to eq Selectors.create_for_latest_for_tag("feat-1") + expect(subject[1].selectors).to eq Selectors.create_for_latest_for_branch("branch-1") + expect(subject[2].selectors).to eq Selectors.create_for_latest_for_tag("feat-2") + expect(subject[3].selectors).to eq Selectors.create_for_latest_for_tag("feat-2") + expect(subject[4].selectors).to eq Selectors.create_for_latest_for_tag("feat-1") + end + end + + context "when the latest pact for a tag has been successfully verified by the given provider branch" do + before do + td.create_pact_with_hierarchy("foo", "1", "bar") + .comment("above not included because it's not the latest prod") + .create_consumer_version("2") + .create_consumer_version_tag("prod") + .create_pact + .create_verification(provider_version: "3", branch: provider_version_branch, comment: "not included because already verified") + end + + it "is not included" do + expect(subject.size).to be 0 + end + end + + context "when the latest pact for a tag has been successfully verified by the given provider tag but it was a WIP verification" do + before do + td.create_provider("bar") + .create_provider_version("333", branch: provider_version_branch) + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .comment("above not included because it's not the latest") + .create_consumer_version("2") + .create_consumer_version_tag("feat-1") + .create_pact + .create_verification(wip: true, success: true, provider_version: "3", branch: provider_version_branch) + end + + it "it is included" do + expect(subject[0].consumer_name).to eq "foo" + expect(subject[0].consumer_version_number).to eq "2" + end + end + + context "when a pact is the latest for a tag and a branch and has no successful verifications" do + before do + td.create_provider("bar") + .create_provider_version("333", branch: provider_version_branch) + .add_day + .create_consumer("foo") + .create_consumer_version("1", branch: "branch-1", tag_names: ["feat-1"]) + .comment("above not included because it's not the latest") + .create_consumer_version("2", branch: "branch-1", tag_names: ["feat-1"]) + .create_pact + end + + it "it has two selectors" do + expect(subject.size).to eq 1 + expect(subject.first.selectors).to eq Selectors.new([Selector.latest_for_branch("branch-1"), Selector.latest_for_tag("feat-1")]) + end + end + + context "when the latest pact for a tag has failed verification from the specified provider version branch" do + before do + td.create_provider("bar") + .create_provider_version("333") + .create_provider_version_tag("dev") + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("feat-1") + .create_verification(provider_version: "3", success: false, branch: provider_version_branch) + .add_day + .create_consumer_version("2", branch: "branch-1") + .create_pact + .create_verification(provider_version: "3", success: false, branch: provider_version_branch) + + end + + it "is included" do + expect(subject.size).to be 2 + end + + it "sets the pending tags" do + expect(subject.first.provider_branch).to eq provider_version_branch + expect(subject.last.provider_branch).to eq provider_version_branch + end + end + + context "when there are no consumer tags" do + before do + td.create_pact_with_hierarchy("foo", "1", "bar") + .create_verification(provider_version: "3", success: false, branch: provider_version_branch) + end + + it "returns an empty list" do + expect(subject).to eq [] + end + end + + context "when the latest pact for a tag has successful then failed verifications" do + before do + td.create_provider("bar") + .create_provider_version("333") + .create_provider_version_tag("dev") + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("dev") + .create_verification(provider_version: "3", success: true, branch: provider_version_branch) + .create_verification(provider_version: "5", success: false, number: 2, branch: provider_version_branch) + end + + it "is not included, but maybe it should be? can't really work out a scenario where this is likely to happen" do + expect(subject).to eq [] + end + end + + context "when the latest pact for a tag has not been verified" do + before do + td.create_provider("bar") + .create_provider_version("333") + .create_provider_version_tag("dev") + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("feat-1") + end + + it "is included" do + expect(subject.size).to be 1 + end + + it "sets the pending tags" do + expect(subject.first.provider_branch).to eq provider_version_branch + end + end + + context "when the provider name does not match the given provider name" do + before do + td.create_pact_with_hierarchy("foo", "1", "baz") + .create_provider("bar") + end + + it "is not included" do + expect(subject.size).to be 0 + end + end + + context "when the pact was published before the specified include_wip_pacts_since" do + before do + td.create_provider("bar") + .create_provider_version("333", branch: provider_version_branch) + .add_day + .create_pact_with_hierarchy("foo", "1", "bar") + .create_consumer_version_tag("prod") + end + + let(:include_wip_pacts_since) { (Date.today + 3).to_datetime } + + it "is not included" do + expect(subject.size).to be 0 + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb index 558e96dfa..1b343b518 100644 --- a/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_find_wip_pact_versions_for_provider_spec.rb @@ -5,10 +5,11 @@ module Pacts describe Repository do describe "find_wip_pact_versions_for_provider" do let(:provider_tags) { %w[dev] } + let(:provider_version_branch) { nil } let(:options) { { include_wip_pacts_since: include_wip_pacts_since } } let(:include_wip_pacts_since) { (Date.today - 1).to_datetime } - subject { Repository.new.find_wip_pact_versions_for_provider("bar", provider_tags, options) } + subject { Repository.new.find_wip_pact_versions_for_provider("bar", provider_version_branch, provider_tags, options) } context "when there are no tags" do let(:provider_tags) { [] } @@ -21,16 +22,17 @@ module Pacts context "when there are multiple wip pacts" do before do td.create_provider("bar") - .create_provider_version("333") - .create_provider_version_tag("dev") + .create_provider_version("333", tag_names: provider_tags) .add_day .create_pact_with_hierarchy("foo", "1", "bar") .create_consumer_version_tag("feat-1") .add_day + .create_consumer_version("2", branch: "branch-1") + .create_pact .create_pact_with_hierarchy("meep", "2", "bar") .create_consumer_version_tag("feat-2") .add_day - .create_pact_with_hierarchy("foo", "2", "bar") + .create_pact_with_hierarchy("foo", "3", "bar") .create_consumer_version_tag("feat-2") .add_day .create_pact_with_hierarchy("meep", "1", "bar") @@ -46,11 +48,22 @@ module Pacts expect(subject[1].consumer_name).to eq "foo" expect(subject[1].consumer_version_number).to eq "2" - expect(subject[2].consumer_name).to eq "meep" - expect(subject[2].consumer_version_number).to eq "2" + expect(subject[2].consumer_name).to eq "foo" + expect(subject[2].consumer_version_number).to eq "3" expect(subject[3].consumer_name).to eq "meep" - expect(subject[3].consumer_version_number).to eq "1" + expect(subject[3].consumer_version_number).to eq "2" + + expect(subject[4].consumer_name).to eq "meep" + expect(subject[4].consumer_version_number).to eq "1" + end + + it "sets the selectors" do + expect(subject[0].selectors).to eq Selectors.create_for_latest_for_tag("feat-1") + expect(subject[1].selectors).to eq Selectors.create_for_latest_for_branch("branch-1") + expect(subject[2].selectors).to eq Selectors.create_for_latest_for_tag("feat-2") + expect(subject[3].selectors).to eq Selectors.create_for_latest_for_tag("feat-2") + expect(subject[4].selectors).to eq Selectors.create_for_latest_for_tag("feat-1") end end diff --git a/spec/lib/pact_broker/pacts/selector_spec.rb b/spec/lib/pact_broker/pacts/selector_spec.rb index 27f2fdcc3..48781960b 100644 --- a/spec/lib/pact_broker/pacts/selector_spec.rb +++ b/spec/lib/pact_broker/pacts/selector_spec.rb @@ -6,6 +6,7 @@ module Pacts describe "<=>" do let(:overall_latest_1) { Selector.overall_latest } let(:overall_latest_2) { Selector.overall_latest } + let(:latest_for_branch_main) { Selector.latest_for_branch('main') } let(:latest_for_tag_prod) { Selector.latest_for_tag('prod') } let(:latest_for_tag_dev) { Selector.latest_for_tag('dev') } let(:all_prod_for_consumer_1) { Selector.all_for_tag_and_consumer('prod', 'Foo') } @@ -15,11 +16,11 @@ module Pacts let(:all_dev) { Selector.all_for_tag('dev') } let(:unsorted_selectors) do - [all_prod, all_dev, all_dev_for_consumer_1, latest_for_tag_prod, overall_latest_1, overall_latest_1, latest_for_tag_dev, all_prod_for_consumer_2, all_prod_for_consumer_1] + [all_prod, all_dev, all_dev_for_consumer_1, latest_for_branch_main, latest_for_tag_prod, overall_latest_1, overall_latest_1, latest_for_tag_dev, all_prod_for_consumer_2, all_prod_for_consumer_1] end let(:expected_sorted_selectors) do - [overall_latest_1, overall_latest_1, latest_for_tag_dev, latest_for_tag_prod, all_dev_for_consumer_1, all_prod_for_consumer_2, all_prod_for_consumer_1, all_dev, all_prod] + [overall_latest_1, overall_latest_1, latest_for_branch_main, latest_for_tag_dev, latest_for_tag_prod, all_dev_for_consumer_1, all_prod_for_consumer_2, all_prod_for_consumer_1, all_dev, all_prod] end it "sorts the selectors" do diff --git a/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb b/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb index a2794a308..dbeb29a47 100644 --- a/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb +++ b/spec/lib/pact_broker/pacts/service_find_for_verification_spec.rb @@ -2,9 +2,7 @@ require 'pact_broker/pacts/service' require 'pact_broker/pacts/pact_params' - module PactBroker - module Pacts describe Service do let(:td) { TestDataBuilder.new } @@ -38,13 +36,14 @@ module Pacts let(:provider_name) { "Bar" } let(:provider_version_tags) { [] } + let(:provider_version_branch) { "main" } let(:consumer_version_selectors) { [] } before do allow(pact_repository).to receive(:find_for_verification).and_return(head_pacts) end - subject { Service.find_for_verification(provider_name, provider_version_tags, consumer_version_selectors) } + subject { Service.find_for_verification(provider_name, provider_version_branch, provider_version_tags, consumer_version_selectors) } end end end diff --git a/spec/lib/pact_broker/pacts/service_spec.rb b/spec/lib/pact_broker/pacts/service_spec.rb index 7e3ef7b04..bae8a253b 100644 --- a/spec/lib/pact_broker/pacts/service_spec.rb +++ b/spec/lib/pact_broker/pacts/service_spec.rb @@ -172,7 +172,7 @@ module Pacts let(:options) { { } } - subject { Service.find_for_verification("Bar", ["master"], Selectors.new, options) } + subject { Service.find_for_verification("Bar", nil, ["master"], Selectors.new, options) } context "when the consumer version tags are empty" do @@ -209,7 +209,7 @@ module Pacts .republish_same_pact(comment: "overall latest") end - subject { Service.find_for_verification("Wiffle", ["master"], Selectors.new, options) } + subject { Service.find_for_verification("Wiffle", "main", ["master"], Selectors.new, options) } it "is not included" do expect(subject.size).to eq 1 diff --git a/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb index 2229381f4..218a941e9 100644 --- a/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb +++ b/spec/lib/pact_broker/pacts/verifiable_pact_messages_spec.rb @@ -7,6 +7,7 @@ module Pacts describe VerifiablePactMessages do let(:pending_provider_tags) { [] } let(:non_pending_provider_tags) { [] } + let(:provider_branch) { nil } let(:pending) { false } let(:wip) { false } let(:selectors) { Selectors.new } @@ -20,7 +21,8 @@ module Pacts non_pending_provider_tags: non_pending_provider_tags, pending?: pending, wip?: wip, - selectors: selectors + selectors: selectors, + provider_branch: provider_branch ) end @@ -35,7 +37,18 @@ module Pacts context "when there is 1 head consumer tags" do let(:selectors) { Selectors.create_for_latest_of_each_tag(%w[dev]) } its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: latest pact for a consumer version tagged 'dev'" } - its(:pact_description) { is_expected.to eq "Pact between Foo and Bar, consumer version 123, latest dev"} + its(:pact_description) { is_expected.to eq "Pact between Foo and Bar, consumer version 123, latest with tag dev"} + end + + context "when there are branches" do + let(:selectors) { Selectors.create_for_latest_of_each_branch(%w[main feat-x]) } + its(:inclusion_reason) { is_expected.to include "latest pact for a consumer version from branch 'feat-x', latest pact for a consumer version from branch 'main'" } + its(:pact_description) { is_expected.to eq "Pact between Foo and Bar, consumer version 123, latest from branch main, latest from branch feat-x"} + end + + context "when there are branches and tags" do + let(:selectors) { Selectors.new([Selector.latest_for_branch("main"), Selector.latest_for_tag("prod")]) } + its(:inclusion_reason) { is_expected.to include "latest pact for a consumer version from branch 'main', latest pact for a consumer version tagged 'prod'" } end context "when there are 2 head consumer tags" do @@ -53,13 +66,30 @@ module Pacts its(:inclusion_reason) { is_expected.to include "latest pact for a consumer version tagged 'master' (fallback tag used as no pact was found with tag 'feat-x')" } end - context "when the pact is a WIP pact" do + context "when the pact was selected by the fallback tag" do + let(:selectors) { Selectors.new(Selector.latest_for_branch_with_fallback("feat-x", "master")) } + its(:inclusion_reason) { is_expected.to include "latest pact for a consumer version from branch 'master' (fallback branch used as no pact was found from branch 'feat-x')" } + end + + context "when the pact is a WIP pact for the specified provider tags" do let(:selectors) { Selectors.create_for_latest_of_each_tag(%w[feat-x]) } let(:wip) { true } let(:pending) { true } let(:pending_provider_tags) { %w[dev] } its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it is a 'work in progress' pact (ie. it is the pact for the latest version of Foo tagged with 'feat-x' and is still in pending state)."} + + context "when the pact is a WIP pact for a consumer branch" do + let(:selectors) { Selectors.create_for_latest_of_each_branch(%w[feat-x feat-y]) } + + its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it is a 'work in progress' pact (ie. it is the pact for the latest versions of Foo from branches 'feat-x' and 'feat-y' (both have the same content) and is still in pending state)."} + end + + context "when the pact is a WIP pact for a consumer branch and consumer rags" do + let(:selectors) { Selectors.create_for_latest_of_each_branch(%w[feat-x feat-y]) + Selectors.create_for_latest_of_each_tag(%w[feat-z feat-w]) } + + its(:inclusion_reason) { is_expected.to include "it is the pact for the latest versions of Foo from branches 'feat-x' and 'feat-y' and tagged with 'feat-z' and 'feat-w' (all have the same content)"} + end end context "when the pact is one of all versions for a tag" do @@ -74,13 +104,19 @@ module Pacts its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: pacts for all Foo versions tagged 'prod'"} end - context "when the pact is the latest versions for a tag and consumer" do + context "when the pact is the latest version for a tag and consumer" do let(:selectors) { Selectors.new(Selector.latest_for_tag_and_consumer('prod', 'Foo')) } its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: latest pact for a version of Foo tagged 'prod'"} end - context "when the pact is the latest version for and consumer" do + context "when the pact is the latest version for a branch and consumer" do + let(:selectors) { Selectors.new(Selector.latest_for_branch_and_consumer('main', 'Foo')) } + + its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: latest pact for a version of Foo from branch 'main'"} + end + + context "when the pact is the latest version for a consumer" do let(:selectors) { Selectors.new(Selector.latest_for_consumer('Foo')) } its(:inclusion_reason) { is_expected.to include "The pact at http://pact is being verified because it matches the following configured selection criterion: latest pact between Foo and Bar"} @@ -89,7 +125,7 @@ module Pacts describe "#pending_reason" do context "when the pact is not pending" do - context "when there are no non_pending_provider_tags" do + context "when there are no non_pending_provider_tags or a provider branch" do its(:pending_reason) { is_expected.to include "This pact has previously been successfully verified by Bar. If this verification fails, it will fail the build." } end @@ -98,15 +134,26 @@ module Pacts its(:pending_reason) { is_expected.to include "This pact has previously been successfully verified by a version of Bar with tag 'dev'. If this verification fails, it will fail the build."} end + + context "when there is a provider branch" do + let(:provider_branch) { "main" } + let(:non_pending_provider_tags) { %w[dev] } + + # uses branch in preference as that's what the WIP pacts logic does + its(:pending_reason) { is_expected.to include "This pact has previously been successfully verified by a version of Bar from branch 'main'. If this verification fails, it will fail the build."} + end end context "when the pact is pending" do let(:pending) { true } - context "when there are no pending_provider_tags" do - context "when there are no non_pending_provider_tags" do - its(:pending_reason) { is_expected.to include "This pact is in pending state for this version of Bar because a successful verification result for Bar has not yet been published. If this verification fails, it will not cause the overall build to fail." } - end + context "when there are no non_pending_provider_tags or a provider_branch" do + its(:pending_reason) { is_expected.to include "This pact is in pending state for this version of Bar because a successful verification result for Bar has not yet been published. If this verification fails, it will not cause the overall build to fail." } + end + + context "when there is a provider_branch" do + let(:provider_branch) { "main" } + its(:pending_reason) { is_expected.to include "This pact is in pending state for this version of Bar because a successful verification result for a version of Bar from branch 'main' has not yet been published. If this verification fails, it will not cause the overall build to fail." } end context "when there is 1 pending_provider_tag" do diff --git a/spec/lib/pact_broker/versions/repository_spec.rb b/spec/lib/pact_broker/versions/repository_spec.rb index 6843ca881..ba01893f6 100644 --- a/spec/lib/pact_broker/versions/repository_spec.rb +++ b/spec/lib/pact_broker/versions/repository_spec.rb @@ -122,6 +122,38 @@ module Versions end end end + + describe "#create_or_update" do + before do + td.subtract_day + .create_consumer("Foo") + .create_consumer_version(version_number, branch: "original-branch", build_url: "original-build-url") + .create_consumer_version_tag("dev") + end + + let(:pacticipant) { td.and_return(:consumer) } + let(:version_number) { "1234" } + let(:version) { PactBroker::Domain::Version.new(branch: "new-branch") } + + subject { Repository.new.create_or_update(pacticipant, version_number, version) } + + it "overwrites the values" do + expect(subject.branch).to eq "new-branch" + expect(subject.build_url).to eq nil + end + + it "does not change the tags" do + expect { subject }.to_not change { PactBroker::Domain::Version.for("Foo", "1234").tags } + end + + it "does not change the created date" do + expect { subject }.to_not change { PactBroker::Domain::Version.for("Foo", "1234").created_at } + end + + it "does change the updated date" do + expect { subject }.to change { PactBroker::Domain::Version.for("Foo", "1234").updated_at } + end + end end end end diff --git a/spec/lib/sequel/plugins/upsert_spec.rb b/spec/lib/sequel/plugins/upsert_spec.rb index d4e22e6bf..62c5f356d 100644 --- a/spec/lib/sequel/plugins/upsert_spec.rb +++ b/spec/lib/sequel/plugins/upsert_spec.rb @@ -24,6 +24,12 @@ class LatestPactPublicationIdForConsumerVersion < Sequel::Model(:latest_pact_pub plugin :upsert, identifying_columns: [:provider_id, :consumer_version_id] end + describe PacticipantNoUpsert do + it "has an _insert_dataset method" do + expect(PacticipantNoUpsert.private_instance_methods).to include (:_insert_dataset) + end + end + describe "LatestPactPublicationIdForConsumerVersion" do before do td.create_pact_with_hierarchy("Foo", "1", "Bar") @@ -102,10 +108,20 @@ class LatestPactPublicationIdForConsumerVersion < Sequel::Model(:latest_pact_pub context "when a duplicate Version is inserted with upsert" do let!(:pacticipant) { Pacticipant.new(name: "Foo").save } - let!(:original_version) { Version.new(number: "1", pacticipant_id: pacticipant.id).upsert } + let!(:original_version) do + version = Version.new( + number: "1", + pacticipant_id: pacticipant.id, + branch: "original-branch", + build_url: "original-url" + ).upsert + Version.where(id: version.id).update(created_at: yesterday, updated_at: yesterday) + version + end + let(:yesterday) { DateTime.now - 2 } subject do - Version.new(number: "1", pacticipant_id: pacticipant.id).upsert + Version.new(number: "1", pacticipant_id: pacticipant.id, branch: "new-branch").upsert end it "does not raise an error" do @@ -113,12 +129,24 @@ class LatestPactPublicationIdForConsumerVersion < Sequel::Model(:latest_pact_pub end it "sets the values on the object" do - expect(subject.id).to eq original_version.id + expect(subject.branch).to eq "new-branch" + end + + it "nils out values that weren't set on the second model" do + expect(subject.build_url).to eq nil end it "does not insert another row" do expect { subject }.to_not change { Version.count } end + + it "does not change the created_at" do + expect { subject }.to_not change { Version.where(number: "1").first.created_at } + end + + it "does change the updated_at" do + expect { subject }.to change { Version.where(number: "1").first.updated_at } + end end end end