From 4e44a955ea26bf1544bb7fd2d6f8d4c35c2f7bbc Mon Sep 17 00:00:00 2001 From: Phillip Davis Date: Tue, 17 Mar 2026 11:44:22 -0400 Subject: [PATCH] bandwidth_tn_repo.rb: fallback to geocode --- lib/bandwidth_tn_repo.rb | 187 +++++++++++++++++-- lib/geo_code.rb | 36 ++-- lib/local_calling_guide_repo.rb | 38 +++- test/test_bandwidth_tn_repo.rb | 255 ++++++++++++++++++++++++-- test/test_geo_code.rb | 16 +- test/test_geo_code_repo.rb | 4 +- test/test_helper.rb | 6 +- test/test_local_calling_guide_repo.rb | 47 ++++- 8 files changed, 514 insertions(+), 75 deletions(-) diff --git a/lib/bandwidth_tn_repo.rb b/lib/bandwidth_tn_repo.rb index 4e936f5fd550588c150839f6824238b25382f8be..0d12e8fb46c64179f13ff036e87a1affb2a0982b 100644 --- a/lib/bandwidth_tn_repo.rb +++ b/lib/bandwidth_tn_repo.rb @@ -1,8 +1,151 @@ # frozen_string_literal: true +require "lazy_object" require "ruby-bandwidth-iris" +require_relative "geo_code_repo" +require_relative "local_calling_guide_repo" class BandwidthTnRepo + class RateCenter + # @return [String, nil] + attr_reader :region + + # @return [String, nil] + attr_reader :locality + + # @return [String, nil] + attr_reader :lcg_region + + # @return [String, nil] + attr_reader :lcg_locality + + # @return [Array(String, String)] + def to_a + [region, locality] + end + + # @param btn [BandwidthIris::Tn] + # @param lcg_repo [LocalCallingGuideRepo] + # @param geo_code_repo [GeoCodeRepo] + # @return [EMPromise] + def self.for( + btn, + lcg_repo: LazyObject.new { LocalCallingGuideRepo.new }, + geo_code_repo: LazyObject.new { GeoCodeRepo.new } + ) + [ + Source::Bandwidth.new(btn), + Source::Geocode.new(btn, lcg_repo, geo_code_repo), + Source::LCG.new(btn, lcg_repo) + ].reduce(EMPromise.resolve(new)) { |promise, source| + promise.then { |acc| + next acc if acc.settled? + + source.fetch(acc).then(&acc.method(:fold)) + } + } + end + + # @return [Boolean] + def settled? + return false unless readable(@region) + return false unless readable(@locality) + + true + end + + protected + + # @param value [String, nil] + # @return [String, nil] + def readable(value) + value if value&.match?(/\d/) == false + end + + # @param kwargs [Hash] + # @return [self] + def fold(kwargs) + kwargs.each_pair do |k, v| + instance_variable_set( + :"@#{k}", + readable(instance_variable_get(:"@#{k}")) || v + ) + end + self + end + + class Source + # @param acc [RateCenter] + # @return [EMPromise] + def fetch(acc) + raise NotImplementedError + end + + class Bandwidth < self + # @param btn [BandwidthIris::Tn] + def initialize(btn) + @btn = btn + end + + # @param _acc [RateCenter] + # @return [EMPromise] + def fetch(_acc) + city, state = @btn.get_details.values_at( + :city, :state + ) + EMPromise.resolve( + { locality: city, region: state } + ) + rescue BandwidthIris::Errors::GenericError + EMPromise.resolve({}) + end + end + + class LCG < self + # @param btn [BandwidthIris::Tn] + # @param lcg_repo [LocalCallingGuideRepo] + def initialize(btn, lcg_repo) + @btn = btn + @lcg_repo = lcg_repo + end + + # @param acc [RateCenter] + # @return [EMPromise] + def fetch(acc) + EMPromise.resolve( + { + locality: acc.lcg_locality, + region: acc.lcg_region + } + ) + end + end + + class Geocode < self + # @param btn [BandwidthIris::Tn] + # @param lcg_repo [LocalCallingGuideRepo] + # @param geo_code_repo [GeoCodeRepo] + def initialize(btn, lcg_repo, geo_code_repo) + @btn = btn + @geo_code_repo = geo_code_repo + @lcg_repo = lcg_repo + end + + # @param _acc [RateCenter] + # @return [EMPromise] + def fetch(_acc) + npa, nxx, * = @btn.telephone_number.scan(/.{1,3}/).to_a + @lcg_repo.find(npa, nxx).then { |lcg| + cache = lcg.to_h.transform_keys(&"lcg_".method(:+)) + @geo_code_repo.reverse(lcg.lat, lcg.lon).then { |gc| + gc.to_h.slice(:locality, :region).merge(cache) + }.catch { cache } + } + end + end + end + end + STASH_QUERY = <<~SQL INSERT INTO tel_inventory ( tel, @@ -21,13 +164,20 @@ class BandwidthTnRepo ) SQL - def initialize + # @param local_calling_guide_repo [LocalCallingGuideRepo] + # @param geo_code_repo [GeoCodeRepo] + def initialize( + local_calling_guide_repo: LazyObject.new { LocalCallingGuideRepo.new }, + geo_code_repo: LazyObject.new { GeoCodeRepo.new } + ) @move_client = BandwidthIris::Client.new( account_id: CONFIG[:keep_area_codes_in][:account], client_id: CONFIG[:creds][:client_id], client_secret: CONFIG[:creds][:client_secret] ) + @local_calling_guide_repo = local_calling_guide_repo + @geo_code_repo = geo_code_repo end def find(tel) @@ -48,29 +198,32 @@ class BandwidthTnRepo raise "Could not set CNAM, please contact support" end - # @param [String] tel - # @param [BandwidthIris::Tn] btn - # @param [Numeric] premium_price - def stash_for_later(tel, btn, premium_price) + # @param btn [BandwidthIris::Tn] + # @param premium_price [Numeric] + # @return [EMPromise] + def stash_for_later(btn, premium_price) + tel = "+1#{btn.telephone_number}" LOG.info "stash_for_later #{tel}\n#{caller}" - details = btn.get_details - region = details[:state] - locality = details[:city] - params = [tel, region, locality, CONFIG[:creds][:account], premium_price] - DB.exec(STASH_QUERY, params) + RateCenter.for( + btn, + lcg_repo: @local_calling_guide_repo, + geo_code_repo: @geo_code_repo + ).then { |loc| + DB.exec(STASH_QUERY, [tel, *loc, CONFIG[:creds][:account], premium_price]) + } end def disconnect(tel, order_name) tn = tel.sub(/\A\+1/, "") - btn = BandwidthIris::Tn.new({ telephone_number: tn }, @move_client) - code_and_price = CONFIG[:keep_area_codes].find { |keep| - tn.start_with?(keep[:area_code]) + code_and_price = CONFIG[:keep_area_codes].find { |k| + tn.start_with?(k[:area_code]) } - if code_and_price - stash_for_later(tel, btn, code_and_price[:premium_price] || 0) - else - BandwidthIris::Disconnect.create(order_name, tn) + unless code_and_price + return BandwidthIris::Disconnect.create(order_name, tn) end + + btn = BandwidthIris::Tn.new({ telephone_number: tn }, @move_client) + stash_for_later(btn, code_and_price[:premium_price] || 0) end def move(tel, order_name, source_account_id) diff --git a/lib/geo_code.rb b/lib/geo_code.rb index f223779ef5a20439e8b757e6a69a3176b5af9824..87c40e42093bd27888b3d5a383f567735fa8d773 100644 --- a/lib/geo_code.rb +++ b/lib/geo_code.rb @@ -26,24 +26,32 @@ class GeoCode @data["longt"] && @data["latt"] end - # geocoder.ca is Canadian; for US coordinates the top-level "prov"/"city" - # may reflect Canadian conventions, so prefer the "usa" sub-object when present + # geocoder.ca is Canadian; for US coordinates the top-level "prov"/"city" may + # reflect Canadian conventions, so prefer the "usa" sub-object when present # @return [String, nil] - def city - if @data["usa"] - @data["usa"]["uscity"] - else - @data["city"] - end + def locality + usa? ? @data["usa"]["uscity"] : @data["city"] end # @return [String, nil] - def state - if @data["usa"] - @data["usa"]["state"] - else - @data["prov"] - end + def region + usa? ? @data["usa"]["state"] : @data["prov"] + end + + # @returh Hash + def to_h + { + locality: locality, + region: region, + country: country + } + end + +private + + # @return [Boolean] + def usa? + @data.key?("usa") end end diff --git a/lib/local_calling_guide_repo.rb b/lib/local_calling_guide_repo.rb index ec91d569e7de4c646fa5f8d20751e0f5ea84f812..5df97c9f29be5157b84849e759054c6ccb3aac72 100644 --- a/lib/local_calling_guide_repo.rb +++ b/lib/local_calling_guide_repo.rb @@ -1,25 +1,49 @@ # frozen_string_literal: true +require "delegate" require "nokogiri" class LocalCallingGuideRepo + class Prefix < SimpleDelegator + # @return [Float] + def lat + at("rc-lat").text.to_f + end + + # @return [Float] + def lon + at("rc-lon").text.to_f + end + + # @return [String] + def region + at("region").text + end + + # @return [String] + def locality + at("rc").text + end + + # @return [Hash] + def to_h + { "region" => region, "locality" => locality } + end + end + # @param npa [String] area code (first 3 digits after country code) # @param nxx [String] exchange (digits 4-6) - # @return [EMPromise Float}>] :lat and :lon of the rate center + # @return [EMPromise] # @raise [RuntimeError] if no prefix data found def find(npa, nxx) EM::HttpRequest.new( "https://localcallingguide.com/xmlprefix.php", tls: { verify_peer: true } ).aget(query: { npa: npa, nxx: nxx }).then { |res| - doc = Nokogiri::XML.parse(res.response) - prefix = doc.at("prefixdata") + prefix = Nokogiri::XML.parse(res.response).at("prefixdata") raise "No prefix data for #{npa}-#{nxx}" unless prefix - { - lat: prefix.at("rc-lat").text.to_f, - lon: prefix.at("rc-lon").text.to_f - } + Prefix.new(prefix) } end end diff --git a/test/test_bandwidth_tn_repo.rb b/test/test_bandwidth_tn_repo.rb index ad258e14ac39e156800eba5b95ca9b3338d2b0c1..c2e40e6aa9f944488d7a80959bbea2b3e6e91e5b 100644 --- a/test/test_bandwidth_tn_repo.rb +++ b/test/test_bandwidth_tn_repo.rb @@ -2,11 +2,22 @@ require "test_helper" require "bandwidth_tn_repo" +require "local_calling_guide_repo" +require "geo_code_repo" BandwidthTnRepo::DB = Minitest::Mock.new class BandwidthTnRepoTest < Minitest::Test def test_local_inventory_recycled_with_price + bw_body = Nokogiri::XML::Builder.new { |xml| + xml.Response { + xml.TelephoneNumberDetails { + xml.City "Austin" + xml.State "TX" + } + } + }.to_xml + stub_request( :get, "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails" @@ -18,14 +29,7 @@ class BandwidthTnRepoTest < Minitest::Test "Authorization" => "Bearer test_bw_oauth_token", "User-Agent" => "Ruby-Bandwidth-Iris" } - ).to_return(status: 200, body: <<~XML, headers: {}) - - - Austin - TX - - - XML + ).to_return(status: 200, body: bw_body, headers: {}) tel = "+15565555555" @@ -38,13 +42,22 @@ class BandwidthTnRepoTest < Minitest::Test ] ) - BandwidthTnRepo.new.disconnect(tel, "test") + BandwidthTnRepo.new.disconnect(tel, "test").sync assert_mock BandwidthTnRepo::DB end em :test_local_inventory_recycled_with_price def test_local_inventory_recycled_no_price + bw_body = Nokogiri::XML::Builder.new { |xml| + xml.Response { + xml.TelephoneNumberDetails { + xml.City "Austin" + xml.State "TX" + } + } + }.to_xml + stub_request( :get, "https://dashboard.bandwidth.com/v1.0/tns/5575555555/tndetails" @@ -56,14 +69,7 @@ class BandwidthTnRepoTest < Minitest::Test "Authorization" => "Bearer test_bw_oauth_token", "User-Agent" => "Ruby-Bandwidth-Iris" } - ).to_return(status: 200, body: <<~XML, headers: {}) - - - Austin - TX - - - XML + ).to_return(status: 200, body: bw_body, headers: {}) tel = "+15575555555" @@ -76,9 +82,222 @@ class BandwidthTnRepoTest < Minitest::Test ] ) - BandwidthTnRepo.new.disconnect(tel, "test") + BandwidthTnRepo.new.disconnect(tel, "test").sync assert_mock BandwidthTnRepo::DB end em :test_local_inventory_recycled_no_price + + def test_stash_falls_back_to_lcg_on_unreadable + tel = "+15565555555" + + # bw_r bw_l geo_r geo_l lcg_r lcg_l exp_r exp_l + [ + ["TX", "Austin", "CA", "LA", "NY", "NYC", "TX", "Austin"], + ["TX", "1City", "CA", "LA", "NY", "NYC", "TX", "LA"], + ["TX", "1City", "CA", "2City", "NY", "NYC", "TX", "NYC"], + ["1ST", "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"], + ["1ST", "1City", "CA", "LA", "NY", "NYC", "CA", "LA"], + ["1ST", "1City", "CA", "2City", "NY", "NYC", "CA", "NYC"], + ["1ST", "Austin", "2ST", "LA", "NY", "NYC", "NY", "Austin"], + ["1ST", "1City", "2ST", "LA", "NY", "NYC", "NY", "LA"], + ["1ST", "1City", "2ST", "2City", "NY", "NYC", "NY", "NYC"], + ["TX", nil, "CA", "LA", "NY", "NYC", "TX", "LA"], + ["TX", nil, "CA", nil, "NY", "NYC", "TX", "NYC"], + [nil, "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"], + [nil, nil, "CA", "LA", "NY", "NYC", "CA", "LA"], + [nil, nil, "CA", nil, "NY", "NYC", "CA", "NYC"], + [nil, "Austin", nil, "LA", "NY", "NYC", "NY", "Austin"], + [nil, nil, nil, "LA", "NY", "NYC", "NY", "LA"], + [nil, nil, nil, nil, "NY", "NYC", "NY", "NYC"] + ].each do |bw_r, bw_l, geo_r, geo_l, lcg_r, lcg_l, exp_r, exp_l| + WebMock.reset! + stub_bw_oauth_token + stub_bw_tndetails(bw_r, bw_l) + + lcg_body = Nokogiri::XML::Builder.new { |xml| + xml.root { + xml.prefixdata { + xml.send(:"rc-lat", "30.267153") + xml.send(:"rc-lon", "-97.743061") + xml.rc(lcg_l) + xml.region(lcg_r) + } + } + }.to_xml + + geo_json = { + "latt" => "30.267153", + "longt" => "-97.743061" + } + geo_json["prov"] = geo_r if geo_r + geo_json["city"] = geo_l if geo_l + + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails" + ) + .with( + headers: { + "Accept" => "application/xml", + "Accept-Encoding" => "gzip, compressed", + "Authorization" => + "Basic dGVzdF9id191c2VyOnRlc3RfYndfcGFzc3dvcmQ=", + "User-Agent" => "Ruby-Bandwidth-Iris" + } + ).to_return(status: 200, body: bw_body, headers: {}) + + stub_request( + :get, + "https://localcallingguide.com/xmlprefix.php?npa=556&nxx=555" + ).to_return(status: 200, body: lcg_body) + + stub_request( + :get, + "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061" + ).to_return(status: 200, body: geo_json.to_json) + + BandwidthTnRepo::DB.expect( + :exec, + nil, + [ + BandwidthTnRepo::STASH_QUERY, + [tel, exp_r, exp_l, "test_bw_account", 10] + ] + ) + + BandwidthTnRepo.new( + local_calling_guide_repo: LocalCallingGuideRepo.new, + geo_code_repo: GeoCodeRepo.new(memcache: FakeMemcache.new) + ).disconnect(tel, "test").sync + + assert_mock BandwidthTnRepo::DB + end + end + em :test_stash_falls_back_to_lcg_on_unreadable + + def test_rate_center_resolves_region_and_locality_independently + fake_btn = Struct.new(:telephone_number, :bw_state, :bw_city) { + def get_details + { city: bw_city, state: bw_state } + end + } + + fake_prefix = Struct.new(:lat, :lon, :region, :locality) { + def to_h + { "region" => region, "locality" => locality } + end + } + + fake_geo_code = Struct.new(:locality, :region) { + def to_h + { locality: locality, region: region } + end + } + + fake_lcg_repo = Struct.new(:prefix) { + def find(_npa, _nxx) + EMPromise.resolve(prefix) + end + } + + fake_geo_repo = Struct.new(:result) { + def reverse(_lat, _lon) + EMPromise.resolve(result) + end + } + + # bw_r bw_l geo_r geo_l lcg_r lcg_l exp_r exp_l + [ + ["TX", "Austin", "CA", "LA", "NY", "NYC", "TX", "Austin"], + ["TX", "1City", "CA", "LA", "NY", "NYC", "TX", "LA"], + ["TX", "1City", "CA", "2City", "NY", "NYC", "TX", "NYC"], + ["1ST", "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"], + ["1ST", "1City", "CA", "LA", "NY", "NYC", "CA", "LA"], + ["1ST", "1City", "CA", "2City", "NY", "NYC", "CA", "NYC"], + ["1ST", "Austin", "2ST", "LA", "NY", "NYC", "NY", "Austin"], + ["1ST", "1City", "2ST", "LA", "NY", "NYC", "NY", "LA"], + ["1ST", "1City", "2ST", "2City", "NY", "NYC", "NY", "NYC"], + ["TX", nil, "CA", "LA", "NY", "NYC", "TX", "LA"], + ["TX", nil, "CA", nil, "NY", "NYC", "TX", "NYC"], + [nil, "Austin", "CA", "LA", "NY", "NYC", "CA", "Austin"], + [nil, nil, "CA", "LA", "NY", "NYC", "CA", "LA"], + [nil, nil, "CA", nil, "NY", "NYC", "CA", "NYC"], + [nil, "Austin", nil, "LA", "NY", "NYC", "NY", "Austin"], + [nil, nil, nil, "LA", "NY", "NYC", "NY", "LA"], + [nil, nil, nil, nil, "NY", "NYC", "NY", "NYC"] + ].each do |bw_r, bw_l, geo_r, geo_l, lcg_r, lcg_l, exp_r, exp_l| + btn = fake_btn.new("5565555555", bw_r, bw_l) + prefix = fake_prefix.new(30.0, -97.0, lcg_r, lcg_l) + geo = fake_geo_code.new(geo_l, geo_r) + + result = BandwidthTnRepo::RateCenter.for( + btn, + lcg_repo: fake_lcg_repo.new(prefix), + geo_code_repo: fake_geo_repo.new(geo) + ).sync + + assert_equal( + [exp_r, exp_l], + result.to_a, + "bw=#{[bw_r, bw_l]} geo=#{[geo_r, geo_l]} " \ + "lcg=#{[lcg_r, lcg_l]}" + ) + end + end + em :test_rate_center_resolves_region_and_locality_independently + + def stub_bw_tndetails(bw_r, bw_l) + if bw_r == :error + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails" + ).to_return(status: 404) + return + end + + bw_body = Nokogiri::XML::Builder.new { |xml| + xml.Response { + xml.TelephoneNumberDetails { + xml.City(bw_l) if bw_l + xml.State(bw_r) if bw_r + } + } + }.to_xml + + stub_request( + :get, + "https://dashboard.bandwidth.com/v1.0/tns/5565555555/tndetails" + ) + .with( + headers: { + "Accept" => "application/xml", + "Accept-Encoding" => "gzip, compressed", + "Authorization" => "Bearer test_bw_oauth_token", + "User-Agent" => "Ruby-Bandwidth-Iris" + } + ).to_return(status: 200, body: bw_body, headers: {}) + end + + def stub_geocoder(geo_r, geo_l) + if geo_r == :error + stub_request( + :get, + "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061" + ).to_return(status: 500) + return + end + + geo_json = { + "latt" => "30.267153", + "longt" => "-97.743061" + } + geo_json["prov"] = geo_r if geo_r + geo_json["city"] = geo_l if geo_l + + stub_request( + :get, + "https://geocoder.ca/?json=1&latt=30.267153&longt=-97.743061" + ).to_return(status: 200, body: geo_json.to_json) + end end diff --git a/test/test_geo_code.rb b/test/test_geo_code.rb index 6e57d859c1889e564bad21a548d781781fda3b02..0656d5086031c7cab0701e775ea67dd1faf3225f 100644 --- a/test/test_geo_code.rb +++ b/test/test_geo_code.rb @@ -5,13 +5,17 @@ require "geo_code" class GeoCodeTest < Minitest::Test def test_city_from_top_level - geo = GeoCode.for("city" => "Toronto", "prov" => "ON", "latt" => "43.7", "longt" => "-79.4") - assert_equal "Toronto", geo.city + geo = GeoCode.for( + "city" => "Toronto", "prov" => "ON", "latt" => "43.7", "longt" => "-79.4" + ) + assert_equal "Toronto", geo.locality end def test_state_from_top_level - geo = GeoCode.for("city" => "Toronto", "prov" => "ON", "latt" => "43.7", "longt" => "-79.4") - assert_equal "ON", geo.state + geo = GeoCode.for( + "city" => "Toronto", "prov" => "ON", "latt" => "43.7", "longt" => "-79.4" + ) + assert_equal "ON", geo.region end def test_city_prefers_usa_object @@ -22,7 +26,7 @@ class GeoCodeTest < Minitest::Test "longt" => "-74.0", "usa" => { "uscity" => "Manhattan", "state" => "NY" } ) - assert_equal "Manhattan", geo.city + assert_equal "Manhattan", geo.locality end def test_state_prefers_usa_object @@ -33,6 +37,6 @@ class GeoCodeTest < Minitest::Test "longt" => "-74.0", "usa" => { "uscity" => "Manhattan", "state" => "NY" } ) - assert_equal "NY", geo.state + assert_equal "NY", geo.region end end diff --git a/test/test_geo_code_repo.rb b/test/test_geo_code_repo.rb index 70364ff0884905e949eec33b0d23497b4e0a6ba6..0be664d3bb2f1341caf8b3c9029706bd42897915 100644 --- a/test/test_geo_code_repo.rb +++ b/test/test_geo_code_repo.rb @@ -19,8 +19,8 @@ class GeoCodeRepoTest < Minitest::Test geo = GeoCodeRepo.new(memcache: FakeMemcache.new) .reverse(40.739362, -73.991043).sync - assert_equal "New York", geo.city - assert_equal "NY", geo.state + assert_equal "New York", geo.locality + assert_equal "NY", geo.region end em :test_reverse diff --git a/test/test_helper.rb b/test/test_helper.rb index 5db6a0d46bc8fa3ac71d3f4af5a06998b6c0d7a3..3387525acdb34ad37add0f2fb2bbf174effda6a8 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -503,7 +503,7 @@ end module Minitest class Test - def setup + def stub_bw_oauth_token oauth_body = { access_token: "test_bw_oauth_token", token_type: "Bearer", @@ -516,6 +516,10 @@ module Minitest status: 200, body: oauth_body, headers: { "Content-Type" => "application/json" } ) + end + + def setup + stub_bw_oauth_token super end diff --git a/test/test_local_calling_guide_repo.rb b/test/test_local_calling_guide_repo.rb index d1e0a2dd7f6b732ac28e1cf7a88b85ef0a323fef..6c8c3dafb3181a7fece706e7a7788728c88534c4 100644 --- a/test/test_local_calling_guide_repo.rb +++ b/test/test_local_calling_guide_repo.rb @@ -6,8 +6,8 @@ require "local_calling_guide_repo" class LocalCallingGuideRepoTest < Minitest::Test def test_find_returns_lat_lon body = Nokogiri::XML::Builder.new { |xml| - xml.root { - xml.prefixdata { + xml.root do + xml.prefixdata do xml.npa "917" xml.nxx "727" xml.x "0" @@ -15,8 +15,8 @@ class LocalCallingGuideRepoTest < Minitest::Test xml.region "NY" xml.send(:"rc-lat", "40.739362") xml.send(:"rc-lon", "-73.991043") - } - xml.prefixdata { + end + xml.prefixdata do xml.npa "917" xml.nxx "727" xml.x "1" @@ -24,8 +24,8 @@ class LocalCallingGuideRepoTest < Minitest::Test xml.region "NY" xml.send(:"rc-lat", "40.739362") xml.send(:"rc-lon", "-73.991043") - } - } + end + end }.to_xml stub_request( @@ -33,14 +33,41 @@ class LocalCallingGuideRepoTest < Minitest::Test "https://localcallingguide.com/xmlprefix.php?npa=917&nxx=727" ).to_return(status: 200, body: body) - result = LocalCallingGuideRepo.new.find("917", "727").sync - assert_equal 40.739362, result[:lat] - assert_equal(-73.991043, result[:lon]) + prefix = LocalCallingGuideRepo.new.find("917", "727").sync + assert_equal(40.739362, prefix.lat) + assert_equal(-73.991043, prefix.lon) end em :test_find_returns_lat_lon - def test_find_raises_on_missing_data + def test_prefix_to_h body = Nokogiri::XML::Builder.new { |xml| + xml.root do + xml.prefixdata do + xml.rc "NEW YORK" + xml.region "NY" + xml.send(:"rc-lat", "40.739362") + xml.send(:"rc-lon", "-73.991043") + end + end + }.to_xml + + stub_request( + :get, + "https://localcallingguide.com/xmlprefix.php?npa=917&nxx=727" + ).to_return(status: 200, body: body) + + prefix = LocalCallingGuideRepo.new.find("917", "727").sync + assert_equal( + { "region" => "NY", "locality" => "NEW YORK" }, + prefix.to_h + ) + end + em :test_prefix_to_h + + def test_find_raises_on_missing_data + # Applying Style/SymbolProc causes Nokogiri + # to raise with "Cannot creating bind from C-level Proc" + body = Nokogiri::XML::Builder.new { |xml| # rubocop:disable Style/SymbolProc xml.root }.to_xml