rets 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 56d0e6818b0033682fe65525f1c73a2eb4ae42bb
4
- data.tar.gz: ee6fea4f6d3f028cd6247fb75896e5d70a6abe73
3
+ metadata.gz: 2ed1e7653c004159b6f9042dc917f9c8a30be926
4
+ data.tar.gz: 4b2275a656b7240b5d94a4d0f9c0ad5a62a18eeb
5
5
  SHA512:
6
- metadata.gz: 56b3c73c03e27236c1e99c13de713af4b45472e6853637bbfb77e90189c7e5a438ec13b8ad5cd0b9c2ada381f1d79bae72592cb3daab6067547a949f5fab51c0
7
- data.tar.gz: 6b67b8bd34c923e54b49232f161d4daf8662c8fd86899f87b02951d75d6eae9bb5bd19c7f42e0c2667ce41d8dbf949794cd5748762315a3feae8c40052e8afb9
6
+ metadata.gz: 3853c2929ab43105aba05ceebe185e4c3b86271bdc99b8ec42e9578921ee9d2740c98cfea2b31a13e22de6e3d5e4fa608f61399133593c43dae14f3c63ffc772
7
+ data.tar.gz: 7d177e112e61b54dc347811bdfa6a44aa2dd472ce45b061dbf180e636b4e5f2c4318e6d8ea08dc5c1013615118eefc4da401fa4b7bd3da629317d7022221f30b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### 0.6.0 / 2013-10-30
2
+
3
+ * fix: fix spelling error that created misleading exceptions
4
+ * feature: track stats for http requests sent
5
+ * feature: raise an exception if the login action doesn't return an http 200 status
6
+ * feature: add better class description and more fields to print tree
7
+ * feature: support http proxies
8
+ * feature: customizable http timeouts
9
+ * feature: add logging http headers when in debug mode
10
+ * feature: strip invalid utf8 from responses before parsing
11
+ * fix: don't raise an exception on a 401 after logout
12
+ * fix: treat no matching records status without a count node as a zero count
13
+ * feature: add an option for loading custom ca_certs
14
+ * feature: remove invalid resource types from metadata
15
+ * feature: special case http 412
16
+ * feature: add max_retries option
17
+
18
+ ### 0.5.1 / 2013-10-30
19
+
20
+ * fix: 0.5.0 was broken, fix gem Manifest to fix gem
21
+
1
22
  ### 0.5.0 / 2013-09-05
2
23
 
3
24
  * feature: Allow client.count to get integer count
data/Manifest.txt CHANGED
@@ -25,3 +25,4 @@ test/test_locking_http_client.rb
25
25
  test/test_metadata.rb
26
26
  test/test_parser_compact.rb
27
27
  test/test_parser_multipart.rb
28
+ test/vcr_cassettes/unauthorized_response.yml
data/README.md CHANGED
@@ -17,6 +17,10 @@ A pure-ruby library for fetching data from [RETS] servers.
17
17
  [net-http-persistent]: http://seattlerb.rubyforge.org/net-http-persistent/
18
18
  [nokogiri]: http://nokogiri.org
19
19
 
20
+ ## EXAMPLE USAGE:
21
+
22
+ We need work in this area! There are currently a few guideline examples in the `example` folder on connecting, fetching a property's data, and fetching a property's photos.
23
+
20
24
  ## LICENSE:
21
25
 
22
26
  (The MIT License)
data/lib/rets.rb CHANGED
@@ -3,7 +3,7 @@ require 'digest/md5'
3
3
  require 'nokogiri'
4
4
 
5
5
  module Rets
6
- VERSION = '0.5.1'
6
+ VERSION = '0.6.0'
7
7
 
8
8
  MalformedResponse = Class.new(ArgumentError)
9
9
  UnknownResponse = Class.new(ArgumentError)
@@ -31,7 +31,7 @@ module Rets
31
31
  attr_reader :capability_name
32
32
  def initialize(capability_name)
33
33
  @capability_name = capability_name
34
- super("unknown capabilitiy #{capability_name}")
34
+ super("unknown capability #{capability_name}")
35
35
  end
36
36
  end
37
37
  end
data/lib/rets/client.rb CHANGED
@@ -31,7 +31,20 @@ module Rets
31
31
  self.logger = @options[:logger] || FakeLogger.new
32
32
  @client_progress = ClientProgressReporter.new(self.logger, options[:stats_collector], options[:stats_prefix])
33
33
  @cached_metadata = @options[:metadata]
34
- @http = HTTPClient.new
34
+ if @options[:http_proxy]
35
+ @http = HTTPClient.new(options.fetch(:http_proxy))
36
+
37
+ if @options[:proxy_username]
38
+ @http.set_proxy_auth(options.fetch(:proxy_username), options.fetch(:proxy_password))
39
+ end
40
+ else
41
+ @http = HTTPClient.new
42
+ end
43
+
44
+ if @options[:receive_timeout]
45
+ @http.receive_timeout = @options[:receive_timeout]
46
+ end
47
+
35
48
  @http.set_cookie_store(options[:cookie_store]) if options[:cookie_store]
36
49
 
37
50
  @http_client = Rets::HttpClient.new(@http, @options, @logger, @login_url)
@@ -61,6 +74,10 @@ module Rets
61
74
  raise NoLogout.new('No logout method found for rets client')
62
75
  end
63
76
  http_get(capability_url("Logout"))
77
+ rescue UnknownResponse => e
78
+ unless e.message.match(/expected a 200, but got 401/)
79
+ raise e
80
+ end
64
81
  end
