From 410f2e80a39ea170b5a37797ee5c0354097c57f6 Mon Sep 17 00:00:00 2001 From: Beth Skurrie Date: Mon, 15 Feb 2021 06:41:37 +1100 Subject: [PATCH] feat: support create, update and delete of environment resources (#379) * feat: support create, update and delete of environment resources * feat: support listing of environments, creation via post * feat: add validation for environment creation * feat: add validation for environment update * feat: add 'production' boolean to environment * feat: add enviroment and environments relation to index * feat: disallow spaces and new lines in environment properties --- .../20210210_create_environments_table.rb | 16 ++++ lib/pact_broker/api.rb | 5 ++ .../contracts/dry_validation_predicates.rb | 8 ++ .../api/contracts/environment_schema.rb | 49 +++++++++++ .../api/decorators/base_decorator.rb | 11 +++ .../api/decorators/environment_decorator.rb | 30 +++++++ .../api/decorators/environments_decorator.rb | 21 +++++ lib/pact_broker/api/pact_broker_urls.rb | 8 ++ .../api/resources/default_base_resource.rb | 9 ++ lib/pact_broker/api/resources/environment.rb | 76 +++++++++++++++++ lib/pact_broker/api/resources/environments.rb | 75 +++++++++++++++++ lib/pact_broker/api/resources/index.rb | 14 ++++ lib/pact_broker/deployments/environment.rb | 15 ++++ .../deployments/environment_service.rb | 39 +++++++++ .../doc/views/index/environment.markdown | 37 +++++++++ .../doc/views/index/environments.markdown | 53 ++++++++++++ lib/pact_broker/locale/en.yml | 4 +- lib/pact_broker/services.rb | 9 ++ lib/pact_broker/test/test_data_builder.rb | 14 ++++ spec/features/create_environment_spec.rb | 47 +++++++++++ spec/features/delete_environment_spec.rb | 16 ++++ spec/features/end_deployment_spec.rb | 29 +++++++ spec/features/get_environment_spec.rb | 19 +++++ spec/features/get_environments_spec.rb | 20 +++++ spec/features/record_deployment_spec.rb | 28 +++++++ spec/features/update_environment_spec.rb | 44 ++++++++++ .../modifiable_resources.approved.json | 6 ++ .../api/contracts/environment_schema_spec.rb | 83 +++++++++++++++++++ .../default_base_resource_approval_spec.rb | 2 +- spec/support/shared_examples_for_responses.rb | 11 +++ 30 files changed, 796 insertions(+), 2 deletions(-) create mode 100644 db/migrations/20210210_create_environments_table.rb create mode 100644 lib/pact_broker/api/contracts/environment_schema.rb create mode 100644 lib/pact_broker/api/decorators/environment_decorator.rb create mode 100644 lib/pact_broker/api/decorators/environments_decorator.rb create mode 100644 lib/pact_broker/api/resources/environment.rb create mode 100644 lib/pact_broker/api/resources/environments.rb create mode 100644 lib/pact_broker/deployments/environment.rb create mode 100644 lib/pact_broker/deployments/environment_service.rb create mode 100644 lib/pact_broker/doc/views/index/environment.markdown create mode 100644 lib/pact_broker/doc/views/index/environments.markdown create mode 100644 spec/features/create_environment_spec.rb create mode 100644 spec/features/delete_environment_spec.rb create mode 100644 spec/features/end_deployment_spec.rb create mode 100644 spec/features/get_environment_spec.rb create mode 100644 spec/features/get_environments_spec.rb create mode 100644 spec/features/record_deployment_spec.rb create mode 100644 spec/features/update_environment_spec.rb create mode 100644 spec/lib/pact_broker/api/contracts/environment_schema_spec.rb diff --git a/db/migrations/20210210_create_environments_table.rb b/db/migrations/20210210_create_environments_table.rb new file mode 100644 index 000000000..5aaca5162 --- /dev/null +++ b/db/migrations/20210210_create_environments_table.rb @@ -0,0 +1,16 @@ +Sequel.migration do + change do + create_table(:environments, charset: 'utf8') do + primary_key :id + String :uuid, nullable: false + String :name, nullable: false + String :display_name + Boolean :production, nullable: false + String :contacts + DateTime :created_at, nullable: false + DateTime :updated_at, nullable: false + index [:uuid], unique: true, name: "environments_uuid_index" + index [:name], unique: true, name: "environments_name_index" + end + end +end diff --git a/lib/pact_broker/api.rb b/lib/pact_broker/api.rb index 715325efd..cbe158c30 100644 --- a/lib/pact_broker/api.rb +++ b/lib/pact_broker/api.rb @@ -105,6 +105,11 @@ def self.build_api(application_context = PactBroker::ApplicationContext.default_ add ['dashboard', 'provider', :provider_name, 'consumer', :consumer_name ], Api::Resources::Dashboard, {resource_name: "integration_dashboard"} add ['test','error'], Api::Resources::ErrorTest, {resource_name: "error_test"} + if PactBroker.feature_enabled?(:environments) + add ['environments'], Api::Resources::Environments, { resource_name: "environments" } + add ['environments', :environment_uuid], Api::Resources::Environment, { resource_name: "environment" } + end + add ['integrations'], Api::Resources::Integrations, {resource_name: "integrations"} add ['integrations', 'provider', :provider_name, 'consumer', :consumer_name], Api::Resources::Integration, {resource_name: "integration"} add ['metrics'], Api::Resources::Metrics, {resource_name: 'metrics'} diff --git a/lib/pact_broker/api/contracts/dry_validation_predicates.rb b/lib/pact_broker/api/contracts/dry_validation_predicates.rb index 953db32d1..4cac4d866 100644 --- a/lib/pact_broker/api/contracts/dry_validation_predicates.rb +++ b/lib/pact_broker/api/contracts/dry_validation_predicates.rb @@ -13,6 +13,14 @@ module DryValidationPredicates predicate(:not_blank?) do | value | value && value.is_a?(String) && value.strip.size > 0 end + + predicate(:single_line?) do | value | + value && value.is_a?(String) && !value.include?("\n") + end + + predicate(:no_spaces?) do | value | + value && value.is_a?(String) && !value.include?(" ") + end end end end diff --git a/lib/pact_broker/api/contracts/environment_schema.rb b/lib/pact_broker/api/contracts/environment_schema.rb new file mode 100644 index 000000000..21a6436fe --- /dev/null +++ b/lib/pact_broker/api/contracts/environment_schema.rb @@ -0,0 +1,49 @@ +require 'dry-validation' +require 'pact_broker/api/contracts/dry_validation_workarounds' +require 'pact_broker/api/contracts/dry_validation_predicates' +require 'pact_broker/messages' + +module PactBroker + module Api + module Contracts + class EnvironmentSchema + extend DryValidationWorkarounds + extend PactBroker::Messages + using PactBroker::HashRefinements + + SCHEMA = Dry::Validation.Schema do + configure do + predicates(DryValidationPredicates) + config.messages_file = File.expand_path("../../../locale/en.yml", __FILE__) + end + required(:name).filled(:str?, :single_line?, :no_spaces?) + required(:displayName).filled(:str?, :single_line?) + required(:production).filled(included_in?: [true, false]) + optional(:contacts).each do + schema do + required(:name).filled(:str?, :single_line?) + optional(:details).schema do + end + end + end + end + + def self.call(params_with_string_keys) + params = params_with_string_keys&.symbolize_keys + results = select_first_message(flatten_indexed_messages(SCHEMA.call(params).messages(full: true))) + validate_name(params, results) + results + end + + def self.validate_name(params, results) + if (environment_with_same_name = PactBroker::Deployments::EnvironmentService.find_by_name(params[:name])) + if environment_with_same_name.uuid != params[:uuid] + results[:name] ||= [] + results[:name] << message('errors.validation.environment_name_must_be_unique', name: params[:name]) + end + end + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/base_decorator.rb b/lib/pact_broker/api/decorators/base_decorator.rb index 1e09eae85..cb1ed57b3 100644 --- a/lib/pact_broker/api/decorators/base_decorator.rb +++ b/lib/pact_broker/api/decorators/base_decorator.rb @@ -3,6 +3,7 @@ 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' module PactBroker module Api @@ -12,6 +13,16 @@ class BaseDecorator < Roar::Decorator include Roar::JSON::HAL::Links include PactBroker::Api::PactBrokerUrls include FormatDateTime + using PactBroker::StringRefinements + + def self.property(name, options={}, &block) + if options.delete(:camelize) + camelized_name = name.to_s.camelcase(false).to_sym + super(name, { as: camelized_name }.merge(options), &block) + else + super + end + end end end end diff --git a/lib/pact_broker/api/decorators/environment_decorator.rb b/lib/pact_broker/api/decorators/environment_decorator.rb new file mode 100644 index 000000000..274adc517 --- /dev/null +++ b/lib/pact_broker/api/decorators/environment_decorator.rb @@ -0,0 +1,30 @@ +require_relative 'base_decorator' +require_relative 'timestamps' + +module PactBroker + module Api + module Decorators + class EnvironmentDecorator < BaseDecorator + property :uuid, writeable: false + property :name + property :display_name, camelize: true + property :production + + collection :contacts, class: OpenStruct do + property :name + property :details + end + + include Timestamps + + link :self do | options | + { + title: 'Environment', + name: represented.name, + href: environment_url(represented, options[:base_url]) + } + end + end + end + end +end diff --git a/lib/pact_broker/api/decorators/environments_decorator.rb b/lib/pact_broker/api/decorators/environments_decorator.rb new file mode 100644 index 000000000..aceb66b82 --- /dev/null +++ b/lib/pact_broker/api/decorators/environments_decorator.rb @@ -0,0 +1,21 @@ +require 'pact_broker/api/decorators/base_decorator' +require 'pact_broker/api/decorators/environment_decorator' +require 'pact_broker/deployments/environment' + +module PactBroker + module Api + module Decorators + class EnvironmentsDecorator < BaseDecorator + + collection :entries, :as => :environments, :class => PactBroker::Deployments::Environment, :extend => PactBroker::Api::Decorators::EnvironmentDecorator, embedded: true + + link :self do | options | + { + title: 'Environments', + href: options[:resource_url] + } + end + end + end + end +end diff --git a/lib/pact_broker/api/pact_broker_urls.rb b/lib/pact_broker/api/pact_broker_urls.rb index d7e8a7460..21e37e648 100644 --- a/lib/pact_broker/api/pact_broker_urls.rb +++ b/lib/pact_broker/api/pact_broker_urls.rb @@ -307,6 +307,14 @@ def group_url(pacticipant_name, base_url = '') "#{base_url}/groups/#{url_encode(pacticipant_name)}" end + def environments_url(base_url = '') + "#{base_url}/environments" + end + + def environment_url(environment, base_url = '') + "#{environments_url(base_url)}/#{environment.uuid}" + end + def hal_browser_url target_url, base_url = '' "#{base_url}/hal-browser/browser.html#" + target_url end diff --git a/lib/pact_broker/api/resources/default_base_resource.rb b/lib/pact_broker/api/resources/default_base_resource.rb index 4de40d10d..c9d6eab78 100644 --- a/lib/pact_broker/api/resources/default_base_resource.rb +++ b/lib/pact_broker/api/resources/default_base_resource.rb @@ -243,6 +243,15 @@ def application_context def decorator_class(name) application_context.decorator_configuration.class_for(name) end + + def validation_errors_for_schema?(schema, params_to_validate = params) + if (errors = schema.call(params_to_validate)).any? + set_json_validation_error_messages(errors) + true + else + false + end + end end end end diff --git a/lib/pact_broker/api/resources/environment.rb b/lib/pact_broker/api/resources/environment.rb new file mode 100644 index 000000000..cd3ec4252 --- /dev/null +++ b/lib/pact_broker/api/resources/environment.rb @@ -0,0 +1,76 @@ +require 'pact_broker/api/resources/base_resource' +require 'pact_broker/api/resources/environment' + +module PactBroker + module Api + module Resources + class Environment < BaseResource + def content_types_provided + [["application/hal+json", :to_json]] + end + + def content_types_accepted + [["application/json", :from_json]] + end + + def allowed_methods + ["GET", "PUT", "DELETE", "OPTIONS"] + end + + def resource_exists? + !!environment + end + + def malformed_request? + if request.put? && environment + invalid_json? || validation_errors_for_schema?(schema, params.merge(uuid: uuid)) + else + false + end + end + + def from_json + if environment + @environment = update_environment + response.body = to_json + else + response.code = 404 + end + end + + def policy_name + :'deployments::environment' + end + + def to_json + decorator_class(:environment_decorator).new(environment).to_json(decorator_options) + end + + def parsed_environment + @parsed_environment ||= decorator_class(:environment_decorator).new(PactBroker::Deployments::Environment.new).from_json(request_body) + end + + def environment + @environment ||= environment_service.find(uuid) + end + + def delete_resource + environment_service.delete(uuid) + true + end + + def uuid + identifier_from_path[:environment_uuid] + end + + def update_environment + environment_service.update(uuid, parsed_environment) + end + + def schema + PactBroker::Api::Contracts::EnvironmentSchema + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/environments.rb b/lib/pact_broker/api/resources/environments.rb new file mode 100644 index 000000000..4eb80a7b5 --- /dev/null +++ b/lib/pact_broker/api/resources/environments.rb @@ -0,0 +1,75 @@ +require 'pact_broker/api/resources/base_resource' +require 'pact_broker/api/resources/environment' +require 'pact_broker/api/contracts/environment_schema' + +module PactBroker + module Api + module Resources + class Environments < BaseResource + def content_types_provided + [["application/hal+json", :to_json]] + end + + def content_types_accepted + [["application/json", :from_json]] + end + + def allowed_methods + ["GET", "POST", "OPTIONS"] + end + + def resource_exists? + true + end + + def post_is_create? + true + end + + def malformed_request? + if request.post? + invalid_json? || validation_errors_for_schema?(schema, params.merge(uuid: uuid)) + else + false + end + end + + def create_path + environment_url(OpenStruct.new(uuid: uuid), base_url) + end + + def from_json + response.body = decorator_class(:environment_decorator).new(create_environment).to_json(decorator_options) + end + + def policy_name + :'deployments::environment' + end + + def to_json + decorator_class(:environments_decorator).new(environments).to_json(decorator_options) + end + + def parsed_environment + @parsed_environment ||= decorator_class(:environment_decorator).new(PactBroker::Deployments::Environment.new).from_json(request_body) + end + + def create_environment + environment_service.create(uuid, parsed_environment) + end + + def uuid + @uuid ||= environment_service.next_uuid + end + + def environments + @environments ||= environment_service.find_all + end + + def schema + PactBroker::Api::Contracts::EnvironmentSchema + end + end + end + end +end diff --git a/lib/pact_broker/api/resources/index.rb b/lib/pact_broker/api/resources/index.rb index 5acb82483..5e72fbbb9 100644 --- a/lib/pact_broker/api/resources/index.rb +++ b/lib/pact_broker/api/resources/index.rb @@ -150,6 +150,20 @@ def links }] } + if PactBroker.feature_enabled?(:environments) + links_hash['pb:environments'] = { + title: "Environments", + href: environments_url(base_url), + templated: false + } + + links_hash['pb:environment'] = { + title: "Environment", + href: environments_url(base_url) + "/{uuid}", + templated: true + } + end + if PactBroker.feature_enabled?('disable_pacts_for_verification', true) links_hash.delete('pb:provider-pacts-for-verification') links_hash.delete('beta:provider-pacts-for-verification') diff --git a/lib/pact_broker/deployments/environment.rb b/lib/pact_broker/deployments/environment.rb new file mode 100644 index 000000000..4cd2029cb --- /dev/null +++ b/lib/pact_broker/deployments/environment.rb @@ -0,0 +1,15 @@ +require 'sequel' +require 'sequel/plugins/serialization' + + +module PactBroker + module Deployments + class Environment < Sequel::Model + OPEN_STRUCT_TO_JSON = lambda { |thing| Sequel.object_to_json(thing.collect(&:to_h)) } + JSON_TO_OPEN_STRUCT = lambda { | json | Sequel.parse_json(json).collect{ | hash| OpenStruct.new(hash) } } + plugin :upsert, identifying_columns: [:uuid] + plugin :serialization + serialize_attributes [OPEN_STRUCT_TO_JSON, JSON_TO_OPEN_STRUCT], :contacts + end + end +end diff --git a/lib/pact_broker/deployments/environment_service.rb b/lib/pact_broker/deployments/environment_service.rb new file mode 100644 index 000000000..b1a3a7631 --- /dev/null +++ b/lib/pact_broker/deployments/environment_service.rb @@ -0,0 +1,39 @@ +require 'pact_broker/deployments/environment' +require 'securerandom' + +module PactBroker + module Deployments + module EnvironmentService + + def self.next_uuid + SecureRandom.uuid + end + + def self.create(uuid, environment) + environment.uuid = uuid + environment.save + end + + def self.update(uuid, environment) + environment.uuid = uuid + environment.upsert + end + + def self.find_all + PactBroker::Deployments::Environment.order(Sequel.function(:lower, :display_name)).all + end + + def self.find(uuid) + PactBroker::Deployments::Environment.where(uuid: uuid).single_record + end + + def self.find_by_name(name) + PactBroker::Deployments::Environment.where(name: name).single_record + end + + def self.delete(uuid) + PactBroker::Deployments::Environment.where(uuid: uuid).delete + end + end + end +end diff --git a/lib/pact_broker/doc/views/index/environment.markdown b/lib/pact_broker/doc/views/index/environment.markdown new file mode 100644 index 000000000..ae4b58ca7 --- /dev/null +++ b/lib/pact_broker/doc/views/index/environment.markdown @@ -0,0 +1,37 @@ +# Environment + +Allowed methods: `GET`, `PUT`, `DELETE` + +Path: `/environment/{uuid}` + +## Viewing an environment + +Example: + + curl http://${PACT_BROKER_HOST}/environments/79060381-269c-4769-9894-9ec3cab44729 \ + -H "Accept: application/hal+json" + { + "uuid": "79060381-269c-4769-9894-9ec3cab44729", + "name": "test", + "displayName": "Test", + "production": false + } + +## Updating an environment + +Example: + + curl -X PUT http://${PACT_BROKER_HOST}/environments/79060381-269c-4769-9894-9ec3cab44729 \ + -H "Content-Type: application/json" \ + -H "Accept: application/hal+json" \ + -d '{ + "name": "test", + "displayName": "Test", + "production": false + }' + +## Deleting an environment + +Example: + + curl -v -X DELETE http://${PACT_BROKER_HOST}/environments/79060381-269c-4769-9894-9ec3cab44729 diff --git a/lib/pact_broker/doc/views/index/environments.markdown b/lib/pact_broker/doc/views/index/environments.markdown new file mode 100644 index 000000000..2713a5899 --- /dev/null +++ b/lib/pact_broker/doc/views/index/environments.markdown @@ -0,0 +1,53 @@ +# Environments + +Allowed methods: `GET`, `POST` + +Path: `/environments` + +## Creating an environment + +Send a POST to `/environments` with the environment payload. + +Example: + + curl http://${PACT_BROKER_HOST}/environments \ + -H "Content-Type: application/json" \ + -H "Accept: application/hal+json" \ + -d '{ + "name": "test", + "displayName": "Test", + "production": false + }' + +Alternatively, you can use the HAL Browser. + +* Click on the `API Browser` link at the top of the Pact Broker index page. +* In the `Links` section on the left, locate the `pb:environments` relation, and click on the yellow `!` "Perform non-GET request" button. +* In the `Body:` text box, fill in the required JSON properties. +* Click `Make Request`. + +Properties: + +* `uuid`: System generated unique identifier. +* `name`: Must be unique. No spaces allowed. This will be the name used in the `can-i-deploy` and `record-deployment` CLI commands. eg. "payments-sit-1" +* `displayName`: A more verbose name for the environment. "Payments Team SIT 1" +* `production`: Whether or not this environment is a production environment. + +If all the services in the Broker are deployed to the same "public" internet, then there only needs to be one Production environment. If there are multiple segregated production environments (eg. when maintaining on-premises software for multiple customers ) then you should create a separate production Environment for each logical deployment environment. + +## Listing environments + +`GET /environments` + + { + "_embedded": { + "environments": [ + { + "uuid": "79060381-269c-4769-9894-9ec3cab44729", + "name": "production", + "displayName": "Production", + "production": true + } + ] + } + } diff --git a/lib/pact_broker/locale/en.yml b/lib/pact_broker/locale/en.yml index 529201932..7d7cc9678 100644 --- a/lib/pact_broker/locale/en.yml +++ b/lib/pact_broker/locale/en.yml @@ -9,7 +9,8 @@ en: valid_consumer_version_number?: "Consumer version number '%{value}' cannot be parsed to a version number. The expected format (unless this configuration has been overridden) is a semantic version. eg. 1.3.0 or 2.0.4.rc1" non_templated_host?: "cannot have a template parameter in the host" pacticipant_exists?: "does not match an existing pacticipant" - + single_line?: "cannot contain multiple lines" + no_spaces?: "cannot contain spaces" pact_broker: messages: @@ -46,6 +47,7 @@ en: connection_encoding_not_utf8: "The Sequel connection encoding (%{encoding}) is strongly recommended to be \"utf8\". If you need to set it to %{encoding} for some particular reason, then disable this check by setting config.validate_database_connection_config = false" invalid_webhook_uuid: The UUID can only contain the characters A-Z, a-z, 0-9, _ and -, and must be 16 or more characters. pacticipant_not_found: No pacticipant with name '%{name}' found + environment_name_must_be_unique: Another environment with name '%{name}' already exists. duplicate_pacticipant: | This is the first time a pact has been published for "%{new_name}". The name "%{new_name}" is very similar to the following existing consumers/providers: diff --git a/lib/pact_broker/services.rb b/lib/pact_broker/services.rb index 701257270..5204a46a8 100644 --- a/lib/pact_broker/services.rb +++ b/lib/pact_broker/services.rb @@ -73,6 +73,10 @@ def metrics_service get(:metrics_service) end + def environment_service + get(:environment_service) + end + def register_default_services register_service(:index_service) do require 'pact_broker/index/service' @@ -148,6 +152,11 @@ def register_default_services require 'pact_broker/webhooks/trigger_service' Webhooks::TriggerService end + + register_service(:environment_service) do + require 'pact_broker/deployments/environment_service' + Deployments::EnvironmentService + 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 6bd25bea7..bfa719851 100644 --- a/lib/pact_broker/test/test_data_builder.rb +++ b/lib/pact_broker/test/test_data_builder.rb @@ -24,6 +24,7 @@ require 'pact_broker/webhooks/repository' require 'pact_broker/certificates/certificate' require 'pact_broker/matrix/row' +require 'pact_broker/deployments/environment_service' require 'ostruct' module PactBroker @@ -45,6 +46,7 @@ class TestDataBuilder attr_reader :webhook attr_reader :webhook_execution attr_reader :triggered_webhook + attr_reader :environment def initialize(params = {}) @now = DateTime.now @@ -381,6 +383,18 @@ def create_certificate options = {path: 'spec/fixtures/single-certificate.pem'} self end + def create_environment(name, params = {}) + uuid = params[:uuid] || PactBroker::Deployments::EnvironmentService.next_uuid + production = params[:production] || false + @environment = PactBroker::Deployments::EnvironmentService.create(uuid, PactBroker::Deployments::Environment.new(params.merge(name: name, production: production))) + set_created_at_if_set(params[:created_at], :environments, id: environment.id) + self + end + + def create_deployment(_) + self + end + def create_everything_for_an_integration create_pact_with_verification("Foo", "1", "Bar", "2") .create_label("label") diff --git a/spec/features/create_environment_spec.rb b/spec/features/create_environment_spec.rb new file mode 100644 index 000000000..2ff1f986c --- /dev/null +++ b/spec/features/create_environment_spec.rb @@ -0,0 +1,47 @@ +require 'pact_broker/api/pact_broker_urls' + +describe "Creating an environment" do + let(:path) { PactBroker::Api::PactBrokerUrls.environments_url } + let(:headers) { { "CONTENT_TYPE" => "application/json" } } + let(:response_body) { JSON.parse(subject.body, symbolize_names: true)} + let(:environment_hash) do + { + name: "test", + displayName: "Test", + production: false, + contacts: [ + { name: "Team Awesome", details: { email: "foo@bar.com", arbitraryThing: "thing" } } + ] + } + end + + subject { post(path, environment_hash.to_json, headers) } + + it "returns a 201 response" do + expect(subject.status).to be 201 + end + + it "returns the Location header" do + expect(subject.headers["Location"]).to eq PactBroker::Api::PactBrokerUrls.environment_url(PactBroker::Deployments::Environment.order(:id).last, "http://example.org") + end + + it "returns a JSON Content Type" do + expect(subject.headers["Content-Type"]).to eq "application/hal+json;charset=utf-8" + end + + it "returns the newly created environment" do + expect(response_body).to include environment_hash.merge(name: "test") + expect(response_body[:uuid]).to_not be nil + end + + context "with invalid params" do + before do + td.create_environment("test") + end + + it "returns a 400 response" do + expect(subject.status).to be 400 + expect(response_body[:errors]).to_not be nil + end + end +end diff --git a/spec/features/delete_environment_spec.rb b/spec/features/delete_environment_spec.rb new file mode 100644 index 000000000..b31287a1e --- /dev/null +++ b/spec/features/delete_environment_spec.rb @@ -0,0 +1,16 @@ +require 'pact_broker/api/pact_broker_urls' + +describe "Deleting an environment" do + before do + td.create_environment("test", uuid: "1234") + end + + let(:path) { PactBroker::Api::PactBrokerUrls.environment_url(td.and_return(:environment)) } + + subject { delete(path, nil) } + + it "returns a 204 response" do + subject + expect(last_response.status).to be 204 + end +end diff --git a/spec/features/end_deployment_spec.rb b/spec/features/end_deployment_spec.rb new file mode 100644 index 000000000..254a68bb0 --- /dev/null +++ b/spec/features/end_deployment_spec.rb @@ -0,0 +1,29 @@ +# +# RPC style seems cleaner than REST here, as setting the endedAt parameter directly +# seems likely to end in Timezone tears +# This endpoint would be called by the pact broker client during `record-deployment` if the +# --end-previous-deployment (on by default) was specified. +# This allows us to know exactly what is deployed to a particular environment at a given time, +# (eg. /environments/test/deployments/current) +# and provides first class support for mobile clients that have multiple versions in prod +# at once. + +describe "Record deployment ended", skip: "Not yet implemented" do + before do + td.create_environment("test") + .create_pacticipant("Foo") + .create_pacticipant_version("1") + .create_deployment("test") + end + let(:path) { "/pacticipants/Foo/deployments/test/latest/end" } + let(:headers) { {} } + let(:response_body) { JSON.parse(last_response.body, symbolize_names: true) } + + subject { post(path, nil, headers) } + + it { is_expected.be_a_hal_json_success_response } + + it "returns the updated deployment" do + expect(subject[:endedAt]).to_not be nil + end +end diff --git a/spec/features/get_environment_spec.rb b/spec/features/get_environment_spec.rb new file mode 100644 index 000000000..fbcb1bab3 --- /dev/null +++ b/spec/features/get_environment_spec.rb @@ -0,0 +1,19 @@ +require 'pact_broker/api/pact_broker_urls' + +describe "Get an environment" do + before do + td.create_environment("test", display_name: "Test", uuid: "1234", contacts: [ { name: "Foo" } ] ) + end + let(:path) { PactBroker::Api::PactBrokerUrls.environment_url(td.and_return(:environment)) } + let(:headers) { {'HTTP_ACCEPT' => 'application/hal+json'} } + let(:response_body) { JSON.parse(subject.body, symbolize_names: true)} + + subject { get(path, nil, headers) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns the environment" do + expect(response_body[:uuid]).to eq "1234" + expect(response_body[:name]).to eq "test" + end +end diff --git a/spec/features/get_environments_spec.rb b/spec/features/get_environments_spec.rb new file mode 100644 index 000000000..3ebb9455e --- /dev/null +++ b/spec/features/get_environments_spec.rb @@ -0,0 +1,20 @@ +require 'pact_broker/api/pact_broker_urls' + +describe "Get all environments" do + before do + td.create_environment("test", display_name: "Test", uuid: "1234", contacts: [ { name: "Foo" } ] ) + .create_environment("prod", display_name: "Production", uuid: "5678", contacts: [ { name: "Foo" } ] ) + end + let(:path) { PactBroker::Api::PactBrokerUrls.environments_url } + let(:headers) { {'HTTP_ACCEPT' => 'application/hal+json'} } + let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)} + + subject { get(path, nil, headers) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns the environments" do + subject + expect(response_body[:_embedded][:environments].size).to be 2 + end +end diff --git a/spec/features/record_deployment_spec.rb b/spec/features/record_deployment_spec.rb new file mode 100644 index 000000000..7f8f03212 --- /dev/null +++ b/spec/features/record_deployment_spec.rb @@ -0,0 +1,28 @@ +# +# pact-broker record-deployment --pacticipant Foo --version 1 --environment test --end-previous-deployment +# + +describe "Record deployment", skip: "Not yet implemented" do + before do + td.create_environment("test") + .create_pacticipant("Foo") + .create_pacticipant_version("1") + end + let(:path) { "/pacticipants/Foo/versions/1/deployments/test" } + let(:headers) { {"CONTENT_TYPE" => "application/json"} } + let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)} + + subject { post(path, nil, headers) } + + it { is_expected.to be_a_hal_json_created_response } + + it "returns the Location header" do + subject + expect(last_response.headers["Location"]).to eq "http://example.org/deployments/123456" + end + + it "returns the newly created deployment" do + subject + expect(response_body).to include_key(:createdAt) + end +end diff --git a/spec/features/update_environment_spec.rb b/spec/features/update_environment_spec.rb new file mode 100644 index 000000000..fd61b1781 --- /dev/null +++ b/spec/features/update_environment_spec.rb @@ -0,0 +1,44 @@ +require 'pact_broker/api/pact_broker_urls' + +describe "Updating an environment" do + before do + td.create_environment("test", uuid: "1234", contacts: [ { name: "Foo" } ] ) + end + let(:path) { PactBroker::Api::PactBrokerUrls.environment_url(td.and_return(:environment)) } + let(:headers) { {'CONTENT_TYPE' => 'application/json'} } + let(:response_body) { JSON.parse(last_response.body, symbolize_names: true)} + let(:environment_hash) do + { + name: "test", + production: false, + displayName: "Testing" + } + end + + subject { put(path, environment_hash.to_json, headers) } + + it { is_expected.to be_a_hal_json_success_response } + + it "returns the updated environment" do + subject + expect(response_body[:displayName]).to eq "Testing" + expect(response_body[:contacts]).to be nil + end + + context "when the environment doesn't exist" do + let(:path) { "/environments/5678" } + + it "returns a 404" do + expect(subject.status).to eq 404 + end + end + + context "with invalid params" do + let(:environment_hash) { {} } + + it "returns a 400 response" do + expect(subject.status).to be 400 + expect(response_body[:errors]).to_not be nil + end + end +end diff --git a/spec/fixtures/approvals/modifiable_resources.approved.json b/spec/fixtures/approvals/modifiable_resources.approved.json index 293e95c05..ca0af5450 100644 --- a/spec/fixtures/approvals/modifiable_resources.approved.json +++ b/spec/fixtures/approvals/modifiable_resources.approved.json @@ -6,6 +6,12 @@ { "resource_class_name": "PactBroker::Api::Resources::Clean" }, + { + "resource_class_name": "PactBroker::Api::Resources::Environment" + }, + { + "resource_class_name": "PactBroker::Api::Resources::Environments" + }, { "resource_class_name": "PactBroker::Api::Resources::ErrorTest" }, diff --git a/spec/lib/pact_broker/api/contracts/environment_schema_spec.rb b/spec/lib/pact_broker/api/contracts/environment_schema_spec.rb new file mode 100644 index 000000000..8f2494894 --- /dev/null +++ b/spec/lib/pact_broker/api/contracts/environment_schema_spec.rb @@ -0,0 +1,83 @@ +require 'pact_broker/api/contracts/environment_schema' + +module PactBroker + module Api + module Contracts + describe EnvironmentSchema do + before do + allow(PactBroker::Deployments::EnvironmentService).to receive(:find_by_name).and_return(existing_environment) + end + let(:existing_environment) { nil } + let(:name) { "test" } + + let(:params) do + { + uuid: "1234", + name: name, + displayName: "Test", + production: false, + contacts: contacts + } + end + + let(:contacts) do + [{ + name: "Foo", + details: { email: "foo@bar.com" } + }] + end + + subject { EnvironmentSchema.call(params) } + + context "with valid params" do + it { is_expected.to be_empty } + end + + context "with a name with a new line" do + let(:name) { "test 1" } + + it { is_expected.to_not be_empty } + end + + context "with empty params" do + let(:params) { {} } + + it { is_expected.to_not be_empty } + end + + context "when there is another environment with the same name but a different uuid" do + let(:existing_environment) { instance_double("PactBroker::Deployments::Environment", uuid: "5678")} + + its([:name]) { is_expected.to eq ["Another environment with name 'test' already exists."] } + end + + context "when there is another environment with the same name and same uuid" do + let(:existing_environment) { instance_double("PactBroker::Deployments::Environment", uuid: "1234")} + + it { is_expected.to be_empty } + end + + context "with no owner name" do + let(:contacts) do + [{ + details: { email: "foo@bar.com" } + }] + end + + its([:contacts, 0]) { is_expected.to eq "name is missing at index 0" } + end + + context "with string contact details" do + let(:contacts) do + [{ + name: "foo", + details: "foo" + }] + end + + its([:contacts, 0]) { is_expected.to eq "details must be a hash at index 0" } + end + end + end + end +end diff --git a/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb b/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb index 511c998cf..4cbc86c0d 100644 --- a/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb +++ b/spec/lib/pact_broker/api/resources/default_base_resource_approval_spec.rb @@ -45,7 +45,7 @@ def resource.resource_exists? if resource.respond_to?(:policy_pacticipant) resource_class_data[:resource_class_data] = resource.policy_pacticipant end - resource_class_data + resource_class_data else nil end diff --git a/spec/support/shared_examples_for_responses.rb b/spec/support/shared_examples_for_responses.rb index 41b962778..e31ab79a4 100644 --- a/spec/support/shared_examples_for_responses.rb +++ b/spec/support/shared_examples_for_responses.rb @@ -22,6 +22,17 @@ end end +RSpec::Matchers.define :be_a_hal_json_created_response do + match do | actual | + expect(actual.status).to be 201 + expect(actual.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8' + end + + failure_message do + "Expected creation successful json response, got #{actual.status} #{actual.headers['Content-Type']} with body #{actual.body}" + end +end + RSpec::Matchers.define :be_a_json_response do match do | actual | expect(actual.headers['Content-Type']).to eq 'application/hal+json;charset=utf-8'