cloudfoundry/cloud_controller_ng

View on GitHub
spec/unit/messages/validators_spec.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'spec_helper'
require 'lightweight_spec_helper'
require 'messages/validators'
require 'messages/base_message'
require 'messages/empty_lifecycle_data_message'
require 'messages/buildpack_lifecycle_data_message'
require 'cloud_controller/diego/lifecycles/app_docker_lifecycle'
require 'cloud_controller/diego/lifecycles/app_buildpack_lifecycle'
require 'cloud_controller/diego/lifecycles/lifecycles'
require 'rspec/collection_matchers'

module VCAP::CloudController::Validators
  RSpec.describe 'Validators' do
    let(:fake_class) do
      Class.new do
        include ActiveModel::Model
        include VCAP::CloudController::Validators

        attr_accessor :field

        def self.model_name
          ActiveModel::Name.new(self, nil, 'fake class')
        end
      end
    end

    describe 'validator extending StandaloneValidator' do
      describe '.validate_each' do
        it 'calls through to the instance method so it can be easily used outside of Active Models' do
          my_validator = Class.new(ActiveModel::EachValidator) do
            extend StandaloneValidator

            def validate_each(record, attr_name, value)
              "hello #{record} #{attr_name} #{value}"
            end
          end

          expect(my_validator.validate_each(1, 2, 3)).to eq('hello 1 2 3')
        end
      end
    end

    describe 'ArrayValidator' do
      let(:array_class) do
        Class.new(fake_class) do
          validates :field, array: true
        end
      end

      it 'adds an error if the field is not an array' do
        fake_class = array_class.new field: 'not array'
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be an array'
      end

      it 'does not add an error if the field is an array' do
        fake_class = array_class.new field: %w[an array]
        expect(fake_class).to be_valid
      end
    end

    describe 'StringValidator' do
      let(:string_class) do
        Class.new(fake_class) do
          validates :field, string: true
        end
      end

      it 'adds an error if the field is not a string' do
        fake_class = string_class.new field: {}
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be a string'
      end

      it 'does not add an error if the field is a string' do
        fake_class = string_class.new field: 'hi i am string'
        expect(fake_class).to be_valid
      end
    end

    describe 'BooleanValidator' do
      let(:boolean_class) do
        Class.new(fake_class) do
          validates :field, boolean: true
        end
      end

      it 'adds an error if the field is not a boolean' do
        instance = boolean_class.new field: {}
        expect(instance).not_to be_valid
        expect(instance.errors[:field]).to include 'must be a boolean'
      end

      it 'does not add an error if the field is a boolean' do
        instance = boolean_class.new field: true
        expect(instance).to be_valid

        instance = boolean_class.new field: false
        expect(instance).to be_valid
      end
    end

    describe 'BooleanStringValidator' do
      let(:boolean_class) do
        Class.new(fake_class) do
          validates :field, boolean_string: true
        end
      end

      it 'adds an error if the field is not a boolean string' do
        instance = boolean_class.new field: 'snarf'
        expect(instance).not_to be_valid
        expect(instance.errors[:field]).to include "must be 'true' or 'false'"
      end

      it 'does not add an error if the field is a boolean string' do
        instance = boolean_class.new field: 'true'
        expect(instance).to be_valid

        instance = boolean_class.new field: 'false'
        expect(instance).to be_valid
      end
    end

    describe 'HashValidator' do
      let(:hash_class) do
        Class.new(fake_class) do
          validates :field, hash: true
        end
      end

      it 'adds an error if the field is not an object' do
        fake_class = hash_class.new field: 'not an object'
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be an object'
      end

      it 'does not add an error if the field is a hash' do
        fake_class = hash_class.new field: { totes: 'hash' }
        expect(fake_class).to be_valid
      end
    end

    describe 'GuidValidator' do
      let(:guid_class) do
        Class.new(fake_class) do
          validates :field, guid: true
        end
      end

      it 'adds an error if the field is not a string' do
        fake_class = guid_class.new field: 4
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be a string'
      end

      it 'adds an error if the field is nil' do
        fake_class = guid_class.new field: nil
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be a string'
      end

      it 'adds an error if the field is too long' do
        fake_class = guid_class.new field: 'a' * 201
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be between 1 and 200 characters'
      end

      it 'adds an error if the field is empty' do
        fake_class = guid_class.new field: ''
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be between 1 and 200 characters'
      end

      it 'does not add an error if the field is a guid' do
        fake_class = guid_class.new field: 'such-a-guid-1234'
        expect(fake_class).to be_valid
      end
    end

    describe 'UriValidator' do
      let(:uri_class) do
        Class.new(fake_class) do
          validates :field, uri: true
        end
      end

      it 'adds an error if the field is not a URI' do
        fake_class = uri_class.new field: 'not a URI'
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be a valid URI'
      end

      it 'does not add an error if the field is a URI' do
        fake_class = uri_class.new field: 'http://www.purple.com'
        expect(fake_class).to be_valid
      end
    end

    describe 'EnvironmentVariablesValidator' do
      let(:environment_variables_class) do
        Class.new(fake_class) do
          validates :field, environment_variables: true
        end
      end

      it 'does not add an error if the environment variables are correct' do
        fake_class = environment_variables_class.new field: { VARIABLE: 'amazing' }
        expect(fake_class).to be_valid
      end

      it 'validates that the input is a hash' do
        fake_class = environment_variables_class.new field: 4
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be an object'
      end

      it 'does not allow variables that start with VCAP_' do
        fake_class = environment_variables_class.new field: { VCAP_BANANA: 'woo' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VCAP_'
      end

      it 'does not allow variables that start with vcap_' do
        fake_class = environment_variables_class.new field: { vcap_donkey: 'hee-haw' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VCAP_'
      end

      it 'does not allow variables that start with VMC_' do
        fake_class = environment_variables_class.new field: { VMC_BANANA: 'woo' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VMC_'
      end

      it 'does not allow variables that start with vmc_' do
        fake_class = environment_variables_class.new field: { vmc_donkey: 'hee-haw' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VMC_'
      end

      it 'does not allow variables that are PORT' do
        fake_class = environment_variables_class.new field: { PORT: 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot set PORT'
      end

      it 'does not allow variables that are port' do
        fake_class = environment_variables_class.new field: { port: 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot set PORT'
      end

      it 'does not allow variables with zero key length' do
        fake_class = environment_variables_class.new field: { '': 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'key must be a minimum length of 1'
      end

      it 'does not allow variables with non-string keys' do
        fake_class = environment_variables_class.new field: { 1 => 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'key must be a string'
      end
    end

    describe 'EnvironmentVariablesStringValuesValidator' do
      let(:environment_variables_class) do
        Class.new(fake_class) do
          validates :field, environment_variables_string_values: true
        end
      end

      it 'does not add an error if the environment variables are correct' do
        fake_class = environment_variables_class.new field: { VARIABLE: 'amazing' }
        expect(fake_class).to be_valid
      end

      it 'validates that the input is a hash' do
        fake_class = environment_variables_class.new field: 4
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be an object'
      end

      it 'does not allow variables that start with VCAP_' do
        fake_class = environment_variables_class.new field: { VCAP_BANANA: 'woo' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VCAP_'
      end

      it 'does not allow variables that start with vcap_' do
        fake_class = environment_variables_class.new field: { vcap_donkey: 'hee-haw' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VCAP_'
      end

      it 'does not allow variables that start with VMC_' do
        fake_class = environment_variables_class.new field: { VMC_BANANA: 'woo' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VMC_'
      end

      it 'does not allow variables that start with vmc_' do
        fake_class = environment_variables_class.new field: { vmc_donkey: 'hee-haw' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot start with VMC_'
      end

      it 'does not allow variables that are PORT' do
        fake_class = environment_variables_class.new field: { PORT: 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot set PORT'
      end

      it 'does not allow variables that are port' do
        fake_class = environment_variables_class.new field: { port: 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'cannot set PORT'
      end

      it 'does not allow variables with zero key length' do
        fake_class = environment_variables_class.new field: { '': 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'key must be a minimum length of 1'
      end

      it 'does not allow variables with non-string keys' do
        fake_class = environment_variables_class.new field: { 1 => 'el lunes nos ponemos camisetas naranjas' }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'key must be a string'
      end

      it 'does not allow variables with array values' do
        fake_class = environment_variables_class.new field: { fibonacci: [1, 1, 2, 3, 5, 8] }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:base]).to eq ["Non-string value in environment variable for key 'fibonacci', value '[1,1,2,3,5,8]'"]
      end

      it 'does not allow variables with object values' do
        fake_class = environment_variables_class.new field: { obj: { wow: 'cool' } }
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:base]).to eq ["Non-string value in environment variable for key 'obj', value '{\"wow\":\"cool\"}'"]
      end
    end

    describe 'FieldsValidator' do
      let(:fields_class) do
        Class.new(fake_class) do
          validates :field, fields: { allowed: { 'space.organization' => ['name'] } }
        end
      end

      it 'rejects values that are not hashes' do
        fake_class = fields_class.new field: 'foo'
        expect(fake_class).not_to be_valid
        expect(fake_class.errors[:field]).to include 'must be an object'
      end

      context 'allowed keys' do
        let(:fields_class_multiple_keys) do
          Class.new(fake_class) do
            validates :field, fields: { allowed: { 'some.resource' => %w[fake-value-1 fake-value-2] } }
          end
        end

        it 'allows a multiple keys to be present' do
          fake_class = fields_class_multiple_keys.new field: { 'some.resource': %w[fake-value-2 fake-value-1] }
          expect(fake_class).to be_valid
        end

        it 'allows a subset of keys' do
          fake_class = fields_class_multiple_keys.new field: { 'some.resource': %w[fake-value-2] }
          expect(fake_class).to be_valid
        end

        it 'reject keys not in the list' do
          fake_class = fields_class_multiple_keys.new field: { 'some.resource': %w[fake-value-2 url] }
          expect(fake_class).not_to be_valid
          expect(fake_class.errors[:field]).to include "valid keys for 'some.resource' are: 'fake-value-1', 'fake-value-2'"
        end
      end

      context 'allowed resources' do
        let(:fields_class_multiple_resources) do
          Class.new(fake_class) do
            validates :field, fields: { allowed: { 'a.resource' => ['fake-value'], 'another.resource' => ['another-fake-value'] } }
          end
        end

        it 'allows a multiple resources to be present' do
          fake_class = fields_class_multiple_resources.new field: { 'a.resource': %w[fake-value], 'another.resource': %w[another-fake-value] }
          expect(fake_class).to be_valid
        end

        it 'allows a subset of the resources to be present' do
          fake_class = fields_class_multiple_resources.new field: { 'another.resource': %w[another-fake-value] }
          expect(fake_class).to be_valid
        end

        it 'rejects resources not specified' do
          fake_class = fields_class_multiple_resources.new field: { 'wrong.resource': %w[another-fake-value] }
          expect(fake_class).not_to be_valid
          expect(fake_class.errors[:field]).to include "[wrong.resource] valid resources are: 'a.resource', 'another.resource'"
        end
      end
    end

    describe 'HealthCheckValidator' do
      let(:health_check_class) do
        Class.new(fake_class) do
          attr_accessor :health_check_type, :health_check_http_endpoint

          validates_with HealthCheckValidator
        end
      end

      context 'when the healthcheck type is not "http"' do
        it 'correctly adds the health_check_type validation errors' do
          message = health_check_class.new({
                                             health_check_type: 'not-http',
                                             health_check_http_endpoint: 'a-great-uri'
                                           })

          expect(message).not_to be_valid
          expect(message.errors_on(:health_check_type)).to include('must be "http" to set a health check HTTP endpoint')
        end
      end
    end

    describe 'LifecycleValidator' do
      let(:lifecycle_class) do
        Class.new(fake_class) do
          attr_accessor :lifecycle

          validates_with LifecycleValidator

          def lifecycle_data
            lifecycle[:data] || lifecycle['data']
          end

          def lifecycle_type
            lifecycle[:type] || lifecycle['type']
          end
        end
      end

      context 'when the lifecycle type provided is invalid' do
        it 'adds lifecycle_type error message to the base class' do
          message = lifecycle_class.new({ lifecycle: { type: 'not valid', data: {} } })

          expect(message).not_to be_valid
          expect(message.errors_on(:lifecycle_type)).to include('is not included in the list: buildpack, docker')
        end
      end

      context 'when the lifecycle type is not provided' do
        it 'correctly adds the buildpack data message validation errors' do
          message = lifecycle_class.new({ lifecycle: { data: { buildpacks: [123] } } })

          expect(message).not_to be_valid
          expect(message.errors_on(:lifecycle)).to contain_exactly('Buildpacks can only contain strings')
        end
      end

      context 'when lifecycle type provided is buildpack' do
        context 'when the buildpack lifecycle data is invalid' do
          it 'correctly adds the buildpack data message validation errors' do
            message = lifecycle_class.new({ lifecycle: { type: 'buildpack', data: { buildpacks: [123] } } })

            expect(message).not_to be_valid
            expect(message.errors_on(:lifecycle)).to include('Buildpacks can only contain strings')
          end
        end
      end
    end

    describe 'DataValidator' do
      class DataMessage < VCAP::CloudController::BaseMessage
        register_allowed_keys [:data]
        validates_with DataValidator

        class Data < VCAP::CloudController::BaseMessage
          register_allowed_keys [:foo]

          validates :foo, numericality: true
        end
      end

      it "adds data's error message to the base class" do
        message = DataMessage.new({ data: { foo: 'not a number' } })
        expect(message).not_to be_valid
        expect(message.errors_on(:data)).to include('Foo is not a number')
      end

      it 'returns early when base class data is not an object' do
        message = DataMessage.new({ data: 'not an object' })
        expect(message).to be_valid
        expect(message.errors_on(:data)).to be_empty
      end
    end

    describe 'RelationshipValidator' do
      class RelationshipMessage < VCAP::CloudController::BaseMessage
        register_allowed_keys [:relationships]

        def relationships_message
          Relationships.new(relationships.deep_symbolize_keys)
        end

        validates_with RelationshipValidator

        class Relationships < VCAP::CloudController::BaseMessage
          register_allowed_keys [:foo]

          validates :foo, numericality: true
        end
      end

      it "adds relationships' error message to the base class" do
        message = RelationshipMessage.new({ relationships: { foo: 'not a number' } })
        expect(message).not_to be_valid
        expect(message.errors_on(:relationships)).to include('Foo is not a number')
      end

      it 'returns early when base class relationships is not an object' do
        message = RelationshipMessage.new({ relationships: 'not an object' })
        expect(message).not_to be_valid
        expect(message.errors_on(:relationships)).to include("'relationships' is not an object")
      end
    end

    describe 'ToOneRelationshipValidator' do
      let(:to_one_class) do
        Class.new(fake_class) do
          validates :field, to_one_relationship: true
        end
      end

      it 'ensures that the data has the correct structure' do
        bad_guid_key = to_one_class.new({ field: { data: { not_a_guid: '1234' } } })
        bad_guid_value = to_one_class.new({ field: { data: { guid: { woah: '1234' } } } })

        bad_data_key = to_one_class.new({ field: { not_data: '1234' } })
        bad_data_value = to_one_class.new({ field: { data: '1234' } })
        missing_data = to_one_class.new({ field: '1234' })

        valid = to_one_class.new(field: { data: { guid: '1234' } })

        expect(bad_guid_key).not_to be_valid
        expect(bad_guid_value).not_to be_valid
        expect(bad_data_key).not_to be_valid
        expect(bad_data_value).not_to be_valid
        expect(missing_data).not_to be_valid
        expect(valid).to be_valid
      end

      it 'allows for nil value in data' do
        valid = to_one_class.new(field: { data: nil })

        expect(valid).to be_valid
      end

      it 'adds an error if the field is not structured correctly' do
        invalid = to_one_class.new({ field: { data: { not_a_guid: 1234 } } })
        expect(invalid).not_to be_valid
        expect(invalid.errors[:field]).to include 'must be structured like this: "field: {"data": {"guid": "valid-guid"}}"'
      end
    end

    describe 'ToManyRelationshipValidator' do
      let(:to_many_class) do
        Class.new(fake_class) do
          validates :field, to_many_relationship: true
        end
      end

      it 'ensures that the data has the correct structure' do
        valid = to_many_class.new({ field: {
                                    data: [{ guid: '1234' }, { guid: '1234' }, { guid: '1234' }, { guid: '1234' }]
                                  } })
        invalid_one = to_many_class.new({ field: { data: { guid: '1234' } } })
        invalid_two = to_many_class.new({ field: { data: [{ guid: 1234 }, { guid: 1234 }] } })
        invalid_three = to_many_class.new({ field: [{ guid: '1234' }, { guid: '1234' }, { guid: '1234' }, { guid: '1234' }] })

        expect(valid).to be_valid
        expect(invalid_one).not_to be_valid
        expect(invalid_two).not_to be_valid
        expect(invalid_three).not_to be_valid
      end
    end

    describe 'OrgVisibilityValidator' do
      let(:visibility_class) do
        Class.new(fake_class) do
          validates :field, org_visibility: true
        end
      end

      it 'ensures that it has correct structure' do
        valid = visibility_class.new({ field: [{ guid: '1234' }, { guid: '1234' }, { guid: '1234' }, { guid: '1234' }] })
        invalid_one = visibility_class.new({ field: { guid: '1234' } })
        invalid_two = visibility_class.new({ field: [{ guid: 1234 }, { guid: 1234 }] })
        invalid_three = visibility_class.new({ field: ['123'] })

        expect(valid).to be_valid
        expect(invalid_one).not_to be_valid
        expect(invalid_two).not_to be_valid
        expect(invalid_three).not_to be_valid
      end
    end

    describe 'TimestampValidator' do
      let(:timestamp_class) do
        Class.new(fake_class) do
          validates :field, timestamp: true
        end
      end

      it 'requires a hash or an array of timestamps' do
        message = timestamp_class.new({ field: 47 })
        expect(message).not_to be_valid
        expect(message.errors[:field]).to include('relational operator and timestamp must be specified')
      end

      it 'requires a valid relational operator' do
        message = timestamp_class.new({ field: { garbage: Time.now.utc.iso8601 } })
        expect(message).not_to be_valid
        expect(message.errors[:field]).to include("Invalid relational operator: 'garbage'")
      end

      context 'requires a valid timestamp' do
        it 'does not accept a malformed timestamp' do
          message = timestamp_class.new({ field: [Time.now.utc.iso8601.to_s, 'bogus'] })
          expect(message).not_to be_valid
          expect(message.errors[:field]).to include("has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
        end

        it 'does not accept garbage' do
          message = timestamp_class.new({ field: { gt: 123 } })
          expect(message).not_to be_valid
          expect(message.errors[:field]).to include("has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
        end

        it "does not accept fractional seconds even though it's ISO 8601-compliant" do
          message = timestamp_class.new({ field: { gt: '2020-06-30T12:34:56.78Z' } })
          expect(message).not_to be_valid
          expect(message.errors[:field]).to include("has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
        end

        it "does not accept local time zones even though it's ISO 8601-compliant" do
          message = timestamp_class.new({ field: { gt: '2020-06-30T12:34:56.78-0700' } })
          expect(message).not_to be_valid
          expect(message.errors[:field]).to include("has an invalid timestamp format. Timestamps should be formatted as 'YYYY-MM-DDThh:mm:ssZ'")
        end
      end

      it 'allows comma-separated timestamps' do
        message = timestamp_class.new({ field: [Time.now.utc.iso8601.to_s, Time.now.utc.iso8601.to_s] })
        expect(message).to be_valid
      end

      it 'allows the lt operator' do
        message = timestamp_class.new({ field: { lt: Time.now.utc.iso8601 } })
        expect(message).to be_valid
      end

      it 'allows the lte operator' do
        message = timestamp_class.new({ field: { lte: Time.now.utc.iso8601 } })
        expect(message).to be_valid
      end

      it 'allows the gt operator' do
        message = timestamp_class.new({ field: { gt: Time.now.utc.iso8601 } })
        expect(message).to be_valid
      end

      it 'allows the gte operator' do
        message = timestamp_class.new({ field: { gte: Time.now.utc.iso8601 } })
        expect(message).to be_valid
      end

      it 'does not allow multiple timestamps with an operator' do
        message = timestamp_class.new({ field: { gte: "#{Time.now.utc.iso8601},#{Time.now.utc.iso8601}" } })
        expect(message).not_to be_valid
        expect(message.errors[:field]).to include('only accepts one value when using a relational operator')
      end

      context 'when the operator is an equals operator' do
        it 'allows the equals operator' do
          message = timestamp_class.new({ field: [Time.now.utc.iso8601] })
          expect(message).to be_valid
        end
      end
    end

    describe 'TargetGuidsValidator' do
      class TargetGuidsMessage < VCAP::CloudController::BaseMessage
        register_allowed_keys [:target_guids]

        validates_with TargetGuidsValidator
      end

      it 'does not allow non-array values' do
        message = TargetGuidsMessage.new({ target_guids: 'not an array' })
        expect(message).not_to be_valid
        expect(message.errors_on(:target_guids)).to contain_exactly('target_guids must be an array')
      end

      it 'is valid for an array' do
        message = TargetGuidsMessage.new({ target_guids: %w[guid1 guid2] })
        expect(message).to be_valid
      end

      it 'does not allow random operators' do
        message = TargetGuidsMessage.new({ target_guids: { weyman: ['not a number'] } })
        expect(message).not_to be_valid
        expect(message.errors_on(:target_guids)).to contain_exactly('target_guids has an invalid operator')
      end

      it 'allows the not operator' do
        message = TargetGuidsMessage.new({ target_guids: { not: ['guid1'] } })
        expect(message).to be_valid
      end

      it 'does not allow non-array values in the "not" field' do
        message = TargetGuidsMessage.new({ target_guids: { not: 'not an array' } })
        expect(message).not_to be_valid
        expect(message.errors_on(:target_guids)).to contain_exactly('target_guids must be an array')
      end
    end
  end
end