65
82
 
66
83
  # Finds records.
@@ -98,7 +115,7 @@ module Rets
98
115
  begin
99
116
  find_every(opts, resolve)
100
117
  rescue AuthorizationFailure, InvalidRequest => e
101
- if retries < 3
118
+ if retries < opts.fetch(:max_retries, 3)
102
119
  retries += 1
103
120
  @client_progress.find_with_retries_failed_a_retry(e, retries)
104
121
  clean_setup
@@ -117,7 +134,7 @@ module Rets
117
134
  if opts[:count] == COUNT.only
118
135
  Parser::Compact.get_count(res.body)
119
136
  else
120
- results = Parser::Compact.parse_document(res.body)
137
+ results = Parser::Compact.parse_document(res.body.encode("UTF-8", "binary", :invalid => :replace, :undef => :replace))
121
138
  if resolve
122
139
  rets_class = find_rets_class(opts[:search_type], opts[:class])
123
140
  decorate_results(results, rets_class)
@@ -239,11 +256,18 @@ module Rets
239
256
  self.metadata = @cached_metadata
240
257
  else
241
258
  @client_progress.bad_cached_metadata(@cached_metadata)
242
- metadata_fetcher = lambda { |type| retrieve_metadata_type(type) }
243
- self.metadata = Metadata::Root.new(&metadata_fetcher)
259
+ self.metadata = Metadata::Root.new(logger, retrieve_metadata)
244
260
  end
245
261
  end
246
262
 
263
+ def retrieve_metadata
264
+ raw_metadata = {}
265
+ Metadata::METADATA_TYPES.each {|type|
266
+ raw_metadata[type] = retrieve_metadata_type(type)
267
+ }
268
+ raw_metadata
269
+ end
270
+
247
271
  def retrieve_metadata_type(type)
