From c011e1775773a0d81a28ac3acfd51b2f95fb458d Mon Sep 17 00:00:00 2001
From: Joe Woodward <j@joewoodward.me>
Date: Tue, 30 Jan 2018 11:13:56 +0700
Subject: [PATCH] add have_jsonapi_errors matcher with chainable expectations

---
 lib/jsonapi/rspec.rb        |   2 +
 lib/jsonapi/rspec/errors.rb | 135 ++++++++++++++++++++++++++++
 spec/errors_spec.rb         | 175 ++++++++++++++++++++++++++++++++++++
 spec/id_spec.rb             |   1 +
 spec/jsonapi_object_spec.rb |   1 +
 spec/meta_spec.rb           |   1 +
 spec/type_spec.rb           |   1 +
 7 files changed, 316 insertions(+)
 create mode 100644 lib/jsonapi/rspec/errors.rb
 create mode 100644 spec/errors_spec.rb

diff --git a/lib/jsonapi/rspec.rb b/lib/jsonapi/rspec.rb
index bcd21e6..9f63fe1 100644
--- a/lib/jsonapi/rspec.rb
+++ b/lib/jsonapi/rspec.rb
@@ -1,3 +1,4 @@
+require 'jsonapi/rspec/errors'
 require 'jsonapi/rspec/id'
 require 'jsonapi/rspec/type'
 require 'jsonapi/rspec/attributes'
@@ -8,6 +9,7 @@
 
 module JSONAPI
   module RSpec
+    include Errors
     include Id
     include Type
     include Attributes
