diff --git a/lib/diaspora_federation.rb b/lib/diaspora_federation.rb index 8ccd903..ebbb1ef 100644 --- a/lib/diaspora_federation.rb +++ b/lib/diaspora_federation.rb @@ -3,6 +3,7 @@ require "diaspora_federation/logging" require "diaspora_federation/callbacks" require "diaspora_federation/properties_dsl" require "diaspora_federation/entity" +require "diaspora_federation/validators" require "diaspora_federation/fetcher" diff --git a/lib/diaspora_federation/validators.rb b/lib/diaspora_federation/validators.rb new file mode 100644 index 0000000..a5f0e86 --- /dev/null +++ b/lib/diaspora_federation/validators.rb @@ -0,0 +1,32 @@ +require "validation" +require "validation/rule/not_empty" +require "validation/rule/email" +require "validation/rule/regular_expression" +require "validation/rule/uri" + +# +valid+ gem namespace +module Validation + # This module contains custom validation rules for various data field types. + # That includes types for which there are no provided rules by the +valid+ gem + # or types that are very specific to Diaspora* federation and need special handling. + # The rules are used inside the {DiasporaFederation::Validators validator classes} + # to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. + module Rule + end +end + +require "diaspora_federation/validators/rules/birthday" +require "diaspora_federation/validators/rules/boolean" +require "diaspora_federation/validators/rules/guid" +require "diaspora_federation/validators/rules/not_nil" +require "diaspora_federation/validators/rules/public_key" +require "diaspora_federation/validators/rules/tag_count" + +module DiasporaFederation + # Validators to perform basic sanity-checks on {DiasporaFederation::Entities federation entities}. + module Validators + end +end + +require "diaspora_federation/validators/person_validator" +require "diaspora_federation/validators/profile_validator" diff --git a/lib/diaspora_federation/validators/person_validator.rb b/lib/diaspora_federation/validators/person_validator.rb new file mode 100644 index 0000000..1e58117 --- /dev/null +++ b/lib/diaspora_federation/validators/person_validator.rb @@ -0,0 +1,17 @@ +module DiasporaFederation + module Validators + class PersonValidator < Validation::Validator + include Validation + + rule :guid, :guid + + rule :diaspora_handle, %i(not_empty email) + + rule :url, :u_r_i # WTF? :uri -> Uri -> "uninitialized constant Uri", :u_r_i -> URI -> \o/ + + rule :profile, :not_nil + + rule :exported_key, :public_key + end + end +end diff --git a/lib/diaspora_federation/validators/profile_validator.rb b/lib/diaspora_federation/validators/profile_validator.rb new file mode 100644 index 0000000..1e6cfa8 --- /dev/null +++ b/lib/diaspora_federation/validators/profile_validator.rb @@ -0,0 +1,22 @@ +module DiasporaFederation + module Validators + class ProfileValidator < Validation::Validator + include Validation + + rule :diaspora_handle, %i(not_empty email) + + # the name must not contain a semicolon because of mentions + # @{ ; } + rule :first_name, regular_expression: {regex: /\A[^;]{,32}\z/} + rule :last_name, regular_expression: {regex: /\A[^;]{,32}\z/} + + rule :tag_string, tag_count: {maximum: 5} + + rule :birthday, :birthday + + rule :searchable, :boolean + + rule :nsfw, :boolean + end + end +end diff --git a/lib/diaspora_federation/validators/rules/birthday.rb b/lib/diaspora_federation/validators/rules/birthday.rb new file mode 100644 index 0000000..3562922 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/birthday.rb @@ -0,0 +1,30 @@ +require "date" + +module Validation + module Rule + class Birthday + attr_reader :params + + # no parameters + def initialize + @params = {} + end + + def error_key + :birthday + end + + def valid_value?(value) + return true if value.nil? || (value.is_a?(String) && value.empty?) + return true if value.is_a? Date + + if value =~ /[0-9]{4}\-[0-9]{2}\-[0-9]{2}/ + date_field = value.split("-").map(&:to_i) + return Date.valid_civil?(date_field[0], date_field[1], date_field[2]) + end + + false + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/boolean.rb b/lib/diaspora_federation/validators/rules/boolean.rb new file mode 100644 index 0000000..f4498df --- /dev/null +++ b/lib/diaspora_federation/validators/rules/boolean.rb @@ -0,0 +1,30 @@ +module Validation + module Rule + class Boolean + attr_reader :params + + # no parameters + def initialize + @params = {} + end + + def error_key + :numeric + end + + def valid_value?(value) + return false if value.nil? + + if value.is_a?(String) + true if value =~ /\A(true|false|t|f|yes|no|y|n|1|0)\z/i + elsif value.is_a?(Fixnum) + true if value == 1 || value == 0 + elsif value.is_a?(TrueClass) || value.is_a?(FalseClass) + true + else + false + end + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/guid.rb b/lib/diaspora_federation/validators/rules/guid.rb new file mode 100644 index 0000000..7aca5d8 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/guid.rb @@ -0,0 +1,20 @@ +module Validation + module Rule + class Guid + attr_reader :params + + # no parameters + def initialize + @params = {} + end + + def error_key + :guid + end + + def valid_value?(value) + value.is_a?(String) && value.downcase =~ /\A[0-9a-z\-_@.:]{16,}\z/ + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/not_nil.rb b/lib/diaspora_federation/validators/rules/not_nil.rb new file mode 100644 index 0000000..834384f --- /dev/null +++ b/lib/diaspora_federation/validators/rules/not_nil.rb @@ -0,0 +1,20 @@ +module Validation + module Rule + class NotNil + attr_reader :params + + # no parameters + def initialize + @params = {} + end + + def error_key + :not_nil + end + + def valid_value?(value) + !value.nil? + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/public_key.rb b/lib/diaspora_federation/validators/rules/public_key.rb new file mode 100644 index 0000000..cc35e07 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/public_key.rb @@ -0,0 +1,24 @@ +module Validation + module Rule + class PublicKey + attr_reader :params + + # no parameters + def initialize + @params = {} + end + + def error_key + :public_key + end + + # allow both "PUBLIC KEY" and "RSA PUBLIC KEY" + def valid_value?(value) + (value.strip.start_with?("-----BEGIN PUBLIC KEY-----") && + value.strip.end_with?("-----END PUBLIC KEY-----")) || + (value.strip.start_with?("-----BEGIN RSA PUBLIC KEY-----") && + value.strip.end_with?("-----END RSA PUBLIC KEY-----")) + end + end + end +end diff --git a/lib/diaspora_federation/validators/rules/tag_count.rb b/lib/diaspora_federation/validators/rules/tag_count.rb new file mode 100644 index 0000000..8f48082 --- /dev/null +++ b/lib/diaspora_federation/validators/rules/tag_count.rb @@ -0,0 +1,27 @@ +module Validation + module Rule + # Rule for validating the number of tags in a string. + # Only the "#" characters will be counted. + class TagCount + attr_reader :params + + # @param [Hash] params + # @option params [Fixnum] :maximum maximum allowed tag count + def initialize(params) + unless params.include?(:maximum) && params[:maximum].is_a?(Fixnum) + raise "A number has to be specified for :maximum" + end + + @params = params + end + + def error_key + :tag_count + end + + def valid_value?(value) + value.count("#") <= params[:maximum] + end + end + end +end diff --git a/spec/factories.rb b/spec/factories.rb index 88173c4..79f2b69 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -5,12 +5,36 @@ def r_str end FactoryGirl.define do + sequence(:diaspora_handle) {|n| "person-#{n}-#{r_str}@localhost:3000" } + sequence(:public_key) { OpenSSL::PKey::RSA.generate(1024).public_key.export } + factory :person do - sequence(:diaspora_handle) {|n| "person-#{n}-#{r_str}@localhost:3000" } + diaspora_handle url "http://localhost:3000/" - serialized_public_key OpenSSL::PKey::RSA.generate(1024).public_key.export + serialized_public_key { generate(:public_key) } after(:create) do |u| u.save end end + + factory :person_entity, class: DiasporaFederation::Entities::Person do + guid UUID.generate :compact + diaspora_handle "testing@example.com" + url "http://localhost:3000/" + exported_key { generate(:public_key) } + profile { + DiasporaFederation::Entities::Profile.new( + FactoryGirl.attributes_for(:profile_entity, diaspora_handle: diaspora_handle)) + } + end + + factory :profile_entity, class: DiasporaFederation::Entities::Profile do + diaspora_handle "testing@example.com" + first_name "my_name" + last_name nil + tag_string "#i #love #tags" + birthday "1988-07-15" + searchable true + nsfw false + end end diff --git a/spec/lib/diaspora_federation/validators/person_validator_spec.rb b/spec/lib/diaspora_federation/validators/person_validator_spec.rb new file mode 100644 index 0000000..f954d36 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/person_validator_spec.rb @@ -0,0 +1,41 @@ +module DiasporaFederation + describe Validators::PersonValidator do + it "validates a well-formed instance" do + instance = OpenStruct.new(FactoryGirl.attributes_for(:person_entity)) + validator = Validators::PersonValidator.new(instance) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it_behaves_like "a diaspora_handle validator" do + let(:entity) { :person_entity } + let(:validator_class) { Validators::PersonValidator } + let(:property) { :diaspora_handle } + end + + it_behaves_like "a guid validator" do + let(:entity) { :person_entity } + let(:validator_class) { Validators::PersonValidator } + let(:property) { :guid } + end + + context "#exported_key" do + it "fails for malformed rsa key" do + instance = OpenStruct.new(FactoryGirl.attributes_for(:person_entity, exported_key: "ASDF")) + validator = Validators::PersonValidator.new(instance) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:exported_key) + end + + it "must not be empty" do + instance = OpenStruct.new(FactoryGirl.attributes_for(:person_entity, exported_key: "")) + validator = Validators::PersonValidator.new(instance) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:exported_key) + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/profile_validator_spec.rb b/spec/lib/diaspora_federation/validators/profile_validator_spec.rb new file mode 100644 index 0000000..ef11474 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/profile_validator_spec.rb @@ -0,0 +1,94 @@ +module DiasporaFederation + describe Validators::ProfileValidator do + def profile_stub(data={}) + OpenStruct.new(FactoryGirl.attributes_for(:profile_entity).merge(data)) + end + + it "validates a well-formed instance" do + validator = Validators::ProfileValidator.new(profile_stub) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it_behaves_like "a diaspora_handle validator" do + let(:entity) { :profile_entity } + let(:validator_class) { Validators::ProfileValidator } + let(:property) { :diaspora_handle } + end + + %i(first_name last_name).each do |prop| + describe "##{prop}" do + it "allowed to contain special chars" do + validator = Validators::ProfileValidator.new(profile_stub(prop => "cool name ©")) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must not exceed 32 chars" do + validator = Validators::ProfileValidator.new(profile_stub(prop => "abcdefghijklmnopqrstuvwxyz_aaaaaaaaaa")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(prop) + end + + it "must not contain semicolons" do + validator = Validators::ProfileValidator.new(profile_stub(prop => "asdf;qwer;yxcv")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(prop) + end + end + end + + describe "#tag_string" do + it "must not contain more than 5 tags" do + validator = Validators::ProfileValidator.new( + profile_stub(tag_string: "#i #have #too #many #tags #in #my #profile")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:tag_string) + end + end + + describe "#birthday" do + it "may be empty or nil" do + [nil, ""].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "may be a Date or date string" do + [Date.parse("2013-06-29"), "2013-06-29"].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "must not be an arbitrary string or other object" do + ["asdf asdf", true, 1234].each do |val| + validator = Validators::ProfileValidator.new(profile_stub(birthday: val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:birthday) + end + end + end + + %i(searchable nsfw).each do |prop| + describe "##{prop}" do + it_behaves_like "a boolean validator" do + let(:entity) { :profile_entity } + let(:validator_class) { Validators::ProfileValidator } + let(:property) { prop } + end + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb b/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb new file mode 100644 index 0000000..e4811cc --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/birthday_spec.rb @@ -0,0 +1,50 @@ +describe Validation::Rule::Birthday do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:birthday, birthday: {param: true}) + }.to raise_error ArgumentError + end + + context "validation" do + it "validates a date object" do + validator = Validation::Validator.new(OpenStruct.new(birthday: Date.new)) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "2015-07-19")) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates an empty string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "")) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates nil" do + validator = Validation::Validator.new(OpenStruct.new(birthday: nil)) + validator.rule(:birthday, :birthday) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails for invalid date string" do + validator = Validation::Validator.new(OpenStruct.new(birthday: "i'm no date")) + validator.rule(:birthday, :birthday) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:birthday) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb b/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb new file mode 100644 index 0000000..dc32f9a --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/boolean_spec.rb @@ -0,0 +1,70 @@ +describe Validation::Rule::Boolean do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:number, numeric: {param: true}) + }.to raise_error ArgumentError + end + + context "validation" do + context "strings" do + it "validates boolean-esque strings" do + %w(true false yes no t f y n 1 0).each do |str| + validator = Validation::Validator.new(OpenStruct.new(boolean: str)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "fails for non-boolean-esque strings" do + validator = Validation::Validator.new(OpenStruct.new(boolean: "asdf")) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end + + context "numbers" do + it "validates 0 and 1 to boolean" do + [0, 1].each do |num| + validator = Validation::Validator.new(OpenStruct.new(boolean: num)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "fails for all other numbers" do + validator = Validation::Validator.new(OpenStruct.new(boolean: 1234)) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end + + context "boolean types" do + it "validates true and false" do + [true, false].each do |bln| + validator = Validation::Validator.new(OpenStruct.new(boolean: bln)) + validator.rule(:boolean, :boolean) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + end + + it "fails for nil" do + validator = Validation::Validator.new(OpenStruct.new(boolean: nil)) + validator.rule(:boolean, :boolean) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:boolean) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/guid_spec.rb b/spec/lib/diaspora_federation/validators/rules/guid_spec.rb new file mode 100644 index 0000000..5cd0b82 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/guid_spec.rb @@ -0,0 +1,59 @@ +describe Validation::Rule::Guid do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:guid, guid: {param: true}) + }.to raise_error ArgumentError + end + + context "validation" do + it "validates a string at least 16 chars long, consisting of [0-9a-f] (diaspora)" do + validator = Validation::Validator.new(OpenStruct.new(guid: "abcdef0123456789")) + validator.rule(:guid, :guid) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a long string with random characters and [-_@.:] (redmatrix)" do + validator = Validation::Validator.new( + OpenStruct.new(guid: "1234567890ABCDefgh_ijkl-mnopqrSTUVwxyz@example.com:3000")) + validator.rule(:guid, :guid) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if the string is too short" do + validator = Validation::Validator.new(OpenStruct.new(guid: "012345")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string contains invalid chars" do + validator = Validation::Validator.new(OpenStruct.new(guid: "ghijklmnopqrstuvwxyz++")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string is empty" do + validator = Validation::Validator.new(OpenStruct.new(guid: "")) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + + it "fails if the string is nil" do + validator = Validation::Validator.new(OpenStruct.new(guid: nil)) + validator.rule(:guid, :guid) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:guid) + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb b/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb new file mode 100644 index 0000000..5d1ada0 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/public_key_spec.rb @@ -0,0 +1,51 @@ +describe Validation::Rule::Guid do + it "will not accept parameters" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:key, public_key: {param: true}) + }.to raise_error ArgumentError + end + + context "validation" do + ["PUBLIC KEY", "RSA PUBLIC KEY"].each do |key_type| + context key_type do + let(:prefix) { "-----BEGIN #{key_type}-----" } + let(:suffix) { "-----END #{key_type}-----" } + + let(:key) { "#{prefix}\nAAAAAA==\n#{suffix}\n" } + + it "validates an exported RSA key" do + validator = Validation::Validator.new(OpenStruct.new(key: key)) + validator.rule(:key, :public_key) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "strips whitespace" do + validator = Validation::Validator.new(OpenStruct.new(key: " \n #{key}\n \n ")) + validator.rule(:key, :public_key) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails if the prefix is missing" do + validator = Validation::Validator.new(OpenStruct.new(key: "\nAAAAAA==\n#{suffix}\n")) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + + it "fails if the suffix is missing" do + validator = Validation::Validator.new(OpenStruct.new(key: "#{prefix}\nAAAAAA==\n\n")) + validator.rule(:key, :public_key) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:key) + end + end + end + end +end diff --git a/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb b/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb new file mode 100644 index 0000000..32c1aa5 --- /dev/null +++ b/spec/lib/diaspora_federation/validators/rules/tag_count_spec.rb @@ -0,0 +1,36 @@ +describe Validation::Rule::TagCount do + it "requires a parameter" do + validator = Validation::Validator.new({}) + expect { + validator.rule(:tags, :tag_count) + }.to raise_error ArgumentError + end + + context "validation" do + let(:tag_str) { "#i #love #tags" } + + it "validates less tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 5}) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates exactly as many tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 3}) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "fails for too many tags" do + validator = Validation::Validator.new(OpenStruct.new(tags: tag_str)) + validator.rule(:tags, tag_count: {maximum: 1}) + + expect(validator).not_to be_valid + expect(validator.errors).to include(:tags) + end + end +end diff --git a/spec/support/shared_validator_specs.rb b/spec/support/shared_validator_specs.rb new file mode 100644 index 0000000..7601335 --- /dev/null +++ b/spec/support/shared_validator_specs.rb @@ -0,0 +1,85 @@ +def entity_stub(entity, property, val=nil) + instance = OpenStruct.new(FactoryGirl.attributes_for(entity)) + instance.public_send("#{property}=", val) unless val.nil? + instance +end + +shared_examples "a diaspora_handle validator" do + it "validates a well-formed diaspora_handle" do + validator = validator_class.new(entity_stub(entity, property)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must not be empty" do + validator = validator_class.new(entity_stub(entity, property, "")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must resemble an email address" do + validator = validator_class.new(entity_stub(entity, property, "i am a weird handle @@@ ### 12345")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a guid validator" do + it "validates a well-formed guid" do + validator = validator_class.new(entity_stub(entity, property)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "validates a well-formed guid from redmatrix" do + validator = validator_class.new(entity_stub(entity, property, "1234567890ABCDefgh_ijkl-mnopQR@example.com:3000")) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + + it "must be at least 16 chars" do + validator = validator_class.new(entity_stub(entity, property, "aaaaaa")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must only contain [0-9a-z-_@.:]" do + validator = validator_class.new(entity_stub(entity, property, "zzz+-#*$$")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + + it "must not be empty" do + validator = validator_class.new(entity_stub(entity, property, "")) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end +end + +shared_examples "a boolean validator" do + it "validates a well-formed boolean" do + [true, "true", false, "false"].each do |val| + validator = validator_class.new(entity_stub(entity, property, val)) + + expect(validator).to be_valid + expect(validator.errors).to be_empty + end + end + + it "must not be an arbitrary string or other object" do + ["asdf", Time.zone.today, 1234].each do |val| + validator = validator_class.new(entity_stub(entity, property, val)) + + expect(validator).not_to be_valid + expect(validator.errors).to include(property) + end + end +end