diff --git a/.github/workflows/release_gem.yml b/.github/workflows/release_gem.yml index 2ee570fec..85d037f7a 100644 --- a/.github/workflows/release_gem.yml +++ b/.github/workflows/release_gem.yml @@ -48,7 +48,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Notify ${{ matrix.repository }} of gem release - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.GHTOKENNOTIFYPBRELEASED }} repository: ${{ matrix.repository }} diff --git a/.github/workflows/trigger_pact_docs_update.yml b/.github/workflows/trigger_pact_docs_update.yml index c916c25a8..5b0a7fe60 100644 --- a/.github/workflows/trigger_pact_docs_update.yml +++ b/.github/workflows/trigger_pact_docs_update.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Trigger docs.pact.io update workflow - uses: peter-evans/repository-dispatch@v2 + uses: peter-evans/repository-dispatch@v3 with: token: ${{ secrets.GHTOKENFORTRIGGERINGPACTDOCSUPDATE }} repository: pact-foundation/docs.pact.io diff --git a/.ruby-version b/.ruby-version index a3ec5a4bd..351227fca 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -3.2 +3.2.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b1e07079..bd3ee1c49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,109 @@ + +### v2.111.0 (2024-07-26) + +#### Features + +* add new label api (#703) ([ff3f84e2](/../../commit/ff3f84e2)) +* search pacticipants by display_name ([c5945801](/../../commit/c5945801)) + +#### Bug Fixes + +* **docs** + * Update OAS with correct ref to Notice schema ([6729b7f8](/../../commit/6729b7f8)) + + +### v2.110.0 (2024-04-02) + +#### Features + +* reduce contention when updating the contract_data_updated_at field for integrations (#671) ([ff72d03c](/../../commit/ff72d03c)) +* support consumer version selector for all branches (#667) ([34334ca8](/../../commit/34334ca8)) + +* **clean** + * use postgres advisory locks to ensure only one process can run a clean at a time (#672) ([637c25fa](/../../commit/637c25fa)) + +#### Bug Fixes + +* use for_all_tag_heads instead of latest_by_consumer_tag when fetching wip by branch ([14148a34](/../../commit/14148a34)) +* optimise WIP pacts by using branch/tag heads (#668) ([871209e1](/../../commit/871209e1)) +* improve performance of WIP pacts by using branch heads instead of calculating latest pact for branch ([f9705583](/../../commit/f9705583)) + + +### v2.109.1 (2024-02-21) + +#### Bug Fixes + +* improve performance for 'pacts for verification' queries ([299a6abe](/../../commit/299a6abe)) +* correct spelling in message when pact is modified ([ae62ae7a](/../../commit/ae62ae7a)) + + +### v2.109.0 (2024-02-01) + +#### Features + +* use SemanticLogger for Padrino (#662) ([5d9d7002](/../../commit/5d9d7002)) +* improve performance of publication for very large pacts by calculating the content SHA only once per request ([a947e409](/../../commit/a947e409)) + +#### Bug Fixes + +* pass in environment to environment policy when getting an individual environment ([5c386a43](/../../commit/5c386a43)) +* Dockerfile to reduce vulnerabilities (#650) ([9aaa3484](/../../commit/9aaa3484)) + + +### v2.108.0 (2024-01-05) + +#### Features + +* bulk delete branches (#652) ([14ac33c8](/../../commit/14ac33c8)) +* add latest version for branch endpoint (#644) ([c216bec8](/../../commit/c216bec8)) +* add no-cache header ([9a637327](/../../commit/9a637327)) +* suppport `page` + `size` as pagination params (#642) ([c71089fe](/../../commit/c71089fe)) +* do not include pb:record-deployment or pb:record-release relations for versions embedded in resources ([2f43590c](/../../commit/2f43590c)) +* remove status from individual error in problem+error response ([a4b3ec58](/../../commit/a4b3ec58)) +* add version_id indexes to deployed_versions and released_versions ([00fc7d10](/../../commit/00fc7d10)) +* add endpoint to list branches for a pacticipant (#638) ([ff7e3a53](/../../commit/ff7e3a53)) +* stop running tests for ruby 2.7 ([034aba3b](/../../commit/034aba3b)) +* update sinatra and rack-protection to ~> 3.0 ([92ebbdd3](/../../commit/92ebbdd3)) +* add branch endpoint supporting GET and DELETE (#635) ([1bb6088d](/../../commit/1bb6088d)) +* optimise matrix by applying specified limit to pact publications before joining to verifications ([c61c324e](/../../commit/c61c324e)) +* optimise matrix query when selectors with pacticipant names only are used ([b98f5d1a](/../../commit/b98f5d1a)) +* include environment name in pact metadata ([e120c4e7](/../../commit/e120c4e7)) +* improve wording of 'no version exits' messaging in can-i-deploy response ([9529c679](/../../commit/9529c679)) +* improve performance of matrix when multiple selectors are specified (#631) ([58a28604](/../../commit/58a28604)) +* add pagination parameter validation for paginated endpoints. (#626) ([abb0a1c6](/../../commit/abb0a1c6)) +* add endpoint to list pacticipant versions by branch ([9b4e3f61](/../../commit/9b4e3f61)) +* add endpoint to return latest pact for consumer, provider and consumer branch ([f77086ef](/../../commit/f77086ef)) +* update required ruby version from 2.2 to 2.7 ([f1b1e906](/../../commit/f1b1e906)) +* add pagination and filtering for integrations endpoint ([68d7cf30](/../../commit/68d7cf30)) +* add contract_data_updated_at to integrations table to speed up dashboard query (#617) ([e43c10f2](/../../commit/e43c10f2)) +* support setting feature toggles via individual environment variables (#609) ([be7d9d52](/../../commit/be7d9d52)) + +* **metrics** + * hardcode matrix count to -1 as calculating it causes performance issues and it has no meaning ([62e121b8](/../../commit/62e121b8)) + +* **matrix** + * optimise identification of the 'latest tag' ([824c516a](/../../commit/824c516a)) + +#### Bug Fixes + +* **metrics** + * correct the query for pactRevisionsPerConsumerVersion ([f76b9935](/../../commit/f76b9935)) + +* fix performance issues due to contention in the integrations table when publishing a large number of contracts (> 20) per request, in parallel (#654) ([321a2291](/../../commit/321a2291)) +* raise 404 on paths with missing path segments (#648) ([930b45cd](/../../commit/930b45cd)) +* do not error when no environment is found by name ([d1501618](/../../commit/d1501618)) +* ensure pact associations are eager loaded when finding a single pact ([c98abda6](/../../commit/c98abda6)) +* gracefully handle validating an array when a hash is expected ([b26ddb46](/../../commit/b26ddb46)) +* fix error occuring when can-i-deploy badge is requested and no version is found ([db7dee3a](/../../commit/db7dee3a)) +* fix bug in error handling for 'can-i-deploy branch to environment' badge ([c23beb6b](/../../commit/c23beb6b)) +* improve performance of network diagram (#614) ([ffd3ec4b](/../../commit/ffd3ec4b)) +* fix error raised when attempting to log warning when webhook_redact_sensitive_data is set to false ([9b66270e](/../../commit/9b66270e)) +* gracefully handle execution of webhooks that are deleted between execution attempts (#613) ([1127b41f](/../../commit/1127b41f)) +* add extra validation to ensure parsed content is a hash when publishing pacts ([913e0a52](/../../commit/913e0a52)) + +* **matrix** + * return only most recent row missing verification when latestby=cp ([b7550e53](/../../commit/b7550e53)) + ### v2.107.1 (2023-05-02) diff --git a/Dockerfile b/Dockerfile index d30b4ad1f..34402ad3d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:3.2.1-alpine3.17 +FROM ruby:3.2.4-alpine3.18 WORKDIR /home diff --git a/Gemfile b/Gemfile index 5b0a2a94c..467bcc449 100644 --- a/Gemfile +++ b/Gemfile @@ -2,8 +2,6 @@ source "https://rubygems.org" gemspec -# While https://github.com/brandonhilkert/sucker_punch/pull/253 is outstanding -gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2" gem "rake", "~>12.3.3" gem "sqlite3", "~>1.3" diff --git a/README.md b/README.md index b6d61974a..baecc231d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ [![Gem Version](https://badge.fury.io/rb/pact_broker.svg)](http://badge.fury.io/rb/pact_broker) ![Build status](https://github.com/pact-foundation/pact_broker/workflows/Test/badge.svg) [![Join the chat at https://pact-foundation.slack.com/](https://img.shields.io/badge/chat-on%20slack-blue.svg?logo=slack)](https://slack.pact.io) - [![security](https://hakiri.io/github/pact-foundation/pact_broker/master.svg)](https://hakiri.io/github/pact-foundation/pact_broker/master) The Pact Broker is an application for sharing of consumer driven contracts and verification results. It is optimised for use with "pacts" (contracts created by the [Pact][pact-docs] framework), but can be used for any type of contract that can be serialized to JSON. @@ -151,7 +150,7 @@ You can use the [Pact Broker Docker image][docker] or [Terraform on AWS][terrafo * Are you sure you don't just want to use the [Pact Broker Docker image][docker]? No Docker at your company yet? Ah well, keep reading. * Create a PostgreSQL database. - * To ensure you're on a supported version of the database that you choose, check the [travis.yml][travisyml] file to see which versions we're currently running our tests against. + * To ensure you're on a supported version of the database that you choose, check the [.github/workflows/test.yml](.github/workflows/test.yml) file to see which versions we're currently running our tests against. * MySQL was supported for the native Ruby application until around 2021, but the official `pactfoundation/pact-broker` Docker image does not support it. New features will not be optimised for MySQL, and some new features may not even be supported on it (eg. the database clean feature). * You'll find a sample database creation script in the [example/config.ru](https://github.com/pact-foundation/pact_broker/blob/master/example/config.ru). * Install ruby 2.7 and the latest version of bundler (if you've come this far, I'm assuming you know how to do both of these. Did I mention there was a [Docker][docker] image?) diff --git a/docker-compose-dev-postgres.yml b/docker-compose-dev-postgres.yml index b992d2d30..e48828360 100644 --- a/docker-compose-dev-postgres.yml +++ b/docker-compose-dev-postgres.yml @@ -34,7 +34,7 @@ services: - ./Rakefile:/home/Rakefile shell: - image: ruby:2.5.3-alpine + image: ruby:3.2.4-alpine depends_on: - pact-broker entrypoint: /bin/sh diff --git a/docs/developer/design_pattern_for_eager_loading_collections.md b/docs/developer/design_pattern_for_eager_loading_collections.md new file mode 100644 index 000000000..ebc759490 --- /dev/null +++ b/docs/developer/design_pattern_for_eager_loading_collections.md @@ -0,0 +1,23 @@ +# Design pattern for eager loading collections + +For collection resources (eg. `/versions` ), associations included in the items (eg. branch versions) must be eager loaded for performance reasons. + +The responsiblities of each class used to render a collection resource are as follows: + +* collection decorator (eg. `VersionsDecorator`) - delegate each item in the collection to be rendered by the decorator for the individual item, render pagination links +* item decorator (eg. `VersionDecorator`) - render the JSON for each item +* resource (eg. `PactBroker::Api::Resources::Versions`) - coordinate between, and delegate to, the service and the decorator +* service (eg. `PactBroker::Versions::Service`) - just delegate to repository, as there is no business logic required +* repository (eg. `PactBroker::Versions::Repository`) - load the domain objects from the database + +If the associations for a model are not eager loaded, then each individual association will be lazy loaded when the decorator for the item calls the association method to render it. This results in at least ` * ` calls to the database, and potentially more if any of the associations have their own associations that are required to render the item. This can cause significant performance issues. + +To efficiently render a collection resource, associations must be eager loaded when the collection items are loaded from the database in the repository. Since the repository method for loading the collection may be used in multiple places, and the eager loaded associations required for each of those places may be different (some may not require any associations to be eager loaded), we do not want to hard code the repository to load a fixed set of associations. The list of associations to eager load is therefore passed in to the repository finder method as an argument `eager_load_associations`. + +The decorator is the class that knows what associations are going to be called on the model to render the JSON, so following the design guideline of "put things together that change together", the best place for the declaration of "what associations should be eager loaded for this decorator" is in the decorator itself. The `PactBroker::Api::Decorators::BaseDecorator` has a default implementation of this method called `eager_load_associations` which attempts to automatically identify the required associations, but this can be overridden when necessary. + +We can therefore add the following responsiblities to our previous list: + +* item decorator - return a list of all the associations (including nested associations) that should be eager loaded in order to render its item +* repository - eager load the associations that have been passed into it +* resource - pass in the eager load associations to the repository from the decorator diff --git a/MATRIX.md b/docs/developer/matrix.md similarity index 100% rename from MATRIX.md rename to docs/developer/matrix.md diff --git a/docs/developer/rack.md b/docs/developer/rack.md new file mode 100644 index 000000000..ff3d5e613 --- /dev/null +++ b/docs/developer/rack.md @@ -0,0 +1,11 @@ +# Rack + +https://medium.com/quick-code/rack-middleware-vs-rack-application-vs-rack-the-gem-vs-rack-the-architecture-912cd583ed24 +https://github.com/rack/rack/blob/main/SPEC.rdoc +https://www.rubyguides.com/2018/09/rack-middleware/ + + +* Responds to `call` +* Accepts a hash of parameters +* Returns an array where the first item is the http status, the second is a hash of headers, and the third is an object that responds to `each` (or `call`) that provides the body (99% of the time it's an array of length 1 with a string) + diff --git a/example/Gemfile b/example/Gemfile index 4d05a180a..3c9aa1199 100644 --- a/example/Gemfile +++ b/example/Gemfile @@ -4,4 +4,3 @@ gem "pact_broker" # Would pin this to a known minor version when using properly gem "puma", "~> 5.3" # Can be replaced with your choice of application server gem "sqlite3", "~>1.3" # Sqlite is just for testing. Replace this with "pg" for production. # gem "pg" # Production database gem -gem "sucker_punch", git: "https://github.com/pact-foundation/sucker_punch.git", ref: "fix/rename-is-singleton-class-method-2" diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 3954a1ce4..a9e5f9ef8 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -5,7 +5,6 @@ require "pact_broker/api/contracts" require "pact_broker/application_context" require "pact_broker/feature_toggle" -require "pact_broker/initializers/subscriptions" module Webmachine class Request @@ -86,6 +85,9 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ["pacticipants", :pacticipant_name], Api::Resources::Pacticipant, {resource_name: "pacticipant"} add ["pacticipants", :pacticipant_name, "labels", :label_name], Api::Resources::Label, {resource_name: "pacticipant_label"} + # Labels + add ["labels"], Api::Resources::Labels, {resource_name: "labels"} + # Versions add ["pacticipants", :pacticipant_name, "versions"], Api::Resources::Versions, {resource_name: "pacticipant_versions"} add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions"], Api::Resources::BranchVersions, {resource_name: "pacticipant_branch_versions"} @@ -93,10 +95,12 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ 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::CanIDeployPacticipantVersionByTagToTag, { resource_name: "can_i_deploy_latest_tagged_version_to_tag" } add ["pacticipants", :pacticipant_name, "latest-version", :tag, "can-i-deploy", "to", :to, "badge"], Api::Resources::CanIDeployPacticipantVersionByTagToTagBadge, { resource_name: "can_i_deploy_latest_tagged_version_to_tag_badge" } + add ["pacticipants", :pacticipant_name, "main-branch", "can-i-merge", "badge"], Api::Resources::CanIMergeBadge, { resource_name: "can_i_merge_badge" } 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, "branches"], Api::Resources::PacticipantBranches, {resource_name: "pacticipant_branches"} add ["pacticipants", :pacticipant_name, "branches", :branch_name], Api::Resources::Branch, { resource_name: "branch" } + add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version"], Api::Resources::LatestVersion, { resource_name: "latest_pacticipant_version_for_branch" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "versions", :version_number], Api::Resources::BranchVersion, { resource_name: "branch_version" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironment, { resource_name: "can_i_deploy_latest_branch_version_to_environment" } add ["pacticipants", :pacticipant_name, "branches", :branch_name, "latest-version", "can-i-deploy", "to-environment", :environment_name, "badge"], Api::Resources::CanIDeployPacticipantVersionByBranchToEnvironmentBadge, { resource_name: "can_i_deploy_latest_branch_version_to_environment_badge" } diff --git a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb index dda334b59..6ce831fc4 100644 --- a/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb +++ b/lib/pact_broker/api/contracts/consumer_version_selector_contract.rb @@ -13,7 +13,7 @@ class ConsumerVersionSelectorContract < BaseContract json do optional(:mainBranch).filled(included_in?: [true]) optional(:tag).filled(:str?) - optional(:branch).filled(:str?) + optional(:branch).filled { str? | eql?(true) } optional(:matchingBranch).filled(included_in?: [true]) optional(:latest).filled(included_in?: [true, false]) optional(:fallbackTag).filled(:str?) diff --git a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb index bf2761b3d..7f3e69a48 100644 --- a/lib/pact_broker/api/contracts/pagination_query_params_schema.rb +++ b/lib/pact_broker/api/contracts/pagination_query_params_schema.rb @@ -5,8 +5,13 @@ module Api module Contracts class PaginationQueryParamsSchema < BaseContract params do + # legacy format optional(:pageNumber).maybe(:integer).value(gteq?: 1) optional(:pageSize).maybe(:integer).value(gteq?: 1) + + # desired format + optional(:page).maybe(:integer).value(gteq?: 1) + optional(:size).maybe(:integer).value(gteq?: 1) end end end diff --git a/lib/pact_broker/api/decorators/base_decorator.rb b/lib/pact_broker/api/decorators/base_decorator.rb index ec5a801c4..b14524cc8 100644 --- a/lib/pact_broker/api/decorators/base_decorator.rb +++ b/lib/pact_broker/api/decorators/base_decorator.rb @@ -1,7 +1,6 @@ require "roar/decorator" require "roar/json/hal" require "pact_broker/api/pact_broker_urls" -require "pact_broker/api/decorators/decorator_context" require "pact_broker/api/decorators/format_date_time" require "pact_broker/string_refinements" require "pact_broker/hash_refinements" @@ -35,7 +34,16 @@ def self.property(name, options={}, &block) end end - # Returns the names of the model associations to eager load for use with this decorator + # Returns the names of the model associations to eager load for use with this decorator. + # The default implementation attempts to do an "auto detect" of the associations. + # For single item decorators, it attempts to identify the attributes that are themselves models. + # For collection decorators, it delegates to the eager_load_associations + # method of the single item decorator used to decorate the collection. + # + # The "auto detect" logic can only go so far. It cannot identify when a child object needs its own + # child object(s) to render the attribute. + # This method should be overridden when the "auto detect" logic cannot identify the correct associations + # to load. eg VersionDecorator # @return [Array] def self.eager_load_associations if is_collection_resource? diff --git a/lib/pact_broker/api/decorators/branch_decorator.rb b/lib/pact_broker/api/decorators/branch_decorator.rb index 915a12975..9f7fee210 100644 --- a/lib/pact_broker/api/decorators/branch_decorator.rb +++ b/lib/pact_broker/api/decorators/branch_decorator.rb @@ -18,7 +18,7 @@ class BranchDecorator < BaseDecorator link "pb:latest-version" do | user_options | { title: "Latest version for branch", - href: branch_versions_url(represented, user_options.fetch(:base_url)) + "?pageSize=1" + href: latest_version_for_branch_url(represented, user_options.fetch(:base_url)) } end diff --git a/lib/pact_broker/api/decorators/decorator_context.rb b/lib/pact_broker/api/decorators/decorator_context.rb deleted file mode 100644 index 2bf4eded7..000000000 --- a/lib/pact_broker/api/decorators/decorator_context.rb +++ /dev/null @@ -1,23 +0,0 @@ -module PactBroker - module Api - module Decorators - class DecoratorContext < Hash - attr_reader :base_url, :resource_url, :resource_title, :env, :query_string, :request_url - - def initialize base_url, resource_url, env, options = {} - @base_url = self[:base_url] = base_url - @resource_url = self[:resource_url] = resource_url - @resource_title = self[:resource_title] = options[:resource_title] - @env = self[:env] = env - @query_string = self[:query_string] = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil) - @request_url = self[:request_url] = query_string ? resource_url + "?" + query_string : resource_url - merge!(options) - end - - def to_s - "DecoratorContext #{super}" - end - end - end - end -end diff --git a/lib/pact_broker/api/decorators/decorator_context_creator.rb b/lib/pact_broker/api/decorators/decorator_context_creator.rb index 6d9441a3c..c712941d3 100644 --- a/lib/pact_broker/api/decorators/decorator_context_creator.rb +++ b/lib/pact_broker/api/decorators/decorator_context_creator.rb @@ -1,11 +1,56 @@ -require "pact_broker/api/decorators/decorator_context" +# Builds the Hash that is passed into the Decorator as the `user_options`. It contains the request details, rack env, the (optional) title +# and anything else that is required by the decorator to render the resource (eg. the pacticipant that the versions belong to) module PactBroker module Api module Decorators class DecoratorContextCreator + + # @param [PactBroker::BaseResource] the Pact Broker webmachine resource + # @param [Hash] options any extra options that need to be passed through to the decorator. + # @return [Hash] decorator_context + + # decorator_context [String] :base_url + # The location where the Pact Broker is hosted. + # eg. http://some.host:9292/pact_broker + # Always present + + # decorator_context [String] :resource_url + # The resource URL without any query string. + # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions + # Always present + + # decorator_context [String] :query_string + # The query string. + # "page=1&size=50" + # May be empty + + # decorator_context [String] :request_url + # The full request URL. + # eg. http://some.host:9292/pact_broker/pacticipants/Foo/versions?page=1&size=50 + # Always present + + # decorator_context [Hash] :env + # The rack env. + # Always present + + # decorator_context [Hash] :resource_title + # eg. "Pacticipant versions for Foo" + # Optional + # Used when a single decorator is being used for multiple resources and the title needs to be + # set from the resource. + def self.call(resource, options) - Decorators::DecoratorContext.new(resource.base_url, resource.resource_url, resource.request.env, options) + env = resource.request.env + decorator_context = {} + decorator_context[:base_url] = resource.base_url + decorator_context[:resource_url] = resource.resource_url + decorator_context[:query_string] = query_string = (env["QUERY_STRING"] && !env["QUERY_STRING"].empty? ? env["QUERY_STRING"] : nil) + decorator_context[:request_url] = query_string ? resource.resource_url + "?" + query_string : resource.resource_url + decorator_context[:env] = env + decorator_context[:resource_title] = options[:resource_title] + decorator_context.merge!(options) + decorator_context end end end diff --git a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb index 61b0e1d18..647f7a504 100644 --- a/lib/pact_broker/api/decorators/deployed_versions_decorator.rb +++ b/lib/pact_broker/api/decorators/deployed_versions_decorator.rb @@ -1,11 +1,11 @@ require "pact_broker/api/decorators/base_decorator" -require "pact_broker/api/decorators/deployed_version_decorator" +require "pact_broker/api/decorators/embedded_deployed_version_decorator" module PactBroker module Api module Decorators class DeployedVersionsDecorator < BaseDecorator - collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::DeployedVersionDecorator + collection :entries, as: :deployedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedDeployedVersionDecorator link :self do | context | href = append_query_if_present(context[:resource_url], context[:query_string]) diff --git a/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb new file mode 100644 index 000000000..26be2573e --- /dev/null +++ b/lib/pact_broker/api/decorators/embedded_deployed_version_decorator.rb @@ -0,0 +1,30 @@ +require "pact_broker/api/decorators/base_decorator" +require "pact_broker/api/decorators/embedded_pacticipant_decorator" +require "pact_broker/api/decorators/embedded_version_decorator" +require "pact_broker/api/decorators/environment_decorator" + +module PactBroker + module Api + module Decorators + class EmbeddedDeployedVersionDecorator < BaseDecorator + property :uuid + property :currently_deployed, camelize: true + property :target, camelize: true # deprecated + property :applicationInstance, getter: lambda { |_| target } + property :undeployedAt, getter: lambda { |_| undeployed_at ? FormatDateTime.call(undeployed_at) : nil }, writeable: false + + property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) } + property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) } + property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) } + + include Timestamps + + link :self do | user_options | + { + href: deployed_version_url(represented, user_options.fetch(:base_url)) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb new file mode 100644 index 000000000..d98cd97fc --- /dev/null +++ b/lib/pact_broker/api/decorators/embedded_released_version_decorator.rb @@ -0,0 +1,27 @@ +require "pact_broker/api/decorators/base_decorator" +require "pact_broker/api/decorators/embedded_pacticipant_decorator" +require "pact_broker/api/decorators/embedded_version_decorator" +require "pact_broker/api/decorators/environment_decorator" + +module PactBroker + module Api + module Decorators + class EmbeddedReleasedVersionDecorator < BaseDecorator + property :uuid + property :currently_supported, camelize: true + include Timestamps + property :supportEndedAt, getter: lambda { |_| support_ended_at ? FormatDateTime.call(support_ended_at) : nil }, writeable: false + + property :pacticipant, :extend => EmbeddedPacticipantDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:pacticipant) } + property :version, :extend => EmbeddedVersionDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:version) } + property :environment, :extend => EnvironmentDecorator, writeable: false, embedded: true, if: -> (user_options:, **_other) { user_options[:expand]&.include?(:environment) } + + link :self do | user_options | + { + href: released_version_url(represented, user_options.fetch(:base_url)) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/extended_pact_decorator.rb b/lib/pact_broker/api/decorators/extended_pact_decorator.rb index aa5568c7b..23daac697 100644 --- a/lib/pact_broker/api/decorators/extended_pact_decorator.rb +++ b/lib/pact_broker/api/decorators/extended_pact_decorator.rb @@ -1,5 +1,10 @@ require "pact_broker/api/decorators/pact_decorator" +# Pactflow notes: +# This decorator was added for Pactflow, but we needed to change it so much that there +# is a separate class in pact_broker_fork/lib/pactflow/api/decorators/extended_pact_decorator.rb now +# and this one isn't used. + module PactBroker module Api module Decorators @@ -47,4 +52,4 @@ def head_tags end end end -end \ No newline at end of file +end diff --git a/lib/pact_broker/api/decorators/label_decorator.rb b/lib/pact_broker/api/decorators/label_decorator.rb index 009df5d1c..098c0bcc1 100644 --- a/lib/pact_broker/api/decorators/label_decorator.rb +++ b/lib/pact_broker/api/decorators/label_decorator.rb @@ -11,20 +11,26 @@ class LabelDecorator < BaseDecorator include Timestamps - link :self do | options | - { - title: "Label", - name: represented.name, - href: label_url(represented, options[:base_url]) - } - end + # This method is overridden to conditionally render the links based on the user_options + def to_hash(options) + hash = super + + unless options.dig(:user_options, :hide_label_decorator_links) + hash[:_links] = { + self: { + title: "Label", + name: represented.name, + href: label_url(represented, options.dig(:user_options, :base_url)) + }, + pacticipant: { + title: "Pacticipant", + name: represented.pacticipant.name, + href: pacticipant_url(options.dig(:user_options, :base_url), represented.pacticipant) + } + } + end - link :pacticipant do | options | - { - title: "Pacticipant", - name: represented.pacticipant.name, - href: pacticipant_url(options.fetch(:base_url), represented.pacticipant) - } + hash end end end diff --git a/lib/pact_broker/api/decorators/labels_decorator.rb b/lib/pact_broker/api/decorators/labels_decorator.rb new file mode 100644 index 000000000..b668f1c92 --- /dev/null +++ b/lib/pact_broker/api/decorators/labels_decorator.rb @@ -0,0 +1,23 @@ +require_relative "base_decorator" +require_relative "pagination_links" +require_relative "label_decorator" +require "pact_broker/domain/label" + +module PactBroker + module Api + module Decorators + class LabelsDecorator < BaseDecorator + collection :entries, :as => :labels, :class => PactBroker::Domain::Label, :extend => PactBroker::Api::Decorators::LabelDecorator, embedded: true + + include PaginationLinks + + link :self do | options | + { + title: "Labels", + href: options.fetch(:resource_url) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/notices_decorator.rb b/lib/pact_broker/api/decorators/notices_decorator.rb new file mode 100644 index 000000000..fe46659de --- /dev/null +++ b/lib/pact_broker/api/decorators/notices_decorator.rb @@ -0,0 +1,11 @@ +require_relative "base_decorator" + +module PactBroker + module Api + module Decorators + class NoticesDecorator < BaseDecorator + property :entries, as: :notices, getter: ->(represented:, **) { represented.collect(&:to_h) } + end + end + end +end diff --git a/lib/pact_broker/api/decorators/pacticipant_decorator.rb b/lib/pact_broker/api/decorators/pacticipant_decorator.rb index c13814743..b2c97eb0b 100644 --- a/lib/pact_broker/api/decorators/pacticipant_decorator.rb +++ b/lib/pact_broker/api/decorators/pacticipant_decorator.rb @@ -20,7 +20,7 @@ class PacticipantDecorator < BaseDecorator property :main_branch property :latest_version, as: :latestVersion, :class => PactBroker::Domain::Version, extend: PactBroker::Api::Decorators::EmbeddedVersionDecorator, embedded: true, writeable: false - collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true + collection :labels, :class => PactBroker::Domain::Label, extend: PactBroker::Api::Decorators::EmbeddedLabelDecorator, embedded: true, writeable: false include Timestamps diff --git a/lib/pact_broker/api/decorators/pagination_links.rb b/lib/pact_broker/api/decorators/pagination_links.rb index eba47eeae..39b00091a 100644 --- a/lib/pact_broker/api/decorators/pagination_links.rb +++ b/lib/pact_broker/api/decorators/pagination_links.rb @@ -24,7 +24,7 @@ module PaginationLinks represented.respond_to?(:page_count) && represented.current_page < represented.page_count { - href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page + 1}", + href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page + 1}", title: "Next page" } @@ -34,7 +34,7 @@ module PaginationLinks link :previous do | context | if represented.respond_to?(:first_page?) && !represented.first_page? { - href: context[:resource_url] + "?pageSize=#{represented.page_size}&pageNumber=#{represented.current_page - 1}", + href: context[:resource_url] + "?size=#{represented.page_size}&page=#{represented.current_page - 1}", title: "Previous page" } end diff --git a/lib/pact_broker/api/decorators/publish_contract_decorator.rb b/lib/pact_broker/api/decorators/publish_contract_decorator.rb index 48e3c40f7..ec336372f 100644 --- a/lib/pact_broker/api/decorators/publish_contract_decorator.rb +++ b/lib/pact_broker/api/decorators/publish_contract_decorator.rb @@ -11,8 +11,13 @@ class PublishContractDecorator < BaseDecorator property :provider_name property :specification property :content_type - property :decoded_content + property :decoded_content, setter: -> (fragment:, represented:, user_options:, **) { + represented.decoded_content = fragment + # Set the pact version sha when we set the content + represented.pact_version_sha = user_options.fetch(:sha_generator).call(fragment) + } property :on_conflict, default: "overwrite" + end end end diff --git a/lib/pact_broker/api/decorators/released_versions_decorator.rb b/lib/pact_broker/api/decorators/released_versions_decorator.rb index d21e94ab5..083a9598a 100644 --- a/lib/pact_broker/api/decorators/released_versions_decorator.rb +++ b/lib/pact_broker/api/decorators/released_versions_decorator.rb @@ -1,11 +1,11 @@ require "pact_broker/api/decorators/base_decorator" -require "pact_broker/api/decorators/released_version_decorator" +require "pact_broker/api/decorators/embedded_released_version_decorator" module PactBroker module Api module Decorators class ReleasedVersionsDecorator < BaseDecorator - collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::ReleasedVersionDecorator + collection :entries, as: :releasedVersions, embedded: true, :extend => PactBroker::Api::Decorators::EmbeddedReleasedVersionDecorator link :self do | context | href = append_query_if_present(context[:resource_url], context[:query_string]) diff --git a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb index a2f92f220..f43f76077 100644 --- a/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb +++ b/lib/pact_broker/api/decorators/runtime_error_problem_json_decorator.rb @@ -3,7 +3,7 @@ module PactBroker module Api module Decorators - class RuntimeErrorProblemJSONDecorator + class RuntimeErrorProblemJsonDecorator # @param message [String] def initialize(message) diff --git a/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb new file mode 100644 index 000000000..c416ca779 --- /dev/null +++ b/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator.rb @@ -0,0 +1,38 @@ +require_relative "base_decorator" + +module PactBroker + module Api + module Decorators + class TriggeredWebhookLogsDecorator < BaseDecorator + class WebhookExecutionDecorator < BaseDecorator + property :success + property :logs + property :created_at, as: :createdAt + end + + + nested :triggeredWebhook, embedded: true do + property :uuid + end + + collection :webhook_executions, as: :executions, :class => PactBroker::Webhooks::Execution, :extend => WebhookExecutionDecorator + + link :self do | options | + { + title: "Triggered webhook logs", + href: options[:resource_url] + } + end + + link :'pb:webhook' do | context | + if represented.webhook + { + href: webhook_url(represented.webhook.uuid, context[:base_url]), + title: "Webhook" + } + end + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/version_decorator.rb b/lib/pact_broker/api/decorators/version_decorator.rb index b033a606b..7b94dbc59 100644 --- a/lib/pact_broker/api/decorators/version_decorator.rb +++ b/lib/pact_broker/api/decorators/version_decorator.rb @@ -1,5 +1,6 @@ require_relative "base_decorator" require_relative "embedded_tag_decorator" +require_relative "embedded_branch_version_decorator" module PactBroker module Api @@ -16,11 +17,26 @@ class VersionDecorator < BaseDecorator include Timestamps + # Returns the list of associations that must be eager loaded to efficiently render a version + # when this decorator is used in a collection (eg. VersionsDecorator) + # The associations that need to be eager loaded for the VersionDecorator + # are hand coded + # @return + def self.eager_load_associations + [ + :pacticipant, + :pact_publications, + { branch_versions: [:version, :branch_head, { branch: :pacticipant }] }, + { tags: :head_tag } + ] + end + link :self do | options | { title: "Version", name: represented.number, - href: version_url(options.fetch(:base_url), represented) + # This decorator is used for multiple Version resources, so dynamically fetch the current resource URL + href: options.fetch(:resource_url) } end @@ -58,7 +74,7 @@ class VersionDecorator < BaseDecorator end links :'pb:record-deployment' do | context | - context.fetch(:environments, []).collect do | environment | + context[:environments]&.collect do | environment | { title: "Record deployment to #{environment.display_name}", name: environment.name, @@ -68,7 +84,7 @@ class VersionDecorator < BaseDecorator end links :'pb:record-release' do | context | - context.fetch(:environments, []).collect do | environment | + context[:environments]&.collect do | environment | { title: "Record release to #{environment.display_name}", name: environment.name, diff --git a/lib/pact_broker/api/decorators/versions_decorator.rb b/lib/pact_broker/api/decorators/versions_decorator.rb index 5637f7b50..3cb016e61 100644 --- a/lib/pact_broker/api/decorators/versions_decorator.rb +++ b/lib/pact_broker/api/decorators/versions_decorator.rb @@ -6,13 +6,23 @@ module PactBroker module Api module Decorators class VersionsDecorator < BaseDecorator + class VersionInCollectionDecorator < PactBroker::Api::Decorators::VersionDecorator + # VersionDecorator has a dynamic self URL, depending which path the Version resource is mounted at. + # Hardcode the URL of the embedded Versions in this collection to use the canonical URL with the version number. + link :self do | user_options | + { + title: "Version", + name: represented.number, + href: version_url(user_options.fetch(:base_url), represented) + } + end + end - collection :entries, as: :versions, embedded: true, :extend => PactBroker::Api::Decorators::VersionDecorator + collection :entries, as: :versions, embedded: true, :extend => VersionInCollectionDecorator link :self do | user_options | - href = append_query_if_present(user_options[:resource_url], user_options[:query_string]) { - href: href, + href: user_options.fetch(:request_url), title: user_options[:resource_title] || "All application versions of #{user_options[:pacticipant_name]}" } end diff --git a/lib/pact_broker/api/decorators/webhook_decorator.rb b/lib/pact_broker/api/decorators/webhook_decorator.rb index aad276dac..348b70931 100644 --- a/lib/pact_broker/api/decorators/webhook_decorator.rb +++ b/lib/pact_broker/api/decorators/webhook_decorator.rb @@ -16,7 +16,7 @@ class WebhookEventDecorator < BaseDecorator property :name end - property :uuid + property :uuid, writeable: false property :description, getter: lambda { |represented:, **| represented.display_description } diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 3ce937d9d..c6bd1d72b 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -243,6 +243,10 @@ def branch_version_url(branch_version, base_url = "") "#{branch_versions_url(branch_version.branch, base_url)}/#{url_encode(branch_version.version_number)}" end + def latest_version_for_branch_url(branch, base_url = "") + "#{branch_url(branch, base_url)}/latest-version" + end + def templated_tag_url_for_pacticipant pacticipant_name, base_url = "" pacticipant_url_from_params({ pacticipant_name: pacticipant_name }, base_url) + "/versions/{version}/tags/{tag}" end @@ -283,7 +287,7 @@ def webhooks_url base_url "#{base_url}/webhooks" end - def webhook_url uuid, base_url + def webhook_url uuid, base_url = "" "#{base_url}/webhooks/#{uuid}" end diff --git a/lib/pact_broker/api/paths.rb b/lib/pact_broker/api/paths.rb index f1ab12e38..81ceb8443 100644 --- a/lib/pact_broker/api/paths.rb +++ b/lib/pact_broker/api/paths.rb @@ -3,11 +3,12 @@ module Api module Paths PACT_BADGE_PATH = %r{^/pacts/provider/[^/]+/consumer/.*/badge(?:\.[A-Za-z]+)?$}.freeze MATRIX_BADGE_PATH = %r{^/matrix/provider/[^/]+/latest/[^/]+/consumer/[^/]+/latest/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze + CAN_I_MERGE_BADGE_PATH = %r{^/pacticipants/[^/]+/main-branch/can-i-merge/badge(?:\.[A-Za-z]+)?$}.freeze CAN_I_DEPLOY_TAG_BADGE_PATH = %r{^/pacticipants/[^/]+/latest-version/[^/]+/can-i-deploy/to/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH = %r{^/pacticipants/[^/]+/branches/[^/]+/latest-version/can-i-deploy/to-environment/[^/]+/badge(?:\.[A-Za-z]+)?$}.freeze VERIFICATION_RESULTS = %r{^/pacts/provider/[^/]+/consumer/[^/]+/pact-version/[^/]+/verification-results/[^/]+} - BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH] + BADGE_PATHS = [PACT_BADGE_PATH, MATRIX_BADGE_PATH, CAN_I_DEPLOY_TAG_BADGE_PATH, CAN_I_DEPLOY_BRANCH_ENV_BADGE_PATH, CAN_I_MERGE_BADGE_PATH] extend self diff --git a/lib/pact_broker/api/resources/after_reply.rb b/lib/pact_broker/api/resources/after_reply.rb new file mode 100644 index 000000000..cbf7da715 --- /dev/null +++ b/lib/pact_broker/api/resources/after_reply.rb @@ -0,0 +1,15 @@ +require "pact_broker/async/after_reply" + +module PactBroker + module Api + module Resources + module AfterReply + + # @param [Callable] block the block to execute after the response has been sent to the user. + def after_reply(&block) + PactBroker::Async::AfterReply.new(request.env).execute(&block) + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/all_webhooks.rb b/lib/pact_broker/api/resources/all_webhooks.rb index 95b492bee..d8d4f544e 100644 --- a/lib/pact_broker/api/resources/all_webhooks.rb +++ b/lib/pact_broker/api/resources/all_webhooks.rb @@ -38,7 +38,7 @@ def to_json end def from_json - saved_webhook = webhook_service.create(next_uuid, webhook, consumer, provider) + saved_webhook = webhook_service.create(next_uuid, webhook, webhook_consumer, webhook_provider) response.body = decorator_class(:webhook_decorator).new(saved_webhook).to_json(**decorator_options) end @@ -60,11 +60,11 @@ def schema api_contract_class(:webhook_contract) end - def consumer + def webhook_consumer webhook.consumer&.name ? pacticipant_service.find_pacticipant_by_name(webhook.consumer.name) : nil end - def provider + def webhook_provider webhook.provider&.name ? pacticipant_service.find_pacticipant_by_name(webhook.provider.name) : nil end diff --git a/lib/pact_broker/api/resources/base_resource.rb b/lib/pact_broker/api/resources/base_resource.rb index 1b4f5da4c..c91468496 100644 --- a/lib/pact_broker/api/resources/base_resource.rb +++ b/lib/pact_broker/api/resources/base_resource.rb @@ -207,7 +207,7 @@ def invalid_json? def find_pacticipant name, role pacticipant_service.find_pacticipant_by_name(name).tap do | pacticipant | if pacticipant.nil? - set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not_found", status: 404) + set_json_error_message("No #{role} with name '#{name}' found", title: "Not found", type: "not-found", status: 404) end end end diff --git a/lib/pact_broker/api/resources/branch_versions.rb b/lib/pact_broker/api/resources/branch_versions.rb index 6bb1b10ea..0a25d9d7a 100644 --- a/lib/pact_broker/api/resources/branch_versions.rb +++ b/lib/pact_broker/api/resources/branch_versions.rb @@ -31,7 +31,7 @@ def to_json end def versions - @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options) + @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, { branch_name: identifier_from_path[:branch_name] }, pagination_options, decorator_class(:versions_decorator).eager_load_associations) end def policy_name diff --git a/lib/pact_broker/api/resources/can_i_merge_badge.rb b/lib/pact_broker/api/resources/can_i_merge_badge.rb new file mode 100644 index 000000000..964b74957 --- /dev/null +++ b/lib/pact_broker/api/resources/can_i_merge_badge.rb @@ -0,0 +1,36 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/resources/badge_methods" + +module PactBroker + module Api + module Resources + class CanIMergeBadge < BaseResource + include BadgeMethods # This module contains all necessary webmachine methods for badge implementation + + def badge_url + if pacticipant.nil? # pacticipant method is defined in BaseResource + # if the pacticipant is nil, we return an error badge url + badge_service.error_badge_url("pacticipant", "not found") + elsif version.nil? + # when there is no main branch version, we return an error badge url + badge_service.error_badge_url("main branch version", "not found") + else + # we call badge_service to build the badge url + badge_service.can_i_merge_badge_url(deployable: results) + end + end + + private + + def results + # can_i_merge returns true or false if the main branch version is compatible with all the integrations + @results ||= matrix_service.can_i_merge(pacticipant: pacticipant, latest_main_branch_version: version) + end + + def version + @version ||= version_service.find_latest_version_from_main_branch(pacticipant) + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb index 1d56f9f7b..b2de08e25 100644 --- a/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb +++ b/lib/pact_broker/api/resources/currently_deployed_versions_for_environment.rb @@ -21,7 +21,7 @@ def resource_exists? end def to_json - decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title)) + decorator_class(decorator_name).new(deployed_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version])) end def policy_name diff --git a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb index 95dd313b6..d34e6c317 100644 --- a/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb +++ b/lib/pact_broker/api/resources/currently_supported_versions_for_environment.rb @@ -21,7 +21,7 @@ def resource_exists? end def to_json - decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title)) + decorator_class(decorator_name).new(released_versions).to_json(**decorator_options(title: title, expand: [:pacticipant, :version])) end def policy_name diff --git a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb index 9e79be72a..00f8c3e0c 100644 --- a/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb +++ b/lib/pact_broker/api/resources/deployed_versions_for_version_and_environment.rb @@ -78,7 +78,7 @@ def application_instance end def title - "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number}" + "Deployed versions for #{pacticipant_name} version #{pacticipant_version_number} in environment #{environment.display_name}" end end end diff --git a/lib/pact_broker/api/resources/environment.rb b/lib/pact_broker/api/resources/environment.rb index 80b1d7707..5f92cd843 100644 --- a/lib/pact_broker/api/resources/environment.rb +++ b/lib/pact_broker/api/resources/environment.rb @@ -42,6 +42,10 @@ def policy_name :'deployments::environment' end + def policy_record + environment + end + def to_json decorator_class(:environment_decorator).new(environment).to_json(**decorator_options) end diff --git a/lib/pact_broker/api/resources/error_response_generator.rb b/lib/pact_broker/api/resources/error_response_generator.rb index d372c5d86..c4f757b40 100644 --- a/lib/pact_broker/api/resources/error_response_generator.rb +++ b/lib/pact_broker/api/resources/error_response_generator.rb @@ -3,8 +3,9 @@ require "pact_broker/errors" require "pact_broker/messages" -# Generates the response headers and body for use when there is an unexpected -# error when executing a Webmachine resource request. +# Generates the response headers and body for use when there is a runtime +# error in the business logic (services and repositories) when executing a Webmachine resource request. +# Obfuscates any exception messages that might expose vulnerablities in production. # Uses the Accept header to determine whether to return application/problem+json # or application/hal+json, for backwards compatibility. # In the next major version of the Pact Broker, all error responses @@ -58,7 +59,7 @@ def self.display_message(error, message, obfuscated_message) end private_class_method def self.problem_json_response_body(message, env) - PactBroker::Api::Decorators::RuntimeErrorProblemJSONDecorator.new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] }) + error_decorator_class(env).new(message).to_hash(user_options: { base_url: env["pactbroker.base_url" ] }) end private_class_method def self.obfuscated_error_message(error_reference) @@ -76,6 +77,10 @@ def self.display_message(error, message, obfuscated_message) private_class_method def self.problem_json?(env) env["HTTP_ACCEPT"]&.include?("application/problem+json") end + + private_class_method def self.error_decorator_class(env) + env["pactbroker.application_context"].decorator_configuration.class_for(:runtime_error_problem_json_decorator) + end end end end diff --git a/lib/pact_broker/api/resources/event_methods.rb b/lib/pact_broker/api/resources/event_methods.rb new file mode 100644 index 000000000..0dcbe3a0b --- /dev/null +++ b/lib/pact_broker/api/resources/event_methods.rb @@ -0,0 +1,15 @@ +require "pact_broker/events/subscriber" + +module PactBroker + module Api + module Resources + module EventMethods + def subscribe(listener) + PactBroker::Events.subscribe(listener) do + yield + end + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb index 0a2653f8e..83877f91e 100644 --- a/lib/pact_broker/api/resources/index.rb +++ b/lib/pact_broker/api/resources/index.rb @@ -116,6 +116,12 @@ def links title: "Get, create or delete a tag for a pacticipant version", templated: true }, + "pb:pacticipant-branch" => + { + href: base_url + "/pacticipants/{pacticipant}/branches/{branch}", + title: "Get or delete a pacticipant branch", + templated: true + }, "pb:pacticipant-branch-version" => { href: base_url + "/pacticipants/{pacticipant}/branches/{branch}/versions/{version}", diff --git a/lib/pact_broker/api/resources/labels.rb b/lib/pact_broker/api/resources/labels.rb new file mode 100644 index 000000000..d6620969a --- /dev/null +++ b/lib/pact_broker/api/resources/labels.rb @@ -0,0 +1,37 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/decorators/labels_decorator" +require "pact_broker/api/resources/pagination_methods" + +module PactBroker + module Api + module Resources + class Labels < BaseResource + include PaginationMethods + + def content_types_provided + [["application/hal+json", :to_json]] + end + + def allowed_methods + ["GET", "OPTIONS"] + end + + def policy_name + :'labels::labels' + end + + def to_json + decorator_class(:labels_decorator).new(labels).to_json( + **decorator_options( + hide_label_decorator_links: true, + ) + ) + end + + def labels + label_service.get_all_unique_labels(pagination_options) + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/latest_version.rb b/lib/pact_broker/api/resources/latest_version.rb index bac4ab7a2..430410fd8 100644 --- a/lib/pact_broker/api/resources/latest_version.rb +++ b/lib/pact_broker/api/resources/latest_version.rb @@ -17,6 +17,8 @@ def allowed_methods 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[:branch_name] + @version ||= version_service.find_latest_by_pacticipant_name_and_branch_name(identifier_from_path[:pacticipant_name], identifier_from_path[:branch_name]) else @version ||= version_service.find_latest_by_pacticpant_name(identifier_from_path) end diff --git a/lib/pact_broker/api/resources/pact.rb b/lib/pact_broker/api/resources/pact.rb index 441ed1496..a27c9a4d6 100644 --- a/lib/pact_broker/api/resources/pact.rb +++ b/lib/pact_broker/api/resources/pact.rb @@ -9,14 +9,17 @@ require "pact_broker/webhooks/execution_configuration" require "pact_broker/api/resources/webhook_execution_methods" require "pact_broker/api/resources/pact_resource_methods" +require "pact_broker/api/resources/event_methods" +require "pact_broker/integrations/event_listener" module PactBroker module Api module Resources class Pact < BaseResource + include EventMethods include PacticipantResourceMethods - include WebhookExecutionMethods include PactResourceMethods + include WebhookExecutionMethods include PactBroker::Messages def content_types_provided @@ -65,11 +68,13 @@ def resource_exists? def from_json response_code = pact ? 200 : 201 - handle_webhook_events do - if request.patch? && resource_exists? - @pact = pact_service.merge_pact(pact_params) - else - @pact = pact_service.create_or_update_pact(pact_params) + subscribe(PactBroker::Integrations::EventListener.new) do + handle_webhook_events do + if request.patch? && resource_exists? + @pact = pact_service.merge_pact(pact_params.merge(pact_version_sha: pact_version_sha)) + else + @pact = pact_service.create_or_update_pact(pact_params.merge(pact_version_sha: pact_version_sha)) + end end end response.body = to_json @@ -109,7 +114,7 @@ def pact end def disallowed_modification? - if request.really_put? && pact_service.disallowed_modification?(pact, pact_params.json_content) + if request.really_put? && pact_service.disallowed_modification?(pact, pact_version_sha) message_params = { consumer_name: pact_params.consumer_name, consumer_version_number: pact_params.consumer_version_number, provider_name: pact_params.provider_name } set_json_error_message(message("errors.validation.pact_content_modification_not_allowed", message_params)) true @@ -121,6 +126,10 @@ def disallowed_modification? def schema api_contract_class(:put_pact_params_contract) end + + def pact_version_sha + @pact_version_sha ||= pact_service.generate_sha(pact_params.json_content) + end end end end diff --git a/lib/pact_broker/api/resources/pacticipant_branches.rb b/lib/pact_broker/api/resources/pacticipant_branches.rb index f97da58a2..e30181b45 100644 --- a/lib/pact_broker/api/resources/pacticipant_branches.rb +++ b/lib/pact_broker/api/resources/pacticipant_branches.rb @@ -1,6 +1,8 @@ require "pact_broker/api/resources/base_resource" require "pact_broker/api/resources/pagination_methods" require "pact_broker/api/resources/filter_methods" +require "pact_broker/api/resources/after_reply" +require "rack/utils" module PactBroker module Api @@ -8,13 +10,14 @@ module Resources class PacticipantBranches < BaseResource include PaginationMethods include FilterMethods + include AfterReply def content_types_provided [["application/hal+json", :to_json]] end def allowed_methods - ["GET", "OPTIONS"] + ["GET", "DELETE", "OPTIONS"] end def resource_exists? @@ -29,6 +32,17 @@ def policy_name :'versions::branches' end + # Allows bulk deletion of pacticipant branches, keeping the specified branches and the main branch. + # Deletes the branches asyncronously, after the response has been sent, for performance reasons. + def delete_resource + after_reply do + branch_service.delete_branches_for_pacticipant(pacticipant, exclude: exclude) + end + notices = branch_service.branch_deletion_notices(pacticipant, exclude: exclude) + response.body = decorator_class(:notices_decorator).new(notices).to_json(**decorator_options) + 202 + end + private def branches @@ -40,6 +54,10 @@ def branches ) end + def exclude + Rack::Utils.parse_nested_query(request.uri.query)["exclude"] || [] + end + def eager_load_associations decorator_class(:pacticipant_branches_decorator).eager_load_associations end diff --git a/lib/pact_broker/api/resources/pagination_methods.rb b/lib/pact_broker/api/resources/pagination_methods.rb index 069f71853..a8c2f4a9f 100644 --- a/lib/pact_broker/api/resources/pagination_methods.rb +++ b/lib/pact_broker/api/resources/pagination_methods.rb @@ -2,8 +2,14 @@ module PactBroker module Api module Resources module PaginationMethods + # rubocop: disable Metrics/CyclomaticComplexity def pagination_options - if request.query["pageNumber"] || request.query["pageSize"] + if request.query["page"] || request.query["size"] + { + page_number: request.query["page"]&.to_i || 1, + page_size: request.query["size"]&.to_i || 100 + } + elsif request.query["pageNumber"] || request.query["pageSize"] { page_number: request.query["pageNumber"]&.to_i || 1, page_size: request.query["pageSize"]&.to_i || 100 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 852e143d4..822bd4310 100644 --- a/lib/pact_broker/api/resources/provider_pacts_for_verification.rb +++ b/lib/pact_broker/api/resources/provider_pacts_for_verification.rb @@ -15,6 +15,10 @@ def content_types_provided [["application/hal+json", :to_json]] end + # TODO drop support for GET in next major version. + # GET was only used by the very first Ruby Pact clients that supported the 'pacts for verification' + # feature, until it became clear that the parameters for the request were going to get nested and complex, + # at which point the POST was added. def allowed_methods ["GET", "POST", "OPTIONS"] end @@ -32,6 +36,7 @@ def process_post end end + # For this endoint, the POST is a "read" action (used for Pactflow) def read_methods super + %w{POST} end diff --git a/lib/pact_broker/api/resources/publish_contracts.rb b/lib/pact_broker/api/resources/publish_contracts.rb index 45b921735..c7e598c6e 100644 --- a/lib/pact_broker/api/resources/publish_contracts.rb +++ b/lib/pact_broker/api/resources/publish_contracts.rb @@ -53,7 +53,7 @@ def policy_record_context private def parsed_contracts - @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params) + @parsed_contracts ||= decorator_class(:publish_contracts_decorator).new(PactBroker::Contracts::ContractsToPublish.new).from_hash(params, { user_options: { sha_generator: PactBroker.configuration.sha_generator } } ) end def params diff --git a/lib/pact_broker/api/resources/triggered_webhook_logs.rb b/lib/pact_broker/api/resources/triggered_webhook_logs.rb index 5925e8db1..097788bff 100644 --- a/lib/pact_broker/api/resources/triggered_webhook_logs.rb +++ b/lib/pact_broker/api/resources/triggered_webhook_logs.rb @@ -7,7 +7,7 @@ module Resources class TriggeredWebhookLogs < BaseResource def content_types_provided - [["text/plain", :to_text]] + [["text/plain", :to_text], ["application/hal+json", :to_json]] end def allowed_methods @@ -27,6 +27,10 @@ def to_text end end + def to_json + decorator_class(:triggered_webhook_logs_decorator).new(triggered_webhook).to_json(**decorator_options) + end + def policy_name :'webhooks::triggered_webhook' end diff --git a/lib/pact_broker/api/resources/verifications.rb b/lib/pact_broker/api/resources/verifications.rb index ac743214f..a6f8ff27d 100644 --- a/lib/pact_broker/api/resources/verifications.rb +++ b/lib/pact_broker/api/resources/verifications.rb @@ -5,6 +5,8 @@ require "pact_broker/api/decorators/verification_decorator" require "pact_broker/api/resources/webhook_execution_methods" require "pact_broker/api/resources/metadata_resource_methods" +require "pact_broker/api/resources/event_methods" +require "pact_broker/integrations/event_listener" module PactBroker module Api @@ -12,6 +14,7 @@ module Resources class Verifications < BaseResource include WebhookExecutionMethods include MetadataResourceMethods + include EventMethods def content_types_accepted [["application/json", :from_json]] @@ -42,10 +45,12 @@ def create_path end def from_json - handle_webhook_events(build_url: verification_params["buildUrl"]) do - verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors]) - verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context) - response.body = decorator_for(verification).to_json(**decorator_options) + subscribe(PactBroker::Integrations::EventListener.new) do + handle_webhook_events(build_url: verification_params["buildUrl"]) do + verified_pacts = pact_service.find_for_verification_publication(pact_params, event_context[:consumer_version_selectors]) + verification = verification_service.create(next_verification_number, verification_params, verified_pacts, event_context) + response.body = decorator_for(verification).to_json(**decorator_options) + end end true end diff --git a/lib/pact_broker/api/resources/versions.rb b/lib/pact_broker/api/resources/versions.rb index e03b2d76d..4e91ca18a 100644 --- a/lib/pact_broker/api/resources/versions.rb +++ b/lib/pact_broker/api/resources/versions.rb @@ -31,7 +31,7 @@ def to_json end def versions - @versions ||= version_service.find_all_pacticipant_versions_in_reverse_order(pacticipant_name, pagination_options) + @versions ||= version_service.find_pacticipant_versions_in_reverse_order(pacticipant_name, {}, pagination_options, decorator_class(:versions_decorator).eager_load_associations) end def policy_name diff --git a/lib/pact_broker/app.rb b/lib/pact_broker/app.rb index af4c0c932..2f6be0006 100644 --- a/lib/pact_broker/app.rb +++ b/lib/pact_broker/app.rb @@ -1,8 +1,3 @@ -# Must be defined before loading Padrino -PADRINO_LOGGER ||= { - ENV.fetch("RACK_ENV", "production").to_sym => { log_level: :error, stream: :stdout, format_datetime: "%Y-%m-%dT%H:%M:%S.000%:z" } -} - require "pact_broker/configuration" require "pact_broker/db" require "pact_broker/initializers/database_connection" @@ -21,6 +16,7 @@ require "rack/pact_broker/configurable_make_it_later" require "rack/pact_broker/no_auth" require "rack/pact_broker/reset_thread_data" +require "rack/pact_broker/add_cache_header" require "rack/pact_broker/add_vary_header" require "rack/pact_broker/use_when" require "rack/pact_broker/application_context" @@ -31,6 +27,7 @@ require "pact_broker/api/authorization/resource_access_policy" require "pact_broker/api/middleware/http_debug_logs" require "pact_broker/application_context" +require "pact_broker/db/advisory_lock" module PactBroker @@ -105,22 +102,19 @@ def post_configure def prepare_database logger.info "Database schema version is #{PactBroker::DB.version(configuration.database_connection)}" + lock = PactBroker::DB::AdvisoryLock.new(configuration.database_connection, :migrate, :pg_advisory_lock) if configuration.auto_migrate_db - migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files } - if PactBroker::DB.is_current?(configuration.database_connection, migration_options) - logger.info "Skipping database migrations as the latest migration has already been applied" - else - logger.info "Migrating database schema" - PactBroker::DB.run_migrations configuration.database_connection, migration_options - logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}" + lock.with_lock do + ensure_all_database_migrations_are_applied end else logger.info "Skipping database schema migrations as database auto migrate is disabled" end if configuration.auto_migrate_db_data - logger.info "Migrating data" - PactBroker::DB.run_data_migrations configuration.database_connection + lock.with_lock do + run_data_migrations + end else logger.info "Skipping data migrations" end @@ -129,6 +123,23 @@ def prepare_database PactBroker::Webhooks::Service.fail_retrying_triggered_webhooks end + def ensure_all_database_migrations_are_applied + migration_options = { allow_missing_migration_files: configuration.allow_missing_migration_files } + + if PactBroker::DB.is_current?(configuration.database_connection, migration_options) + logger.info "Skipping database migrations as the latest migration has already been applied" + else + logger.info "Migrating database schema" + PactBroker::DB.run_migrations(configuration.database_connection, migration_options) + logger.info "Database schema version is now #{PactBroker::DB.version(configuration.database_connection)}" + end + end + + def run_data_migrations + logger.info "Migrating data" + PactBroker::DB.run_data_migrations(configuration.database_connection) + end + def load_configuration_from_database configuration.load_from_database! end @@ -180,16 +191,17 @@ def configure_middleware @app_builder.use PactBroker::Api::Middleware::HttpDebugLogs if configuration.http_debug_logging_enabled configure_basic_auth configure_rack_protection + @app_builder.use Rack::PactBroker::ApplicationContext, application_context @app_builder.use Rack::PactBroker::InvalidUriProtection @app_builder.use Rack::PactBroker::ResetThreadData @app_builder.use Rack::PactBroker::AddPactBrokerVersionHeader @app_builder.use Rack::PactBroker::AddVaryHeader @app_builder.use Rack::Static, :urls => ["/stylesheets", "/css", "/fonts", "/js", "/javascripts", "/images"], :root => PactBroker.project_root.join("public") @app_builder.use Rack::Static, :urls => ["/favicon.ico"], :root => PactBroker.project_root.join("public/images"), header_rules: [[:all, {"Content-Type" => "image/x-icon"}]] + @app_builder.use Rack::PactBroker::AddCacheHeader @app_builder.use Rack::PactBroker::ConvertFileExtensionToAcceptHeader # Rack::PactBroker::SetBaseUrl needs to be before the Rack::PactBroker::HalBrowserRedirect @app_builder.use Rack::PactBroker::SetBaseUrl, configuration.base_urls - @app_builder.use Rack::PactBroker::ApplicationContext, application_context if configuration.use_hal_browser logger.info "Mounting HAL browser" diff --git a/lib/pact_broker/async/after_reply.rb b/lib/pact_broker/async/after_reply.rb new file mode 100644 index 000000000..f25676fef --- /dev/null +++ b/lib/pact_broker/async/after_reply.rb @@ -0,0 +1,30 @@ +# Saves a block for execution after the HTTP response has been sent to the user. +# When the block is executed, it connects to the database before executing the code. +# This is good for doing things that might take a while and don't have to be done before +# the response is sent, and don't need retries (in which case, it might be better to use a SuckerPunch Job). +# +# This leverages a feature of Puma which I'm not sure is meant to be public or not. +# There are serveral mentions of it on the internet, so I assume it's ok to use it. +# Puma itself uses the rack.after_reply for http request logging. +# +# https://github.com/search?q=repo%3Apuma%2Fpuma%20rack.after_reply&type=code + +module PactBroker + module Async + class AfterReply + def initialize(rack_env) + @rack_env = rack_env + @database_connector = rack_env.fetch("pactbroker.database_connector") + end + + def execute(&block) + dc = @database_connector + @rack_env["rack.after_reply"] << lambda { + dc.call do + block.call + end + } + end + end + end +end diff --git a/lib/pact_broker/badges/service.rb b/lib/pact_broker/badges/service.rb index 5a79a5e22..a96b53220 100644 --- a/lib/pact_broker/badges/service.rb +++ b/lib/pact_broker/badges/service.rb @@ -40,6 +40,24 @@ def can_i_deploy_badge_url(tag, environment_tag, label, deployable) build_shield_io_uri(title, status, color) end + def can_i_merge_badge_url(deployable: nil) + title = "can-i-merge" + + # rubocop:disable Layout/EndAlignment + color, status = case deployable + when nil + [ "lightgrey", "unknown" ] + when true + [ "brightgreen", "success" ] + else + [ "red", "failed" ] + end + # rubocop:enable Layout/EndAlignment + + # left text is "can-i-merge", right text is the version number + build_shield_io_uri(title, status, color) + end + def error_badge_url(left_text, right_text) build_shield_io_uri(left_text, right_text, "lightgrey") end diff --git a/lib/pact_broker/contracts/contract_to_publish.rb b/lib/pact_broker/contracts/contract_to_publish.rb index 96a7ad5a5..a710925cf 100644 --- a/lib/pact_broker/contracts/contract_to_publish.rb +++ b/lib/pact_broker/contracts/contract_to_publish.rb @@ -1,11 +1,10 @@ module PactBroker module Contracts - ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict) do - # rubocop: disable Metrics/ParameterLists - def self.from_hash(consumer_name: nil, provider_name: nil, decoded_content: nil, content_type: nil, specification: nil, on_conflict: nil) - new(consumer_name, provider_name, decoded_content, content_type, specification, on_conflict) + ContractToPublish = Struct.new(:consumer_name, :provider_name, :decoded_content, :content_type, :specification, :on_conflict, :pact_version_sha, keyword_init: true) do + + def self.from_hash(hash) + new(**hash) end - # rubocop: enable Metrics/ParameterLists def pact? specification == "pact" diff --git a/lib/pact_broker/contracts/service.rb b/lib/pact_broker/contracts/service.rb index e13bff71c..698430a65 100644 --- a/lib/pact_broker/contracts/service.rb +++ b/lib/pact_broker/contracts/service.rb @@ -35,6 +35,7 @@ def publish(parsed_contracts, base_url: ) version, version_notices = create_version(parsed_contracts) tags = create_tags(parsed_contracts, version) pacts, pact_notices = create_pacts(parsed_contracts, base_url) + create_or_update_integrations(pacts) notices = version_notices + pact_notices ContractsPublicationResults.from_hash( pacticipant: version.pacticipant, @@ -56,7 +57,7 @@ def add_pact_conflict_notices(notices, parsed_contracts) parsed_contracts.contracts.collect do | contract_to_publish | pact_params = create_pact_params(parsed_contracts, contract_to_publish) existing_pact = pact_service.find_pact(pact_params) - if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.decoded_content) + if existing_pact && pact_service.disallowed_modification?(existing_pact, contract_to_publish.pact_version_sha) add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_pact.json_content, contract_to_publish.decoded_content) end end @@ -66,7 +67,7 @@ def add_pact_conflict_notices(notices, parsed_contracts) def add_pact_conflict_notice(notices, parsed_contracts, contract_to_publish, existing_json_content, new_json_content) message_params = { - consumer_name: contract_to_publish.provider_name, + consumer_name: contract_to_publish.consumer_name, consumer_version_number: parsed_contracts.pacticipant_version_number, provider_name: contract_to_publish.provider_name } @@ -133,7 +134,7 @@ def create_pacts(parsed_contracts, base_url) pact_params = create_pact_params(parsed_contracts, contract_to_publish) existing_pact = pact_service.find_pact(pact_params) listener = TriggeredWebhooksCreatedListener.new - created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params, listener) + created_pact = create_or_merge_pact(contract_to_publish.merge?, existing_pact, pact_params.merge(pact_version_sha: contract_to_publish.pact_version_sha), listener) notices.concat(notices_for_pact(parsed_contracts, contract_to_publish, existing_pact, created_pact, listener, base_url)) created_pact end @@ -303,6 +304,14 @@ def url_for_triggered_webhook(triggered_webhook, base_url) PactBroker::Api::PactBrokerUrls.triggered_webhook_logs_url(triggered_webhook, base_url) end + # Creating/updating the integrations all at once at the end of the transaction instead + # of one by one, as each pact is created, reduces the amount of time that + # a lock is held on the integrations table, therefore reducing contention + # and potential for deadlocks when there are many pacts being published at once. + def create_or_update_integrations(pacts) + integration_service.handle_bulk_contract_data_published(pacts) + end + private :url_for_triggered_webhook end end diff --git a/lib/pact_broker/db/advisory_lock.rb b/lib/pact_broker/db/advisory_lock.rb new file mode 100644 index 000000000..d59e1a257 --- /dev/null +++ b/lib/pact_broker/db/advisory_lock.rb @@ -0,0 +1,58 @@ +require "pact_broker/logging" + +# Uses a Postgres advisory lock to ensure that a given block of code can only have ONE +# thread in excution at a time against a given database. +# When the database is not Postgres, the block will yield without any locks, allowing +# this class to be used safely with other database types, but without the locking +# functionality. +# +# This is a wrapper around the actual implementation code in the Sequel extension from https://github.com/yuryroot/sequel-pg_advisory_lock +# which was copied into this codebase and modified for usage in this codebase. +# +# See https://www.postgresql.org/docs/16/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS for docs on lock types +# + +module PactBroker + module DB + class AdvisoryLock + include PactBroker::Logging + + def initialize(database_connection, name, type = :pg_try_advisory_lock) + @database_connection = database_connection + @name = name + @type = type + @lock_obtained = false + register_advisory_lock if postgres? + end + + def with_lock + if postgres? + @database_connection.with_advisory_lock(@name) do + logger.debug("Lock #{@name} obtained") + @lock_obtained = true + yield + end + else + logger.warn("Executing block without lock as this is not a Postgres database") + @lock_obtained = true + yield + end + end + + def lock_obtained? + @lock_obtained + end + + private + + def postgres? + @database_connection.adapter_scheme.to_s =~ /postgres/ + end + + def register_advisory_lock + @database_connection.extension :pg_advisory_lock + @database_connection.register_advisory_lock(@name, @type) + end + end + end +end diff --git a/lib/pact_broker/doc/views/index/pacticipant-branch.markdown b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown new file mode 100644 index 000000000..481d29518 --- /dev/null +++ b/lib/pact_broker/doc/views/index/pacticipant-branch.markdown @@ -0,0 +1,25 @@ +# Pacticipant branch + +Allowed methods: `GET`, `DELETE` + +Path: `/pacticipants/{pacticipant}/branches/{branch}` + +Get or delete a pacticipant branch. + +## Create + +Branches cannot be created via the resource URL. They are created automatically when publishing contracts. + +## Get + +### Example + + curl http://broker/pacticipants/Bar/branches/main -H "Accept: application/hal+json" + +## Delete + +Deletes a pacticipant branch. Does NOT delete the associated pacticipant versions. + +Send a `DELETE` request to the branch resource. + + curl -XDELETE http://broker/pacticipants/Bar/branches/main diff --git a/lib/pact_broker/domain/version.rb b/lib/pact_broker/domain/version.rb index 7e053710a..7dc3733c2 100644 --- a/lib/pact_broker/domain/version.rb +++ b/lib/pact_broker/domain/version.rb @@ -4,9 +4,10 @@ module PactBroker module Domain - VERSION_COLUMNS = [:id, :number, :repository_ref, :pacticipant_id, :order, :created_at, :updated_at, :build_url] + class Version < Sequel::Model + VERSION_COLUMNS = Sequel::Model.db.schema(:versions).collect(&:first) - [:branch] # do not include the branch column, as we now have a branches table + set_dataset(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) })) - class Version < Sequel::Model(Sequel::Model.db[:versions].select(*VERSION_COLUMNS.collect{ | column | Sequel.qualify(:versions, column) })) set_primary_key :id plugin :timestamps, update_on_create: true diff --git a/lib/pact_broker/initializers/subscriptions.rb b/lib/pact_broker/initializers/subscriptions.rb deleted file mode 100644 index 7ac2bc32d..000000000 --- a/lib/pact_broker/initializers/subscriptions.rb +++ /dev/null @@ -1,4 +0,0 @@ -require "pact_broker/events/subscriber" -require "pact_broker/integrations/event_listener" - -PactBroker::Events.subscribe(PactBroker::Integrations::EventListener.new) diff --git a/lib/pact_broker/integrations/integration.rb b/lib/pact_broker/integrations/integration.rb index 78c6750ab..9f5fa4ea1 100644 --- a/lib/pact_broker/integrations/integration.rb +++ b/lib/pact_broker/integrations/integration.rb @@ -7,7 +7,17 @@ module PactBroker module Integrations - class Integration < Sequel::Model(Sequel::Model.db[:integrations].select(:id, :consumer_id, :provider_id, :contract_data_updated_at)) + # The columns are explicitly specified for the Integration object so that the consumer_name and provider_name columns aren't included + # in the model. + # Those columns exist in the integrations table because the integrations table used to be an integrations view based on the + # pact_publications table, and those columns existed in the view. + # When the view was migrated to be a table (in db/migrations/20211102_create_table_temp_integrations.rb and the following migrations) + # the columns had to be maintained for backwards compatiblity. + # They are not used by the current code, however. + class Integration < Sequel::Model + INTEGRATION_COLUMNS = Sequel::Model.db.schema(:integrations).collect(&:first) - [:consumer_name, :provider_name] + set_dataset(Sequel::Model.db[:integrations].select(*INTEGRATION_COLUMNS)) + set_primary_key :id plugin :insert_ignore, identifying_columns: [:consumer_id, :provider_id] associate(:many_to_one, :consumer, :class => "PactBroker::Domain::Pacticipant", :key => :consumer_id, :primary_key => :id) diff --git a/lib/pact_broker/integrations/repository.rb b/lib/pact_broker/integrations/repository.rb index e790a32da..9890171a6 100644 --- a/lib/pact_broker/integrations/repository.rb +++ b/lib/pact_broker/integrations/repository.rb @@ -21,12 +21,27 @@ def create_for_pact(consumer_id, provider_id) Integration.new( consumer_id: consumer_id, provider_id: provider_id, - created_at: Sequel.datetime_class.now + created_at: Sequel.datetime_class.now, + contract_data_updated_at: Sequel.datetime_class.now ).insert_ignore end nil end + # Ensure an Integration exists for each consumer/provider pair. + # Using SELECT ... INSERT IGNORE rather than just INSERT IGNORE so that we do not + # need to lock the table at all when the integrations already exist, which will + # be the most common use case. New integrations get created incredibly rarely. + # The INSERT IGNORE is used rather than just INSERT to handle race conditions + # when requests come in parallel. + # @param [Array] where each object has a consumer and a provider + def create_for_pacts(objects_with_consumer_and_provider) + published_integrations = objects_with_consumer_and_provider.collect{ |i| { consumer_id: i.consumer.id, provider_id: i.provider.id } } + existing_integrations = Sequel::Model.db[:integrations].select(:consumer_id, :provider_id).where(Sequel.|(*published_integrations) ).all + new_integrations = (published_integrations - existing_integrations).collect{ |i| i.merge(created_at: Sequel.datetime_class.now, contract_data_updated_at: Sequel.datetime_class.now) } + Integration.dataset.insert_ignore.multi_insert(new_integrations) + end + def delete(consumer_id, provider_id) Integration.where(consumer_id: consumer_id, provider_id: provider_id).delete end @@ -35,8 +50,43 @@ def delete(consumer_id, provider_id) # @param [PactBroker::Domain::Pacticipant, nil] consumer the consumer for the integration, or nil if for a provider-only event (eg. Pactflow provider contract published) # @param [PactBroker::Domain::Pacticipant] provider the provider for the integration def set_contract_data_updated_at(consumer, provider) + set_contract_data_updated_at_for_multiple_integrations([OpenStruct.new(consumer: consumer, provider: provider)]) + end + + + # Sets the contract_data_updated_at for the integrations as specified by an array of objects which each have a consumer and provider. + # + # The contract_data_updated_at attribute is only ever used for ordering the list of integrations on the index page of the *Pact Broker* UI, + # so that the most recently updated integrations (the ones you're likely working on) are showed at the top of the first page. + # There is often contention around updating it however, which can cause deadlocks, and slow down API responses. + # Because it's not a critical field (eg. it won't change any can-i-deploy results), the easiest way to reduce this contention + # is to just not update it if the row is locked, because if it is locked, the value of contract_data_updated_at is already + # going to be a date from a few seconds ago, which is perfectly fine for the purposes for which we are using the value. + # + # Notes on SKIP LOCKED: + # SKIP LOCKED is only supported by Postgres. + # When executing SELECT ... FOR UPDATE SKIP LOCKED, the SELECT will run immediately, not waiting for any other transactions, + # and only return rows that are not already locked by another transaction. + # The FOR UPDATE is required to make it work this way - SKIP LOCKED on its own does not work. + # + # @param [Array] where each object MAY have a consumer and does have a provider (for Pactflow provider contract published there is no consumer) + def set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + consumer_and_provider_ids = objects_with_consumer_and_provider.collect{ | object | { consumer_id: object.consumer&.id, provider_id: object.provider.id }.compact }.uniq + + # MySQL doesn't support an UPDATE with a subquery. FFS. Really need to do a major version release and delete the support code. + criteria = if Integration.dataset.supports_skip_locked? + integration_ids_to_update = Integration + .select(:id) + .where(Sequel.|(*consumer_and_provider_ids)) + .for_update + .skip_locked + { id: integration_ids_to_update } + else + Sequel.|(*consumer_and_provider_ids) + end + Integration - .where({ consumer_id: consumer&.id, provider_id: provider.id }.compact ) + .where(criteria) .update(contract_data_updated_at: Sequel.datetime_class.now) end end diff --git a/lib/pact_broker/integrations/service.rb b/lib/pact_broker/integrations/service.rb index 22a3c8e16..5f11c92b0 100644 --- a/lib/pact_broker/integrations/service.rb +++ b/lib/pact_broker/integrations/service.rb @@ -21,9 +21,18 @@ def self.find_all(filter_options = {}, pagination_options = {}, eager_load_assoc # @param [PactBroker::Domain::Pacticipant] consumer or nil # @param [PactBroker::Domain::Pacticipant] provider def self.handle_contract_data_published(consumer, provider) + integration_repository.create_for_pact(consumer.id, provider.id) integration_repository.set_contract_data_updated_at(consumer, provider) end + + # Callback to invoke when a batch of contract data is published (eg. the publish contracts endpoint) + # @param [Array] where each object has a consumer and a provider + def self.handle_bulk_contract_data_published(objects_with_consumer_and_provider) + integration_repository.create_for_pacts(objects_with_consumer_and_provider) + integration_repository.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + end + def self.delete(consumer_name, provider_name) consumer = pacticipant_service.find_pacticipant_by_name!(consumer_name) provider = pacticipant_service.find_pacticipant_by_name!(provider_name) diff --git a/lib/pact_broker/labels/repository.rb b/lib/pact_broker/labels/repository.rb index 7efe0df3f..aee665261 100644 --- a/lib/pact_broker/labels/repository.rb +++ b/lib/pact_broker/labels/repository.rb @@ -3,6 +3,11 @@ module PactBroker module Labels class Repository + + def get_all_unique_labels pagination_options = {} + PactBroker::Domain::Label.distinct.select(:name).all_with_pagination_options(pagination_options) + end + def create args Domain::Label.new(name: args.fetch(:name), pacticipant: args.fetch(:pacticipant)).save end diff --git a/lib/pact_broker/labels/service.rb b/lib/pact_broker/labels/service.rb index 02ae239fa..af9bc5e6f 100644 --- a/lib/pact_broker/labels/service.rb +++ b/lib/pact_broker/labels/service.rb @@ -9,6 +9,10 @@ module Service extend PactBroker::Repositories + def get_all_unique_labels pagination_options = {} + label_repository.get_all_unique_labels(pagination_options) + end + def create args pacticipant = pacticipant_repository.find_by_name_or_create args.fetch(:pacticipant_name) label_repository.create pacticipant: pacticipant, name: args.fetch(:label_name) diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index f3489fc79..5204919a8 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -50,7 +50,7 @@ en: contract: pact_published: Pact successfully published for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. same_pact_content_published: Pact successfully republished for %{consumer_name} version %{consumer_version_number} and provider %{provider_name} with no content changes. - pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning + pact_modified_for_same_version: Pact with changed content published over existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning pact_merged: Pact content merged with existing content for %{consumer_name} version %{consumer_version_number} and provider %{provider_name}. events: pact_published_unchanged_with_single_tag: pact content is the same as previous version with tag %{tag_name} and no new tags were applied @@ -59,6 +59,8 @@ en: verifications: Add Pact verification tests to the %{provider_name} build. See https://docs.pact.io/go/provider_verification webhooks: Configure separate %{provider_name} pact verification build and webhook to trigger it when the pact content changes. See https://docs.pact.io/go/webhooks version_branch: Configure the version branch to be the value of your repository branch. + branch: + bulk_delete: "Scheduled deletion of %{count} branches for pacticipant %{pacticipant_name}. Remaining branches are: %{remaining}" errors: runtime: with_error_reference: "An error has occurred. The details have been logged with the reference %{error_reference}" diff --git a/lib/pact_broker/logging.rb b/lib/pact_broker/logging.rb index 5e2df2779..3ff159a3c 100644 --- a/lib/pact_broker/logging.rb +++ b/lib/pact_broker/logging.rb @@ -42,6 +42,16 @@ def log_with_tag(tag) end end + def measure_info(message, payload: {}) + if logger.respond_to?(:measure_info) + logger.measure_info(message, payload: payload) do + yield + end + else + yield + end + end + def log_error e, description = nil if logger.instance_of?(SemanticLogger::Logger) if description diff --git a/lib/pact_broker/matrix/matrix_row.rb b/lib/pact_broker/matrix/matrix_row.rb index abf66a3e3..8a00da013 100644 --- a/lib/pact_broker/matrix/matrix_row.rb +++ b/lib/pact_broker/matrix/matrix_row.rb @@ -55,7 +55,6 @@ class Verification < Sequel::Model(Sequel.as(:latest_verification_id_for_pact_ve Sequel[:v][:verification_id], Sequel[:v][:provider_version_id], Sequel[:v][:provider_version_created_at] - ] # Must be kept in sync with PactBroker::Matrix::EveryRow diff --git a/lib/pact_broker/matrix/resolved_selector.rb b/lib/pact_broker/matrix/resolved_selector.rb index ad7051a83..e490aa284 100644 --- a/lib/pact_broker/matrix/resolved_selector.rb +++ b/lib/pact_broker/matrix/resolved_selector.rb @@ -11,7 +11,6 @@ module PactBroker module Matrix class ResolvedSelector < Hash - using PactBroker::HashRefinements # A version ID of -1 will not match any rows, which is what we want to ensure that diff --git a/lib/pact_broker/matrix/service.rb b/lib/pact_broker/matrix/service.rb index 398b6c81c..687be95c5 100644 --- a/lib/pact_broker/matrix/service.rb +++ b/lib/pact_broker/matrix/service.rb @@ -22,6 +22,32 @@ def can_i_deploy(selectors, options = {}) QueryResultsWithDeploymentStatusSummary.new(query_results, DeploymentStatusSummary.new(query_results)) end + def can_i_merge(pacticipant_name: nil, pacticipant: nil, latest_main_branch_version: nil) + # first we find the pacticipant by name (or use the one passed in) if pacticipant is nil + if pacticipant.nil? + pacticipant = pacticipant_service.find_pacticipant_by_name(pacticipant_name) + raise PactBroker::Error.new("No pacticipant found with name '#{pacticipant_name}'") unless pacticipant + else + pacticipant_name = pacticipant.name + end + + # then we find the latest version from the main branch if not passed in + if latest_main_branch_version.nil? + latest_main_branch_version = version_service.find_latest_version_from_main_branch(pacticipant) + raise PactBroker::Error.new("No main branch version found for pacticipant '#{pacticipant_name}'") unless latest_main_branch_version + end + + selectors = PactBroker::Matrix::UnresolvedSelector.from_hash( + pacticipant_name: pacticipant_name, + pacticipant_version_number: latest_main_branch_version.number + ) + + options = { main_branch: true, latest: true, latestby: "cvp" } + query_results = can_i_deploy([selectors], options) + + query_results.deployable? + end + def find selectors, options = {} logger.info "Querying matrix", selectors: selectors, options: options matrix_repository.find(selectors, options) diff --git a/lib/pact_broker/metrics/service.rb b/lib/pact_broker/metrics/service.rb index 0e70ffff7..c0c94461b 100644 --- a/lib/pact_broker/metrics/service.rb +++ b/lib/pact_broker/metrics/service.rb @@ -101,8 +101,8 @@ def interactions_counts end def pact_revision_counts - query = "select revision_count as number_of_revisions, count(consumer_version_id) as consumer_version_count - from (select consumer_version_id, count(*) as revision_count from pact_publications group by consumer_version_id) foo + query = "select revision_count as number_of_revisions, count(*) as consumer_version_count + from (select count(*) as revision_count from pact_publications group by consumer_version_id, provider_id) foo group by revision_count order by 1" PactBroker::Pacts::PactPublication.db[query].all.each_with_object({}) { |row, hash| hash[row[:number_of_revisions]] = row[:consumer_version_count] } diff --git a/lib/pact_broker/pacticipants/repository.rb b/lib/pact_broker/pacticipants/repository.rb index 4b1da03f5..26f0e74e8 100644 --- a/lib/pact_broker/pacticipants/repository.rb +++ b/lib/pact_broker/pacticipants/repository.rb @@ -102,7 +102,16 @@ def handle_multiple_pacticipants_found(name, pacticipants) def search_by_name(pacticipant_name) terms = pacticipant_name.split.map { |v| v.gsub("_", "\\_") } - string_match_query = Sequel.|( *terms.map { |term| Sequel.ilike(Sequel[:pacticipants][:name], "%#{term}%") }) + columns = [:name, :display_name] + string_match_query = Sequel.|( + *terms.map do |term| + Sequel.|( + *columns.map do |column| + Sequel.ilike(Sequel[:pacticipants][column], "%#{term}%") + end + ) + end + ) scope_for(PactBroker::Domain::Pacticipant).where(string_match_query) end diff --git a/lib/pact_broker/pacts/generate_sha.rb b/lib/pact_broker/pacts/generate_sha.rb index 65e89c92b..4719763cd 100644 --- a/lib/pact_broker/pacts/generate_sha.rb +++ b/lib/pact_broker/pacts/generate_sha.rb @@ -3,22 +3,27 @@ require "pact_broker/pacts/sort_content" require "pact_broker/pacts/parse" require "pact_broker/pacts/content" +require "pact_broker/logging" module PactBroker module Pacts class GenerateSha + include PactBroker::Logging + # @param [String] json_content - def self.call json_content, _options = {} + def self.call(json_content, _options = {}) content_for_sha = if PactBroker.configuration.base_equality_only_on_content_that_affects_verification_results extract_verifiable_content_for_sha(json_content) else json_content end - Digest::SHA1.hexdigest(content_for_sha) + measure_info("Generating SHA1 hexdigest for pact", payload: { length: content_for_sha.length } ){ Digest::SHA1.hexdigest(content_for_sha) } end - def self.extract_verifiable_content_for_sha json_content - Content.from_json(json_content).sort.content_that_affects_verification_results.to_json + def self.extract_verifiable_content_for_sha(json_content) + objects = Content.from_json(json_content) + sorted_content = measure_info("Sorting content", payload: { length: json_content.length }){ objects.sort } + sorted_content.content_that_affects_verification_results.to_json end end end diff --git a/lib/pact_broker/pacts/pact_params.rb b/lib/pact_broker/pacts/pact_params.rb index e7f75723c..c6d9cb733 100644 --- a/lib/pact_broker/pacts/pact_params.rb +++ b/lib/pact_broker/pacts/pact_params.rb @@ -20,7 +20,7 @@ def self.from_path_info path_info ) end - def self.from_request request, path_info + def self.from_request(request, path_info) json_content = request.body.to_s parsed_content = begin parsed = JSON.parse(json_content, PACT_PARSING_OPTIONS) diff --git a/lib/pact_broker/pacts/pact_publication_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_dataset_module.rb index e4260f955..d6c8f6a5c 100644 --- a/lib/pact_broker/pacts/pact_publication_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_dataset_module.rb @@ -57,6 +57,13 @@ def for_consumer_name_and_maybe_version_number(consumer_name, consumer_version_n end end + # Returns the latest pact for each branch, returning a pact for every branch, even if + # the most recent version of that branch does not have a pact. + # This is different from for_all_branch_heads, which will find the branch head versions, + # and return the pacts associated with those versions. + # This method should not be used for 'pacts for verification', because it will return + # a pact for branches where that integration should no longer exist. + # @return [Dataset] def latest_by_consumer_branch branch_versions_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_versions][:version_id] @@ -111,10 +118,29 @@ def overall_latest_for_consumer_id_and_provider_id(consumer_id, provider_id) .limit(1) end - # Return the pacts (if they exist) for the branch heads. + # Returns the pacts (if they exist) for all the branch heads. + # If the version for the branch head does not have a pact, then no pact is returned, + # (unlike latest_by_consumer_branch) + # This is much more performant than latest_by_consumer_branch and should be used + # for the 'pacts for verification' response + # @return [Dataset] + def for_all_branch_heads + base_query = self + base_query = base_query.join(:branch_heads, { Sequel[:bh][:version_id] => Sequel[:pact_publications][:consumer_version_id] }, { table_alias: :bh }) + + if no_columns_selected? + base_query = base_query.select_all_qualified.select_append(Sequel[:bh][:branch_name].as(:branch_name)) + end + + base_query.remove_overridden_revisions + end + + # Return the pacts (if they exist) for the branch heads of the given branch names # This uses the new logic of finding the branch head and returning any associated pacts, # rather than the old logic of returning the pact for the latest version # on the branch that had a pact. + # @param [String] branch_name + # @return [Sequel::Dataset] def for_branch_heads(branch_name) branch_head_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:branch_heads][:version_id], @@ -173,7 +199,10 @@ def old_latest_for_consumer_branch(branch_name) # The latest pact publication for each tag # This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact) # rather than "the pact for the latest version with a tag" - # Need to see about updating this. + # + # For 'pacts for verification' this has been replaced by for_all_tag_heads + # This should only be used for the UI + # @return [Sequel::Dataset] def latest_by_consumer_tag tags_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id], @@ -202,6 +231,7 @@ def latest_by_consumer_tag # This uses the old logic of "the latest pact for a version that has a tag" (which always returns a pact) # rather than "the pact for the latest version with a tag" # Need to see about updating this. + # @return [Sequel::Dataset] def latest_for_consumer_tag(tag_name) tags_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:tags][:version_id], @@ -252,6 +282,30 @@ def for_latest_consumer_versions_with_tag(tag_name) .remove_overridden_revisions_from_complete_query end + # The pacts for the latest versions for each tag. + # Will not return a pact if the pact is no longer published for a particular tag + # NEW LOGIC + # @return [Sequel::Dataset] + def for_all_tag_heads + head_tags = PactBroker::Domain::Tag + .select_group(:pacticipant_id, :name) + .select_append{ max(version_order).as(:latest_version_order) } + + head_tags_join = { + Sequel[:pact_publications][:consumer_id] => Sequel[:head_tags][:pacticipant_id], + Sequel[:pact_publications][:consumer_version_order] => Sequel[:head_tags][:latest_version_order] + } + + base_query = self + if no_columns_selected? + base_query = base_query.select_all_qualified.select_append(Sequel[:head_tags][:name].as(:tag_name)) + end + + base_query + .join(head_tags, head_tags_join, { table_alias: :head_tags }) + .remove_overridden_revisions_from_complete_query + end + def in_environments currently_deployed_join = { Sequel[:pact_publications][:consumer_version_id] => Sequel[:currently_deployed_version_ids][:version_id] diff --git a/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb index 8822a6094..017821090 100644 --- a/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_selector_dataset_module.rb @@ -15,6 +15,7 @@ def for_provider_and_consumer_version_selector provider, selector # Do the "latest" logic last so that the provider/consumer criteria get included in the "latest" query before the join, rather than after query = query.latest_for_main_branches if selector.latest_for_main_branch? + query = query.for_all_branch_heads if selector.latest_for_each_branch? query = query.latest_for_consumer_branch(selector.branch) if selector.latest_for_branch? query = query.for_latest_consumer_versions_with_tag(selector.tag) if selector.latest_for_tag? query = query.overall_latest if selector.overall_latest? diff --git a/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb b/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb index eb6ade199..9613089d4 100644 --- a/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb +++ b/lib/pact_broker/pacts/pact_publication_wip_dataset_module.rb @@ -1,35 +1,76 @@ module PactBroker module Pacts module PactPublicationWipDatasetModule + + # Use a cut down model of the verifications table just for the WIP calculations. + # Don't need all the associations and normal domain methods. + class VerificationForWipCalculations < Sequel::Model(:verifications) + dataset_module do + def successful_non_wip_by_provider(provider_id) + distinct.where(success: true, wip: false, provider_id: provider_id) + end + + def verified_before_creation_date_of(record) + if record + verified_before_date(record.created_at) + else + self + end + end + + def join_branch_versions_excluding_branch(provider_id, branch_name) + branch_versions_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:branch_versions][:version_id], + Sequel[:branch_versions][:pacticipant_id] => provider_id + } + join(:branch_versions, branch_versions_join) do + Sequel.lit("branch_versions.branch_name != ?", branch_name) + end + end + + def join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) + branch_versions_join = { + Sequel[:verifications][:provider_version_id] => Sequel[:branch_versions][:version_id], + Sequel[:branch_versions][:pacticipant_id] => provider_id, + Sequel[:branch_versions][:branch_name] => provider_version_branch + } + + join(:branch_versions, branch_versions_join) + end + + def verified_before_date(date) + where { Sequel[:verifications][:execution_date] < date } + end + end + end + def successfully_verified_by_provider_branch_when_not_wip(provider_id, provider_version_branch) + successful_verifications = VerificationForWipCalculations + .select(:pact_version_id) + .distinct + .successful_non_wip_by_provider(provider_id) + .join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) + + from_self(alias: :pp) .select(Sequel[:pp].*) - .where(Sequel[:pp][:provider_id] => provider_id) - .join_successful_non_wip_verifications_for_provider_id(provider_id) - .join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) .distinct + .join(successful_verifications, { Sequel[:pp][:pact_version_id] => Sequel[:v][:pact_version_id] }, { table_alias: :v }) end def successfully_verified_by_provider_another_branch_before_this_branch_first_created(provider_id, provider_version_branch) first_version_for_branch = PactBroker::Domain::Version.first_for_pacticipant_id_and_branch(provider_id, provider_version_branch) + successful_verifications = VerificationForWipCalculations + .select(:pact_version_id) + .distinct + .successful_non_wip_by_provider(provider_id) + .join_branch_versions_excluding_branch(provider_id, provider_version_branch) + .verified_before_creation_date_of(first_version_for_branch) + from_self(alias: :pp) .select(Sequel[:pp].*) - .join_successful_non_wip_verifications_for_provider_id(provider_id) - .join_provider_versions_for_provider_id(provider_id) - .join_branch_versions_excluding_branch(provider_version_branch) - .where(Sequel[:pp][:provider_id] => provider_id) - .verified_before_creation_date_of(first_version_for_branch) - .distinct - end - - def join_branch_versions_excluding_branch(branch_name) - branch_versions_join = { - Sequel[:provider_versions][:id] => Sequel[:branch_versions][:version_id] - } - join(:branch_versions, branch_versions_join) do - Sequel.lit("branch_versions.branch_name != ?", branch_name) - end + .join(successful_verifications, { Sequel[:pp][:pact_version_id] => Sequel[:v][:pact_version_id] }, { table_alias: :v }) end def successfully_verified_by_provider_tag_when_not_wip(provider_tag) @@ -43,7 +84,6 @@ def successfully_verified_by_provider_tag_when_not_wip(provider_tag) .select(Sequel[:pp].*) .join(:pact_version_provider_tag_successful_verifications, pact_version_provider_tag_verifications_join, { table_alias: :sv }) .distinct - end def successfully_verified_by_provider_another_tag_before_this_tag_first_created(provider_id, provider_tag) @@ -71,66 +111,6 @@ def successfully_verified_by_provider_another_tag_before_this_tag_first_created( end .distinct end - - protected - - def verified_before_date(date) - where { Sequel[:verifications][:execution_date] < date } - end - - def join_successful_non_wip_verifications_for_provider_id(provider_id, &block) - verifications_join = { - pact_version_id: :pact_version_id, - Sequel[:verifications][:success] => true, - Sequel[:verifications][:wip] => false, - Sequel[:verifications][:provider_id] => provider_id - } - join(:verifications, verifications_join, {}, &block) - end - - def join_provider_version_tags &block - tags_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], - } - join(:tags, tags_join, { table_alias: :provider_tags }, &block) - end - - def join_provider_version_tags_for_tag(tag) - tags_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_tags][:version_id], - Sequel[:provider_tags][:name] => tag - } - join(:tags, tags_join, { table_alias: :provider_tags } ) - end - - def join_provider_versions_for_provider_id_and_branch(provider_id, provider_version_branch) - versions_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_versions][:id], - Sequel[:provider_versions][:pacticipant_id] => provider_id - } - branch_versions_join = { - Sequel[:provider_versions][:id] => Sequel[:branch_versions][:version_id], - Sequel[:branch_versions][:branch_name] => provider_version_branch - } - join(:versions, versions_join, { table_alias: :provider_versions } ) - .join(:branch_versions, branch_versions_join) - end - - def join_provider_versions_for_provider_id(provider_id, &block) - versions_join = { - Sequel[:verifications][:provider_version_id] => Sequel[:provider_versions][:id], - Sequel[:provider_versions][:pacticipant_id] => provider_id - } - join(:versions, versions_join, { table_alias: :provider_versions }, &block) - end - - def verified_before_creation_date_of(record) - if record - verified_before_date(record.created_at) - else - self - end - 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 index 8cd4190b5..4f72dc455 100644 --- a/lib/pact_broker/pacts/pacts_for_verification_repository.rb +++ b/lib/pact_broker/pacts/pacts_for_verification_repository.rb @@ -7,6 +7,7 @@ require "pact_broker/pacts/selectors" require "pact_broker/feature_toggle" require "pact_broker/repositories/scopes" +require "pact_broker/matrix/unresolved_selector" module PactBroker module Pacts @@ -63,7 +64,7 @@ def find_wip(provider_name, provider_version_branch, provider_tags_names, explic provider_tags_names, wip_start_date, explicitly_specified_verifiable_pacts, - :latest_by_consumer_tag + :for_all_tag_heads ) wip_by_consumer_branches = find_wip_pact_versions_for_provider_by_provider_tags( @@ -71,7 +72,7 @@ def find_wip(provider_name, provider_version_branch, provider_tags_names, explic provider_tags_names, wip_start_date, explicitly_specified_verifiable_pacts, - :latest_by_consumer_branch + :for_all_branch_heads ) deduplicate_verifiable_pacts(wip_by_consumer_tags + wip_by_consumer_branches).sort @@ -228,8 +229,8 @@ def find_wip_pact_versions_for_provider_by_provider_branch(provider_name, provid provider = pacticipant_repository.find_by_name(provider_name) wip_start_date = options.fetch(:include_wip_pacts_since) - potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_branch - potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).latest_by_consumer_tag + potential_wip_by_consumer_branch = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_branch_heads + potential_wip_by_consumer_tag = PactPublication.for_provider(provider).created_after(wip_start_date).for_all_tag_heads log_debug_for_wip do log_pact_publications_from_query("Potential WIP pacts for provider branch #{provider_version_branch} created after #{wip_start_date} by consumer branch", potential_wip_by_consumer_branch) diff --git a/lib/pact_broker/pacts/repository.rb b/lib/pact_broker/pacts/repository.rb index c5f816b8f..f1e05291d 100644 --- a/lib/pact_broker/pacts/repository.rb +++ b/lib/pact_broker/pacts/repository.rb @@ -33,8 +33,8 @@ def unscoped(scope) scope end - def create params - integration_repository.create_for_pact(params.fetch(:consumer_id), params.fetch(:provider_id)) + # @return [PactBroker::Domain::Pact] + def create(params) pact_version = find_or_create_pact_version( params.fetch(:consumer_id), params.fetch(:provider_id), diff --git a/lib/pact_broker/pacts/selector.rb b/lib/pact_broker/pacts/selector.rb index eb886b39e..108751dd1 100644 --- a/lib/pact_broker/pacts/selector.rb +++ b/lib/pact_broker/pacts/selector.rb @@ -31,6 +31,8 @@ def resolve_for_environment(consumer_version, environment, target = nil) def type if latest_for_main_branch? :latest_for_main_branch + elsif latest_for_each_branch? + :latest_for_each_branch elsif latest_for_branch? :latest_for_branch elsif matching_branch? @@ -265,12 +267,16 @@ def latest_for_tag? potential_tag = nil # Not sure if the fallback_tag logic is needed def latest_for_branch? potential_branch = nil if potential_branch - !!(latest && branch == potential_branch) + latest == true && branch == potential_branch else - !!(latest && !!branch) + latest == true && branch.is_a?(String) end end + def latest_for_each_branch? + latest == true && branch == true + end + def all_for_tag_and_consumer? !!(tag && !latest? && consumer) end diff --git a/lib/pact_broker/pacts/service.rb b/lib/pact_broker/pacts/service.rb index fb59bd7ad..6ec1adacb 100644 --- a/lib/pact_broker/pacts/service.rb +++ b/lib/pact_broker/pacts/service.rb @@ -22,6 +22,10 @@ module Service extend PactBroker::Messages extend SquashPactsForVerification + def generate_sha(json_content) + PactBroker.configuration.sha_generator.call(json_content) + end + def find_latest_pact params pact_repository.find_latest_pact(params[:consumer_name], params[:provider_name], params[:tag], params[:branch_name]) end @@ -154,10 +158,12 @@ def find_for_verification_publication(pact_params, consumer_version_selector_has end # Overwriting an existing pact with the same consumer/provider/consumer version number + # by creating a new revision (that is, a new PactPublication with an incremented revision number) + # Modifing pacts is strongly discouraged now, and support for it will be dropped in the next major version of the Pact Broker def create_pact_revision params, existing_pact logger.info("Updating existing pact publication", params.without(:json_content)) logger.debug("Content #{params[:json_content]}") - pact_version_sha = generate_sha(params[:json_content]) + pact_version_sha = params.fetch(:pact_version_sha) json_content = add_interaction_ids(params[:json_content]) update_params = { pact_version_sha: pact_version_sha, json_content: json_content } updated_pact = pact_repository.update(existing_pact.id, update_params) @@ -178,21 +184,20 @@ def create_pact_revision params, existing_pact private :create_pact_revision - def disallowed_modification?(existing_pact, new_json_content) - !PactBroker.configuration.allow_dangerous_contract_modification && existing_pact && existing_pact.pact_version_sha != generate_sha(new_json_content) + def disallowed_modification?(existing_pact, new_pact_version_sha) + !PactBroker.configuration.allow_dangerous_contract_modification && existing_pact && existing_pact.pact_version_sha != new_pact_version_sha end # When no publication for the given consumer/provider/consumer version number exists - def create_pact params, version, provider + def create_pact(params, version, provider) logger.info("Creating new pact publication", params.without(:json_content)) logger.debug("Content #{params[:json_content]}") - pact_version_sha = generate_sha(params[:json_content]) json_content = add_interaction_ids(params[:json_content]) pact = pact_repository.create( version_id: version.id, provider_id: provider.id, consumer_id: version.pacticipant_id, - pact_version_sha: pact_version_sha, + pact_version_sha: params.fetch(:pact_version_sha), json_content: json_content, version: version ) @@ -212,12 +217,6 @@ def create_pact params, version, provider private :create_pact - def generate_sha(json_content) - PactBroker.configuration.sha_generator.call(json_content) - end - - private :generate_sha - def add_interaction_ids(json_content) Content.from_json(json_content).with_ids.to_json end diff --git a/lib/pact_broker/tasks/clean_task.rb b/lib/pact_broker/tasks/clean_task.rb index d356a8c5a..699c2886c 100644 --- a/lib/pact_broker/tasks/clean_task.rb +++ b/lib/pact_broker/tasks/clean_task.rb @@ -1,3 +1,8 @@ +# This task is used to clean up old data in a Pact Broker database +# to stop performance issues from slowing down responses when there is +# too much data. +# See https://docs.pact.io/pact_broker/administration/maintenance + module PactBroker module DB class CleanTask < ::Rake::TaskLib @@ -7,11 +12,13 @@ class CleanTask < ::Rake::TaskLib attr_accessor :version_deletion_limit attr_accessor :logger attr_accessor :dry_run + attr_accessor :use_lock # allow disabling of postgres lock if it is causing problems def initialize &block require "pact_broker/db/clean_incremental" @version_deletion_limit = 1000 @dry_run = false + @use_lock = true @keep_version_selectors = PactBroker::DB::CleanIncremental::DEFAULT_KEEP_SELECTORS rake_task(&block) end @@ -28,42 +35,77 @@ def rake_task &block namespace :db do desc "Clean unnecessary pacts and verifications from database" task :clean do | _t, _args | - instance_eval(&block) - require "pact_broker/db/clean_incremental" - require "pact_broker/error" - require "yaml" - require "benchmark" + with_lock do + perform_clean + end + end + end + end + end + + def perform_clean + require "pact_broker/db/clean_incremental" + require "pact_broker/error" + require "yaml" + require "benchmark" - raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit + raise PactBroker::Error.new("You must specify the version_deletion_limit") unless version_deletion_limit - prefix = dry_run ? "[DRY RUN] " : "" + if keep_version_selectors.nil? || keep_version_selectors.empty? + raise PactBroker::Error.new("You must specify which versions to keep") + else + add_defaults_to_keep_selectors + output "Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash) + end - if keep_version_selectors.nil? || keep_version_selectors.empty? - raise PactBroker::Error.new("You must specify which versions to keep") - else - add_defaults_to_keep_selectors - output "#{prefix}Deleting oldest #{version_deletion_limit} versions, keeping versions that match the configured selectors", keep_version_selectors.collect(&:to_hash) - end + start_time = Time.now + results = PactBroker::DB::CleanIncremental.call(database_connection, + keep: keep_version_selectors, + limit: version_deletion_limit, + logger: logger, + dry_run: dry_run + ) + end_time = Time.now + elapsed_seconds = (end_time - start_time).to_i + output "Results (#{elapsed_seconds} seconds)", results + end - start_time = Time.now - results = PactBroker::DB::CleanIncremental.call(database_connection, - keep: keep_version_selectors, - limit: version_deletion_limit, - logger: logger, - dry_run: dry_run - ) - end_time = Time.now - elapsed_seconds = (end_time - start_time).to_i - output "Results (#{elapsed_seconds} seconds)", results - end + # Use a Postgres advisory lock to ensure that only one clean can run at a time. + # This allows a cron schedule to be used on the Pact Broker Docker image when deployed + # on a multi-instance architecture, without all the instances stepping on each other's toes. + # + # Any tasks that attempt to run while a clean job is running will skip the clean + # and exit with a message and a success code. + # + # To test that the lock works, run: + # script/docker/db-start.sh + # script/docker/db-migrate.sh + # for i in {0..3}; do PACT_BROKER_TEST_DATABASE_URL=postgres://postgres:postgres@localhost/postgres bundle exec rake pact_broker:db:clean &; done; + # + # There will be 3 messages saying "Clean was not performed" and output from one thread showing the clean is being done. + def with_lock + if use_lock + require "pact_broker/db/advisory_lock" + + lock = PactBroker::DB::AdvisoryLock.new(database_connection, :clean, :pg_try_advisory_lock) + results = lock.with_lock do + yield + end + + if !lock.lock_obtained? + output("Clean was not performed as a clean is already in progress. Exiting.") end + results + else + yield end end - def output string, payload = {} - logger ? logger.info(string, payload) : puts("#{string} #{payload.to_json}") + def output(string, payload = {}) + prefix = dry_run ? "[DRY RUN] " : "" + logger ? logger.info("#{prefix}#{string}") : puts("#{prefix}#{string} #{payload.to_json}") end def add_defaults_to_keep_selectors diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 289fca2a2..b9126817d 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -43,7 +43,6 @@ class TestDataBuilder include PactBroker::Services using PactBroker::StringRefinements - attr_reader :pacticipant attr_reader :consumer attr_reader :provider @@ -88,6 +87,12 @@ def create_condor self end + # Creates a consumer, consumer version, provider and pact with the specified JSON content + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version_number + # @param [Strig] provider_name + # @param [String] json_content def create_pact_with_hierarchy consumer_name = "Consumer", consumer_version_number = "1.2.3", provider_name = "Provider", json_content = nil use_consumer(consumer_name) create_consumer(consumer_name) if !consumer @@ -99,18 +104,39 @@ def create_pact_with_hierarchy consumer_name = "Consumer", consumer_version_numb self end + # Creates a consumer, consumer version with tag, provider and pact + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version_number + # @param [String] consumer_version_tag_name + # @param [Strig] provider_name def create_pact_with_consumer_version_tag consumer_name, consumer_version_number, consumer_version_tag_name, provider_name create_pact_with_hierarchy(consumer_name, consumer_version_number, provider_name) create_consumer_version_tag(consumer_version_tag_name) self end + # Creates a consumer, consumer version, provider, pact and verification + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version + # @param [String] provider_name + # @param [String] provider_version + # @param [Boolean] success default true def create_pact_with_verification consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", provider_name = "Provider", provider_version = "1.0.#{model_counter}", success = true create_pact_with_hierarchy(consumer_name, consumer_version, provider_name) create_verification(number: model_counter, provider_version: provider_version, success: success) self end + # Creates a consumer, consumer version, provider, pact and verification + # Does NOT rely on previous state. + # @param [String] consumer_name + # @param [String] consumer_version + # @param [Array] consumer_version_tags + # @param [String] provider_name + # @param [String] provider_version + # @param [Array] provider_version_tags def create_pact_with_verification_and_tags consumer_name = "Consumer", consumer_version = "1.0.#{model_counter}", consumer_version_tags = [], provider_name = "Provider", provider_version = "1.0.#{model_counter}", provider_version_tags = [] create_pact_with_hierarchy(consumer_name, consumer_version, provider_name) consumer_version_tags.each do | tag | @@ -120,6 +146,10 @@ def create_pact_with_verification_and_tags consumer_name = "Consumer", consumer_ self end + # Create a pacticipant and version + # Does NOT rely on previous state + # @param [String] pacticipant_name + # @param [String] pacticipant_version def create_version_with_hierarchy pacticipant_name, pacticipant_version pacticipant = pacticipant_service.create(:name => pacticipant_name) version = PactBroker::Domain::Version.create(:number => pacticipant_version, :pacticipant => pacticipant) @@ -135,9 +165,16 @@ def create_tag_with_hierarchy pacticipant_name, pacticipant_version, tag_name def create_pacticipant pacticipant_name, params = {} params.delete(:comment) + version_to_create = params.delete(:version) + repository_url = "https://github.com/#{params[:repository_namespace] || "example-organization"}/#{params[:repository_name] || pacticipant_name}" merged_params = { name: pacticipant_name, repository_url: repository_url }.merge(params) @pacticipant = PactBroker::Domain::Pacticipant.create(merged_params) + + version = create_pacticipant_version(version_to_create, @pacticipant) if version_to_create + main_branch = params[:main_branch] + PactBroker::Versions::BranchVersionRepository.new.add_branch(version, main_branch) if version && main_branch + self end @@ -161,8 +198,11 @@ def create_provider provider_name = "Provider #{model_counter}", params = {} self end + # Create an Integration object for the current consumer and provider + # @return [PactBroker::Test::TestDataBuilder] def create_integration - PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id) + @integration = PactBroker::Integrations::Repository.new.create_for_pact(consumer.id, provider.id) + set_created_at_if_set(@now, :integrations, { consumer_id: consumer.id, provider_id: provider.id }) self end @@ -225,7 +265,13 @@ def create_label label_name def publish_pact(consumer_name:, provider_name:, consumer_version_number: , tags: nil, branch: nil, build_url: nil, json_content: nil) json_content = json_content || random_json_content(consumer_name, provider_name) contracts = [ - PactBroker::Contracts::ContractToPublish.from_hash(consumer_name: consumer_name, provider_name: provider_name, decoded_content: json_content, content_type: "application/json", specification: "pact") + PactBroker::Contracts::ContractToPublish.from_hash( + consumer_name: consumer_name, + provider_name: provider_name, + decoded_content: json_content, + content_type: "application/json", + specification: "pact", + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content)) ] contracts_to_publish = PactBroker::Contracts::ContractsToPublish.from_hash( pacticipant_name: consumer_name, @@ -243,7 +289,9 @@ def publish_pact(consumer_name:, provider_name:, consumer_version_number: , tags self end - def create_pact params = {} + # Creates a pact (and integration if one does not already exist) from the given params + # @return [PactBroker::Test::TestDataBuilder] + def create_pact(params = {}) params.delete(:comment) json_content = params[:json_content] || default_json_content pact_version_sha = params[:pact_version_sha] || generate_pact_version_sha(json_content) @@ -256,6 +304,7 @@ def create_pact params = {} json_content: prepare_json_content(json_content), version: @consumer_version ) + integration_service.handle_bulk_contract_data_published([@pact]) pact_versions_count_after = PactBroker::Pacts::PactVersion.count set_created_at_if_set(params[:created_at], :pact_publications, id: @pact.id) set_created_at_if_set(params[:created_at], :pact_versions, sha: @pact.pact_version_sha) if pact_versions_count_after > pact_versions_count_before @@ -597,8 +646,6 @@ def fixed_json_content(consumer_name, provider_name, differentiator) }.to_json end - private - def create_pacticipant_version(version_number, pacticipant, params = {}) params.delete(:comment) tag_names = [params.delete(:tag_names), params.delete(:tag_name)].flatten.compact @@ -623,6 +670,8 @@ def create_pacticipant_version(version_number, pacticipant, params = {}) version end + private + def create_deployed_version(uuid: , currently_deployed: , version:, environment_name: , target: nil, created_at: nil) env = find_environment(environment_name) @deployed_version = PactBroker::Deployments::DeployedVersionService.find_or_create(uuid, version, env, target) diff --git a/lib/pact_broker/ui.rb b/lib/pact_broker/ui.rb index 7c005008a..101af7e15 100644 --- a/lib/pact_broker/ui.rb +++ b/lib/pact_broker/ui.rb @@ -1,14 +1,25 @@ -require "pact_broker/configuration" +# Must be defined before loading Padrino # Stop Padrino creating a log file, as it will try to create it in the gems directory # http://www.padrinorb.com/api/Padrino/Logger.html -unless defined? PADRINO_LOGGER - log_path = File.join(PactBroker.configuration.log_dir, "ui.log") - PADRINO_LOGGER = { - production: { log_level: :error, stream: :to_file, log_path: log_path }, - staging: { log_level: :error, stream: :to_file, log_path: log_path }, - test: { log_level: :warn, stream: :to_file, log_path: log_path }, - development: { log_level: :warn, stream: :to_file, log_path: log_path } - } +# This configuration will be replaced by the SemanticLogger later on. +PADRINO_LOGGER ||= { + ENV.fetch("RACK_ENV", "production").to_sym => { stream: :stdout } +} + +require "padrino-core" + +class PactBrokerPadrinoLogger < SemanticLogger::Logger + include Padrino::Logger::Extensions + + # Padrino expects level to return an integer, not a symbol + def level + Padrino::Logger::Levels[SemanticLogger.default_level] + end end +Padrino.logger = PactBrokerPadrinoLogger.new("Padrino") +# Log a test message to ensure that the logger works properly, as it only +# seems to be used in production. +Padrino.logger.info("Padrino has been configured with SemanticLogger") + require "pact_broker/ui/app" diff --git a/lib/pact_broker/ui/controllers/base_controller.rb b/lib/pact_broker/ui/controllers/base_controller.rb index 6125e0bfa..d3657a5ce 100644 --- a/lib/pact_broker/ui/controllers/base_controller.rb +++ b/lib/pact_broker/ui/controllers/base_controller.rb @@ -10,8 +10,10 @@ class Base < Padrino::Application using PactBroker::StringRefinements set :root, File.join(File.dirname(__FILE__), "..") - set :show_exceptions, ENV["RACK_ENV"] != "production" - set :dump_errors, false # The padrino logger logs these for us. If this is enabled we get duplicate logging. + set :show_exceptions, ENV["RACK_ENV"] == "development" + # The padrino logger logs these for us, but only in production. If this is enabled we get duplicate logging. + set :dump_errors, ENV["RACK_ENV"] != "production" + set :raise_errors, ENV["RACK_ENV"] == "test" def base_url # Using the X-Forwarded headers in the UI can leave the app vulnerable diff --git a/lib/pact_broker/version.rb b/lib/pact_broker/version.rb index 090c220d1..d214aa505 100644 --- a/lib/pact_broker/version.rb +++ b/lib/pact_broker/version.rb @@ -1,3 +1,3 @@ module PactBroker - VERSION = "2.107.1" + VERSION = "2.111.0" end diff --git a/lib/pact_broker/versions/branch_repository.rb b/lib/pact_broker/versions/branch_repository.rb index 1ce658377..ffeed8eeb 100644 --- a/lib/pact_broker/versions/branch_repository.rb +++ b/lib/pact_broker/versions/branch_repository.rb @@ -40,8 +40,42 @@ def delete_branch(branch) end def delete_branch_and_associated_versions(branch) + # TODO branch.delete end + + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + # @param [Integer] the number of branches that will be deleted + def count_branches_to_delete(pacticipant, exclude: ) + build_query_for_pacticipant_branches(pacticipant, exclude: exclude).count + end + + # Returns the list of branches which will NOT be deleted (the bulk delete is executed async after the request has finished) + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + # @return [Array] + def remaining_branches_after_future_deletion(pacticipant, exclude: ) + exclude_dup = exclude.dup + if pacticipant.main_branch + exclude_dup << pacticipant.main_branch + end + Branch.where(pacticipant_id: pacticipant.id).where(name: exclude_dup) + end + + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @params [Array] exclude the names of the branches to NOT delete + def delete_branches_for_pacticipant(pacticipant, exclude:) + build_query_for_pacticipant_branches(pacticipant, exclude: exclude).delete + end + + def build_query_for_pacticipant_branches(pacticipant, exclude: ) + exclude_dup = exclude.dup + if pacticipant.main_branch + exclude_dup << pacticipant.main_branch + end + Branch.where(pacticipant_id: pacticipant.id).exclude(name: exclude_dup) + end end end end diff --git a/lib/pact_broker/versions/branch_service.rb b/lib/pact_broker/versions/branch_service.rb index 85e616121..f787280da 100644 --- a/lib/pact_broker/versions/branch_service.rb +++ b/lib/pact_broker/versions/branch_service.rb @@ -1,17 +1,29 @@ +require "forwardable" require "pact_broker/logging" require "pact_broker/repositories" require "pact_broker/messages" -require "forwardable" +require "pact_broker/contracts/notice" module PactBroker module Versions class BranchService extend PactBroker::Repositories + extend PactBroker::Messages class << self extend Forwardable delegate [:find_branch_version, :find_or_create_branch_version, :delete_branch_version] => :branch_version_repository - delegate [:find_branch, :delete_branch, :find_all_branches_for_pacticipant] => :branch_repository + delegate [:find_branch, :delete_branch, :find_all_branches_for_pacticipant, :delete_branches_for_pacticipant] => :branch_repository + + # Returns a list of notices to display to the user in the terminal + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @param [Array] exclude the list of branches to NOT delete + # @return [Array] + def branch_deletion_notices(pacticipant, exclude:) + count = branch_repository.count_branches_to_delete(pacticipant, exclude: exclude) + remaining = branch_repository.remaining_branches_after_future_deletion(pacticipant, exclude: exclude).sort_by(&:created_at).collect(&:name).join(", ") + [PactBroker::Contracts::Notice.success(message("messages.branch.bulk_delete", count: count, pacticipant_name: pacticipant.name, remaining: remaining))] + end end end end diff --git a/lib/pact_broker/versions/repository.rb b/lib/pact_broker/versions/repository.rb index 699559cb0..06e93e880 100644 --- a/lib/pact_broker/versions/repository.rb +++ b/lib/pact_broker/versions/repository.rb @@ -57,29 +57,11 @@ def find_by_pacticipant_name_and_number pacticipant_name, number .single_record end - # The eager loaded relations are hardcoded here to support the PactBroker::Api::Decorators::VersionDecorator - # Newer "find all" implementations for other models pass the relations to eager load in - # from the decorator via the resource. - def find_all_pacticipant_versions_in_reverse_order name, pagination_options = {} - pacticipant = pacticipant_repository.find_by_name!(name) - query = PactBroker::Domain::Version - .where(pacticipant: pacticipant) - .eager(:pacticipant) - .eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }]) - .eager(tags: :head_tag) - .eager(:pact_publications) - .reverse_order(:order) - query.all_with_pagination_options(pagination_options) - end - - def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {}) + def find_pacticipant_versions_in_reverse_order(pacticipant_name, options = {}, pagination_options = {}, eager_load_associations = []) pacticipant = pacticipant_repository.find_by_name!(pacticipant_name) query = PactBroker::Domain::Version .where(pacticipant: pacticipant) - .eager(:pacticipant) - .eager(branch_versions: [:version, :branch_head, { branch: :pacticipant }]) - .eager(tags: :head_tag) - .eager(:pact_publications) + .eager(*eager_load_associations) .reverse_order(:order) if options[:branch_name] diff --git a/lib/pact_broker/versions/service.rb b/lib/pact_broker/versions/service.rb index 439133509..2f5d876c2 100644 --- a/lib/pact_broker/versions/service.rb +++ b/lib/pact_broker/versions/service.rb @@ -26,12 +26,8 @@ def self.find_latest_by_pacticipant_name_and_branch_name(pacticipant_name, branc version_repository.find_latest_by_pacticipant_name_and_branch_name(pacticipant_name, branch_name) end - def self.find_all_pacticipant_versions_in_reverse_order(name, pagination_options = {}) - version_repository.find_all_pacticipant_versions_in_reverse_order(name, pagination_options) - end - - def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {}) - version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options) + def self.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options = {}, eager_load_associations = []) + version_repository.find_pacticipant_versions_in_reverse_order(pacticipant_name, options, pagination_options, eager_load_associations) end def self.create_or_overwrite(pacticipant_name, version_number, version) diff --git a/lib/pact_broker/webhooks/execution.rb b/lib/pact_broker/webhooks/execution.rb index a54d7e1e6..e91de2b63 100644 --- a/lib/pact_broker/webhooks/execution.rb +++ b/lib/pact_broker/webhooks/execution.rb @@ -2,14 +2,14 @@ module PactBroker module Webhooks - class Execution < Sequel::Model( - Sequel::Model.db[:webhook_executions].select( - Sequel[:webhook_executions][:id], - :triggered_webhook_id, - :success, - :logs, - Sequel[:webhook_executions][:created_at]) - ) + class Execution < Sequel::Model(:webhook_executions) + # Ignore the columns that were used before the TriggeredWebhook class existed. + # It used to go Webhook -> Execution, and now it goes Webhook -> TriggeredWebhook -> Execution + # If we ever release a major version where we drop unused columns, those columns could be deleted. + EXECUTION_COLUMNS = Sequel::Model.db.schema(:webhook_executions).collect(&:first) - [:webhook_id, :pact_publication_id, :consumer_id, :provider_id] + + set_dataset(Sequel::Model.db[:webhook_executions].select(*EXECUTION_COLUMNS)) + set_primary_key :id plugin :timestamps diff --git a/lib/rack/pact_broker/add_cache_header.rb b/lib/rack/pact_broker/add_cache_header.rb new file mode 100644 index 000000000..1f17e9cc9 --- /dev/null +++ b/lib/rack/pact_broker/add_cache_header.rb @@ -0,0 +1,14 @@ +module Rack + module PactBroker + class AddCacheHeader + def initialize app + @app = app + end + + def call(env) + status, headers, body = @app.call(env) + [status, { "Cache-Control" => "no-cache" }.merge(headers || {}), body] + end + end + end +end diff --git a/lib/rack/pact_broker/invalid_uri_protection.rb b/lib/rack/pact_broker/invalid_uri_protection.rb index 536e4fb98..850c6aa7e 100644 --- a/lib/rack/pact_broker/invalid_uri_protection.rb +++ b/lib/rack/pact_broker/invalid_uri_protection.rb @@ -12,6 +12,8 @@ module PactBroker class InvalidUriProtection include ::PactBroker::Messages + CONSECUTIVE_SLASH = /\/{2,}/ + def initialize app @app = app end @@ -19,12 +21,12 @@ def initialize app def call env if (uri = valid_uri?(env)) if (error_message = validate(uri)) - [422, {"Content-Type" => "text/plain"}, [error_message]] + [422, headers, [body(env, error_message, "Unprocessable", "invalid-request-parameter-value", 422)]] else app.call(env) end else - [404, {}, []] + [404, headers, [body(env, "Empty path component found", "Not Found", "not-found", 404)]] end end @@ -34,7 +36,9 @@ def call env def valid_uri? env begin - parse(::Rack::Request.new(env).url) + uri = parse(::Rack::Request.new(env).url) + return nil if CONSECUTIVE_SLASH.match(uri.path) + uri rescue URI::InvalidURIError, ArgumentError nil end @@ -52,6 +56,18 @@ def validate(uri) message("errors.tab_in_url_path") end end + + def headers + {"Content-Type" => "application/problem+json"} + end + + def body(env, detail, title, type, status) + env["pactbroker.application_context"] + .decorator_configuration + .class_for(:custom_error_problem_json_decorator) + .new(detail: detail, title: title, type: type, status: status) + .to_json(user_options: { base_url: env["pactbroker.base_url"] }) + end end end end diff --git a/lib/sequel/extensions/pg_advisory_lock.rb b/lib/sequel/extensions/pg_advisory_lock.rb new file mode 100644 index 000000000..b9a1ea1b1 --- /dev/null +++ b/lib/sequel/extensions/pg_advisory_lock.rb @@ -0,0 +1,101 @@ +# Copied with thanks from https://github.com/yuryroot/sequel-pg_advisory_lock/blob/d7509aa/lib/sequel/extensions/pg_advisory_lock.rb +# The reason this is copy/pasted and modified is that I wanted to allow exact duplicate +# locks to be registered because different threads running the same code +# should not cause a Sequel::Error to be raised. +# Also, I wanted it to use Concurrent::Hash for multi-threaded environments. + +require "sequel" +require "zlib" +require "concurrent/hash" + +module Sequel + module Postgres + module PgAdvisoryLock + + SESSION_LEVEL_LOCKS = [ + :pg_advisory_lock, + :pg_try_advisory_lock + ].freeze + + TRANSACTION_LEVEL_LOCKS = [ + :pg_advisory_xact_lock, + :pg_try_advisory_xact_lock + ].freeze + + LOCK_FUNCTIONS = (SESSION_LEVEL_LOCKS + TRANSACTION_LEVEL_LOCKS).freeze + + DEFAULT_LOCK_FUNCTION = :pg_advisory_lock + UNLOCK_FUNCTION = :pg_advisory_unlock + + class LockAlreadyRegistered < Sequel::Error; end + + def registered_advisory_locks + @registered_advisory_locks ||= Concurrent::Hash.new + end + + def with_advisory_lock(name, id = nil) + options = registered_advisory_locks.fetch(name.to_sym) + + lock_key = options.fetch(:key) + function_params = [lock_key, id].compact + + lock_function = options.fetch(:lock_function) + transaction_level_lock = TRANSACTION_LEVEL_LOCKS.include?(lock_function) + + if transaction_level_lock + # TODO: It's allowed to specify additional options (in particular, :server) + # while opening database transaction. + # That's why this check must be smarter. + unless in_transaction? + raise Error, "Transaction must be manually opened before using transaction level lock '#{lock_function}'" + end + + if get(Sequel.function(lock_function, *function_params)) + yield + end + else + synchronize do + if get(Sequel.function(lock_function, *function_params)) + begin + result = yield + ensure + get(Sequel.function(UNLOCK_FUNCTION, *function_params)) + result + end + end + end + end + end + + # Beth: not sure how much extra value this registration provides. + # It turns the name into a number, and makes sure the name/number is unique, + # and that you don't try and use a different lock function with the same name. + def register_advisory_lock(name, lock_function = DEFAULT_LOCK_FUNCTION) + name = name.to_sym + + if registered_advisory_locks.key?(name) && registered_advisory_locks[name][:lock_function] != lock_function + raise LockAlreadyRegistered, "Lock with name :#{name} is already registered with a different lock function (#{registered_advisory_locks[name][:lock_function]})" + end + + key = advisory_lock_key_for(name) + name_for_key = registered_advisory_locks.keys.find { |n| registered_advisory_locks[n].fetch(:key) == key } + if name_for_key && name_for_key != name + raise Error, "Lock key #{key} is already taken" + end + + function = lock_function.to_sym + unless LOCK_FUNCTIONS.include?(function) + raise Error, "Invalid lock function :#{function}" + end + + registered_advisory_locks[name] = { key: key, lock_function: function } + end + + def advisory_lock_key_for(lock_name) + Zlib.crc32(lock_name.to_s) % 2 ** 31 + end + end + end + + Database.register_extension(:pg_advisory_lock, Postgres::PgAdvisoryLock) +end diff --git a/lib/webmachine/describe_routes.rb b/lib/webmachine/describe_routes.rb index 47c3cb2e5..c75970a89 100644 --- a/lib/webmachine/describe_routes.rb +++ b/lib/webmachine/describe_routes.rb @@ -1,6 +1,10 @@ require "webmachine/adapters/rack_mapped" require "pact_broker/string_refinements" +# Code to describe the routes in a Webmachine API, including +# path, resource class, allowed methods, schemas, policy class. +# Used in tests and in the pact_broker:routes task + module Webmachine class DescribeRoutes using PactBroker::StringRefinements @@ -12,18 +16,11 @@ class DescribeRoutes :resource_name, :resource_class_location, :allowed_methods, - :policy_class, + :policy_names, + :policy_classes, # only used by pactflow :schemas, keyword_init: true) do - def [](key) - if respond_to?(key) - send(key) - else - nil - end - end - def path_include?(component) path.include?(component) end @@ -47,7 +44,7 @@ def build_resource(env, application_context, path_param_values) }.merge(path_params) rack_req = ::Rack::Request.new({ "REQUEST_METHOD" => "GET", "rack.input" => StringIO.new("") }.merge(env) ) - dummy_request = Webmachine::Adapters::Rack::RackRequest.new( + request = Webmachine::Adapters::Rack::RackRequest.new( rack_req.env["REQUEST_METHOD"], path, Webmachine::Headers.from_cgi({"HTTP_HOST" => "example.org"}.merge(env)), @@ -56,13 +53,13 @@ def build_resource(env, application_context, path_param_values) {}, rack_req.env ) - dummy_request.path_info = path_info - resource_class.new(dummy_request, Webmachine::Response.new) + request.path_info = path_info + resource_class.new(request, Webmachine::Response.new) end end def self.call(webmachine_applications, search_term: nil) - path_mappings = webmachine_applications.flat_map { | webmachine_application | paths_to_resource_class_mappings(webmachine_application) } + path_mappings = webmachine_applications.flat_map { | webmachine_application | build_routes(webmachine_application) } if search_term path_mappings = path_mappings.select{ |(route, _)| route[:path].include?(search_term) } @@ -71,8 +68,10 @@ def self.call(webmachine_applications, search_term: nil) path_mappings.sort_by{ | mapping | mapping[:path] } end - def self.paths_to_resource_class_mappings(webmachine_application) - webmachine_application.routes.collect do | webmachine_route | + # Build a Route object to describe every Webmachine route defined in the app.routes block + # @return [Array] + def self.build_routes(webmachine_application) + webmachine_routes_to_describe(webmachine_application).collect do | webmachine_route | resource_path_absolute = Pathname.new(source_location_for(webmachine_route.resource)) Route.new({ path: "/" + webmachine_route.path_spec.collect{ |part| part.is_a?(Symbol) ? ":#{part}" : part }.join("/"), @@ -80,19 +79,23 @@ def self.paths_to_resource_class_mappings(webmachine_application) resource_class: webmachine_route.resource, resource_name: webmachine_route.instance_variable_get(:@bindings)[:resource_name], resource_class_location: resource_path_absolute.relative_path_from(Pathname.pwd).to_s - }.merge(info_from_resource_instance(webmachine_route, webmachine_application.application_context))) - end.reject{ | route | route.resource_class == Webmachine::Trace::TraceResource } + }.merge(properties_for_webmachine_route(webmachine_route, webmachine_application.application_context))) + end end - def self.info_from_resource_instance(webmachine_route, application_context) + def self.webmachine_routes_to_describe(webmachine_application) + webmachine_application.routes.reject{ | route | route.resource == Webmachine::Trace::TraceResource }.collect + end + + def self.properties_for_webmachine_route(webmachine_route, application_context) with_no_logging do path_info = { application_context: application_context, pacticipant_name: "foo", pacticipant_version_number: "1", resource_name: "foo" } path_info.default = "1" - dummy_request = dummy_request(http_method: "GET", path_info: path_info) + request = build_request(http_method: "GET", path_info: path_info) - dummy_resource = webmachine_route.resource.new(dummy_request, Webmachine::Response.new) - if dummy_resource - info_from_dummy_resource(dummy_resource, webmachine_route, path_info) + resource = webmachine_route.resource.new(request, Webmachine::Response.new) + if resource + properties_for_resource(resource.allowed_methods - ["OPTIONS"], webmachine_route, application_context) else {} end @@ -102,29 +105,38 @@ def self.info_from_resource_instance(webmachine_route, application_context) {} end - def self.info_from_dummy_resource(dummy_resource, webmachine_route, path_info) + # Return the properties of the resource that can only be determined by instantiating the resource + # @return [Hash] + def self.properties_for_resource(allowed_methods, webmachine_route, application_context) + schemas = [] + policy_names = [] + allowed_methods.each do | http_method | + resource = build_resource(webmachine_route, http_method, application_context) + if (schema_class = resource.respond_to?(:schema, true) && resource.send(:schema)) + schemas << { http_method: http_method, class: schema_class, location: source_location_for(schema_class)} + end + + policy_names << resource.policy_name + end + { - allowed_methods: dummy_resource.allowed_methods, - schemas: dummy_resource.respond_to?(:schema, true) && dummy_resource.send(:schema) ? schemas(dummy_resource.allowed_methods, webmachine_route.resource, path_info) : nil - }.compact + allowed_methods: allowed_methods, + schemas: schemas, + policy_names: policy_names.uniq + } end - # This is not entirely accurate, because some GET requests have schemas too, but we can't tell that statically at the moment - def self.schemas(allowed_methods, resource, path_info) - (allowed_methods - ["GET", "OPTIONS", "DELETE"]).collect do | http_method | - resource.new(dummy_request(http_method: http_method, path_info: path_info), Webmachine::Response.new).send(:schema) - end.uniq.collect do | schema_class | - { - class: schema_class, - location: source_location_for(schema_class) - } - end + def self.build_resource(webmachine_route, http_method, application_context) + path_info = { application_context: application_context, pacticipant_name: "foo", pacticipant_version_number: "1", resource_name: "foo" } + path_info.default = "1" + request = build_request(http_method: http_method, path_info: path_info) + webmachine_route.resource.new(request, Webmachine::Response.new) end - def self.dummy_request(http_method: "GET", path_info: ) - dummy_request = Webmachine::Adapters::Rack::RackRequest.new(http_method, "/", Webmachine::Headers["host" => "example.org"], nil, {}, {}, { "REQUEST_METHOD" => http_method }) - dummy_request.path_info = path_info - dummy_request + def self.build_request(http_method: "GET", path_info: ) + request = Webmachine::Adapters::Rack::RackRequest.new(http_method, "/", Webmachine::Headers["host" => "example.org"], nil, {}, {}, { "REQUEST_METHOD" => http_method }) + request.path_info = path_info + request end def self.source_location_for(clazz) diff --git a/pact_broker.gemspec b/pact_broker.gemspec index 8221f7de1..3fc710787 100644 --- a/pact_broker.gemspec +++ b/pact_broker.gemspec @@ -50,7 +50,7 @@ Gem::Specification.new do |gem| gem.license = "MIT" gem.add_runtime_dependency "json", "~> 2.3" - gem.add_runtime_dependency "psych", "~> 4.0" # TODO identify breaking changes and see if we can use 5 + gem.add_runtime_dependency "psych", "~> 5.0" gem.add_runtime_dependency "roar", "~> 1.1" gem.add_runtime_dependency "dry-validation", "~> 1.8" gem.add_runtime_dependency "reform", "~> 2.6" @@ -64,7 +64,7 @@ Gem::Specification.new do |gem| gem.add_runtime_dependency "padrino-core", ">= 0.14.3", "~> 0.14" gem.add_runtime_dependency "sinatra", "~> 3.0" gem.add_runtime_dependency "haml", "~>5.0" - gem.add_runtime_dependency "sucker_punch", "~>2.0" + gem.add_runtime_dependency "sucker_punch", "~>3.0" gem.add_runtime_dependency "rack-protection", "~> 3.0" gem.add_runtime_dependency "table_print", "~> 1.5" gem.add_runtime_dependency "semantic_logger", "~> 4.11" diff --git a/pact_broker_oas.yaml b/pact_broker_oas.yaml index 2fc8b397f..3b21373fe 100644 --- a/pact_broker_oas.yaml +++ b/pact_broker_oas.yaml @@ -53,7 +53,7 @@ paths: notices: type: array items: - $ref: '#/components/schemas/notice' + $ref: '#/components/schemas/Notice' "400": description: Validation errror content: diff --git a/script/dev/console.rb b/script/dev/console.rb index fca2f77fb..16b095d39 100755 --- a/script/dev/console.rb +++ b/script/dev/console.rb @@ -3,7 +3,7 @@ Bundler.require require "sequel" -require "logger" +require "semantic_logger" require "fileutils" require "pact_broker/initializers/database_connection" @@ -24,13 +24,14 @@ FileUtils.mkdir_p(File.absolute_path(File.dirname(URI(database_connection_string).path))) end -logger = Logger.new($stdout) -logger.level = Logger::DEBUG +SemanticLogger.default_level = :info +SemanticLogger.add_appender(io: $stdout) +logger = SemanticLogger["console"] database_opts = { logger: logger, encoding: "utf8", - sql_log_level: "debug" + sql_log_level: "trace" } puts "Connecting to #{database_connection_string}" diff --git a/script/docker/db-migrate.sh b/script/docker/db-migrate.sh new file mode 100755 index 000000000..5be8db83e --- /dev/null +++ b/script/docker/db-migrate.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +PACT_BROKER_TEST_DATABASE_URL=postgres://postgres:postgres@localhost/postgres bundle exec rake pact_broker:db:migrate \ No newline at end of file diff --git a/script/test/run-rake-on-docker-compose-mysql.sh b/script/test/run-rake-on-docker-compose-mysql.sh index e7a3e73c7..48d65faa5 100755 --- a/script/test/run-rake-on-docker-compose-mysql.sh +++ b/script/test/run-rake-on-docker-compose-mysql.sh @@ -1,8 +1,8 @@ #/bin/sh cleanup() { - docker-compose -f docker-compose-ci-mysql.yml down + docker compose -f docker-compose-ci-mysql.yml down } trap cleanup EXIT -docker-compose -f docker-compose-ci-mysql.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans +docker compose -f docker-compose-ci-mysql.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans diff --git a/script/test/run-rake-on-docker-compose-postgres.sh b/script/test/run-rake-on-docker-compose-postgres.sh index 6e88f9bf5..a7076f1ef 100755 --- a/script/test/run-rake-on-docker-compose-postgres.sh +++ b/script/test/run-rake-on-docker-compose-postgres.sh @@ -1,8 +1,8 @@ #/bin/sh cleanup() { - docker-compose -f docker-compose-ci-postgres.yml down + docker compose -f docker-compose-ci-postgres.yml down } trap cleanup EXIT -docker-compose -f docker-compose-ci-postgres.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans +docker compose -f docker-compose-ci-postgres.yml up --exit-code-from tests --abort-on-container-exit --remove-orphans diff --git a/spec/features/delete_pacticipant_branches_spec.rb b/spec/features/delete_pacticipant_branches_spec.rb new file mode 100644 index 000000000..7930c6274 --- /dev/null +++ b/spec/features/delete_pacticipant_branches_spec.rb @@ -0,0 +1,35 @@ +describe "Delete pacticipant branches" do + before do + td.create_consumer("Bar") + .create_consumer_version("1", branch: "main") + .create_consumer("Foo", main_branch: "main") + .create_consumer_version("1", branch: "main") + .create_consumer_version("2", branch: "feat/bar") + .create_consumer_version("3", branch: "feat/foo") + end + let(:path) { PactBroker::Api::PactBrokerUrls.pacticipant_branches_url(td.and_return(:pacticipant)) } + let(:rack_env) do + { + "pactbroker.database_connector" => lambda { |&block| block.call } + } + end + + subject { delete(path + "?exclude[]=feat%2Fbar", nil, rack_env) } + + its(:status) { is_expected.to eq 202 } + + it "returns a list of notices to be displayed to the user" do + expect(JSON.parse(subject.body)["notices"]).to be_instance_of(Array) + expect(JSON.parse(subject.body)["notices"]).to include(hash_including("text")) + end + + it "after the request, it deletes all except the excluded branches for a pacticipant" do + expect { subject }.to change { + PactBroker::Versions::Branch + .where(pacticipant_id: td.and_return(:pacticipant).id) + .all + .collect(&:name) + .sort + }.from(["feat/bar", "feat/foo", "main"]).to(["feat/bar", "main"]) + end +end diff --git a/spec/features/get_branch_versions_spec.rb b/spec/features/get_branch_versions_spec.rb index 31774d407..dadb3eb18 100644 --- a/spec/features/get_branch_versions_spec.rb +++ b/spec/features/get_branch_versions_spec.rb @@ -1,4 +1,4 @@ -describe "Get a branch version" do +describe "Get versions for branch" do before do td.create_consumer("Foo") .create_consumer_version("1", branch: "main") @@ -27,9 +27,9 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "page" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the size" do expect(JSON.parse(subject.body).dig("_embedded", "versions").size).to eq 2 end diff --git a/spec/features/get_currently_deployed_versions_for_version_spec.rb b/spec/features/get_currently_deployed_versions_for_version_spec.rb index 02e25cc67..fed74c744 100644 --- a/spec/features/get_currently_deployed_versions_for_version_spec.rb +++ b/spec/features/get_currently_deployed_versions_for_version_spec.rb @@ -21,7 +21,7 @@ it "returns a list of deployed versions" do expect(response_body_hash[:_embedded][:deployedVersions]).to be_a(Array) expect(response_body_hash[:_embedded][:deployedVersions].size).to eq 1 - expect(response_body_hash[:_links][:self][:title]).to eq "Deployed versions for Foo version 1" + expect(response_body_hash[:_links][:self][:title]).to eq "Deployed versions for Foo version 1 in environment Test" expect(response_body_hash[:_links][:self][:href]).to end_with(path) end end diff --git a/spec/features/get_integrations_spec.rb b/spec/features/get_integrations_spec.rb index 04b01482c..c2389301f 100644 --- a/spec/features/get_integrations_spec.rb +++ b/spec/features/get_integrations_spec.rb @@ -23,7 +23,7 @@ end context "with pagination options" do - let(:query) { { "pageSize" => "2", "pageNumber" => "1" } } + let(:query) { { "size" => "2", "page" => "1" } } it_behaves_like "a paginated response" end diff --git a/spec/features/get_labels_spec.rb b/spec/features/get_labels_spec.rb new file mode 100644 index 000000000..a63bca7b4 --- /dev/null +++ b/spec/features/get_labels_spec.rb @@ -0,0 +1,36 @@ +describe "Get labels" do + + let(:path) { "/labels" } + let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } + + subject { get(path) } + + context "when labels exists" do + before do + td.create_pacticipant("foo") + .create_label("ios") + .create_label("consumer") + .create_pacticipant("bar") + .create_label("ios") + .create_label("consumer") + end + + it "returns a 200 OK" do + expect(subject).to be_a_hal_json_success_response + end + + it "returns the labels in the body" do + expect(response_body_hash[:_embedded][:labels].map { |label| label[:name] }).to contain_exactly("ios", "consumer") + end + + context "with pagination options" do + subject { get(path, { "size" => "1", "page" => "1" }) } + + it "only returns the number of items specified in the page" do + expect(response_body_hash[:_embedded][:labels].size).to eq 1 + end + + it_behaves_like "a paginated response" + end + end +end diff --git a/spec/features/get_latest_version_for_branch_spec.rb b/spec/features/get_latest_version_for_branch_spec.rb new file mode 100644 index 000000000..64bdefd52 --- /dev/null +++ b/spec/features/get_latest_version_for_branch_spec.rb @@ -0,0 +1,18 @@ +describe "Get latest version for branch" do + before do + td.create_consumer("Foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("2", branch: "main") + .create_consumer_version("3", branch: "not-main") + end + let(:path) { PactBroker::Api::PactBrokerUrls.latest_version_for_branch_url(PactBroker::Versions::Branch.order(:id).first) } + let(:rack_env) { { "CONTENT_TYPE" => "application/json" } } + + subject { get(path, {}, rack_env) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns the latest version for the branch" do + expect(JSON.parse(subject.body)["number"]).to eq "2" + end +end diff --git a/spec/features/get_pacticipant_branches_spec.rb b/spec/features/get_pacticipant_branches_spec.rb index 4fb6b4b2b..56fcf41e6 100644 --- a/spec/features/get_pacticipant_branches_spec.rb +++ b/spec/features/get_pacticipant_branches_spec.rb @@ -21,9 +21,9 @@ it_behaves_like "a page" context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "number" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the size" do expect(response_body_hash[:_links][:"pb:branches"].size).to eq 2 end diff --git a/spec/features/get_pacticipants_spec.rb b/spec/features/get_pacticipants_spec.rb index 97232bc97..4f10667ca 100644 --- a/spec/features/get_pacticipants_spec.rb +++ b/spec/features/get_pacticipants_spec.rb @@ -23,9 +23,9 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + subject { get(path, { "size" => "2", "page" => "1" }) } - it "only returns the number of items specified in the pageSize" do + it "only returns the number of items specified in the page" do expect(response_body_hash[:_links][:"pacticipants"].size).to eq 2 end diff --git a/spec/features/get_released_versions_for_version_and_environment.rb b/spec/features/get_released_versions_for_version_and_environment_spec.rb similarity index 100% rename from spec/features/get_released_versions_for_version_and_environment.rb rename to spec/features/get_released_versions_for_version_and_environment_spec.rb diff --git a/spec/features/get_versions_spec.rb b/spec/features/get_versions_spec.rb index f0a6ff1c2..e2c24fb8d 100644 --- a/spec/features/get_versions_spec.rb +++ b/spec/features/get_versions_spec.rb @@ -26,7 +26,7 @@ end context "with pagination options" do - subject { get(path, { "pageSize" => "1", "pageNumber" => "1" }) } + subject { get(path, { "size" => "1", "page" => "1" }) } it "paginates the response" do expect(last_response_body[:_links][:"versions"].size).to eq 1 diff --git a/spec/features/update_pacticipant_spec.rb b/spec/features/update_pacticipant_spec.rb index 5f4cc39b7..efd24a525 100644 --- a/spec/features/update_pacticipant_spec.rb +++ b/spec/features/update_pacticipant_spec.rb @@ -72,6 +72,35 @@ it "returns a json body with the updated pacticipant" do expect(subject.headers["Content-Type"]).to eq "application/hal+json;charset=utf-8" end + + context "when request body contains embedded labels" do + context "when labels has values" do + let(:request_body) { { "displayName": "Updated Consumer Name", "_embedded": { "labels": [{ "name": "ios" }, { "name": "consumer" }] } } } + + it "returns a 200 response" do + expect(subject.status).to be 200 + end + + it "should not create the labels for the pacticipant" do + expect(response_body_hash[:_embedded][:labels]).to be_empty + end + + it "only updates pacticipant attribute ignoring the labels" do + expect(response_body_hash[:displayName]).to eq "Updated Consumer Name" + expect{ subject }.to change { + PactBroker::Domain::Label.where(name: "ios").count + }.by(0) + end + end + + context "when labels is empty" do + let(:request_body) {{"displayName": "Updated Consumer Name", "_embedded": { "labels": []}}} + + it "returns a 200 OK" do + expect(subject.status).to be 200 + end + end + end end context "with application/merge-patch+json" do diff --git a/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json b/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json index 4a380ecf9..71523f368 100644 --- a/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json +++ b/spec/fixtures/approvals/docs_webhooks_logs_of_triggered_webhook_get.approved.json @@ -13,7 +13,8 @@ "response": { "status": 200, "headers": { - "Content-Type": "text/plain;charset=utf-8" + "Content-Type": "text/plain;charset=utf-8", + "Vary": "Accept" }, "body": "logs" } diff --git a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json index def84d52f..4a3204790 100644 --- a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json +++ b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json @@ -11,7 +11,7 @@ }, "pb:latest-version": { "title": "Latest version for branch", - "href": "http://example.org/pacticipants/Foo/branches/main/versions?pageSize=1" + "href": "http://example.org/pacticipants/Foo/branches/main/latest-version" } } } diff --git a/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json b/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json index 62ebb0f77..8c3dfc3bc 100644 --- a/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json +++ b/spec/fixtures/approvals/publish_contract_verification_already_exists.approved.json @@ -40,7 +40,7 @@ }, { "level": "warning", - "message": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning", + "message": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning", "deprecationWarning": "Replaced by notices" }, { @@ -71,7 +71,7 @@ }, { "type": "warning", - "text": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal cicumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning" + "text": "Pact with changed content published over existing content for Foo version 1 and provider Bar. This is not recommended in normal circumstances and may indicate that you have not configured your Pact pipeline correctly. For more information see https://docs.pact.io/go/versioning" }, { "type": "debug", diff --git a/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json b/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json new file mode 100644 index 000000000..4202297ea --- /dev/null +++ b/spec/fixtures/approvals/triggered_webhook_logs_decorator.approved.json @@ -0,0 +1,24 @@ +{ + "executions": [ + { + "success": true, + "logs": "foo", + "createdAt": "2024-01-01T00:00:00+00:00" + } + ], + "_embedded": { + "triggeredWebhook": { + "uuid": "1234" + } + }, + "_links": { + "self": { + "title": "Triggered webhook logs", + "href": null + }, + "pb:webhook": { + "href": "http://example.org/webhooks/1234", + "title": "Webhook" + } + } +} diff --git a/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb b/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb index da4205546..da0fca3fa 100644 --- a/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb +++ b/spec/lib/pact_broker/api/contracts/pacts_for_verification_json_query_schema_spec.rb @@ -523,6 +523,36 @@ module Contracts it { is_expected.to_not have_key(:consumerVersionSelectors) } end + + context "when branch is true, and latest is true" do + let(:params) do + { + consumerVersionSelectors: [ { branch: true, latest: true }] + } + end + + it { is_expected.to_not have_key(:consumerVersionSelectors) } + end + + context "when branch is true, and latest is false" do + let(:params) do + { + consumerVersionSelectors: [ { branch: true, latest: false }] + } + end + + its([:consumerVersionSelectors, 0]) { is_expected.to match("cannot specify") } + end + + context "when branch is false" do + let(:params) do + { + consumerVersionSelectors: [ { branch: false }] + } + end + + its([:consumerVersionSelectors, 0]) { is_expected.to match("branch must be a string or branch must be equal to true") } + end end end end diff --git a/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb b/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb index 7e7afc6ae..1277f5352 100644 --- a/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb +++ b/spec/lib/pact_broker/api/contracts/pagination_query_params_schema_spec.rb @@ -19,25 +19,51 @@ module Contracts context "with values that are not numeric" do let(:params) do { - "pageNumber" => "a", - "pageSize" => "3.2" + "page" => "a", + "size" => "3.2" } end - its([:pageNumber]) { is_expected.to contain_exactly(match("integer"))} - its([:pageSize]) { is_expected.to contain_exactly(match("integer"))} + its([:page]) { is_expected.to contain_exactly(match("integer"))} + its([:size]) { is_expected.to contain_exactly(match("integer"))} end context "with values that are 0" do let(:params) do { - "pageNumber" => "-0", - "pageSize" => "-0" + "page" => "-0", + "size" => "-0" } end - its([:pageNumber]) { is_expected.to contain_exactly(match(/greater.*1/))} - its([:pageSize]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:page]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:size]) { is_expected.to contain_exactly(match(/greater.*1/))} + end + + context "legacy format" do + context "with values that are not numeric" do + let(:params) do + { + "pageNumber" => "a", + "pageSize" => "3.2" + } + end + + its([:pageNumber]) { is_expected.to contain_exactly(match("integer"))} + its([:pageSize]) { is_expected.to contain_exactly(match("integer"))} + end + + context "with values that are 0" do + let(:params) do + { + "pageNumber" => "-0", + "pageSize" => "-0" + } + end + + its([:pageNumber]) { is_expected.to contain_exactly(match(/greater.*1/))} + its([:pageSize]) { is_expected.to contain_exactly(match(/greater.*1/))} + end end end end diff --git a/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb new file mode 100644 index 000000000..f61302e6b --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/labels_decorator_spec.rb @@ -0,0 +1,43 @@ +require "pact_broker/api/decorators/labels_decorator" +require "pact_broker/labels/service" + +module PactBroker + module Api + module Decorators + + describe LabelsDecorator do + before do + td.create_consumer("Foo") + .create_label("consumer") + .create_consumer("Bar") + .create_label("provider") + .create_consumer("Wiffle") + .create_label("provider") + end + + let(:label) do + PactBroker::Labels::Service.get_all_unique_labels + end + + let(:options) { { user_options: { resource_url: "http://example.org/labels", hide_label_decorator_links: true } } } + subject { JSON.parse LabelsDecorator.new(label).to_json(options), symbolize_names: true } + + it "includes the label names" do + expect(subject[:_embedded][:labels].map { |label| label[:name] }).to contain_exactly("provider", "consumer") + end + + it "includes the resource url" do + expect(subject[:_links][:self][:href]).to eq "http://example.org/labels" + end + + it "labels field doest not include any links" do + expect(subject[:_embedded][:labels][0][:_links]).to be_nil + end + + it "doest not include createdAt" do + expect(subject[:_embedded][:labels][0][:createdAt]).to be_nil + end + end + end + end +end diff --git a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb index 280c040ec..f7ad6c7d6 100644 --- a/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pact_version_decorator_spec.rb @@ -33,9 +33,9 @@ module Decorators build_url: "http://build" ) end - let(:decorator_context) { DecoratorContext.new(base_url, "", {}) } + let(:user_options) { { base_url: base_url } } - let(:json) { PactVersionDecorator.new(pact).to_json(user_options: decorator_context) } + let(:json) { PactVersionDecorator.new(pact).to_json(user_options: user_options) } subject { JSON.parse(json, symbolize_names: true) } diff --git a/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb index df5562e0e..274b5278d 100644 --- a/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb @@ -12,12 +12,15 @@ module Decorators end describe "from_json" do - let(:pacticipant) { OpenStruct.new } + let(:pacticipant) { OpenStruct.new(labels: [OpenStruct.new(name: "existing_label")]) } let(:decorator) { PacticipantDecorator.new(pacticipant) } let(:hash) do { name: "Foo", - mainBranch: "main" + mainBranch: "main", + labels: [ + {name: "new_label"} + ] } end @@ -25,6 +28,10 @@ module Decorators its(:name) { is_expected.to eq "Foo" } its(:main_branch) { is_expected.to eq "main" } + + it "does not modify the labels collection" do + expect(subject.labels.map(&:name)).to contain_exactly("existing_label") + end end describe "to_json" do diff --git a/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb new file mode 100644 index 000000000..3f7c7c568 --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/triggered_webhook_logs_decorator_spec.rb @@ -0,0 +1,31 @@ +require "pact_broker/api/decorators/triggered_webhook_logs_decorator" + +module PactBroker + module Api + module Decorators + describe TriggeredWebhookLogsDecorator do + let(:triggered_webhook) do + double("PactBroker::Webhooks::TriggeredWebhook", + uuid: "1234", + webhook_executions: [webhook_execution], + webhook: webhook + ) + end + + let(:webhook) { double("webhook", uuid: "1234") } + + let(:webhook_execution) do + instance_double(PactBroker::Webhooks::Execution, logs: "foo", success: true, created_at: td.in_utc { DateTime.new(2024, 1, 1) } ) + end + + let(:user_options) { { base_url: "http://example.org" } } + + subject { TriggeredWebhookLogsDecorator.new(triggered_webhook).to_json(user_options: user_options) } + + it { + Approvals.verify(subject, :name => "triggered_webhook_logs_decorator", format: :json) + } + end + end + end +end diff --git a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb index 045a6ee01..98b5297a9 100644 --- a/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/version_decorator_spec.rb @@ -23,6 +23,7 @@ module Decorators describe "to_json" do before do allow(decorator).to receive(:deployed_versions_for_version_and_environment_url).and_return("http://deployed-versions") + allow(decorator).to receive(:released_versions_for_version_and_environment_url).and_return("http://released-versions") end let(:version) do @@ -47,13 +48,13 @@ module Decorators end let(:base_url) { "http://example.org" } - let(:options) { { user_options: { base_url: base_url, environments: environments } } } + let(:options) { { user_options: { base_url: base_url, resource_url: "resource_url", environments: environments } } } let(:decorator) { VersionDecorator.new(version) } subject { JSON.parse(decorator.to_json(options), symbolize_names: true) } it "includes a link to itself" do - expect(subject[:_links][:self][:href]).to eq "http://example.org/pacticipants/Consumer/versions/1.2.3" + expect(subject[:_links][:self][:href]).to eq "resource_url" end it "includes the version number in the link" do @@ -112,6 +113,25 @@ module Decorators href: "http://deployed-versions" ) end + + it "includes a list of environments that this version can be released to" do + expect(decorator).to receive(:released_versions_for_version_and_environment_url).with(version, environments.first, base_url) + expect(subject[:_links][:'pb:record-release']).to be_instance_of(Array) + expect(subject[:_links][:'pb:record-release'].first).to eq( + name: "test", + title: "Record release to Test", + href: "http://released-versions" + ) + end + + context "when the environments option is not present" do + let(:options) { { user_options: { base_url: base_url, resource_url: "resource_url" } } } + + it "does not include the pb:record-deployment or pb:record-release" do + expect(subject[:_links]).to_not have_key(:'pb:record-deployment') + expect(subject[:_links]).to_not have_key(:'pb:record-release') + end + end end end end diff --git a/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb index e4cd7c58b..867b0f20a 100644 --- a/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/versions_decorator_spec.rb @@ -5,21 +5,18 @@ module PactBroker module Api module Decorators describe VersionsDecorator do + before do + allow_any_instance_of(VersionsDecorator::VersionInCollectionDecorator).to receive(:version_url).and_return("version_url") + end - let(:options) { { resource_url: "http://versions", base_url: "http://example.org", pacticipant_name: "Consumer", query_string: query_string}} - let(:query_string) { nil } + let(:options) { { request_url: "http://versions?foo=bar", base_url: "http://example.org", pacticipant_name: "Consumer", resource_url: "http://versions" } } let(:versions) { [] } + let(:decorator) { VersionsDecorator.new(versions) } + let(:json) { decorator.to_json(user_options: options) } - subject { JSON.parse VersionsDecorator.new(versions).to_json(user_options: options), symbolize_names: true } - - context "with no query string" do - its([:_links, :self, :href]) { is_expected.to eq "http://versions" } - end + subject { JSON.parse(json, symbolize_names: true) } - context "with a query string" do - let(:query_string) { "foo=bar" } - its([:_links, :self, :href]) { is_expected.to eq "http://versions?foo=bar" } - end + its([:_links, :self, :href]) { is_expected.to eq "http://versions?foo=bar" } context "with no versions" do it "doesn't blow up" do @@ -41,6 +38,10 @@ module Decorators expect(subject[:_embedded][:versions]).to be_instance_of(Array) expect(subject[:_embedded][:versions].size).to eq 1 end + + it "has the version href with the version number" do + expect(subject[:_embedded][:versions].first[:_links][:self][:href]).to eq "version_url" + end end end end diff --git a/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb index 320fb4ea7..082abeedb 100644 --- a/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/webhooks_decorator_spec.rb @@ -15,15 +15,19 @@ module Decorators let(:base_url) { "http://example.org" } let(:resource_url) { "http://example.org/webhooks" } - let(:decorator_context) do - DecoratorContext.new(base_url, resource_url, {}, resource_title: "Title") + let(:user_options) do + { + base_url: base_url, + resource_url: resource_url, + resource_title: "Title" + } end let(:webhooks) { [webhook] } describe "to_json" do - let(:json) { WebhooksDecorator.new(webhooks).to_json(user_options: decorator_context) } + let(:json) { WebhooksDecorator.new(webhooks).to_json(user_options: user_options) } subject { JSON.parse(json, symbolize_names: true) } diff --git a/spec/lib/pact_broker/api/resources/all_routes_spec.rb b/spec/lib/pact_broker/api/resources/all_routes_spec.rb index d1b4a5beb..2f7d861c1 100644 --- a/spec/lib/pact_broker/api/resources/all_routes_spec.rb +++ b/spec/lib/pact_broker/api/resources/all_routes_spec.rb @@ -29,27 +29,22 @@ uuid: UUID } -REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD = YAML.safe_load(File.read("spec/support/all_routes_spec_support.yml"))["requests_which_are_exected_to_have_no_policy_record"] - -RSpec.describe "all the routes" do - it "has a name for every route" do - expect(PactBroker.routes.reject(&:resource_name)).to eq [] - end - - it "has a unique name (except for the ones that don't which we can't change now because it would ruin the PF metrics)" do - dupliates = PactBroker.routes.collect(&:resource_name).group_by(&:itself).select { | _, values | values.size > 1 }.keys - expect(dupliates).to eq(["pact_publication", "verification_results", "verification_result"]) - end -end +REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD = YAML.safe_load(File.read("spec/support/all_routes_spec_support.yml"))["requests_which_are_exected_to_have_no_policy_record_or_pacticipant"] +# Ensure that every resource/http method has a way of determining whether or not a user is allowed to call it. +# The actual permissions logic will be performed in Pactflow as the Pact Broker does not support a permissions model. +# Almost every route should either be associated with a pacticipant, or it should have a method to provide the policy_record. +# Some routes, as listed in spec/support/all_routes_spec_support.yml do not need either. +# This test is here to ensure that every new route added to the Pact Broker API has been considered, and explictly whitelisted if necessary. PactBroker.routes.each do | pact_broker_route | describe "#{pact_broker_route.path} (#{pact_broker_route.resource_name})" do - pact_broker_route.allowed_methods.each do | allowed_method | + (pact_broker_route.allowed_methods - ["OPTIONS"]).each do | allowed_method | - if allowed_method != "OPTIONS" && !REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD.include?("#{pact_broker_route.resource_name} #{allowed_method}") + if !REQUESTS_WHICH_ARE_EXECTED_TO_HAVE_NO_POLICY_RECORD.include?("#{pact_broker_route.resource_name} #{allowed_method}") describe allowed_method do before do + # Create test data that matches the POTENTIAL_PARAMS above so that every route has data present for it td.create_consumer("foo") .create_provider("bar") .create_consumer_version("1", branch: "main", tag_names: ["prod"]) @@ -63,12 +58,26 @@ PactBroker::Pacts::PactVersion.first.update(sha: PACT_VERSION_SHA) end - it "has a policy record object" do - dummy_resource = pact_broker_route.build_resource({ "REQUEST_METHOD" => allowed_method }, PactBroker::ApplicationContext.default_application_context, POTENTIAL_PARAMS) - expect(dummy_resource.policy_record).to_not be nil + it "has a policy record object or a consumer/provider/pacticipant in the route" do + resource = pact_broker_route.build_resource({ "REQUEST_METHOD" => allowed_method }, PactBroker::ApplicationContext.default_application_context, POTENTIAL_PARAMS) + + thing = resource.consumer || resource.provider || resource.pacticipant || resource.respond_to?(:policy_record) + + expect(thing).to_not be_falsy end end end end end end + +RSpec.describe "all the routes" do + it "has a name for every route" do + expect(PactBroker.routes.reject(&:resource_name)).to eq [] + end + + it "has a unique name (except for the ones that don't which we can't change now because it would ruin the PF metrics)" do + duplicates = PactBroker.routes.collect(&:resource_name).group_by(&:itself).select { | _, values | values.size > 1 }.keys + expect(duplicates).to eq(["pact_publication", "verification_results", "verification_result"]) + end +end diff --git a/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb b/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb index ab048a040..e657c2f5c 100644 --- a/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb +++ b/spec/lib/pact_broker/api/resources/all_webhooks_spec.rb @@ -129,7 +129,7 @@ module Resources it "generates a JSON representation of the webhook" do expect(Decorators::WebhooksDecorator).to receive(:new).with(webhooks) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end diff --git a/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb new file mode 100644 index 000000000..b0ebb8e4d --- /dev/null +++ b/spec/lib/pact_broker/api/resources/can_i_merge_badge_spec.rb @@ -0,0 +1,61 @@ +require "pact_broker/api/resources/can_i_merge_badge" +require "pact_broker/api/resources/base_resource" + +module PactBroker + module Api + module Resources + describe CanIMergeBadge do + before do + allow_any_instance_of(described_class).to receive(:badge_service).and_return(badge_service) + + allow(badge_service). to receive(:can_i_merge_badge_url).and_return("http://badge_url") + allow(badge_service). to receive(:error_badge_url).and_return("http://error_badge_url") + + allow_any_instance_of(CanIMergeBadge).to receive(:pacticipant).and_return(pacticipant) + allow_any_instance_of(CanIMergeBadge).to receive(:version).and_return(version) + allow_any_instance_of(CanIMergeBadge).to receive(:results).and_return(results) + end + + let(:branch_service) { class_double("PactBroker::Versions::BranchService").as_stubbed_const } + let(:badge_service) { class_double("PactBroker::Badges::Service").as_stubbed_const } + + let(:pacticipant) { double("pacticipant") } + let(:version) { double("version", number: "1") } + let(:results) { true } + + let(:path) { "/pacticipants/Foo/main-branch/can-i-merge/badge" } + + subject { get(path) } + + context "when everything is found" do + it "returns a 307" do + expect(subject.status).to eq 307 + end + + it "return the badge URL" do + expect(badge_service). to receive(:can_i_merge_badge_url).with(deployable: true) + expect(subject.headers["Location"]).to eq "http://badge_url" + end + end + + context "when the pacticipant is not found" do + let(:pacticipant) { nil } + + it "returns an error badge URL" do + expect(badge_service).to receive(:error_badge_url).with("pacticipant", "not found") + expect(subject.headers["Location"]).to eq "http://error_badge_url" + end + end + + context "when the version is not found" do + let(:version) { nil } + + it "returns an error badge URL" do + expect(badge_service).to receive(:error_badge_url).with("main branch version", "not found") + expect(subject.headers["Location"]).to eq "http://error_badge_url" + end + end + end + end + end +end diff --git a/spec/lib/pact_broker/api/resources/dashboard_spec.rb b/spec/lib/pact_broker/api/resources/dashboard_spec.rb index 5e669cd81..cb50ac5b7 100644 --- a/spec/lib/pact_broker/api/resources/dashboard_spec.rb +++ b/spec/lib/pact_broker/api/resources/dashboard_spec.rb @@ -24,7 +24,7 @@ module Resources end context "with pagination" do - subject { get(path, { pageNumber: 1, pageSize: 1 }) } + subject { get(path, { page: 1, size: 1 }) } it "only returns the items for the page" do expect(response_body_hash["items"].size).to eq 1 @@ -32,7 +32,7 @@ module Resources end context "with invalid pagination" do - subject { get(path, { pageNumber: -1, pageSize: -1 }) } + subject { get(path, { page: -1, size: -1 }) } it_behaves_like "an invalid pagination params response" end diff --git a/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb b/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb index 4f66b5537..b3e172ef3 100644 --- a/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb +++ b/spec/lib/pact_broker/api/resources/error_response_generator_spec.rb @@ -1,4 +1,6 @@ require "pact_broker/api/resources/error_response_generator" +require "pact_broker/application_context" +require "pact_broker/api/decorators/runtime_error_problem_json_decorator" module PactBroker module Api @@ -13,7 +15,7 @@ module Resources let(:error_reference) { "bYWfnyWPlf" } let(:headers_and_body) { ErrorResponseGenerator.call(error, error_reference, rack_env) } - let(:rack_env) { { "pactbroker.base_url" => "http://example.org" } } + let(:rack_env) { { "pactbroker.base_url" => "http://example.org", "pactbroker.application_context" => PactBroker::ApplicationContext.default_application_context } } let(:headers) { headers_and_body.first } subject { JSON.parse(headers_and_body.last) } @@ -35,7 +37,7 @@ module Resources end context "when the Accept header includes application/problem+json" do - let(:rack_env) { { "HTTP_ACCEPT" => "application/hal+json, application/problem+json", "pactbroker.base_url" => "http://example.org" } } + let(:rack_env) { { "HTTP_ACCEPT" => "application/hal+json, application/problem+json", "pactbroker.base_url" => "http://example.org", "pactbroker.application_context" => PactBroker::ApplicationContext.default_application_context } } it "returns headers" do expect(headers).to eq("Content-Type" => "application/problem+json;charset=utf-8") diff --git a/spec/lib/pact_broker/api/resources/integrations_spec.rb b/spec/lib/pact_broker/api/resources/integrations_spec.rb index 3aacb1be5..ff0246510 100644 --- a/spec/lib/pact_broker/api/resources/integrations_spec.rb +++ b/spec/lib/pact_broker/api/resources/integrations_spec.rb @@ -23,7 +23,7 @@ module Resources let(:errors) { {} } let(:path) { "/integrations" } - let(:params) { { "pageNumber" => "1", "pageSize" => "2" } } + let(:params) { { "page" => "1", "size" => "2" } } subject { get(path, params, rack_headers) } @@ -41,7 +41,7 @@ module Resources it "renders the integrations" do expect(decorator_class).to receive(:new).with(integrations) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) expect(subject.body).to eq json end diff --git a/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb b/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb index a997c15e4..a58300ac8 100644 --- a/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb +++ b/spec/lib/pact_broker/api/resources/pacticipant_webhooks_spec.rb @@ -40,7 +40,7 @@ module Resources it "generates a JSON body" do expect(Decorators::WebhooksDecorator).to receive(:new).with(webhooks) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end diff --git a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb index 6120aacb8..034306b11 100644 --- a/spec/lib/pact_broker/api/resources/pacticipants_spec.rb +++ b/spec/lib/pact_broker/api/resources/pacticipants_spec.rb @@ -7,8 +7,8 @@ module Resources describe "GET" do let(:query) do { - "pageSize" => "10", - "pageNumber" => "1", + "size" => "10", + "page" => "1", "q" => "search" } end @@ -44,8 +44,8 @@ module Resources context "with invalid pagination params" do let(:query) do { - "pageSize" => "0", - "pageNumber" => "0", + "size" => "0", + "page" => "0", } end @@ -123,7 +123,7 @@ module Resources it "creates a JSON representation of the new pacticipant" do expect(decorator_class).to receive(:new).with(created_model) - expect(decorator).to receive(:to_json).with(user_options: instance_of(Decorators::DecoratorContext)) + expect(decorator).to receive(:to_json).with(user_options: instance_of(Hash)) subject end diff --git a/spec/lib/pact_broker/badges/service_spec.rb b/spec/lib/pact_broker/badges/service_spec.rb index f66d71152..fc2b451ff 100644 --- a/spec/lib/pact_broker/badges/service_spec.rb +++ b/spec/lib/pact_broker/badges/service_spec.rb @@ -29,6 +29,22 @@ module Badges allow(Service).to receive(:logger).and_return(logger) end + describe "can_i_merge_badge_url" do + let(:version_number) { "abcd1234" } + let(:deployable) { true } + + subject { Service.can_i_merge_badge_url(deployable: deployable) } + + context "when deployable is true" do + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-success-brightgreen.svg") } + end + + context "when deployable is false" do + let(:deployable) { false } + it { is_expected.to eq URI("https://img.shields.io/badge/can--i--merge-failed-red.svg") } + end + end + describe "can_i_deploy_badge_url" do subject { Service.can_i_deploy_badge_url("main", "prod", nil, true) } diff --git a/spec/lib/pact_broker/contracts/service_spec.rb b/spec/lib/pact_broker/contracts/service_spec.rb index fafa7891f..349fa38f7 100644 --- a/spec/lib/pact_broker/contracts/service_spec.rb +++ b/spec/lib/pact_broker/contracts/service_spec.rb @@ -18,10 +18,11 @@ module Contracts let(:branch) { "main" } let(:contracts) { [contract_1] } let(:contract_1) do - ContractToPublish.from_hash( + ContractToPublish.new( consumer_name: "Foo", provider_name: "Bar", decoded_content: decoded_contract, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(decoded_contract), specification: "pact", on_conflict: on_conflict ) @@ -149,17 +150,19 @@ module Contracts let(:branch) { "main" } let(:contracts) { [contract_1] } let(:contract_1) do - ContractToPublish.from_hash( + ContractToPublish.new( consumer_name: "Foo", provider_name: "Bar", decoded_content: decoded_contract, specification: "pact", - on_conflict: on_conflict + on_conflict: on_conflict, + pact_version_sha: new_pact_version_sha ) end let(:contract_hash) { { consumer: { name: "Foo" }, provider: { name: "Bar" }, interactions: [{a: "b"}] } } let(:decoded_contract) { contract_hash.to_json } + let(:new_pact_version_sha) { PactBroker::Pacts::GenerateSha.call(decoded_contract) } subject { Service.conflict_notices(contracts_to_publish, base_url: "base_url") } diff --git a/spec/lib/pact_broker/doc/coverage_spec.rb b/spec/lib/pact_broker/doc/coverage_spec.rb index 95c6704f1..19e6ecc34 100644 --- a/spec/lib/pact_broker/doc/coverage_spec.rb +++ b/spec/lib/pact_broker/doc/coverage_spec.rb @@ -3,6 +3,8 @@ RSpec.describe "the HAL docs for the index" do let(:app) do + require "pact_broker/api" + Rack::Builder.new do map "/docs" do run PactBroker::Doc::Controllers::App diff --git a/spec/lib/pact_broker/integrations/repository_spec.rb b/spec/lib/pact_broker/integrations/repository_spec.rb index a37d5f98d..dd21a882a 100644 --- a/spec/lib/pact_broker/integrations/repository_spec.rb +++ b/spec/lib/pact_broker/integrations/repository_spec.rb @@ -22,10 +22,11 @@ module Integrations td.create_verification(provider_version: "2") end - # No contract data date + # Nil contract data date td.create_consumer("Dog") .create_provider("Cat") .create_integration + Integration.order(:id).last.update(contract_data_updated_at: nil) end subject { Repository.new.find } @@ -37,6 +38,46 @@ module Integrations end end + describe "#create_for_pacts" do + before do + Timecop.freeze(Date.today - 5) do + td.create_consumer("A") + .create_provider("B") + .create_integration + .create_pacticipant("C") + .create_pacticipant("D") + end + end + + let(:objects_with_consumer_and_provider) do + [ + double("i1", consumer: td.find_pacticipant("A"), provider: td.find_pacticipant("B")), + double("i2", consumer: td.find_pacticipant("C"), provider: td.find_pacticipant("D")) + ] + end + + subject { Repository.new.create_for_pacts(objects_with_consumer_and_provider) } + + it "inserts any missing integrations" do + now = Time.utc(2024) + Timecop.freeze(now) do + subject + end + + integrations = Integration.eager(:consumer, :provider).order(:id).all + expect(integrations).to contain_exactly( + have_attributes(consumer_name: "A", provider_name: "B"), + have_attributes(consumer_name: "C", provider_name: "D") + ) + expect(integrations.last.created_at).to be_date_time(now) + expect(integrations.last.contract_data_updated_at).to be_date_time(now) + end + + it "does not change the created_at or contract_data_updated_at of the existing integrations" do + expect { subject }.to_not change { Integration.order(:id).select(:created_at, :contract_data_updated_at).first.created_at } + end + end + describe "#set_contract_data_updated_at" do before do # A -> B @@ -93,6 +134,46 @@ module Integrations end end end + + describe "#set_contract_data_updated_at_for_multiple_integrations" do + before do + Timecop.freeze(date_1) do + td.create_consumer("Foo1") + .create_provider("Bar1") + .create_integration + .create_consumer("Foo2") + .create_provider("Bar2") + .create_integration + .create_consumer("Foo3") + .create_provider("Bar3") + .create_integration + end + end + + let(:date_1) { Time.new(2023, 1, 1).utc.to_datetime } + let(:date_2) { Time.new(2023, 1, 2).utc.to_datetime } + + let(:objects_with_consumer_and_provider) do + [ + OpenStruct.new(consumer: td.find_pacticipant("Foo1"), provider: td.find_pacticipant("Bar1")), + OpenStruct.new(consumer: td.find_pacticipant("Foo2"), provider: td.find_pacticipant("Bar2")) + ] + end + + subject do + Timecop.freeze(date_2) do + Repository.new.set_contract_data_updated_at_for_multiple_integrations(objects_with_consumer_and_provider) + end + end + + it "sets the contract_data_updated_at of the specified integrations" do + subject + integrations = Integration.order(:id).all + expect(integrations[0].contract_data_updated_at).to be_date_time(date_2) + expect(integrations[1].contract_data_updated_at).to be_date_time(date_2) + expect(integrations[2].contract_data_updated_at).to be_date_time(date_1) + end + end end end end diff --git a/spec/lib/pact_broker/labels/repository_spec.rb b/spec/lib/pact_broker/labels/repository_spec.rb index b3e8824e8..41a29d006 100644 --- a/spec/lib/pact_broker/labels/repository_spec.rb +++ b/spec/lib/pact_broker/labels/repository_spec.rb @@ -4,6 +4,41 @@ module PactBroker module Labels describe Repository do + describe ".get_all_unique_labels" do + before do + td.create_pacticipant("bar") + .create_label("ios") + .create_pacticipant("foo") + .create_label("android") + .create_pacticipant("wiffle") + .create_label("ios") + end + + let(:labels_repository) { Repository.new } + + context "when there are no pagination options" do + subject { labels_repository.get_all_unique_labels } + + it "returns all the unique labels" do + expect(subject.collect(&:name)).to contain_exactly("ios", "android") + end + end + + context "when there are pagination options" do + let(:pagination_options) do + { + :page_number => 1, + :page_size => 1 + } + end + subject { labels_repository.get_all_unique_labels pagination_options } + + it "returns paginated unique labels" do + expect(subject.collect(&:name)).to contain_exactly("ios") + end + end + end + describe ".find" do let(:pacticipant_name) { "foo" } diff --git a/spec/lib/pact_broker/labels/service_spec.rb b/spec/lib/pact_broker/labels/service_spec.rb index 39e242a3c..dd8f45970 100644 --- a/spec/lib/pact_broker/labels/service_spec.rb +++ b/spec/lib/pact_broker/labels/service_spec.rb @@ -5,7 +5,17 @@ module Labels describe Service do let(:pacticipant_name) { "foo" } let(:label_name) { "ios" } - let(:options) { {pacticipant_name: pacticipant_name, label_name: label_name}} + let(:options) { { pacticipant_name: pacticipant_name, label_name: label_name } } + let(:pagination_options) { { page_number: 1, page_size: 1 } } + + describe ".get_all_unique_labels" do + subject { Service.get_all_unique_labels(pagination_options) } + + it "calls the labels repository" do + expect_any_instance_of(Labels::Repository).to receive(:get_all_unique_labels).with(pagination_options) + subject + end + end describe ".create" do subject { Service.create(options) } diff --git a/spec/lib/pact_broker/matrix/service_spec.rb b/spec/lib/pact_broker/matrix/service_spec.rb index 277470bea..ac68c2597 100644 --- a/spec/lib/pact_broker/matrix/service_spec.rb +++ b/spec/lib/pact_broker/matrix/service_spec.rb @@ -4,6 +4,46 @@ module PactBroker module Matrix describe Service do + describe "can-i-merge" do + before do + td.create_consumer("A", main_branch: "main_branch", version: "1") + .create_provider("B", main_branch: "main_branch", version: "1") + .create_pact_with_hierarchy("A", "1", "B") + .create_verification(provider_version: "1", number: 1, success: false, branch: "main_branch") + .create_verification(provider_version: "1", number: 2, success: true, branch: "main_branch") + .create_verification(provider_version: "2", number: 3, success: true, branch: "dev") + end + + let(:pacticipant_name_param) { "B" } + + subject { Service.can_i_merge(pacticipant_name: pacticipant_name_param) } + + context "for pacticipant that has verification on it's main branch" do + let(:options) { + { + latest: true, + main_branch: true, + latestby: "cvp" + } + } + + let(:unresolved_selectors) { + [ + PactBroker::Matrix::UnresolvedSelector.new(pacticipant_name: "B", pacticipant_version_number: "1") + ] + } + + it "returns true because the mergeble status is true" do + expect(subject).to be_truthy + end + + it "calls the can_i_deploy method" do + expect(Service).to receive(:can_i_deploy).with(unresolved_selectors, options).and_call_original + subject + end + end + end + describe "validate_selectors" do before do allow(PactBroker::Deployments::EnvironmentService).to receive(:find_by_name).and_return(environment) diff --git a/spec/lib/pact_broker/metrics/service_spec.rb b/spec/lib/pact_broker/metrics/service_spec.rb index 7fa07f43f..0bdac2994 100644 --- a/spec/lib/pact_broker/metrics/service_spec.rb +++ b/spec/lib/pact_broker/metrics/service_spec.rb @@ -121,7 +121,7 @@ module Service describe "pactRevisionsPerPactPublication" do before do - td.create_pact_with_hierarchy + td.create_pact_with_hierarchy("Foo", "1", "Bar") .comment("this consumer version will have 3 revisions") .revise_pact .revise_pact @@ -129,12 +129,15 @@ module Service .create_pact .comment("this consumer version will have 1 revision") .revise_pact + .create_pact_with_hierarchy("Foo", "1", "Bar2", td.random_json_content("Foo", "Bar2")) + .create_pact_with_hierarchy("Foo", "2", "Bar", td.random_json_content("Foo", "Bar")) end let(:distribution) { subject[:pactRevisionsPerConsumerVersion][:distribution] } it "returns a distribution of pact revisions per consumer version" do - expect(distribution).to eq(2 => 1, 3 => 1) + # numberOfRevisions => countOfPactsWithThisNumberOfRevisions + expect(distribution).to eq({ 1 => 2, 2 => 1, 3 => 1 }) end end end diff --git a/spec/lib/pact_broker/pacticipants/repository_spec.rb b/spec/lib/pact_broker/pacticipants/repository_spec.rb index 448971a27..7509c479f 100644 --- a/spec/lib/pact_broker/pacticipants/repository_spec.rb +++ b/spec/lib/pact_broker/pacticipants/repository_spec.rb @@ -216,8 +216,8 @@ module Pacticipants before do td - .create_consumer(consumer_name) - .create_consumer(provider_name) + .create_consumer(consumer_name, { display_name: "Pretty Consumer" }) + .create_consumer(provider_name, { display_name: "Fancy Provider" }) end context "when there is a consumer/provider name which matches the search term" do @@ -242,6 +242,14 @@ module Pacticipants searched_dataset = Repository.new.search_by_name "TEST" expect(searched_dataset.collect(&:name)).to include(*[consumer_name, provider_name]) end + + it "searches by display_name" do + searched_dataset = Repository.new.search_by_name "Pretty" + expect(searched_dataset.collect(&:name)).to eq([consumer_name]) + + searched_dataset = Repository.new.search_by_name "Fancy" + expect(searched_dataset.collect(&:name)).to eq([provider_name]) + end end context "when there is NO consumer/provider name which matches the search term" do diff --git a/spec/lib/pact_broker/pacts/pact_params_spec.rb b/spec/lib/pact_broker/pacts/pact_params_spec.rb index ca9990277..490745811 100644 --- a/spec/lib/pact_broker/pacts/pact_params_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_params_spec.rb @@ -1,4 +1,5 @@ require "pact_broker/pacts/pact_params" +require "webmachine/request" module PactBroker module Pacts @@ -45,7 +46,7 @@ module Pacts describe "from_request" do context "from a PUT request" do - let(:request) { Webmachine::Request.new("PUT", "/", headers, body)} + let(:request) { ::Webmachine::Request.new("PUT", "/", headers, body)} subject { PactParams.from_request(request, path_info) } diff --git a/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb b/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb index 1c6dbf0a0..bfe251e0e 100644 --- a/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_publication_clean_selector_dataset_module_spec.rb @@ -1,3 +1,5 @@ +require "pact_broker/db/clean/selector" + module PactBroker module Pacts describe PactPublicationCleanSelectorDatasetModule do 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 index 476e4b4de..d9b892e59 100644 --- a/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb +++ b/spec/lib/pact_broker/pacts/pact_publication_dataset_module_spec.rb @@ -131,6 +131,71 @@ module Pacts end end + describe "for_all_branch_heads" 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 + .revise_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.for_all_branch_heads } + + it "returns the pacts for all the branch heads" do + all = subject.all_allowing_lazy_load.sort_by{ |pact_publication| pact_publication.consumer_version.order } + expect(all.size).to eq 3 + 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.first.revision_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 + + it "does not return extra columns" do + expect(subject.first.values.keys.sort).to eq (PactPublication.columns + [:branch_name]).sort + end + + context "when there is no pact for the branch head" do + before do + td.create_consumer_version("12", branch: "main") + end + + it "does not return a pact" do + all = subject.all_allowing_lazy_load + expect(all.size).to eq 2 + end + end + + context "when columns are already selected" do + subject { PactPublication.select(Sequel[:pact_publications][:id]).latest_for_consumer_branch("main") } + + it "does not override them" do + expect(subject.all.first.values.keys).to eq [:id] + end + end + + context "when chained" do + it "works" do + all = PactPublication.for_provider(td.find_pacticipant("Bar")).latest_for_consumer_branch("main").all_allowing_lazy_load + expect(all.collect(&:provider_name).uniq).to eq ["Bar"] + end + end + end + describe "latest_by_consumer_tag" do before do td.create_consumer("Foo") @@ -179,6 +244,53 @@ module Pacts end end + describe "for_all_tag_heads" 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.for_all_tag_heads.all_allowing_lazy_load } + + 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 pacts belonging to the latest tagged version for each tag" do + expect(subject.size).to eq 2 + 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" + end + + it "does not return extra columns" do + expect(subject.first.values.keys.sort).to eq (PactPublication.columns + [:tag_name]).sort + end + + context "when columns are already selected" do + subject { PactPublication.select(Sequel[:pact_publications][:id]).latest_by_consumer_tag } + + it "does not override them" do + expect(subject.all.first.values.keys).to eq [:id] + end + end + end + describe "overall_latest" do before do td.create_consumer("Foo") diff --git a/spec/lib/pact_broker/pacts/repository_spec.rb b/spec/lib/pact_broker/pacts/repository_spec.rb index 8a9fe65a9..68952e257 100644 --- a/spec/lib/pact_broker/pacts/repository_spec.rb +++ b/spec/lib/pact_broker/pacts/repository_spec.rb @@ -73,12 +73,6 @@ module Pacts expect(PactVersion.order(:id).last.messages_count).to eq 0 end - it "creates an integration" do - expect { subject }.to change { - PactBroker::Integrations::Integration.where(consumer_id: consumer.id, provider_id: provider.id).count - }.from(0).to(1) - end - context "when a pact already exists with exactly the same content" do let(:another_version) { Versions::Repository.new.create number: "2.0.0", pacticipant_id: consumer.id } diff --git a/spec/lib/pact_broker/pacts/service_spec.rb b/spec/lib/pact_broker/pacts/service_spec.rb index a656a710c..3aa0a156c 100644 --- a/spec/lib/pact_broker/pacts/service_spec.rb +++ b/spec/lib/pact_broker/pacts/service_spec.rb @@ -31,7 +31,8 @@ module Pacts consumer_name: "Foo", provider_name: "Bar", consumer_version_number: "1", - json_content: json_content + json_content: json_content, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content) } end let(:content) { double("content") } @@ -48,12 +49,6 @@ module Pacts subject { Service.create_or_update_pact(params) } context "when no pact exists with the same params" do - it "creates the sha before adding the interaction ids" do - expect(PactBroker::Pacts::GenerateSha).to receive(:call).ordered - expect(content).to receive(:with_ids).ordered - subject - end - it "saves the pact interactions/messages with ids added to them" do expect(pact_repository).to receive(:create).with hash_including(json_content: json_content_with_ids) subject @@ -135,12 +130,6 @@ module Pacts let(:expected_event_context) { { consumer_version_tags: ["dev"] } } - it "creates the sha before adding the interaction ids" do - expect(PactBroker::Pacts::GenerateSha).to receive(:call).ordered - expect(content).to receive(:with_ids).ordered - subject - end - it "saves the pact interactions/messages with ids added to them" do expect(pact_repository).to receive(:update).with(anything, hash_including(json_content: json_content_with_ids)) subject diff --git a/spec/lib/pact_broker/ui/controllers/index_spec.rb b/spec/lib/pact_broker/ui/controllers/index_spec.rb index 83371fbaf..196d3b6c2 100644 --- a/spec/lib/pact_broker/ui/controllers/index_spec.rb +++ b/spec/lib/pact_broker/ui/controllers/index_spec.rb @@ -40,7 +40,7 @@ module Controllers context "when pagination parameters are present" do it "passes through pagination parameters to the search" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 2, page_size: 40)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 2, page_size: 40)).and_call_original get "/", { page: "2", pageSize: "40" } end end @@ -48,14 +48,14 @@ module Controllers context "when pagination parameters are not present" do context "when tags=true" do it "passes through default pagination parameters to the search with page_size=30" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 30)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 30)).and_call_original get "/", { tags: "true" } end end context "when not tags=true" do it "passes through default pagination parameters to the search with page_size=100" do - expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 100)) + expect(PactBroker::Index::Service).to receive(:find_index_items).with(hash_including(page_number: 1, page_size: 100)).and_call_original get "/" end end diff --git a/spec/lib/pact_broker/versions/branch_repository_spec.rb b/spec/lib/pact_broker/versions/branch_repository_spec.rb index 68ab71d95..e7a2fd44b 100644 --- a/spec/lib/pact_broker/versions/branch_repository_spec.rb +++ b/spec/lib/pact_broker/versions/branch_repository_spec.rb @@ -92,6 +92,70 @@ module Versions expect{ subject }.to_not change { PactBroker::Domain::Version.count } end end + + describe "count_branches_to_delete" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.count_branches_to_delete(pacticipant, exclude: ["foo"]) } + + it "returns a count of the number of branches that will be deleted" do + expect(subject).to eq 3 + end + end + + describe "delete_branches_for_pacticipant" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.delete_branches_for_pacticipant(pacticipant, exclude: ["foo"]) } + + it "deletes all the branches except for the excluded ones and the main branch" do + subject + expect(Branch.where(pacticipant: pacticipant).collect(&:name)).to contain_exactly("foo", "main") + end + end + + describe "remaining_branches_after_future_deletion" do + before do + td.create_consumer("foo") + .create_consumer_version("1", branch: "main") + .create_consumer_version("3", branch: "not-main") + .create_consumer_version("4", branch: "foo") + .create_consumer_version("5", branch: "bar") + .create_consumer_version("6", branch: "not-bar") + .create_consumer("bar") + .create_consumer_version("1", branch: "main") + end + + let(:pacticipant) { td.find_pacticipant("foo") } + + subject { BranchRepository.new.remaining_branches_after_future_deletion(pacticipant, exclude: ["foo"]) } + + it "returns the branches that will not be deleted" do + expect(subject).to contain_exactly(have_attributes(name: "main"), have_attributes(name: "foo")) + end + end end end end diff --git a/spec/lib/pact_broker/versions/branch_service_spec.rb b/spec/lib/pact_broker/versions/branch_service_spec.rb index e3176e822..3ba433114 100644 --- a/spec/lib/pact_broker/versions/branch_service_spec.rb +++ b/spec/lib/pact_broker/versions/branch_service_spec.rb @@ -91,6 +91,28 @@ module Versions end end end + + describe "#branch_deletion_notices" do + let(:pacticipant) { instance_double(PactBroker::Domain::Pacticipant, name: "some-service") } + let(:exclude) { ["foo", "bar" ] } + let(:branch_repository) { instance_double(PactBroker::Versions::BranchRepository, count_branches_to_delete: 3, remaining_branches_after_future_deletion: remaining_branches) } + let(:remaining_branches) do + [ + instance_double(PactBroker::Versions::Branch, name: "foo", created_at: DateTime.now - 10), + instance_double(PactBroker::Versions::Branch, name: "bar", created_at: DateTime.now - 20) + ] + end + + before do + allow(BranchService).to receive(:branch_repository).and_return(branch_repository) + end + + subject { BranchService.branch_deletion_notices(pacticipant, exclude: exclude) } + + it "returns a list of notices" do + expect(subject).to contain_exactly(have_attributes(text: "Scheduled deletion of 3 branches for pacticipant some-service. Remaining branches are: bar, foo")) + end + end end end end diff --git a/spec/lib/pact_broker/versions/repository_spec.rb b/spec/lib/pact_broker/versions/repository_spec.rb index b66dba643..61a935435 100644 --- a/spec/lib/pact_broker/versions/repository_spec.rb +++ b/spec/lib/pact_broker/versions/repository_spec.rb @@ -25,31 +25,6 @@ module Versions end end - describe "#find_all_pacticipant_versions_in_reverse_order" do - before do - td - .create_consumer("Foo") - .create_consumer_version("1.2.3") - .create_consumer_version("4.5.6") - .create_consumer("Bar") - .create_consumer_version("8.9.0") - end - - subject { Repository.new.find_all_pacticipant_versions_in_reverse_order "Foo" } - - it "returns all the application versions for the given consumer" do - expect(subject.collect(&:number)).to eq ["4.5.6", "1.2.3"] - end - - context "with pagination options" do - subject { Repository.new.find_all_pacticipant_versions_in_reverse_order "Foo", page_number: 1, page_size: 1 } - - it "paginates the query" do - expect(subject.collect(&:number)).to eq ["4.5.6"] - end - end - end - describe "#find_pacticipant_versions_in_reverse_order" do before do td diff --git a/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb b/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb index 73e8e3004..9e2617ade 100644 --- a/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb +++ b/spec/lib/rack/pact_broker/invalid_uri_protection_spec.rb @@ -1,4 +1,6 @@ require "rack/pact_broker/invalid_uri_protection" +require "pact_broker/application_context" +require "pact_broker/api/decorators/custom_error_problem_json_decorator" module Rack module PactBroker @@ -7,7 +9,7 @@ module PactBroker let(:app) { InvalidUriProtection.new(target_app) } let(:path) { "/foo" } - subject { get(path) } + subject { get(path, {}, {"pactbroker.application_context" => ::PactBroker::ApplicationContext.default_application_context} ) } context "with a URI that the Ruby default URI library cannot parse" do let(:path) { "/badpath" } @@ -27,6 +29,14 @@ module PactBroker expect(subject.status).to eq 200 end + context "when the path contains missing path segments" do + let(:path) { "/foo//bar" } + + it "returns a 404" do + expect(subject.status).to eq 404 + end + end + context "when the URI contains a new line because someone forgot to strip the result of `git rev-parse HEAD`, and I have totally never done this before myself" do let(:path) { "/foo%0A/bar" } diff --git a/spec/lib/sequel/extensions/lock_names_keys.yml b/spec/lib/sequel/extensions/lock_names_keys.yml new file mode 100644 index 000000000..3893a6d13 --- /dev/null +++ b/spec/lib/sequel/extensions/lock_names_keys.yml @@ -0,0 +1,100 @@ +--- +5b25e9d56cf531395d7eb91474acf01c600235d24090ecbf107a: 922798582 +9fb20fc10a71bc70f7556ee2cb2d9fd73a14d4ea21533cb57f4e6a84d0b7a4d247f7c64fa09aeb750ef5fe51bf9d: 897625924 +45780a5ee58571f5a2feac03913dd4597ed5bcbafb357231f1e6bc9e40b02afc45f219200f8ff6bbcb: 1473933586 +54afaac7d7c7ecc0734c301c11a26adc7ce2bc053024: 55786877 +41b3170b207e66b1eae1a84a03db26a45ffad81c013c81c388c1ac0a: 483987601 +316c92: 1995393960 +01213c6faa1234eaef8d5ee0b927ddd8605a8de4f4d720c444e096722e6c8767595ef3820526: 1182101158 +d670c662978e4c5ece07962b93053352b169f3d4b4545b53d0857ab8cb4b2d2496e4f4536d5c0c7108e72de62b3b26bc: 1119328736 +287ec6811624273828d5cbdc3ce6: 365621557 +3ab555ecc1b687ea68b7966637cb8b6aae6ea087f573f1f54c63f9b6191cf5985794ef1fc2: 1637375338 +4922ba19c153457e4077e12b6f42c049e9fe2888e4c263357a9f6e43: 1039229035 +8ba8cf151a952b8e86acc8397bcbc6f6c0f22d7b2d09de2977c6de095e7d: 76296185 +9a2aad22a067750f65336ad874f28f6b8fe6660fffb9abee57a52ea856e9db84346363401d3082e3cc38d1: 2012818523 +1df37db5ca4759a41cab515d4f8c7be4864c76b647fa60a8397ae72bd2: 511628245 +448501e579e9cf595961317530eecd55b32559d1b9c889b06440e9903cf5c0b6e6f648aa6304e451: 1570896729 +e595ee62d9e5253dc404615149ef5ce7: 1194816858 +038aca8adbfa2379e2785daf82adb536234acd9606ca4b3c3a: 732478070 +6eb1: 1007515829 +73653d90fc4a86080ecc3aa7595dccbf29c642cfebadb5e3402f113b9237b48027079324f4c1ae: 660466134 +3d500f41c70beef0184fc70ef0f6c462c45071bc2f68fdce97e607418d55813db7ce926194776c5be6: 220327056 +d51387ab1a5b20d1160b063f073dc2853faa7c05da11e5bd0f6c26b1210c718bb7f1f6cfd4a55d6dc8ff: 182882809 +9a13a82a50433497728dc2edb2: 666506043 +afd607619128a8a18471a6c64456eeb947187c8d8c9d831d319c22e3ca91: 1585000330 +88758d3f4db23cefae7e9c1ed1d1da: 353123979 +589ef5e96046a05a08f073e8715d94d932a2343b8c: 241791474 +a15059545c772be2fa34123b9d8a91ff9065232eb23163f5e3b406f5a1617024d36efe06b5a6735d71c5: 501631426 +12fa2ef6582fe8c237ad87124f3c5230d2cd: 182175882 +56328a1fa9ff3a7ffbbeca7bd6b2b3128d5bc87bc9cf: 78183177 +4f72: 1250424524 +78b5b7520420abd83de7d4d8a04b1d778b4879cffbb9037d9a8352c9e8: 1441815315 +6136fb4240624b722118a06eece42a5d9a2c: 1751735639 +3091db355a8bf8ed0a5e: 456038127 +f96dcc2843cafdd4a34c780e1a01338ecda8fcd6655a3d796f243194a5dc63da83541971f1cd5eb7ba35: 212906644 +c0e650b6139390b0c145b71fe8ee: 1717118197 +ccb8845ac4b34d23be99: 414287445 +'49': 636905728 +303ff5f54e: 539528939 +478b8f89622f42d3e52337ef4466202ac98a3f1376924075db75ffcb752655d5e8420468022d2ac1: 40606646 +f599f96da671fc62cefa106dfeffa25e2d68f48e6027087ba6870a9d0393970b3920be: 1567333467 +3cb95e3bf12bde6957569d0dca890a22cd5e4afa94f2d15b1e61e01d381508fc0537fcb2eb: 1862269908 +70da2cacbdc197ce12be3d3eb480fe01a13ebcff83d26ce98e49d2: 128376115 +5c253269f8952c0e2676: 1782203278 +0bcdd137e90916568b: 2039072251 +af0872c2f276902384779c81cf3febfdc72b2566e42aba3aba2738ec8102d693b7032ebc239a8b9471602fa50f1d488b1979: 459687685 +a9f1f80b4b9faac29173d8beef5220e4ad9993f05be5f950245e4b0035a98f58d830defce2dc: 1696241422 +d2ee666571ece020bd8137c39580b1abc9fb4a89991ce0aa7822b5d960d3e8ca09: 91351839 +3fdda8adf582eaf51ea40b5b9122515502612470c2bb66a9c5558201e2c84694a8cbe092fcde6a6dd942dc5eb8ede08b: 656571790 +ae: 15195598 +269741a9d8b944cb815bc9fd6db15d3f5106b7811541043a7448a32b7d8f17: 974353071 +603770f07d4571f83f2974aae0be42041c18e2be7ce0138df8d0: 1366051146 +e07d82b7f9cd8c5fdc: 874617480 +8d83a1f774bcd95a8aae: 1687774301 +ea5f6d68f75dbc5fdddffc2752f33ce4cfdcc5aeb5ce03851e61a61c6724d55ff4dc66442a: 1377712884 +93b0bfedc1910576f173b5646490474d2042a2387f6279416e7faa7df15aa56ea0da4cd8416a: 154691814 +bedea59011b243b84bda7466c37e88df8b82: 694569716 +7c79b01e18f95a30cd57b9fc: 683762260 +5ab349cb2b167c9760365e0aac391c5496d76ae7: 1689745541 +438328ca1f61a489cd6e: 1219960715 +969b44ec30748b198eda45256b3de53cff82485e5bc3f107c23b009440fe2fa40d1e8bd7aa: 1151588521 +e374274d01c6d3a83cc4: 1515901746 +ea9533b35a1c83ba4e259c5a56e36b8a32c632408210ddef474e: 245084129 +6a6b1b7b0f427cfbc2afbd9b7ec3: 1838557419 +7afe477c5fbc7d0ef167bec6b0309fb2996f172e06b176: 210109733 +189ed9a4acf0d419045b67dec4: 399970956 +aa9f60bd2ff23370628570a6f9cb7a57e06509920e3cbf8a: 1050621436 +daf9a25a8fe0775b99e2e394e03e95da53dc59403edf08ecbec72c4491411fabdb5f4410049c46f919fe48ad9dc6d6: 935832483 +7299937593973d8b988198116f77b0c41f8b80009d1755604925783aee66bdc5ad0b6a81dcc4c7763819dce5201c: 38658330 +254dd86414a411e4dd5bd74a21dfbebf397fdcfa56c802: 56596741 +42a11ea77233f9115bbd5e2b12a7fa171fdb05fbe511d67d94000e9724e17608cc798a1afe: 727298738 +2f79708c54e9e2f16dd82f57789c: 1608939254 +1fdafdbf1129dbe52b4e6570e120cc2dac025b42c9a486bd6b: 1938076612 +6b532b8d568c44711b7f918c2e360781600142de1b1164a32297d52fd7ff65e985360348b90201ab: 347982673 +a65245aeee7900178b1bbc3cc87191e72db441cfd5b49f400763982d906e32ab92b4de417f: 1991069376 +d839d6da17be0b45a1734153a1b5f8e5d02ea0c28ff299234526b0ccb15c72f5775dff8dd7: 1487367662 +2156419fc3adc5178b6d679619f08da1a90fb8f4500c5be52cf840428f1229884032f3fcd242e58e: 548015352 +efc22626e5621243: 1359916413 +04ed6ee27eb54a7bb15edb1f45fdc7035b1bbcaabeac8213d5bc9dacbc0d8c70: 1585988197 +'7199': 948999441 +0a0472f5087f23a623b5: 454820883 +6e4a104a6eb44bc3a6bf1107c06058ac9ca9fc7288e73cfad82b98d66feafc06df182ac732a1d11f9feec8: 1024970306 +42e22b983db5ec1fcd3a945d2fae53: 1649388059 +749ae1c0355da123926654c40885aab25eb5: 1046362064 +1dc4a8c65d67aa3863d6f2: 1072437948 +4e: 1194030303 +bd7fc32f3e1bdcefed3ebd8359b2eea1fc8b25486609aa86d87abe0e37aaa0: 2116128184 +c5aadfa7468e6a82f99c62876a1047bfebba9d36b4d4f97aff51b2540db8: 1415072913 +c450: 794638503 +8796f9ddabaf101ffb34542bd424bcc8a2b47822bc4b2010cc5e8799d19e3e7e438618: 1744306497 +7ce00be84fd51986bfb7faec522c7eb2c3e1a4e084d5: 1088708292 +89a600cf778d1ad0713e28cc660c6756507b87a4e6f347d430f333623f: 695948817 +c50b8ae10dcc2e29: 1193146886 +424793cb6fc0c9cb653d2bdd9248: 1395788399 +c979e881144c0c33b9: 1933771153 +185d70e249be25267445fb3ba544898e277384d09feaf021e89b44e1a01ba6eb743c: 1568080655 +3cc0192772e43af59ea642542a5cf830584ed6efb2122b66: 28535105 +6f8db868686de7c175: 277293610 +30985c01c3a94f39f4616be2a013a95b94b5b4f0a302f1595b8f589cce6da8: 1741848048 +d907ab072c35fd33: 2141142045 +7e8e3d3587c4adaa1c4bcb5b78b7c5c337c4b4492f4a071283fd649c47cd7e: 1010112831 diff --git a/spec/lib/sequel/extensions/register_advisory_lock_test.rb b/spec/lib/sequel/extensions/register_advisory_lock_test.rb new file mode 100644 index 000000000..3115c64be --- /dev/null +++ b/spec/lib/sequel/extensions/register_advisory_lock_test.rb @@ -0,0 +1,89 @@ +require "sequel/extensions/pg_advisory_lock" + +describe Sequel::Postgres::PgAdvisoryLock do + subject { Sequel::Model.db } + + describe "#register_advisory_lock" do + let(:supported_lock_functions) do + [ + :pg_advisory_lock, + :pg_try_advisory_lock, + :pg_advisory_xact_lock, + :pg_try_advisory_xact_lock + ] + end + + let(:default_lock_function) { :pg_advisory_lock } + + before :all do + Sequel::Model.db.extension(:pg_advisory_lock) + end + + before do + subject.registered_advisory_locks.clear + end + + it "base check" do + lock_name = :test_lock + + expect(subject.registered_advisory_locks[lock_name]).to be nil + subject.register_advisory_lock(lock_name) + expect(default_lock_function).to eq subject.registered_advisory_locks[lock_name].fetch(:lock_function) + end + + it "should register locks for all supported PostgreSQL functions" do + supported_lock_functions.each do |lock_function| + lock_name = "#{lock_function}_test".to_sym + + expect(subject.registered_advisory_locks[lock_name]).to be nil + subject.register_advisory_lock(lock_name, lock_function) + expect(lock_function).to eq subject.registered_advisory_locks[lock_name].fetch(:lock_function) + end + end + + it "should prevent specifying not supported PostgreSQL function as lock type" do + lock_name = :not_supported_lock_function_test + lock_function = :not_supported_lock_function + + expect { subject.register_advisory_lock(lock_name, lock_function) }.to raise_error(Sequel::Error, /Invalid lock function/) + end + + it "should prevent registering multiple locks with same name and different functions" do + lock_name = :multiple_locks_with_same_name_test + subject.register_advisory_lock(lock_name, supported_lock_functions[0]) + + expect { subject.register_advisory_lock(lock_name, supported_lock_functions[1]) }.to raise_error(Sequel::Error, /Lock with name .+ is already registered/) + end + + it "should allow registering multiple locks with same name and same functions" do + lock_name = :multiple_locks_with_same_name_test + subject.register_advisory_lock(lock_name, supported_lock_functions[0]) + + expect { subject.register_advisory_lock(lock_name, supported_lock_functions[0]) }.to_not raise_error + end + + it "registered locks must have different lock keys" do + quantity = 100 + quantity.times do |index| + lock_name = "test_lock_#{index}".to_sym + subject.register_advisory_lock(lock_name) + end + + expect(quantity).to eq subject.registered_advisory_locks.size + all_keys = subject.registered_advisory_locks.values.map { |v| v.fetch(:key) } + expect(all_keys.size).to eq all_keys.uniq.size + end + + it "mapping between lock name and lock key must be constant" do + expect(subject.registered_advisory_locks).to be_empty + + lock_names_keys_mapping = YAML.load_file(File.join(File.dirname(__FILE__), "lock_names_keys.yml")) + + lock_names_keys_mapping.each do |lock_name, valid_lock_key| + lock_name = lock_name.to_sym + subject.register_advisory_lock(lock_name) + expect(valid_lock_key).to eq subject.registered_advisory_locks[lock_name].fetch(:key) + end + end + end +end diff --git a/spec/migrations/23_pact_versions_spec.rb b/spec/migrations/23_pact_versions_spec.rb index 3ecbaa892..f2e4b9701 100644 --- a/spec/migrations/23_pact_versions_spec.rb +++ b/spec/migrations/23_pact_versions_spec.rb @@ -65,12 +65,14 @@ it "allows a new pact to be inserted with no duplicate ID error" do subject + json_content = load_fixture("a_consumer-a_provider.json") PactBroker::Pacts::Service.create_or_update_pact( { consumer_name: consumer[:name], provider_name: provider[:name], consumer_version_number: "1.2.3", - json_content: load_fixture("a_consumer-a_provider.json") + json_content: json_content, + pact_version_sha: PactBroker::Pacts::GenerateSha.call(json_content) } ) end diff --git a/spec/service_consumers/hal_relation_proxy_app.rb b/spec/service_consumers/hal_relation_proxy_app.rb index 57d2123b8..bca43e3d2 100644 --- a/spec/service_consumers/hal_relation_proxy_app.rb +++ b/spec/service_consumers/hal_relation_proxy_app.rb @@ -27,7 +27,9 @@ class HalRelationProxyApp "/PLACEHOLDER-ENVIRONMENT-CURRENTLY-DEPLOYED-16926ef3-590f-4e3f-838e-719717aa88c9" => "/environments/16926ef3-590f-4e3f-838e-719717aa88c9/deployed-versions/currently-deployed", "/HAL-REL-PLACEHOLDER-PB-ENVIRONMENT-16926ef3-590f-4e3f-838e-719717aa88c9" => - "/environments/16926ef3-590f-4e3f-838e-719717aa88c9" + "/environments/16926ef3-590f-4e3f-838e-719717aa88c9", + "/HAL-REL-PLACEHOLDER-PB-PACTICIPANT-BRANCH-Foo-main" => + "/pacticipants/Foo/branches/main" } RESPONSE_BODY_REPLACEMENTS = { diff --git a/spec/service_consumers/provider_states_for_pact_broker_client.rb b/spec/service_consumers/provider_states_for_pact_broker_client.rb index a6e1ce372..1c64cd917 100644 --- a/spec/service_consumers/provider_states_for_pact_broker_client.rb +++ b/spec/service_consumers/provider_states_for_pact_broker_client.rb @@ -338,4 +338,16 @@ .create_deployed_version_for_consumer_version(uuid: "ff3adecf-cfc5-4653-a4e3-f1861092f8e0") end end + + provider_state "the pb:pacticipant-branch relation exists in the index resource" do + no_op + end + + provider_state "a branch named main exists for pacticipant Foo" do + set_up do + TestDataBuilder.new + .create_consumer("Foo") + .create_consumer_version("1", branch: "main") + end + end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 46c06871a..40e22bce2 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -54,8 +54,3 @@ config.filter_run_excluding skip: true config.include PactBroker::RackHelpers end - -if ENV["DEBUG"] == "true" - SemanticLogger.default_level = :info - SemanticLogger.add_appender(io: $stdout) -end diff --git a/spec/support/all_routes_spec_support.yml b/spec/support/all_routes_spec_support.yml index f99f47597..03e748698 100644 --- a/spec/support/all_routes_spec_support.yml +++ b/spec/support/all_routes_spec_support.yml @@ -1,114 +1,21 @@ -# Need to change this to use the path because there are resources with the same resource_name (pact_publications, verification_results and verification_result) -requests_which_are_exected_to_have_no_policy_record: - - publish_contracts POST +requests_which_are_exected_to_have_no_policy_record_or_pacticipant: - index GET - can_i_deploy GET - dashboard GET - - integration_dashboard GET - environments GET - environments POST - environment GET - environment PUT - environment DELETE - - group GET - integrations GET - integrations DELETE - - integration DELETE + - labels GET - matrix GET - - matrix_consumer_provider GET - - matrix_tag_badge GET - metrics GET - - latest_pact_publications GET - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - pacticipants GET - pacticipants POST - - pacticipant GET - - pacticipant PUT - - pacticipant PATCH - - pacticipant DELETE - - can_i_deploy_latest_branch_version_to_environment GET - - can_i_deploy_latest_branch_version_to_environment_badge GET - - branch GET - - branch DELETE - - branch_version GET - - branch_version PUT - - branch_version DELETE - - pacticipant_label GET - - pacticipant_label PUT - - pacticipant_label DELETE - - latest_pacticipant_version GET - - latest_tagged_pacticipant_version GET - - can_i_deploy_latest_tagged_version_to_tag GET - - can_i_deploy_latest_tagged_version_to_tag_badge GET - - pacticipant_versions GET - - pacticipant_version GET - - pacticipant_version PUT - - pacticipant_version PATCH - - pacticipant_version DELETE - - pacticipant_version_tag GET - - pacticipant_version_tag PUT - - pacticipant_version_tag DELETE - - pacticipant_branch_versions GET - pacticipants_for_label GET - latest_pacts GET - - provider_pact_publications GET - - pact_publications_for_branch DELETE - - latest_pact_publication GET - - latest_untagged_pact_publication GET - - latest_untagged_pact_badge GET - - latest_pact_publication_for_branch GET - - latest_tagged_pact_publication GET - - latest_tagged_pact_badge GET - - latest_verification_results_for_latest_tagged_pact_publication GET - - latest_verification_results_for_latest_tagged_pact_publication DELETE - - latest_pact_badge GET - - latest_verification_results_for_latest_pact_publication GET - - latest_verification_results_for_latest_pact_publication DELETE - - pact_publication GET - - pact_version_diff_by_pact_version_sha GET - - pact_publication GET - - verification_results POST - - verification_result GET - - verification_result DELETE - - verification_results POST - - verification_result GET - - verification_result DELETE - - latest_verification_results_for_pact_version GET - - latest_verification_results_for_pact_version DELETE - - tagged_pact_publications GET - - tagged_pact_publications DELETE - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - - previous_distinct_pact_version_diff GET - - pact_version_diff_by_consumer_version GET - - previous_distinct_pact_version GET - - latest_verification_results_for_pact_publication GET - - latest_verification_results_for_pact_publication DELETE - - pact_publications GET - - pact_publications DELETE - - pact_publication GET - - pact_publication PUT - - pact_publication DELETE - - pact_publication PATCH - - pact_webhooks POST - - pact_webhooks GET - - pact_webhooks_status GET - - pacts_for_verification GET - - pacts_for_verification POST - - latest_provider_pact_publications GET - - latest_tagged_provider_pact_publications GET - - tagged_provider_pact_publications GET - relationships GET - error_test GET - error_test POST - - verification_results_for_consumer_version GET - - webhooks GET - - consumer_webhooks GET - - provider_webhooks GET - - pacticipant_branches GET - - pacticipant_webhooks GET diff --git a/spec/support/logging.rb b/spec/support/logging.rb index 38551529d..b1a31ee49 100644 --- a/spec/support/logging.rb +++ b/spec/support/logging.rb @@ -3,4 +3,28 @@ FileUtils.mkdir_p("log") SemanticLogger.default_level = :error -SemanticLogger.add_appender(file_name: "log/test.log", formatter: PactBroker::Logging::DefaultFormatter.new) + +if ENV["DEBUG"] == "true" + SemanticLogger.default_level = :info + SemanticLogger.add_appender(io: $stdout) +end + +# Print out the request and response when DEBUG=true +RSpec.configure do | config | + config.after(:each) do + if ENV["DEBUG"] == "true" && defined?(last_response) + last_request.env["rack.input"]&.rewind + puts "------------------------------------------------------------" + puts "Request: #{last_request.request_method} #{last_request.path}\n\n" + puts "Rack env:\n#{last_request.env}\n\n" + puts "Request body:\n#{last_request.env["rack.input"]&.read}" + + puts "\n\n" + puts "Response status: #{last_response.status}\n\n" + puts "Response headers: #{last_response.headers}\n\n" + puts "Response body:\n#{last_response.body}" + puts "------------------------------------------------------------" + puts "" + end + end +end diff --git a/spec/support/pact_broker/middleware/mock_puma.rb b/spec/support/pact_broker/middleware/mock_puma.rb new file mode 100644 index 000000000..a94eadea0 --- /dev/null +++ b/spec/support/pact_broker/middleware/mock_puma.rb @@ -0,0 +1,26 @@ +# Mock out the rack.after_reply functionality provided by Puma +# I'm not sure if this is meant to be a public feature or not, but +# there are several mentions of it on the net, so I assume it's ok to use it. +# Puma itself uses the rack.after_reply for http request logging. +# +# See https://github.com/puma/puma/search?q=rack.after_reply +# This middleware executes the hooks that would normally run after the request +# *before* the request ends, for the purposes of testing. + +module PactBroker + module Middleware + class MockPuma + + def initialize(app) + @app = app + end + + def call(env) + after_reply = [] + response = @app.call({ "rack.after_reply" => after_reply }.merge(env)) + after_reply.each(&:call) + response + end + end + end +end diff --git a/spec/support/shared_context_for_app.rb b/spec/support/shared_context_for_app.rb index 1de3af135..975319e9a 100644 --- a/spec/support/shared_context_for_app.rb +++ b/spec/support/shared_context_for_app.rb @@ -43,6 +43,7 @@ builder.use OpenapiFirst::PactBrokerCoverage, endpoints_to_be_called end + builder.use(PactBroker::Middleware::MockPuma) builder.use(Rack::PactBroker::ApplicationContext, application_context) builder.run(PactBroker.build_api(application_context)) builder.to_app diff --git a/spec/support/shared_examples_for_responses.rb b/spec/support/shared_examples_for_responses.rb index 7e7a713a5..02885ee93 100644 --- a/spec/support/shared_examples_for_responses.rb +++ b/spec/support/shared_examples_for_responses.rb @@ -45,8 +45,8 @@ end it "includes the parameter validation errors" do - expect(response_body_hash[:errors].has_key?(:pageNumber)).to be_truthy - expect(response_body_hash[:errors].has_key?(:pageSize)).to be_truthy + expect(response_body_hash[:errors].has_key?(:page)).to be_truthy + expect(response_body_hash[:errors].has_key?(:size)).to be_truthy end end diff --git a/tasks/development.rake b/tasks/development.rake index 2bcc3efa1..ef62580a2 100644 --- a/tasks/development.rake +++ b/tasks/development.rake @@ -46,12 +46,14 @@ task :'pact_broker:routes', [:search_term] do | _t, args | puts "#{route.path}" puts " allowed_methods: #{route.allowed_methods.join(", ")}" puts " class: #{route.resource_class}" - puts " location: #{route.resource_class_location}" - if route[:schemas] + puts " location: #{route.resource_class_location}" + puts " policy_name: #{route.policy_names.collect(&:to_s).join(", ")}" + if route.schemas.any? puts " schemas:" - route[:schemas].each do | schema | - puts " class: #{schema[:class]}" - puts " location: #{schema[:location]}" + route.schemas.each do | schema | + puts " - method: #{schema[:http_method]}" + puts " class: #{schema[:class]}" + puts " location: #{schema[:location]}" end end