diff --git a/lib/jsonapi/rspec/errors.rb b/lib/jsonapi/rspec/errors.rb
new file mode 100644
index 0000000..b8a2116
--- /dev/null
+++ b/lib/jsonapi/rspec/errors.rb
@@ -0,0 +1,135 @@
+module JSONAPI
+  module RSpec
+    module Errors
+      IncompatibleSourcesError = Class.new(StandardError)
+
+      ::RSpec::Matchers.define :have_jsonapi_errors do
+        match do |actual|
+          actual ||= {}
+
+          if @pointer && @parameter
+            fail IncompatibleSourcesError.new('Only one source per example')
+          end
+
+          errors = actual['errors'] || {}
+
+          on_parameter = true
+          on_pointer = true
+          if @check_on_parameter || @check_on_pointer
+            source_type = 'pointer' if @pointer
+            source_type = 'parameter' if @parameter
+            errors.keep_if do |e|
+              !e['source'].nil? &&
+                e['source'][source_type] == (@pointer || @parameter)
+            end
+
+            if errors.empty?
+              on_parameter = false if @parameter
+              on_pointer = false if @pointer
+            end
+          end
+
+          errors_present = actual.key?('errors')
+          # Use these values to see if we need to run the check.
+          # If the value is set to true the test will run
+          correct_count = !@check_with_count
+          with_ids = !@check_with_ids
+          with_about_link = !@check_with_about_link
+          with_status = !@check_with_status
+          with_code = !@check_with_code
+          with_title = !@check_with_title
+          with_detail = !@check_with_detail
+          with_source = !@check_with_source
+          with_meta = !@check_with_meta
+
+          correct_count ||= errors.count == @count
+
+          with_ids ||= errors.map { |e| e['id'] }.count == errors.count
+
+          with_about_link ||=
+            errors.map { |e| e['links']['about'] }.include?(@about_link)
+
+          with_status ||= errors.map { |e| e['status'] }.include?(@status)
+
+          with_code ||= errors.map { |e| e['code'] }.include?(@code)
+
+          with_title ||= errors.map { |e| e['title'] }.include?(@title)
+
+          with_detail ||= errors.map { |e| e['detail'] }.include?(@detail)
+
+          with_source ||= errors.map { |e| e['source'] }.any?
+
+          with_meta ||= errors.map { |e| e['meta'] }.include?(@meta)
+
+          [
+            errors_present,
+            on_parameter,
+            on_pointer,
+            correct_count,
+            with_ids,
+            with_about_link,
+            with_status,
+            with_code,
+            with_title,
+            with_detail,
+            with_source,
+            with_meta
+          ].all?(true)
+        end
+
+        chain :count do |count|
+          @check_with_count = true
+          @count = count
+        end
+
+        chain :with_ids do
+          @check_with_ids = true
+        end
+
+        chain :with_about_link do |about_link|
+          @check_with_about_link = true
+          @about_link = about_link
+        end
+
+        chain :with_status do |status|
+          @check_with_status = true
+          @status = status
+        end
+
+        chain :with_code do |code|
+          @check_with_code = true
+          @code = code
+        end
+
+        chain :with_title do |title|
+          @check_with_title = true
+          @title = title
+        end
+
+        chain :with_detail do |detail|
+          @check_with_detail = true
+          @detail = detail
+        end
+
+        chain :with_source do
+          @check_with_source = true
+        end
+
+        chain :on_pointer do |pointer|
+          @check_on_pointer = true
+          @pointer = pointer
+        end
+
+        chain :on_parameter do |parameter|
+          @check_on_parameter = true
+          @parameter = parameter
+        end
+
+        chain :with_meta do |meta|
+          @check_with_meta = true
+          @meta = meta
+        end
+      end
+    end
+  end
+end
diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb
new file mode 100644
index 0000000..cdfc4c6
--- /dev/null
+++ b/spec/errors_spec.rb
@@ -0,0 +1,175 @@
+require 'spec_helper'
+RSpec.describe JSONAPI::RSpec, '#have_jsonapi_error' do
+  let(:id) { '1' }
+  let(:about_link) { 'http://about.me' }
+  let(:status) { '200' }
+  let(:code) { 'bad_thing' }
+  let(:title) { 'A bad thing happened' }
+  let(:detail) { 'The thing was bad' }
+  let(:source) { pointer_source }
+  let(:pointer) { '/data/attribute/title' }
+  let(:pointer_source) { { 'pointer' => pointer } }
+  let(:parameter) { 'page' }
+  let(:parameter_source) { { 'parameter' => parameter } }
+  let(:meta) do
+    {
+      "copyright" => "Copyright 2015 Example Corp.",
+      "authors" => [
+        "Yehuda Katz",
+        "Steve Klabnik",
+        "Dan Gebhardt",
+        "Tyler Kellen"
+      ]
+    }
+  end
+
+  subject do
+    {
+      'errors' => [
+        {
+          'id' => id,
+          'links' => { 'about' => about_link },
+          'status' => status,
+          'code' => code,
+          'title' => title,
+          'detail' => detail,
+          'source' => source,
+          'meta' => meta
+        }
+      ]
+    }
+  end
+
+  it 'succeeds when errors key is present' do
+    is_expected.to have_jsonapi_errors
+  end
+
+  it 'fails when errors key is absent' do
+    expect({}).to_not have_jsonapi_errors
+  end
+
+  context 'chained methods' do
+    describe '#count(count)' do
+      it 'succeeds when errors count == count' do
+        is_expected.to have_jsonapi_errors.count(1)
+      end
+
+      it 'fails when errors exclude ids' do
+        expect({}).to_not have_jsonapi_errors.count(1)
+      end
+    end
+
+    describe '#with_ids' do
+      it 'succeeds when errors include ids' do
+        is_expected.to have_jsonapi_errors.with_ids
+      end
+
+      it 'fails when errors exclude ids' do
+        expect({}).to_not have_jsonapi_errors.with_ids
+      end
+    end
+
+    describe '#with_about_link(about_link)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_about_link(about_link)
+      end
+
+      it 'fails when errors exclude about_link' do
+        expect({}).to_not have_jsonapi_errors.with_about_link(about_link)
+      end
+    end
+
+    describe '#with_status(status)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_status(status)
+      end
+
+      it 'fails when errors exclude status' do
+        expect({}).to_not have_jsonapi_errors.with_status(status)
+      end
+    end
+
+    describe '#with_code(code)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_code(code)
+      end
+
+      it 'fails when errors exclude code' do
+        expect({}).to_not have_jsonapi_errors.with_code(code)
+      end
+    end
+
+    describe '#with_title(title)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_title(title)
+      end
+
+      it 'fails when errors exclude title' do
+        expect({}).to_not have_jsonapi_errors.with_title(title)
+      end
+    end
+
+    describe '#with_detail(detail)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_detail(detail)
+      end
+
+      it 'fails when errors exclude detail' do
+        expect({}).to_not have_jsonapi_errors.with_detail(detail)
+      end
+    end
+
+    describe '#with_source' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_source
+      end
+
+      it 'fails when errors exclude source' do
+        expect({}).to_not have_jsonapi_errors.with_source
+      end
+    end
+
+    describe '#on_pointer(pointer)' do
+      let!(:source) { pointer_source }
+
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.on_pointer(pointer)
+      end
+
+      it 'fails when errors exclude pointer_source' do
+        expect({}).to_not have_jsonapi_errors.on_pointer(pointer)
+      end
+    end
+
+    describe '#on_parameter(parameter)' do
+      let!(:source) { parameter_source }
+
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.on_parameter(parameter)
+      end
+
+      it 'fails when errors exclude parameter_source' do
+        expect({}).to_not have_jsonapi_errors.on_parameter(parameter)
+      end
+    end
+
+    describe '#with_meta(meta)' do
+      it 'succeeds when errors include about links' do
+        is_expected.to have_jsonapi_errors.with_meta(meta)
+      end
+
+      it 'fails when errors exclude meta' do
+        expect({}).to_not have_jsonapi_errors.with_meta(meta)
+      end
+    end
+
+    describe '#on_parameter and #on_pointer' do
+      it 'raises a warning' do
+        expect {
+          expect({}).to have_jsonapi_errors.
+            on_pointer(pointer).on_parameter(parameter)
+        }.to raise_error(JSONAPI::RSpec::Errors::IncompatibleSourcesError)
+      end
+    end
+  end
+end
diff --git a/spec/id_spec.rb b/spec/id_spec.rb
index 620df73..fbf5333 100644
--- a/spec/id_spec.rb
+++ b/spec/id_spec.rb
@@ -1,3 +1,4 @@
+require 'spec_helper'
 RSpec.describe JSONAPI::RSpec, '#have_id' do
   it 'succeeds when id matches' do
     expect('id' => 'foo').to have_id('foo')
