diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 1847108ee..3954a1ce4 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -95,6 +95,7 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ 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, "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, "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" } diff --git a/lib/pact_broker/api/decorators/branch_decorator.rb b/lib/pact_broker/api/decorators/branch_decorator.rb index 759687e57..915a12975 100644 --- a/lib/pact_broker/api/decorators/branch_decorator.rb +++ b/lib/pact_broker/api/decorators/branch_decorator.rb @@ -23,6 +23,12 @@ class BranchDecorator < BaseDecorator end include Timestamps + + # When this decorator is embedded in the PacticipantBranchesDecorator, + # we need to eager load the pacticipants for generating the URL + def self.eager_load_associations + super + [:pacticipant] + end end end end diff --git a/lib/pact_broker/api/decorators/decorator_context.rb b/lib/pact_broker/api/decorators/decorator_context.rb index 1aa2b7c93..2bf4eded7 100644 --- a/lib/pact_broker/api/decorators/decorator_context.rb +++ b/lib/pact_broker/api/decorators/decorator_context.rb @@ -2,7 +2,7 @@ module PactBroker module Api module Decorators class DecoratorContext < Hash - attr_reader :base_url, :resource_url, :resource_title, :env, :query_string + 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 @@ -10,6 +10,7 @@ def initialize base_url, resource_url, env, options = {} @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 diff --git a/lib/pact_broker/api/decorators/pacticipant_branches_decorator.rb b/lib/pact_broker/api/decorators/pacticipant_branches_decorator.rb new file mode 100644 index 000000000..45ecc7030 --- /dev/null +++ b/lib/pact_broker/api/decorators/pacticipant_branches_decorator.rb @@ -0,0 +1,32 @@ +require "pact_broker/api/decorators/base_decorator" +require "pact_broker/api/decorators/timestamps" +require "pact_broker/api/decorators/pagination_links" +require "pact_broker/api/decorators/branch_decorator" + +module PactBroker + module Api + module Decorators + class PacticipantBranchesDecorator < BaseDecorator + collection :entries, as: :branches, embedded: true, :extend => PactBroker::Api::Decorators::BranchDecorator + + link :self do | user_options | + { + title: "#{user_options.fetch(:pacticipant).name} branches", + href: user_options.fetch(:request_url) + } + end + + links "pb:branches" do | user_options | + represented.collect do | branch | + { + name: branch.name, + href: branch_url(branch, user_options.fetch(:base_url)) + } + end + end + + include PaginationLinks + 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 94edd9447..c13814743 100644 --- a/lib/pact_broker/api/decorators/pacticipant_decorator.rb +++ b/lib/pact_broker/api/decorators/pacticipant_decorator.rb @@ -39,6 +39,10 @@ def self.eager_load_associations versions_url(options[:base_url], represented) end + link :'pb:branches' do | options | + pacticipant_branches_url(represented, options[:base_url]) + end + link :'pb:version' do | options | { title: "Get, create or delete a pacticipant version", diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index 01f4f1412..3ce937d9d 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -228,7 +228,11 @@ def tag_url base_url, tag end def branch_url(branch, base_url = "") - "#{pacticipant_url(base_url, branch.pacticipant)}/branches/#{url_encode(branch.name)}" + "#{pacticipant_branches_url(branch.pacticipant, base_url)}/#{url_encode(branch.name)}" + end + + def pacticipant_branches_url(pacticipant, base_url = "") + "#{pacticipant_url(base_url, pacticipant)}/branches" end def branch_versions_url(branch, base_url = "") diff --git a/lib/pact_broker/api/resources/pacticipant_branches.rb b/lib/pact_broker/api/resources/pacticipant_branches.rb new file mode 100644 index 000000000..f97da58a2 --- /dev/null +++ b/lib/pact_broker/api/resources/pacticipant_branches.rb @@ -0,0 +1,49 @@ +require "pact_broker/api/resources/base_resource" +require "pact_broker/api/resources/pagination_methods" +require "pact_broker/api/resources/filter_methods" + +module PactBroker + module Api + module Resources + class PacticipantBranches < BaseResource + include PaginationMethods + include FilterMethods + + def content_types_provided + [["application/hal+json", :to_json]] + end + + def allowed_methods + ["GET", "OPTIONS"] + end + + def resource_exists? + !!pacticipant + end + + def to_json + decorator_class(:pacticipant_branches_decorator).new(branches).to_json(**decorator_options(pacticipant: pacticipant)) + end + + def policy_name + :'versions::branches' + end + + private + + def branches + @branches ||= branch_service.find_all_branches_for_pacticipant( + pacticipant, + filter_options, + default_pagination_options.merge(pagination_options), + eager_load_associations + ) + end + + def eager_load_associations + decorator_class(:pacticipant_branches_decorator).eager_load_associations + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/pagination_methods.rb b/lib/pact_broker/api/resources/pagination_methods.rb index 0f163c6e7..069f71853 100644 --- a/lib/pact_broker/api/resources/pagination_methods.rb +++ b/lib/pact_broker/api/resources/pagination_methods.rb @@ -12,6 +12,10 @@ def pagination_options {} end end + + def default_pagination_options + { page_number: 1, page_size: 100 } + end end end end diff --git a/lib/pact_broker/test/test_data_builder.rb b/lib/pact_broker/test/test_data_builder.rb index 8e37ee21b..d26e7eb48 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -614,6 +614,10 @@ def create_pacticipant_version(version_number, pacticipant, params = {}) tag = PactBroker::Domain::Tag.create(name: tag_name, version: version) set_created_at_if_set(params[:created_at], :tags, { name: tag.name, version_id: version.id }) end + if params[:branch] + set_created_at_if_set params[:created_at], :branches, { name: params[:branch], pacticipant_id: pacticipant.id } + set_created_at_if_set params[:created_at], :branch_versions, { branch_name: params[:branch], pacticipant_id: pacticipant.id, version_id: version.id } + end version end @@ -650,7 +654,7 @@ def set_created_at_if_set created_at, table_name, selector, date_column_name = : if date_to_set Sequel::Model.db[table_name].where(selector).update(date_column_name => date_to_set) if Sequel::Model.db.schema(table_name).any?{ |col| col.first == :updated_at } - Sequel::Model.db[table_name].where(selector.keys.first => selector.values.first).update(updated_at: date_to_set) + Sequel::Model.db[table_name].where(selector).update(updated_at: date_to_set) end end end diff --git a/lib/pact_broker/versions/branch_repository.rb b/lib/pact_broker/versions/branch_repository.rb index 3635413eb..7e41b00f5 100644 --- a/lib/pact_broker/versions/branch_repository.rb +++ b/lib/pact_broker/versions/branch_repository.rb @@ -1,7 +1,22 @@ +require "pact_broker/repositories/scopes" module PactBroker module Versions class BranchRepository include PactBroker::Services + include PactBroker::Repositories::Scopes + + # @param [PactBroker::Domain::Pacticipant] pacticipant + # @param [Hash] filter_options with key :query_string + # @param [Hash] pagination_options with keys :page_size and :page_number + # @param [Array] eager_load_associations the associations to eager load + def find_all_branches_for_pacticipant(pacticipant, filter_options = {}, pagination_options = {}, eager_load_associations = []) + query = scope_for(Branch).where(pacticipant_id: pacticipant.id).select_all_qualified + query = query.filter(:name, filter_options[:query_string]) if filter_options[:query_string] + query + .order(Sequel.desc(:created_at), Sequel.desc(:id)) + .eager(*eager_load_associations) + .all_with_pagination_options(pagination_options) + end # @param [String] pacticipant_name # @param [String] branch_name diff --git a/lib/pact_broker/versions/branch_service.rb b/lib/pact_broker/versions/branch_service.rb index de5c2903e..85e616121 100644 --- a/lib/pact_broker/versions/branch_service.rb +++ b/lib/pact_broker/versions/branch_service.rb @@ -11,7 +11,7 @@ class BranchService class << self extend Forwardable delegate [:find_branch_version, :find_or_create_branch_version, :delete_branch_version] => :branch_version_repository - delegate [:find_branch, :delete_branch] => :branch_repository + delegate [:find_branch, :delete_branch, :find_all_branches_for_pacticipant] => :branch_repository end end end diff --git a/spec/features/get_pacticipant_branches_spec.rb b/spec/features/get_pacticipant_branches_spec.rb new file mode 100644 index 000000000..4fb6b4b2b --- /dev/null +++ b/spec/features/get_pacticipant_branches_spec.rb @@ -0,0 +1,46 @@ +describe "Get pacticipant branches" do + before do + td.create_consumer("Foo") + .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(:headers) { { "CONTENT_TYPE" => "application/json" } } + let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } + let(:params) { nil } + + subject { get(path, params, headers) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns a list of branches" do + expect(response_body_hash[:_embedded][:branches].size).to eq 3 + end + + it_behaves_like "a page" + + context "with pagination options" do + subject { get(path, { "pageSize" => "2", "pageNumber" => "1" }) } + + it "only returns the number of items specified in the pageSize" do + expect(response_body_hash[:_links][:"pb:branches"].size).to eq 2 + end + + it_behaves_like "a paginated response" + end + + context "with filter options" do + let(:params) { { "q" => "feat" } } + + it "returns a list of branches matching the filter" do + expect(response_body_hash[:_embedded][:branches].size).to eq 2 + end + end + + context "when the pacticipant does not exist" do + let(:path) { PactBroker::Api::PactBrokerUrls.pacticipant_branches_url(OpenStruct.new(name: "Bar")) } + + its(:status) { is_expected.to eq 404 } + end +end diff --git a/spec/fixtures/approvals/docs_pacticipants_pacticipant_get.approved.json b/spec/fixtures/approvals/docs_pacticipants_pacticipant_get.approved.json index 26940c9c1..1da181413 100644 --- a/spec/fixtures/approvals/docs_pacticipants_pacticipant_get.approved.json +++ b/spec/fixtures/approvals/docs_pacticipants_pacticipant_get.approved.json @@ -42,6 +42,9 @@ "pb:versions": { "href": "https://pact-broker/pacticipants/foo/versions" }, + "pb:branches": { + "href": "https://pact-broker/pacticipants/foo/branches" + }, "pb:version": { "title": "Get, create or delete a pacticipant version", "href": "https://pact-broker/pacticipants/foo/versions/{version}", diff --git a/spec/fixtures/approvals/docs_pacticipants_pacticipant_patch.approved.json b/spec/fixtures/approvals/docs_pacticipants_pacticipant_patch.approved.json index 269bbe5d8..aa1550d2a 100644 --- a/spec/fixtures/approvals/docs_pacticipants_pacticipant_patch.approved.json +++ b/spec/fixtures/approvals/docs_pacticipants_pacticipant_patch.approved.json @@ -54,6 +54,9 @@ "pb:versions": { "href": "https://pact-broker/pacticipants/foo/versions" }, + "pb:branches": { + "href": "https://pact-broker/pacticipants/foo/branches" + }, "pb:version": { "title": "Get, create or delete a pacticipant version", "href": "https://pact-broker/pacticipants/foo/versions/{version}", diff --git a/spec/fixtures/approvals/docs_pacticipants_pacticipant_put.approved.json b/spec/fixtures/approvals/docs_pacticipants_pacticipant_put.approved.json index 8d4a17983..060d304db 100644 --- a/spec/fixtures/approvals/docs_pacticipants_pacticipant_put.approved.json +++ b/spec/fixtures/approvals/docs_pacticipants_pacticipant_put.approved.json @@ -54,6 +54,9 @@ "pb:versions": { "href": "https://pact-broker/pacticipants/foo/versions" }, + "pb:branches": { + "href": "https://pact-broker/pacticipants/foo/branches" + }, "pb:version": { "title": "Get, create or delete a pacticipant version", "href": "https://pact-broker/pacticipants/foo/versions/{version}", diff --git a/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json new file mode 100644 index 000000000..def84d52f --- /dev/null +++ b/spec/fixtures/approvals/pacticipant_branches_decorator.approved.json @@ -0,0 +1,32 @@ +{ + "_embedded": { + "branches": [ + { + "name": "main", + "createdAt": "2020-01-01T00:00:00+00:00", + "_links": { + "self": { + "title": "Branch", + "href": "http://example.org/pacticipants/Foo/branches/main" + }, + "pb:latest-version": { + "title": "Latest version for branch", + "href": "http://example.org/pacticipants/Foo/branches/main/versions?pageSize=1" + } + } + } + ] + }, + "_links": { + "self": { + "title": "Foo branches", + "href": "http://example.org/pacticipants/Foo/branches" + }, + "pb:branches": [ + { + "name": "main", + "href": "http://example.org/pacticipants/Foo/branches/main" + } + ] + } +} diff --git a/spec/lib/pact_broker/api/decorators/pacticipant_branches_decorator_spec.rb b/spec/lib/pact_broker/api/decorators/pacticipant_branches_decorator_spec.rb new file mode 100644 index 000000000..171991417 --- /dev/null +++ b/spec/lib/pact_broker/api/decorators/pacticipant_branches_decorator_spec.rb @@ -0,0 +1,35 @@ +require "pact_broker/api/decorators/pacticipant_branches_decorator" + +module PactBroker + module Api + module Decorators + describe PacticipantBranchesDecorator do + it "ensures the pacticipant is eager loaded for the branches collection" do + expect(PacticipantBranchesDecorator.eager_load_associations).to include :pacticipant + end + + describe "to_json" do + let(:branch_1) { instance_double("PactBroker::Versions::Branch", name: "main", pacticipant: pacticipant_1, created_at: td.in_utc { DateTime.new(2020, 1, 1) } ) } + let(:pacticipant_1) { instance_double("PactBroker::Domain::Pacticipant", name: "Foo") } + let(:branches) { [branch_1] } + let(:options) do + { + user_options: { + pacticipant: pacticipant_1, + base_url: "http://example.org", + request_url: "http://example.org/pacticipants/Foo/branches" + } + } + end + let(:decorator) { PacticipantBranchesDecorator.new(branches) } + + subject { JSON.parse(decorator.to_json(options)) } + + it "generates json" do + Approvals.verify(subject, :name => "pacticipant_branches_decorator", format: :json) + end + end + end + end + end +end 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 56a0834a4..df5562e0e 100644 --- a/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb +++ b/spec/lib/pact_broker/api/decorators/pacticipant_decorator_spec.rb @@ -43,6 +43,7 @@ module Decorators pacticipant.updated_at = updated_at allow_any_instance_of(PacticipantDecorator).to receive(:templated_tag_url_for_pacticipant).and_return("version_tag_url") allow_any_instance_of(PacticipantDecorator).to receive(:templated_version_url_for_pacticipant).and_return("version_url") + allow_any_instance_of(PacticipantDecorator).to receive(:pacticipant_branches_url).and_return("pacticipant_branches_url") end subject { JSON.parse PacticipantDecorator.new(pacticipant).to_json(user_options: { base_url: base_url }), symbolize_names: true } @@ -67,6 +68,11 @@ module Decorators expect(subject[:_links][:'pb:version'][:href]).to eq "version_url" end + it "includes a relation for the branches" do + expect_any_instance_of(PacticipantDecorator).to receive(:pacticipant_branches_url).with(pacticipant, base_url) + expect(subject[:_links][:'pb:branches'][:href]).to eq "pacticipant_branches_url" + end + context "when there is a latest_version" do before { td.create_version("1.2.107") } 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 79593eb30..d1b4a5beb 100644 --- a/spec/lib/pact_broker/api/resources/all_routes_spec.rb +++ b/spec/lib/pact_broker/api/resources/all_routes_spec.rb @@ -1,5 +1,5 @@ # The purpose of this spec is to ensure that every new resource either has a policy_record, or it does not need a policy_record -# (because the all the context can be implied from the route). +# (because the all the context can be implied from the route, which will most likely contain a :pacticipant, or a :consumer, and/or a :provider). # This test will fail when a new resource is added that does not either have a policy_record which returns an object, # or has not been explicitly ignored in the spec/support/all_routes_spec_support.yml file. diff --git a/spec/lib/pact_broker/api/resources/base_resource_spec.rb b/spec/lib/pact_broker/api/resources/base_resource_spec.rb index c9f11d04d..145897739 100644 --- a/spec/lib/pact_broker/api/resources/base_resource_spec.rb +++ b/spec/lib/pact_broker/api/resources/base_resource_spec.rb @@ -160,7 +160,8 @@ def process_post resource_url: "http://example.org/path", env: env, resource_title: nil, - query_string: "foo=bar" + query_string: "foo=bar", + request_url: "http://example.org/path?foo=bar" } ) end @@ -175,7 +176,8 @@ def process_post env: env, resource_title: "foo", something: "else", - query_string: "foo=bar" + query_string: "foo=bar", + request_url: "http://example.org/path?foo=bar" } ) end diff --git a/spec/lib/pact_broker/versions/branch_repository_spec.rb b/spec/lib/pact_broker/versions/branch_repository_spec.rb index b966bbe50..68ab71d95 100644 --- a/spec/lib/pact_broker/versions/branch_repository_spec.rb +++ b/spec/lib/pact_broker/versions/branch_repository_spec.rb @@ -3,6 +3,65 @@ module PactBroker module Versions describe BranchRepository do + describe "find_all_branches_for_pacticipant" do + before do + td.create_consumer("Other") + .create_consumer_version("1", branch: "blah") + .create_consumer("Foo") + .create_consumer_version("1", branch: "main") + .add_day + .create_consumer_version("2", branch: "main") + .add_day + .create_consumer_version("3", branch: "feat/foo") + .add_day + .create_consumer_version("4", branch: "feat/bar") + .add_day + end + + let(:filter_options) { {} } + let(:pagination_options) { {} } + let(:eager_load_associations) { [] } + let(:pacticipant) { td.and_return(:pacticipant) } + + subject { BranchRepository.new.find_all_branches_for_pacticipant(pacticipant, filter_options, pagination_options, eager_load_associations) } + + it "does not eager load the associations" do + expect(subject.first.associations[:pacticipant]).to be_nil + end + + context "with no options" do + it "returns all the branches for the pacticipant starting with the most recent" do + expect(subject.size).to eq 3 + expect(subject.first.name).to eq "feat/bar" + expect(subject.last.name).to eq "main" + end + end + + context "with pagination options" do + let(:pagination_options) { { page_size: 1, page_number: 2 } } + + it "uses the pagination options" do + expect(subject).to contain_exactly(have_attributes(name: "feat/foo")) + end + end + + context "with filter options" do + let(:filter_options) { { query_string: "feat" } } + + it "returns the matching branches" do + expect(subject).to contain_exactly(have_attributes(name: "feat/foo"), have_attributes(name: "feat/bar")) + end + end + + context "with eager_load_associations" do + let(:eager_load_associations) { [:pacticipant] } + + it "eager loads the associations" do + expect(subject.first.associations[:pacticipant]).to_not be_nil + end + end + end + describe "delete_branch" do before do td.create_consumer("foo") diff --git a/spec/support/all_routes_spec_support.yml b/spec/support/all_routes_spec_support.yml index 92cf0845c..f99f47597 100644 --- a/spec/support/all_routes_spec_support.yml +++ b/spec/support/all_routes_spec_support.yml @@ -110,4 +110,5 @@ requests_which_are_exected_to_have_no_policy_record: - webhooks GET - consumer_webhooks GET - provider_webhooks GET + - pacticipant_branches GET - pacticipant_webhooks GET diff --git a/spec/support/shared_examples_for_responses.rb b/spec/support/shared_examples_for_responses.rb index 6f8377bf3..7e7a713a5 100644 --- a/spec/support/shared_examples_for_responses.rb +++ b/spec/support/shared_examples_for_responses.rb @@ -5,10 +5,6 @@ end end -shared_examples_for "a 200 JSON response" do - -end - shared_examples_for "a paginated response" do let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) } @@ -28,6 +24,19 @@ end end +shared_examples_for "a page" do + it "includes the page details" do + expect(response_body_hash).to include( + page: { + number: instance_of(Integer), + size: instance_of(Integer), + totalElements: instance_of(Integer), + totalPages: instance_of(Integer), + } + ) + end +end + shared_examples_for "an invalid pagination params response" do let(:response_body_hash) { JSON.parse(subject.body, symbolize_names: true) }