From 2aad5e95293b256da2951518048b7fe1c66beff6 Mon Sep 17 00:00:00 2001 From: Sebastian Serth Date: Wed, 4 Oct 2023 00:08:47 +0200 Subject: [PATCH] Add bidirectional converter specs to test namespaces Previously, we might have duplicated namespaces when converting the same string back and forth. With the latest change in openHPI/dachsfisch#52, we're avoiding this error and are now adding respective test coverage for this phenomena. --- .../equal_namespace_definitions_spec.rb | 98 +++++++++++++++++++ .../bidirectional_converter_spec.rb | 39 ++++++++ .../equal_namespace_definitions.rb | 59 +++++++++++ 3 files changed, 196 insertions(+) create mode 100644 spec/custom_matchers/equal_namespace_definitions_spec.rb create mode 100644 spec/dachsfisch/bidirectional_converter_spec.rb create mode 100644 spec/support/expectations/equal_namespace_definitions.rb diff --git a/spec/custom_matchers/equal_namespace_definitions_spec.rb b/spec/custom_matchers/equal_namespace_definitions_spec.rb new file mode 100644 index 0000000..02b51aa --- /dev/null +++ b/spec/custom_matchers/equal_namespace_definitions_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +RSpec.describe 'equal_namespace_definitions matcher' do + let(:xml) { Examples::Example2.xml } + let(:xml2) { Examples::Example2.xml } + + it 'successfully compares two similar xml strings' do + expect(xml).to have_equal_namespace_definitions_as xml2 + end + + context 'when one of the classes is not a string' do + let(:string) { 'hello' } + let(:integer) { 1234 } + + it 'fails' do + expect(string).not_to have_equal_namespace_definitions_as integer + end + + it 'provides a useful error message' do + expect { expect(string).to have_equal_namespace_definitions_as integer } + .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/) + end + end + + context 'when one of the strings does not contain valid xml' do + let(:xml2) { 'foobar' } + + it 'fails' do + expect(xml).not_to have_equal_namespace_definitions_as xml2 + end + + it 'provides a useful error message' do + expect { expect(xml).to have_equal_namespace_definitions_as xml2 } + .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/) + end + end + + context 'when one xml string is different' do + let(:xml2) { Examples::Example3.json } + + it 'fails the comparison' do + expect(xml).not_to have_equal_namespace_definitions_as xml2 + end + + it 'provides a useful error message' do + expect { expect(xml).to have_equal_namespace_definitions_as xml2 } + .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/) + end + end + + context 'with namespaces' do + let(:xml) do + <<-XML + + david + frank + + XML + end + + context 'with unequal namespace definitions' do + let(:xml2) do + <<~XML + + david + + frank + + + XML + end + + it 'fails the comparison' do + expect(xml).not_to have_equal_namespace_definitions_as xml2 + end + + it 'provides a useful error message' do + expect { expect(xml).to have_equal_namespace_definitions_as xml2 } + .to raise_error(RSpec::Expectations::ExpectationNotMetError, /does not have equal namespaces as/) + end + end + + context 'with similar namespace definitions' do + let(:xml2) do + <<~XML + + david + frank + + XML + end + + it 'successfully compares two similar xml strings' do + expect(xml).to have_equal_namespace_definitions_as xml2 + end + end + end +end diff --git a/spec/dachsfisch/bidirectional_converter_spec.rb b/spec/dachsfisch/bidirectional_converter_spec.rb new file mode 100644 index 0000000..f32c789 --- /dev/null +++ b/spec/dachsfisch/bidirectional_converter_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.describe 'BidirectionalConverter' do + let(:xml2_json) { Dachsfisch::XML2JSONConverter } + let(:json2_xml) { Dachsfisch::JSON2XMLConverter } + + context 'with valid XML' do + describe '#perform' do + subject { json2_xml.perform json: } + + let(:json) { xml2_json.perform xml: } + + Examples.each :json2xml do |example| + context "with #{example.name}" do + let(:xml) { example.xml } + + it { is_expected.to be_equivalent_to(xml) } + it { is_expected.to have_equal_namespace_definitions_as(xml) } + end + end + end + end + + context 'with valid JSON' do + describe '#perform' do + subject { xml2_json.perform xml: } + + let(:xml) { json2_xml.perform json: } + + Examples.each :xml2json do |example| + context "with #{example.name}" do + let(:json) { example.json } + + it { is_expected.to be_an_equal_json_as(json) } + end + end + end + end +end diff --git a/spec/support/expectations/equal_namespace_definitions.rb b/spec/support/expectations/equal_namespace_definitions.rb new file mode 100644 index 0000000..8f33794 --- /dev/null +++ b/spec/support/expectations/equal_namespace_definitions.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'rspec/expectations' + +RSpec::Matchers.define :have_equal_namespace_definitions_as do |expected| + attr_reader :actual, :expected + + match do |actual| + return false unless actual.is_a?(String) && expected.is_a?(String) + + expected_xml = parse_fragment(expected) + @expected = expected_xml.to_xml + actual_xml = parse_fragment(actual) + @actual = actual_xml.to_xml + + return false if expected_xml.errors.length.positive? || actual_xml.errors.length.positive? + return false unless EquivalentXml.equivalent?(expected_xml, actual_xml) + + return compare_namespaces(actual_xml.children.first, expected_xml.children.first) + end + + failure_message do |actual| + "#{@actual || actual} does not have equal namespaces as \n#{@expected || expected}." + end + + diffable + + private + + def compare_namespaces(actual, expected) + return false unless same_namespace_definitions?(actual, expected) + + actual.children.each_with_index.all? do |actual_child, index| + expected_child = expected.children[index] + compare_namespaces(actual_child, expected_child) + end + end + + def same_namespace_definitions?(actual, expected) + return true if actual.nil? && expected.nil? + return false if actual.nil? || expected.nil? + + actual_namespaces = namespaces(actual) + expected_namespaces = namespaces(expected) + actual_namespaces == expected_namespaces + end + + def namespaces(node) + node.namespace_definitions.map {|namespace| namespace.deconstruct_keys(%i[prefix href]) }.sort_by {|ns| ns[:prefix].to_s } + end + + def parse_fragment(xml) + # This is a workaround for an unintended behavior in Nokogiri's XML::DocumentFragment.parse method. + # Originally, the method de-duplicates the namespace definitions of all nodes in the fragment, if possible. + # However, for this test, we want to compare the namespace definitions of the actual and expected XML. + # Therefore, we parse the XML with a root node, and compare the resulting document. + Nokogiri::XML::Document.parse("#{xml}", &:noblanks) + end +end