From a10cb05bb776f68568456cb43ee0763b205ea8e9 Mon Sep 17 00:00:00 2001 From: n atta kusi adusei Date: Sun, 25 Aug 2024 22:09:03 +0000 Subject: [PATCH] Add `#have_error` spec expectation (#1062) * Add `#have_error` spec expectation * Add missing method return types * Fix lint errors As reported by *Ameba*: ``` src/avram/expectations/have_custom_error_expectation.cr:29:7 [Correctable] [C] Style/RedundantReturn: Redundant `return` detected > return <<-MSG ^ src/avram/expectations/have_error_expectation.cr:67:29 [W] Lint/ShadowingOuterLocalVar: Shadowing outer local variable `errors` > errors.join do |name, errors| ``` --- .../have_custom_error_expectation_spec.cr | 112 +++++++++++++++ .../have_error_expectation_spec.cr | 128 ++++++++++++++++++ src/avram/expectations.cr | 35 +++++ .../have_custom_error_expectation.cr | 35 +++++ .../expectations/have_error_expectation.cr | 81 +++++++++++ 5 files changed, 391 insertions(+) create mode 100644 spec/avram/expectations/have_custom_error_expectation_spec.cr create mode 100644 spec/avram/expectations/have_error_expectation_spec.cr create mode 100644 src/avram/expectations.cr create mode 100644 src/avram/expectations/have_custom_error_expectation.cr create mode 100644 src/avram/expectations/have_error_expectation.cr diff --git a/spec/avram/expectations/have_custom_error_expectation_spec.cr b/spec/avram/expectations/have_custom_error_expectation_spec.cr new file mode 100644 index 000000000..44497d229 --- /dev/null +++ b/spec/avram/expectations/have_custom_error_expectation_spec.cr @@ -0,0 +1,112 @@ +require "../../spec_helper" + +private class SaveEmailAddress < EmailAddress::SaveOperation +end + +include Avram::Expectations + +describe Avram::Expectations::HaveCustomErrorExpectation do + describe "#have_error" do + context "in positive assertions" do + it "passes if attribute is invalid" do + operation = SaveEmailAddress.new + operation.add_error(:providers, "is empty") + + operation.should have_error + operation.should have_error("is empty") + operation.should have_error(/\sempty/) + + operation.should have_error(:providers) + operation.should have_error(:providers, "is empty") + operation.should have_error(:providers, /\sempty/) + end + + it "fails if attribute is valid" do + operation = SaveEmailAddress.new + + expect_raises Spec::AssertionFailed, "have an error" do + operation.should have_error(:providers) + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(:providers, "is empty") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(:providers, /\sempty/) + end + end + + it "fails if attribute is invalid but without the given message" do + operation = SaveEmailAddress.new + operation.add_error(:providers, "is empty") + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error("wrong message") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(/\smessage/) + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(:providers, "wrong message") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(:providers, /\smessage/) + end + end + end + + context "in negative assertions" do + it "passes if attribute is valid" do + operation = SaveEmailAddress.new + + operation.should_not have_error(:providers) + operation.should_not have_error(:providers, "is empty") + operation.should_not have_error(:providers, /\sempty/) + end + + it "fails if attribute is invalid" do + operation = SaveEmailAddress.new + operation.add_error(:providers, "is empty") + + expect_raises Spec::AssertionFailed, "not have an error" do + operation.should_not have_error + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error("is empty") + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error(/\sempty/) + end + + expect_raises Spec::AssertionFailed, "not have an error" do + operation.should_not have_error(:providers) + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error(:providers, "is empty") + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error(:providers, /\sempty/) + end + end + + it "passes if attribute is invalid but without the given message" do + operation = SaveEmailAddress.new + operation.add_error(:providers, "is required") + + operation.should_not have_error("wrong message") + operation.should_not have_error(/\smessage/) + + operation.should_not have_error(:providers, "wrong message") + operation.should_not have_error(:providers, /\smessage/) + end + end + end +end diff --git a/spec/avram/expectations/have_error_expectation_spec.cr b/spec/avram/expectations/have_error_expectation_spec.cr new file mode 100644 index 000000000..c33448797 --- /dev/null +++ b/spec/avram/expectations/have_error_expectation_spec.cr @@ -0,0 +1,128 @@ +require "../../spec_helper" + +private class SaveEmailAddress < EmailAddress::SaveOperation +end + +include Avram::Expectations + +describe Avram::Expectations::HaveErrorExpectation do + describe "#have_error" do + context "in positive assertions" do + it "passes if attribute is invalid" do + operation = SaveEmailAddress.new + operation.address.add_error("is required") + + operation.should have_error + operation.should have_error("is required") + operation.should have_error(/\srequired/) + + operation.address.should have_error + operation.address.should have_error("is required") + operation.address.should have_error(/\srequired/) + end + + it "fails if attribute is valid" do + operation = SaveEmailAddress.new + + expect_raises Spec::AssertionFailed, "have an error" do + operation.should have_error + end + + expect_raises Spec::AssertionFailed, "have the error" do + operation.should have_error("is required") + end + + expect_raises Spec::AssertionFailed, "have the error" do + operation.should have_error(/\srequired/) + end + + expect_raises Spec::AssertionFailed, "have an error" do + operation.address.should have_error + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.address.should have_error("is required") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.address.should have_error(/\srequired/) + end + end + + it "fails if attribute is invalid but without the given message" do + operation = SaveEmailAddress.new + operation.address.add_error("is required") + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error("wrong message") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.should have_error(/\smessage/) + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.address.should have_error("wrong message") + end + + expect_raises Spec::AssertionFailed, "have the error " do + operation.address.should have_error(/\smessage/) + end + end + end + + context "in negative assertions" do + it "passes if attribute is valid" do + operation = SaveEmailAddress.new + + operation.should_not have_error + operation.should_not have_error("is required") + operation.should_not have_error(/\srequired/) + + operation.address.should_not have_error + operation.address.should_not have_error("is required") + operation.address.should_not have_error(/\srequired/) + end + + it "fails if attribute is invalid" do + operation = SaveEmailAddress.new + operation.address.add_error("is required") + + expect_raises Spec::AssertionFailed, "not have an error" do + operation.should_not have_error + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error("is required") + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.should_not have_error(/\srequired/) + end + + expect_raises Spec::AssertionFailed, "not have an error" do + operation.address.should_not have_error + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.address.should_not have_error("is required") + end + + expect_raises Spec::AssertionFailed, "not have the error " do + operation.address.should_not have_error(/\srequired/) + end + end + + it "passes if attribute is invalid but without the given message" do + operation = SaveEmailAddress.new + operation.address.add_error("is required") + + operation.should_not have_error("wrong message") + operation.should_not have_error(/\smessage/) + + operation.address.should_not have_error("wrong message") + operation.address.should_not have_error(/\smessage/) + end + end + end +end diff --git a/src/avram/expectations.cr b/src/avram/expectations.cr new file mode 100644 index 000000000..d59299b35 --- /dev/null +++ b/src/avram/expectations.cr @@ -0,0 +1,35 @@ +module Avram::Expectations + # Tests that an operation or attribute has an error + # + # ``` + # CreateReceipt.create(params) do |operation, receipt| + # receipt.should be_nil + # + # operation.should have_error + # operation.should have_error("is required") + # operation.should have_error(/\srequired/) + # + # operation.user_id.should have_error + # operation.user_id.should have_error("is required") + # operation.user_id.should have_error(/\srequired/) + # end + # ``` + def have_error(message = nil) + HaveErrorExpectation.new(message) + end + + # Tests that an operation has a custom error + # + # ``` + # CreateUser.create(params) do |operation, user| + # user.should be_nil + # + # operation.should have_error(:roles) + # operation.should have_error(:roles, "is empty") + # operation.should have_error(:roles, /\sempty/) + # end + # ``` + def have_error(name : Symbol, message = nil) + HaveCustomErrorExpectation.new(name, message) + end +end diff --git a/src/avram/expectations/have_custom_error_expectation.cr b/src/avram/expectations/have_custom_error_expectation.cr new file mode 100644 index 000000000..10e42bf6a --- /dev/null +++ b/src/avram/expectations/have_custom_error_expectation.cr @@ -0,0 +1,35 @@ +module Avram::Expectations + struct HaveCustomErrorExpectation + def initialize(@name : Symbol, @message : Regex? = nil) + end + + def self.new(name, message : String) + new(name, /#{message}/) + end + + def match(operation : OperationErrors) : Bool + return false unless errors = operation.custom_errors[@name]? + @message.try { |message| return errors.any?(&.=~ message) } + !errors.empty? + end + + def failure_message(operation : OperationErrors) : String + @message.try do |message| + return "Expected :#{@name} to have the error '#{message.source}'" + end + + "Expected :#{@name} to have an error" + end + + def negative_failure_message(operation : OperationErrors) : String + @message.try do |message| + return "Expected :#{@name} to not have the error '#{message.source}'" + end + + <<-MSG + Expected :#{@name} to not have an error, got errors: + #{HaveErrorExpectation.list(operation.custom_errors[@name])} + MSG + end + end +end diff --git a/src/avram/expectations/have_error_expectation.cr b/src/avram/expectations/have_error_expectation.cr new file mode 100644 index 000000000..a301f5518 --- /dev/null +++ b/src/avram/expectations/have_error_expectation.cr @@ -0,0 +1,81 @@ +module Avram::Expectations + struct HaveErrorExpectation + def initialize(@message : Regex? = nil) + end + + def self.new(message : String) + new(/#{message}/) + end + + def match(attribute : Attribute) : Bool + @message.try do |message| + return attribute.errors.any?(&.=~ message) + end + + !attribute.errors.empty? + end + + def match(operation : OperationErrors) : Bool + @message.try do |message| + return operation.errors.flat_map { |_, errors| errors }.any? do |error| + error =~ message + end + end + + !operation.errors.empty? + end + + def failure_message(attribute : Attribute) : String + @message.try do |message| + return "Expected :#{attribute.name} to have the error '#{message.source}'" + end + + "Expected :#{attribute.name} to have an error" + end + + def failure_message(operation : OperationErrors) : String + @message.try do |message| + return "Expected operation to have the error '#{message.source}'" + end + + "Expected operation to have an error" + end + + def negative_failure_message(attribute : Attribute) : String + @message.try do |message| + return "Expected :#{attribute.name} to not have the error '#{message.source}'" + end + + <<-MSG + Expected :#{attribute.name} to not have an error, got errors: + #{self.class.list(attribute.errors)} + MSG + end + + def negative_failure_message(operation : OperationErrors) : String + @message.try do |message| + return "Expected operation to not have the error '#{message.source}'" + end + + <<-MSG + Expected operation to not have an error, got errors: + #{self.class.list(operation.errors)} + MSG + end + + protected def self.list(errors : Hash) + errors.join do |name, _errors| + list _errors.map { |error| "#{name}: #{error}" } + end + end + + protected def self.list(errors : Array) + errors.join do |error| + <<-ERROR + - #{error} + + ERROR + end + end + end +end