248
272
  res = http_post(capability_url("GetMetadata"),
249
273
  { "Format" => "COMPACT",
@@ -322,6 +346,13 @@ module Rets
322
346
 
323
347
  class ErrorChecker
324
348
  def self.check(response)
349
+ # some RETS servers returns HTTP code 412 when session cookie expired, yet the response body
350
+ # passes XML check. We need to special case for this situation.
351
+ # This method is also called from multipart.rb where there are headers and body but no status_code
352
+ if response.respond_to?(:status_code) && response.status_code == 412
353
+ raise HttpError, "HTTP status: #{response.status_code}, body: #{response.body}"
354
+ end
355
+
325
356
  # some RETS servers return success code in XML body but failure code 4xx in http status
326
357
  # If xml body is present we ignore http status
327
358
 
@@ -7,13 +7,16 @@ module Rets
7
7
  @options = options
8
8
  @logger = logger
9
9
  @login_url = login_url
10
+ @options.fetch(:ca_certs, []).each {|c| @http.ssl_config.add_trust_ca(c) }
10
11
  end
11
12
 
12
13
  def http_get(url, params=nil, extra_headers={})
13
14
  http.set_auth(url, options[:username], options[:password])
14
15
  headers = extra_headers.merge(rets_extra_headers)
15
- res = http.get(url, params, headers)
16
- log_http_traffic("POST", url, params, headers, res)
16
+ res = nil
17
+ log_http_traffic("POST", url, params, headers) do
18
+ res = http.get(url, params, headers)
19
+ end
17
20
  Client::ErrorChecker.check(res)
18
21
  res
19
22
  end
@@ -21,12 +24,34 @@ module Rets
21
24
  def http_post(url, params, extra_headers = {})
22
25
  http.set_auth(url, options[:username], options[:password])
23
26
  headers = extra_headers.merge(rets_extra_headers)
24
- res = http.post(url, params, headers)
25
- log_http_traffic("POST", url, params, headers, res)
27
+ res = nil
28
+ log_http_traffic("POST", url, params, headers) do
29
+ res = http.post(url, params, headers)
30
+ end
26
31
  Client::ErrorChecker.check(res)
27
32
  res
28
33
  end
29
34
 
35
+ def log_http_traffic(method, url, params, headers, &block)
36
+ # optimization, we don't want to compute log params
37
+ # if logging is off
38
+ if logger.debug?
39
+ logger.debug "Rets::Client >> #{method} #{url}"
40
+ logger.debug "Rets::Client >> params = #{params.inspect}"
41
+ logger.debug "Rets::Client >> headers = #{headers.inspect}"
42
+ end
43
+
44
+ res = block.call
45
+
46
+ # optimization, we don't want to compute log params
47
+ # if logging is off, especially when there is a loop just
48
+ # for logging
49
+ if logger.debug?
50
+ logger.debug "Rets::Client << Status #{res.status_code}"
51
+ res.headers.each { |k, v| logger.debug "Rets::Client << #{k}: #{v}" }
52
+ end
53
+ end
54
+
30
55
  def save_cookie_store(force=nil)
31
56
  if options[:cookie_store]
32
57
  if force
@@ -37,15 +62,6 @@ module Rets
37
62
  end
38
63
  end
39
64
 
40
- def log_http_traffic(method, url, params, headers, res)
41
- return unless logger.debug?
42
- logger.debug "Rets::Client >> #{method} #{url}"
43
- logger.debug "Rets::Client >> params = #{params.inspect}"
44
- logger.debug "Rets::Client >> headers = #{headers.inspect}"
45
- logger.debug "Rets::Client << Status #{res.status_code}"
46
- res.headers.each { |k, v| logger.debug "Rets::Client << #{k}: #{v}" }
47
- end
48
-
49
65
  def rets_extra_headers
50
66
  user_agent = options[:agent] || "Client/1.0"
51
67
  rets_version = options[:version] || "RETS/1.7.2"
@@ -10,7 +10,7 @@ module Rets
10
10
  end
11
11
 
12
12
  def print_tree
13
- puts " #{long_value} -> #{value}"
13
+ puts " #{long_value} -> #{value}"
14
14
  end
15
15
  end
16
16
  end
@@ -1,6 +1,7 @@
1
1
  module Rets
2
2
  module Metadata
3
3
  class Resource
4
+ class MissingRetsClass < RuntimeError; end
4
5
  attr_accessor :rets_classes
5
6
  attr_accessor :lookup_types
6
7
  attr_accessor :key_field
@@ -24,7 +25,12 @@ module Rets
24
25
  end
25
26
 
26
27
  def self.find_rets_classes(metadata, resource)
27
- metadata[:class].detect { |c| c.resource == resource.id }.classes
28
+ class_container = metadata[:class].detect { |c| c.resource == resource.id }
29
+ if class_container.nil?
30
+ raise MissingRetsClass.new("No Metadata classes for #{resource.id}")
31
+ else
32
+ class_container.classes
33
+ end
28
34
  end
29
35
 
30
36
  def self.build_lookup_tree(resource, metadata)
@@ -52,12 +58,15 @@ module Rets
52
58
  end
53
59
  end
54
60
 
55
- def self.build(resource_fragment, metadata)
61
+ def self.build(resource_fragment, metadata, logger)
56
62
  resource = new(resource_fragment)
57
63
 
58
64
  resource.lookup_types = build_lookup_tree(resource, metadata)
59
65
  resource.rets_classes = build_classes(resource, metadata)
60
66
  resource
67
+ rescue MissingRetsClass => e
68
+ logger.warn(e.message)
69
+ nil
61
70
  end
62
71
 
63
72
  def print_tree
@@ -3,12 +3,16 @@ module Rets
3
3
  class RetsClass
4
4
  attr_accessor :tables
5
5
  attr_accessor :name
6
+ attr_accessor :visible_name
7
+ attr_accessor :description
6
8
  attr_accessor :resource
7
9
 
8
10
  def initialize(rets_class_fragment, resource)
9
11
  self.resource = resource
10
12
  self.tables = []
11
13
  self.name = rets_class_fragment["ClassName"]
14
+ self.visible_name = rets_class_fragment["VisibleName"]
15
+ self.description = rets_class_fragment["Description"]
12
16
  end
13
17
 
14
18
  def self.find_table_container(metadata, resource, rets_class)
@@ -31,6 +35,8 @@ module Rets
31
35
 
32
36
  def print_tree
33
37
  puts " Class: #{name}"
38
+ puts " Visible Name: #{visible_name}"
39
+ puts " Description : #{description}"
34
40
  tables.each(&:print_tree)
35
41
  end
36
42
 
@@ -47,33 +47,25 @@ module Rets
47
47
  # Sources are the raw xml documents fetched for each metadata type
48
48
  # they are stored as a hash with the type names as their keys
49
49
  # and the raw xml as the values
50
- attr_accessor :sources
50
+ attr_reader :sources
51
+
52
+ # Metadata can be unmarshalled from cache. @logger is not set during that process, constructor is not called.
53
+ # Client code must set it after unmarshalling.
54
+ attr_reader :logger
51
55
 
52
56
  # fetcher is a proc that inverts control to the client to retrieve metadata
53
57
  # types
54
- def initialize(&fetcher)
58
+ def initialize(logger, sources)
59
+ @logger = logger
55
60
  @tree = nil
56
61
  @metadata_types = nil # TODO think up a better name ... containers?
57
- @sources = {}
58
-
59
- # allow Root's to be built with no fetcher. Makes for easy testing
60
- return unless block_given?
61
-
62
- fetch_sources(&fetcher)
63
- end
64
-
65
- def fetch_sources(&fetcher)
66
- self.sources = Hash[*METADATA_TYPES.map {|type| [type, fetcher.call(type)] }.flatten]
62
+ @sources = sources
67
63
  end
68
64
 
69
65
  def marshal_dump
70
66
  sources
71
67
  end
72
68
 
73
- def marshal_load(sources)
74
- self.sources = sources
75
- end
76
-
77
69
  def version
78
70
  metadata_types[:system].first.version
79
71
  end
@@ -87,11 +79,11 @@ module Rets
87
79
  # version was published, or a version number. These values may or may
88
80
  # not exist on any given rets server.
89
81
  def current?(current_timestamp, current_version)
90
- if !current_version.to_s.empty? && !version.to_s.empty?
91
- current_version == version
92
- else
82
+ if !current_version.to_s.empty? && !version.to_s.empty?
83
+ current_version == version
84
+ else
93
85
  current_timestamp ? current_timestamp == date : true
94
- end
86
+ end
95
87
  end
96
88
 
97
89
  def build_tree
@@ -101,8 +93,9 @@ module Rets
101
93
 
102
94
  resource_containers.each do |resource_container|
103
95
  resource_container.rows.each do |resource_fragment|
104
- resource = Resource.build(resource_fragment, metadata_types)
105
- tree[resource.id.downcase] = resource
96
+ resource = Resource.build(resource_fragment, metadata_types, @logger)
97
+ #some mlses list resource types without an associated data, throw those away
98
+ tree[resource.id.downcase] = resource if resource
106
99
  end
107
100
  end
108
101
 
@@ -33,6 +33,7 @@ module Rets
33
33
  puts " StandardName: #{ table_fragment["StandardName"] }"
34
34
  puts " Units: #{ table_fragment["Units"] }"
35
35
  puts " Searchable: #{ table_fragment["Searchable"] }"
36
+ puts " Required: #{table_fragment['Required']}"
36
37
  end
37
38
 
38
39
  def resolve(value)
@@ -67,6 +68,13 @@ module Rets
67
68
 
68
69
  def print_tree
69
70
  puts " LookupTable: #{name}"
71
+ puts " Required: #{table_fragment['Required']}"
72
+ puts " Searchable: #{ table_fragment["Searchable"] }"
73
+ puts " Units: #{ table_fragment["Units"] }"
74
+ puts " ShortName: #{ table_fragment["ShortName"] }"
75
+ puts " LongName: #{ table_fragment["LongName"] }"
76
+ puts " StandardName: #{ table_fragment["StandardName"] }"
77
+ puts " Types:"
70
78
 
71
79
  lookup_types.each(&:print_tree)
72
80
  end
@@ -49,8 +49,14 @@ module Rets
49
49
 
50
50
  def self.get_count(xml)
51
51
  doc = Nokogiri.parse(xml.to_s)
52
- doc.at("//COUNT").attr('Records').to_i
52
+ if node = doc.at("//COUNT")
53
+ return node.attr('Records').to_i
54
+ elsif node = doc.at("//RETS-STATUS")
55
+ # Handle <RETS-STATUS ReplyCode="20201" ReplyText="No matching records were found" />
56
+ return 0 if node.attr('ReplyCode') == '20201'
57
+ end
53
58
  end
59
+
54
60
  end
55
61
  end
56
62
  end
data/test/fixtures.rb CHANGED
@@ -27,6 +27,13 @@ COUNT_ONLY = <<XML
27
27
  </RETS>
28
28
  XML
29
29
 
30
+ RETS_STATUS_NO_MATCHING_RECORDS = <<XML
31
+ <?xml version="1.0"?>
32
+ <RETS ReplyCode="0" ReplyText="Operation Successful">
33
+ <RETS-STATUS ReplyCode="20201" ReplyText="No matching records were found" />
34
+ </RETS>
35
+ XML
36
+
30
37
  CAPABILITIES_WITH_WHITESPACE = <<XML
31
38
  <RETS ReplyCode="0" ReplyText="Operation Successful">
32
39
  <RETS-RESPONSE>
data/test/test_client.rb CHANGED
@@ -37,21 +37,23 @@ class TestClient < MiniTest::Test
37
37
  end
38
38
 
39
39
  def test_metadata_when_not_initialized_with_metadata
40
+ new_raw_metadata = stub(:new_raw_metadata)
41
+
40
42
  client = Rets::Client.new(:login_url => "http://example.com")
41
- Rets::Metadata::Root.expects(:new)
42
- client.metadata
43
+ client.stubs(:retrieve_metadata).returns(new_raw_metadata)
44
+
45
+ assert_same new_raw_metadata, client.metadata.marshal_dump
43
46
  end
44
47
 
45
- def test_initialize_with_old_metadata_cached_gets_new_metadata
48
+ def test_initialize_with_old_metadata_cached_contstructs_new_metadata_from_request
46
49
  metadata = stub(:current? => false)
47
- new_metadata = stub(:current? => false)
50
+ new_raw_metadata = stub(:new_raw_metadata)
51
+
48
52
  client = Rets::Client.new(:login_url => "http://example.com", :metadata => metadata)
49
- client.stubs(:capabilities => {})
50
- Rets::Metadata::Root.expects(:new => new_metadata).once
53
+ client.stubs(:capabilities).returns({})
54
+ client.stubs(:retrieve_metadata).returns(new_raw_metadata)
51
55
 
52
- assert_same new_metadata, client.metadata
53
- # This second call ensures the expectations on Root are met
54
- client.metadata
56
+ assert_same new_raw_metadata, client.metadata.marshal_dump
55
57
  end
56
58
 
57
59
  def test_initialize_with_current_metadata_cached_return_cached_metadata
@@ -210,4 +212,27 @@ class TestClient < MiniTest::Test
210
212
  assert_equal response, result
211
213
  end
212
214
 
215
+ def test_clean_setup_with_receive_timeout
216
+ HTTPClient.any_instance.expects(:receive_timeout=).with(1234)
217
+ @client = Rets::Client.new(
218
+ login_url: 'http://example.com/login',
219
+ receive_timeout: 1234
220
+ )
221
+ end
222
+
223
+ def test_clean_setup_with_proxy_auth
224
+ @login_url = 'http://example.com/login'
225
+ @proxy_url = 'http://example.com/proxy'
226
+ @proxy_username = 'username'
227
+ @proxy_password = 'password'
228
+ HTTPClient.any_instance.expects(:set_proxy_auth).with(@proxy_username, @proxy_password)
229
+
230
+ @client = Rets::Client.new(
231
+ login_url: @login_url,
232
+ http_proxy: @proxy_url,
233
+ proxy_username: @proxy_username,
234
+ proxy_password: @proxy_password
235
+ )
236
+ end
237
+
213
238
  end
@@ -1,8 +1,9 @@
1
1
  require_relative "helper"
2
+ require 'logger'
2
3
 
3
4
  class TestMetadata < MiniTest::Test
4
5
  def setup
5
- @root = Rets::Metadata::Root.new
6
+ @root = Rets::Metadata::Root.new(Logger.new(STDOUT), {})
6
7
  $VERBOSE = true
7
8
  end
8
9
 
@@ -10,23 +11,6 @@ class TestMetadata < MiniTest::Test
10
11
  $VERBOSE = false
11
12
  end
12
13
 
13
- def test_metadata_root_fetch_sources_returns_hash_of_metadata_types
14
- types = []
15
- fake_fetcher = lambda do |type|
16
- types << type
17
- end
18
-
19
- @root.fetch_sources(&fake_fetcher)
20
-
21
- assert_equal(Rets::Metadata::METADATA_TYPES, types)
22
- end
23
-
24
- def test_metadata_root_intialized_with_block
25
- external = false
26
- Rets::Metadata::Root.new { |source| external = true }
27
- assert external
28
- end
29
-
30
14
  def test_metadata_root_build_tree
31
15
  resource = stub(:id => "X")
32
16
  Rets::Metadata::Resource.stubs(:build => resource)
@@ -111,10 +95,11 @@ class TestMetadata < MiniTest::Test
111
95
 
112
96
  def test_metadata_root_metadata_types_constructs_a_hash_of_metadata_types_from_sources
113
97
  test_sources = { "X" => "Y", "Z" => "W" }
114
- @root.stubs(:sources => test_sources, :build_containers => "Y--")
115
- @root.metadata_types = nil
98
+ root = Rets::Metadata::Root.new(stub(:logger), test_sources)
99
+ root.stubs(:build_containers => "Y--")
116
100
  Nokogiri.stubs(:parse => "Y-")
117
- assert_equal({:x => "Y--", :z => "Y--"}, @root.metadata_types)
101
+
102
+ assert_equal({:x => "Y--", :z => "Y--"}, root.metadata_types)
118
103
  end
119
104
 
120
105
  def test_metadata_root_build_containers_selects_correct_tags
@@ -211,12 +196,30 @@ class TestMetadata < MiniTest::Test
211
196
  Rets::Metadata::Resource.stubs(:build_lookup_tree => lookup_types)
212
197
  Rets::Metadata::Resource.stubs(:build_classes => classes)
213
198
 
214
- resource = Rets::Metadata::Resource.build(fragment, metadata)
199
+ resource = Rets::Metadata::Resource.build(fragment, metadata, Logger.new(STDOUT))
215
200
 
216
201
  assert_equal(lookup_types, resource.lookup_types)
217
202
  assert_equal(classes, resource.rets_classes)
218
203
  end
219
204
 
205
+ def test_resource_build_with_incomplete_classes
206
+ fragment = { "ResourceID" => "test" }
207
+
208
+ lookup_types = stub(:lookup_types)
209
+ metadata = stub(:metadata)
210
+
211
+ Rets::Metadata::Resource.stubs(:build_lookup_tree => lookup_types)
212
+ Rets::Metadata::Resource.stubs(:build_classes).raises(Rets::Metadata::Resource::MissingRetsClass)
213
+
214
+ error_log = StringIO.new
215
+ resource = Rets::Metadata::Resource.build(fragment, metadata, Logger.new(error_log))
216
+
217
+ error_log.rewind
218
+ error_msg = error_log.read
219
+ assert error_msg.include?('MissingRetsClass')
220
+ assert_equal(nil, resource)
221
+ end
222
+
220
223
  def test_resource_find_lookup_containers
221
224
  resource = stub(:id => "id")
222
225
  metadata = { :lookup => [stub(:resource => "id"), stub(:resource => "id"), stub(:resource => "a")] }
@@ -440,18 +443,17 @@ class TestMetadata < MiniTest::Test
440
443
 
441
444
  def test_root_can_be_serialized
442
445
  sources = { :x => "a" }
443
-
444
- @root.sources = sources
445
-
446
- assert_equal sources, @root.marshal_dump
446
+ root = Rets::Metadata::Root.new(stub(:logger), sources)
447
+ assert_equal sources, root.marshal_dump
447
448
  end
448
449
 
449
450
  def test_root_can_be_unserialized
451
+ logger = stub(:logger)
450
452
  sources = { :x => "a" }
451
453
 
452
- @root.marshal_load(sources)
454
+ root_to_serialize = Rets::Metadata::Root.new(logger, sources)
455
+ new_root = Rets::Metadata::Root.new(logger, root_to_serialize.marshal_dump)
453
456
 
454
- assert_equal sources, @root.sources
457
+ assert_equal root_to_serialize.marshal_dump, new_root.marshal_dump
455
458
  end
456
-
457
459
  end
@@ -66,6 +66,11 @@ class TestParserCompact < MiniTest::Test
66
66
  assert_equal 1234, count
67
67
  end
68
68
 
69
+ def test_get_count_with_no_matching_records
70
+ count = Rets::Parser::Compact.get_count(RETS_STATUS_NO_MATCHING_RECORDS)
71
+ assert_equal 0, count
72
+ end
73
+
69
74
  def test_parse_example
70
75
  rows = Rets::Parser::Compact.parse_document(Nokogiri.parse(SAMPLE_COMPACT))
71
76
 
@@ -0,0 +1,262 @@
1
+ ---
2
+ http_interactions:
3
+ - request:
4
+ method: post
5
+ uri: http://rets.example.com/GetObject.asmx/GetObject
6
+ body:
7
+ encoding: US-ASCII
8
+ string: Resource=Property&Type=Photo&ID=2661580%3A1&Location=0
9
+ headers:
10
+ User-Agent:
11
+ - Estately/1.0
12
+ Host:
13
+ - rets.example.com:80
14
+ Rets-Version:
15
+ - RETS/1.7.2
16
+ Authorization:
17
+ - Digest username="login", realm="fake_realm", qop="auth",
18
+ uri="/Login.asmx/Login", nonce="a8f4bc805062602c8ba7a87b2109f808", nc=00000001,
19
+ cnonce="6e2dd038eea6cbacf0b956fd11f914b6", response="fake_digest",
20
+ opaque="96eda461-41d0-4624-b7fe-b59e966035f3"
21
+ Cookie:
22
+ - ASP.NET_SessionId=mapij045k3bphj3gqqgpmmrx
23
+ Accept:
24
+ - image/jpeg, image/png;q=0.5, image/gif;q=0.1
25
+ Content-Type:
26
+ - application/x-www-form-urlencoded
27
+ Content-Length:
28
+ - '54'
29
+ response:
30
+ status:
31
+ code: 401
32
+ message: Unauthorized
33
+ headers:
34
+ Connection:
35
+ - close
36
+ Date:
37
+ - Thu, 26 Jul 2012 00:11:02 GMT
38
+ Server:
39
+ - Microsoft-IIS/6.0
40
+ Cache-Control:
41
+ - private
42
+ - private
43
+ X-Aspnet-Version:
44
+ - 2.0.50727
45
+ Www-Authenticate:
46
+ - Digest realm="fake_realm",nonce="dfd74deac03147e1a4fff598679f1a80",opaque="8e5029c6-bb9c-4562-b440-e7c2b8960743",qop="auth"
47
+ Content-Type:
48
+ - text/plain
49
+ body:
50
+ encoding: US-ASCII
51
+ string: ! "Unauthorized Request. Digest Session Invalid Reauthenticate\r\nReference:
52
+ e0ff89c0-2d64-4287-a6b7-6be27c7d3ade"
53
+ http_version:
54
+ recorded_at: Thu, 26 Jul 2012 00:11:13 GMT
55
+ - request:
56
+ method: post
57
+ uri: http://rets.example.com/Login.asmx/Login
58
+ body:
59
+ encoding: US-ASCII
60
+ string: ''
61
+ headers:
62
+ User-Agent:
63
+ - Estately/1.0
64
+ Host:
65
+ - rets.example.com:80
66
+ Rets-Version:
67
+ - RETS/1.7.2
68
+ Accept:
69
+ - ! '*/*'
70
+ response:
71
+ status:
72
+ code: 401
73
+ message: Unauthorized
74
+ headers:
75
+ Connection:
76
+ - close
77
+ Date:
78
+ - Thu, 26 Jul 2012 00:11:03 GMT
79
+ Server:
80
+ - Microsoft-IIS/6.0
81
+ Cache-Control:
82
+ - private
83
+ - private
84
+ X-Aspnet-Version:
85
+ - 2.0.50727
86
+ Www-Authenticate:
87
+ - Digest realm="fake_realm",nonce="17fdfe2c6125296bd93bb81bd88dc4be",opaque="aee1e7da-60eb-41b3-b704-8eb3969645fd",qop="auth"
88
+ Content-Type:
89
+ - text/plain
90
+ body:
91
+ encoding: US-ASCII
92
+ string: ! "Unauthorized Request. Authentication needed\r\nReference: c2c5ab9f-8c12-42bb-94ce-5b6ebf156da4"
93
+ http_version:
94
+ recorded_at: Thu, 26 Jul 2012 00:11:13 GMT
95
+ - request:
96
+ method: post
97
+ uri: http://rets.example.com/Login.asmx/Login
98
+ body:
99
+ encoding: US-ASCII
100
+ string: ''
101
+ headers:
102
+ User-Agent:
103
+ - Estately/1.0
104
+ Host:
105
+ - rets.example.com:80
106
+ Rets-Version:
107
+ - RETS/1.7.2
108
+ Authorization:
109
+ - Digest username="login", realm="fake_realm", qop="auth",
110
+ uri="/Login.asmx/Login", nonce="17fdfe2c6125296bd93bb81bd88dc4be", nc=00000001,
111
+ cnonce="ad805b9e2d6f08ad82175085e5febd8d", response="7402076f53663602f052e1aa343d3dab",
112
+ opaque="aee1e7da-60eb-41b3-b704-8eb3969645fd"
113
+ Accept:
114
+ - ! '*/*'
115
+ response:
116
+ status:
117
+ code: 200
118
+ message: OK
119
+ headers:
120
+ Date:
121
+ - Thu, 26 Jul 2012 00:11:03 GMT
122
+ Server:
123
+ - Microsoft-IIS/6.0
124
+ Cache-Control:
125
+ - private
126
+ - private, max-age=0
127
+ X-Aspnet-Version:
128
+ - 2.0.50727
129
+ Rets-Version:
130
+ - RETS/1.7.2
131
+ Rets-Server:
132
+ - Interealty-RETS/1.5.247.0
133
+ Transfer-Encoding:
134
+ - chunked
135
+ Set-Cookie:
136
+ - ASP.NET_SessionId=vuesmq55l0cpjy45zyn0e555; path=/; HttpOnly
137
+ - RETS-Session-ID=vuesmq55l0cpjy45zyn0e555; path=/
138
+ Content-Type:
139
+ - text/xml
140
+ body:
141
+ encoding: US-ASCII
142
+ string: ! "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<RETS ReplyCode=\"0\"
143
+ ReplyText=\"Success. Reference ID: a3073726-a011-4391-b22e-900adae5184b\">\r\n
144
+ \ <RETS-RESPONSE>\r\nMemberName=John Wolff\r\nUser=23756,60,RH,login\r\nBroker=BRDA,BRDA01\r\nMetadataVersion=31.76.69587\r\nMetadataTimeStamp=2012-07-24T17:33:07Z\r\nMinMetadataTimeStamp=2012-07-24T17:33:07Z\r\nTimeoutSeconds=7200\r\nChangePassword=/ChangePassword.asmx/ChangePassword\r\nGetObject=/GetObject.asmx/GetObject\r\nLogin=/Login.asmx/Login\r\nLogout=/Logout.asmx/Logout\r\nSearch=/Search.asmx/Search\r\nGetMetadata=/GetMetadata.asmx/GetMetadata\r\n</RETS-RESPONSE>\r\n</RETS>"
145
+ http_version:
146
+ recorded_at: Thu, 26 Jul 2012 00:11:14 GMT
147
+ - request:
148
+ method: post
149
+ uri: http://rets.example.com/Login.asmx/Login
150
+ body:
151
+ encoding: US-ASCII
152
+ string: ''
153
+ headers:
154
+ User-Agent:
155
+ - Estately/1.0
156
+ Host:
157
+ - rets.example.com:80
158
+ Rets-Version:
159
+ - RETS/1.7.2
160
+ Authorization:
161
+ - Digest username="login", realm="fake_realm", qop="auth",
162
+ uri="/Login.asmx/Login", nonce="17fdfe2c6125296bd93bb81bd88dc4be", nc=00000001,
163
+ cnonce="ad805b9e2d6f08ad82175085e5febd8d", response="7402076f53663602f052e1aa343d3dab",
164
+ opaque="aee1e7da-60eb-41b3-b704-8eb3969645fd"
165
+ Cookie:
166
+ - ASP.NET_SessionId=vuesmq55l0cpjy45zyn0e555; RETS-Session-ID=vuesmq55l0cpjy45zyn0e555
167
+ Accept:
168
+ - ! '*/*'
169
+ response:
170
+ status:
171
+ code: 200
172
+ message: OK
173
+ headers:
174
+ Date:
175
+ - Thu, 26 Jul 2012 00:11:03 GMT
176
+ Server:
177
+ - Microsoft-IIS/6.0
178
+ Cache-Control:
179
+ - private
180
+ - private, max-age=0
181
+ X-Aspnet-Version:
182
+ - 2.0.50727
183
+ Rets-Version:
184
+ - RETS/1.7.2
185
+ Rets-Server:
186
+ - Interealty-RETS/1.5.247.0
187
+ Transfer-Encoding:
188
+ - chunked
189
+ Set-Cookie:
190
+ - RETS-Session-ID=vuesmq55l0cpjy45zyn0e555; path=/
191
+ Content-Type:
192
+ - text/xml
193
+ body:
194
+ encoding: US-ASCII
195
+ string: ! "<?xml version=\"1.0\" encoding=\"utf-8\"?>\r\n<RETS ReplyCode=\"0\"
196
+ ReplyText=\"Success. Reference ID: a48b4f59-cb22-41be-8e7b-1cee28fb8644\">\r\n
197
+ \ <RETS-RESPONSE>\r\nMemberName=John Wolff\r\nUser=23756,60,RH,login\r\nBroker=BRDA,BRDA01\r\nMetadataVersion=31.76.69587\r\nMetadataTimeStamp=2012-07-24T17:33:07Z\r\nMinMetadataTimeStamp=2012-07-24T17:33:07Z\r\nTimeoutSeconds=7200\r\nChangePassword=/ChangePassword.asmx/ChangePassword\r\nGetObject=/GetObject.asmx/GetObject\r\nLogin=/Login.asmx/Login\r\nLogout=/Logout.asmx/Logout\r\nSearch=/Search.asmx/Search\r\nGetMetadata=/GetMetadata.asmx/GetMetadata\r\n</RETS-RESPONSE>\r\n</RETS>"
198
+ http_version:
199
+ recorded_at: Thu, 26 Jul 2012 00:11:14 GMT
200
+ - request:
201
+ method: post
202
+ uri: http://rets.example.com/GetObject.asmx/GetObject
203
+ body:
204
+ encoding: US-ASCII
205
+ string: Resource=Property&Type=Photo&ID=2661580%3A1&Location=0
206
+ headers:
207
+ User-Agent:
208
+ - Estately/1.0
209
+ Host:
210
+ - rets.example.com:80
211
+ Rets-Version:
212
+ - RETS/1.7.2
213
+ Authorization:
214
+ - Digest username="login", realm="fake_realm", qop="auth",
215
+ uri="/Login.asmx/Login", nonce="17fdfe2c6125296bd93bb81bd88dc4be", nc=00000001,
216
+ cnonce="ad805b9e2d6f08ad82175085e5febd8d", response="7402076f53663602f052e1aa343d3dab",
217
+ opaque="aee1e7da-60eb-41b3-b704-8eb3969645fd"
218
+ Cookie:
219
+ - ASP.NET_SessionId=vuesmq55l0cpjy45zyn0e555; RETS-Session-ID=vuesmq55l0cpjy45zyn0e555
220
+ Accept:
221
+ - image/jpeg, image/png;q=0.5, image/gif;q=0.1
222
+ Content-Type:
223
+ - application/x-www-form-urlencoded
224
+ Content-Length:
225
+ - '54'
226
+ response:
227
+ status:
228
+ code: 200
229
+ message: OK
230
+ headers:
231
+ Date:
232
+ - Thu, 26 Jul 2012 00:11:04 GMT
233
+ Server:
234
+ - Microsoft-IIS/6.0
235
+ Cache-Control:
236
+ - private
237
+ - private, max-age=0
238
+ X-Aspnet-Version:
239
+ - 2.0.50727
240
+ Mime-Version:
241
+ - '1.0'
242
+ Rets-Version:
243
+ - RETS/1.7.2
244
+ Rets-Server:
245
+ - Interealty-RETS/1.5.247.0
246
+ Content-Id:
247
+ - '2661580'
248
+ Object-Id:
249
+ - '1'
250
+ Transfer-Encoding:
251
+ - chunked
252
+ Set-Cookie:
253
+ - RETS-Session-ID=vuesmq55l0cpjy45zyn0e555; path=/
254
+ Content-Type:
255
+ - image/jpeg
256
+ body:
257
+ encoding: ASCII-8BIT
258
+ string: !binary |-
259
+ /9j/4AAQSkZJRgABAQEAYABgAAD///2Q==
260
+ http_version:
261
+ recorded_at: Thu, 26 Jul 2012 00:11:14 GMT
262
+ recorded_with: VCR 2.2.2
metadata CHANGED
@@ -1,119 +1,118 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rets
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Estately, Inc. Open Source
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-30 00:00:00.000000000 Z
11
+ date: 2014-11-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ~>
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
19
  version: 2.3.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ~>
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: 2.3.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: nokogiri
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: 1.5.2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 1.5.2
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rdoc
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '4.0'
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '4.0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mocha
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: 0.11.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: 0.11.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: vcr
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ~>
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: 2.2.2
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ~>
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 2.2.2
83
83
  - !ruby/object:Gem::Dependency
84
84
  name: webmock
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - ~>
87
+ - - "~>"
88
88
  - !ruby/object:Gem::Version
89
89
  version: 1.8.0
90
90
  type: :development
91
91
  prerelease: false
92
92
  version_requirements: !ruby/object:Gem::Requirement
93
93
  requirements:
94
- - - ~>
94
+ - - "~>"
95
95
  - !ruby/object:Gem::Version
96
96
  version: 1.8.0
97
97
  - !ruby/object:Gem::Dependency
98
98
  name: hoe
99
99
  requirement: !ruby/object:Gem::Requirement
100
100
  requirements:
101
- - - ~>
101
+ - - "~>"
102
102
  - !ruby/object:Gem::Version
103
- version: '3.7'
103
+ version: '3.6'
104
104
  type: :development
105
105
  prerelease: false
106
106
  version_requirements: !ruby/object:Gem::Requirement
107
107
  requirements:
108
- - - ~>
108
+ - - "~>"
109
109
  - !ruby/object:Gem::Version
110
- version: '3.7'
111
- description: ! '[![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
112
-
110
+ version: '3.6'
111
+ description: |-
112
+ [![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
113
113
  A pure-ruby library for fetching data from [RETS] servers.
114
114
 
115
-
116
- [RETS]: http://www.rets.org'
115
+ [RETS]: http://www.rets.org
117
116
  email:
118
117
  - opensource@estately.com
119
118
  executables:
@@ -124,6 +123,7 @@ extra_rdoc_files:
124
123
  - Manifest.txt
125
124
  - README.md
126
125
  files:
126
+ - ".gemtest"
127
127
  - CHANGELOG.md
128
128
  - Manifest.txt
129
129
  - README.md
@@ -151,34 +151,33 @@ files:
151
151
  - test/test_metadata.rb
152
152
  - test/test_parser_compact.rb
153
153
  - test/test_parser_multipart.rb
154
- - .gemtest
154
+ - test/vcr_cassettes/unauthorized_response.yml
155
155
  homepage: http://github.com/estately/rets
156
- licenses:
157
- - MIT
156
+ licenses: []
158
157
  metadata: {}
159
158
  post_install_message:
160
159
  rdoc_options:
161
- - --main
160
+ - "--main"
162
161
  - README.md
163
162
  require_paths:
164
163
  - lib
165
164
  required_ruby_version: !ruby/object:Gem::Requirement
166
165
  requirements:
167
- - - ! '>='
166
+ - - ">="
168
167
  - !ruby/object:Gem::Version
169
168
  version: '0'
170
169
  required_rubygems_version: !ruby/object:Gem::Requirement
171
170
  requirements:
172
- - - ! '>='
171
+ - - ">="
173
172
  - !ruby/object:Gem::Version
174
173
  version: '0'
175
174
  requirements: []
176
175
  rubyforge_project: rets
177
- rubygems_version: 2.1.10
176
+ rubygems_version: 2.2.0
178
177
  signing_key:
179
178
  specification_version: 4
180
- summary: ! '[![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
181
- A pure-ruby library for fetching data from [RETS] servers'
179
+ summary: "[![Build Status](https://secure.travis-ci.org/estately/rets.png?branch=master)](http://travis-ci.org/estately/rets)
180
+ A pure-ruby library for fetching data from [RETS] servers"
182
181
  test_files:
183
182
  - test/test_client.rb
184
183
  - test/test_locking_http_client.rb