diff --git a/spec/jsonapi_object_spec.rb b/spec/jsonapi_object_spec.rb
index c462861..c0081ee 100644
--- a/spec/jsonapi_object_spec.rb
+++ b/spec/jsonapi_object_spec.rb
@@ -1,3 +1,4 @@
+require 'spec_helper'
 RSpec.describe JSONAPI::RSpec, '#have_jsonapi_object' do
   context 'when providing no value' do
     it 'succeeds when jsonapi object is present' do
diff --git a/spec/meta_spec.rb b/spec/meta_spec.rb
index e5f9401..06150b1 100644
--- a/spec/meta_spec.rb
+++ b/spec/meta_spec.rb
@@ -1,3 +1,4 @@
+require 'spec_helper'
 RSpec.describe JSONAPI::RSpec, '#have_meta' do
   context 'when providing no value' do
     it 'succeeds when meta is present' do
diff --git a/spec/type_spec.rb b/spec/type_spec.rb
index 500df1d..19e76be 100644
--- a/spec/type_spec.rb
+++ b/spec/type_spec.rb
@@ -1,3 +1,4 @@
+require 'spec_helper'
 RSpec.describe JSONAPI::RSpec, '#have_type' do
   it 'succeeds when type matches' do
     expect('type' => 'foo').to have_type('foo')
