bandwidth_tn_repo.rb: fallback to geocode

Phillip Davis created

Change summary

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(-)

Detailed changes

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<RateCenter>]
+		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<Hash>]
+			def fetch(acc)
+				raise NotImplementedError
+			end
+
+			class Bandwidth < self
+				# @param btn [BandwidthIris::Tn]
+				def initialize(btn)
+					@btn = btn
+				end
+
+				# @param _acc [RateCenter]
+				# @return [EMPromise<Hash>]
+				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<Hash>]
+				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<Hash>]
+				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)

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<Symbol, String>
+	def to_h
+		{
+			locality: locality,
+			region: region,
+			country: country
+		}
+	end
+
+private
+
+	# @return [Boolean]
+	def usa?
+		@data.key?("usa")
 	end
 end

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<String, String>]
+		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<Hash{Symbol => Float}>] :lat and :lon of the rate center
+	# @return [EMPromise<Prefix>]
 	# @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

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: {})
-				<Response>
-					<TelephoneNumberDetails>
-						<City>Austin</City>
-						<State>TX</State>
-					</TelephoneNumberDetails>
-				</Response>
-			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: {})
-				<Response>
-					<TelephoneNumberDetails>
-						<City>Austin</City>
-						<State>TX</State>
-					</TelephoneNumberDetails>
-				</Response>
-			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

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

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
 

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
 

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