rets 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,46 @@
1
+ module Rets
2
+ module Parser
3
+ class Compact
4
+ TAB = /\t/
5
+
6
+ INCLUDE_NULL_FIELDS = -1
7
+
8
+ InvalidDelimiter = Class.new(ArgumentError)
9
+
10
+ def self.parse_document(xml)
11
+ doc = Nokogiri.parse(xml.to_s)
12
+
13
+ delimiter = doc.at("//DELIMITER")
14
+ delimiter = delimiter ? Regexp.new(Regexp.escape(delimiter.attr(:value).to_i.chr)) : TAB
15
+
16
+ if delimiter == // || delimiter == /,/
17
+ raise InvalidDelimiter, "Empty or invalid delimiter found, unable to parse."
18
+ end
19
+
20
+ columns = doc.at("//COLUMNS").text
21
+ rows = doc.xpath("//DATA")
22
+
23
+ rows.map do |data|
24
+ self.parse(columns, data.text, delimiter)
25
+ end
26
+ end
27
+
28
+ # Parses a single row of RETS-COMPACT data.
29
+ #
30
+ # Delimiter must be a regexp because String#split behaves differently when
31
+ # given a string pattern. (It removes leading spaces).
32
+ #
33
+ def self.parse(columns, data, delimiter = TAB)
34
+ raise ArgumentError, "Delimiter must be a regular expression" unless Regexp === delimiter
35
+
36
+ column_names = columns.split(delimiter)
37
+ data_values = data.split(delimiter, INCLUDE_NULL_FIELDS)
38
+
39
+ zipped_key_values = column_names.zip(data_values).map { |k, v| [k, v.to_s] }
40
+
41
+ hash = Hash[*zipped_key_values.flatten]
42
+ hash.reject { |key, value| key.empty? && value.to_s.empty? }
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,36 @@
1
+ module Rets
2
+ module Parser
3
+
4
+ # Inspired by Mail.
5
+ class Multipart
6
+ CRLF = "\r\n"
7
+ WSP = "\s"
8
+
9
+ HEADER_LINE = /^([!-9;-~]+:\s*.+)$/
10
+
11
+ Part = Struct.new(:headers, :body)
12
+
13
+ def self.parse(raw, boundary)
14
+ parts = []
15
+
16
+ boundary_regexp = /--#{Regexp.quote(boundary)}(--)?#{CRLF}/
17
+
18
+ raw.split(boundary_regexp).each do |chunk|
19
+
20
+ header_part, body_part = chunk.split(/#{CRLF}#{WSP}*#{CRLF}/m, 2)
21
+
22
+ if header_part =~ HEADER_LINE
23
+ headers = header_part.split(/\r\n/).map { |kv| k,v = kv.split(/:\s?/); [k.downcase, v] }
24
+ headers = Hash[*headers.flatten]
25
+
26
+ parts << Part.new(headers, body_part)
27
+ else
28
+ next # not a valid chunk.
29
+ end
30
+ end
31
+
32
+ parts
33
+ end
34
+ end
35
+ end
36
+ end
data/test/fixtures.rb ADDED
@@ -0,0 +1,142 @@
1
+ RETS_ERROR = <<-XML
2
+ <?xml version="1.0"?>
3
+ <RETS ReplyCode="123" ReplyText="Error message">
4
+ </RETS>
5
+ XML
6
+
7
+ RETS_REPLY = <<-XML
8
+ <?xml version="1.0"?>
9
+ <RETS ReplyCode="0" ReplyText="OK">
10
+ </RETS>
11
+ XML
12
+
13
+ CAPABILITIES = <<-XML
14
+ <RETS ReplyCode="0" ReplyText="OK">
15
+ <RETS-RESPONSE>
16
+
17
+ Abc=123
18
+ Def=ghi=jk
19
+
20
+ </RETS-RESPONSE>
21
+ </RETS>
22
+ XML
23
+
24
+ # 44 is the ASCII code for comma; an invalid delimiter.
25
+ INVALID_DELIMETER = <<-XML
26
+ <?xml version="1.0"?>
27
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
28
+ <DELIMITER value="44" />
29
+ <COLUMNS>A\tB</COLUMNS>
30
+ <DATA>1\t2</DATA>
31
+ <DATA>4\t5</DATA>
32
+ </METADATA>
33
+ XML
34
+
35
+ COMPACT = <<-XML
36
+ <?xml version="1.0"?>
37
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
38
+ <COLUMNS>A\tB</COLUMNS>
39
+ <DATA>1\t2</DATA>
40
+ <DATA>4\t5</DATA>
41
+ </METADATA>
42
+ XML
43
+
44
+ METADATA_UNKNOWN = <<-XML
45
+ <?xml version="1.0"?>
46
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
47
+ <METADATA-FOO Version="01.72.10306" Date="2011-03-15T19:51:22">
48
+ <UNKNOWN />
49
+ </METADATA-FOO>
50
+ XML
51
+
52
+ METADATA_SYSTEM = <<-XML
53
+ <?xml version="1.0"?>
54
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
55
+ <METADATA-SYSTEM Version="01.72.10306" Date="2011-03-15T19:51:22">
56
+ <SYSTEM />
57
+ <COMMENTS />
58
+ </METADATA-SYSTEM>
59
+ XML
60
+
61
+ METADATA_RESOURCE = <<-XML
62
+ <?xml version="1.0"?>
63
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
64
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
65
+ <COLUMNS> ResourceID StandardName VisibleName Description KeyField ClassCount ClassVersion ClassDate ObjectVersion ObjectDate SearchHelpVersion SearchHelpDate EditMaskVersion EditMaskDate LookupVersion LookupDate UpdateHelpVersion UpdateHelpDate ValidationExpressionVersion ValidationExpressionDate ValidationLookupVersionValidationLookupDate ValidationExternalVersion ValidationExternalDate </COLUMNS>
66
+ <DATA> ActiveAgent ActiveAgent Active Agent Search Contains information about active agents. MemberNumber 1 01.72.10304 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
67
+ <DATA> Agent Agent Agent Search Contains information about all agents. MemberNumber 1 01.72.10303 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
68
+ <DATA> History History History Search Contains information about accumulated changes to each listing. TransactionRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T15:08:20 </DATA>
69
+ <DATA> MemberAssociation Member Association Contains MLS member Association information. MemberAssociationKey 1 01.72.10277 2011-02-23T19:11:10 01.72.10214 2011-01-06T16:41:05 01.72.10220 2011-01-06T16:41:06 </DATA>
70
+ <DATA> Office Office Office Search Contains information about broker offices. OfficeNumber 1 01.72.10302 2011-03-03T00:29:23 01.72.10000 2010-08-16T15:08:20 01.72.10305 2011-03-09T21:33:41 01.72.10284 2011-02-24T06:56:43 </DATA>
71
+ <DATA> OfficeAssociation Office Association Contains MLS office Association information. OfficeAssociationKey 1 01.72.10306 2011-03-15T19:51:22 01.72.10245 2011-01-06T16:41:08 01.72.10251 2011-01-06T16:41:08 </DATA>
72
+ <DATA> OpenHouse OpenHouse Open House Search Contains information about public open house activities. OpenHouseRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10134 2010-11-12T13:57:32 01.72.10000 2010-08-16T15:08:20 </DATA>
73
+ <DATA> Property Property Property Search Contains information about listed properties. ListingRid 8 01.72.10288 2011-02-24T06:59:11 01.72.10000 2010-08-16T15:08:20 01.72.10289 2011-02-24T06:59:19 01.72.10290 2011-03-01T11:06:31 </DATA>
74
+ <DATA> PropertyDeleted Deleted Property Search Contains information about deleted properties. ListingRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T22:08:34 </DATA>
75
+ <DATA> PropertyWithheld Withheld Property Search Contains information about withheld properties. ListingRid 8 01.72.10201 2011-01-05T19:34:36 01.72.10000 2010-08-16T15:08:20 01.72.10200 2011-01-05T19:34:34 01.72.10000 2010-08-16T22:08:34 </DATA>
76
+ <DATA> Prospect Prospect Prospect Search Contains information about sales or listing propects. ProspectRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T15:08:20 </DATA>
77
+ <DATA> Tour Tour Tour Search Contains information about private tour activities. TourRid 1 01.72.10185 2010-12-02T02:02:58 01.72.10000 2010-08-16T15:08:20 01.72.10000 2010-08-16T22:08:30 01.72.10000 2010-08-16T15:08:20 </DATA>
78
+ <DATA> VirtualMedia Virtual Media Contains information about virtual media for MLS listings. VirtualMediaRid 1 01.72.10126 2010-11-12T13:47:41 01.72.10127 2010-11-12T13:47:41 01.72.10086 2010-11-10T09:59:11 </DATA>
79
+ </METADATA-RESOURCE>
80
+ </RETS>
81
+ XML
82
+
83
+ METADATA_OBJECT = "<RETS ReplyCode=\"0\" ReplyText=\"V2.6.0 728: Success\">\r\n<METADATA-OBJECT Resource=\"Property\" Version=\"1.12.24\" Date=\"Wed, 1 Dec 2010 00:00:00 GMT\">\r\n<COLUMNS>\tMetadataEntryID\tObjectType\tStandardName\tMimeType\tVisibleName\tDescription\tObjectTimeStamp\tObjectCount\t</COLUMNS>\r\n<DATA>\t50045650619\tMedium\tMedium\timage/jpeg\tMedium\tA 320 x 240 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n<DATA>\t20101753230\tDocumentPDF\tDocumentPDF\tapplication/pdf\tDocumentPDF\tDocumentPDF\t\t\t</DATA>\r\n<DATA>\t50045650620\tPhoto\tPhoto\timage/jpeg\tPhoto\tA 640 x 480 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n<DATA>\t50045650621\tThumbnail\tThumbnail\timage/jpeg\tThumbnail\tA 128 x 96 Size Photo\tLastPhotoDate\tTotalPhotoCount\t</DATA>\r\n</METADATA-OBJECT>\r\n</RETS>\r\n"
84
+
85
+ MULITPART_RESPONSE = [
86
+ "--simple boundary",
87
+ "Content-Type: image/jpeg",
88
+ "Content-Length: 10",
89
+ "Content-ID: 90020062739",
90
+ "Object-ID: 1",
91
+ "",
92
+ "xxxxxxxx",
93
+ "--simple boundary",
94
+ "Content-Type: image/jpeg",
95
+ "Content-Length: 10",
96
+ "Content-ID: 90020062739",
97
+ "Object-ID: 2",
98
+ "",
99
+ "yyyyyyyy",
100
+ "--simple boundary",
101
+ ""
102
+ ].join("\r\n")
103
+
104
+
105
+ SAMPLE_COMPACT = <<XML
106
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
107
+ <METADATA-TABLE Resource="ActiveAgent" Class="MEMB" Version="01.72.10236" Date="2011-03-03T00:29:23">
108
+ <COLUMNS> MetadataEntryID SystemName StandardName LongName DBName ShortName MaximumLength DataType Precision Searchable Interpretation Alignment UseSeparator EditMaskID LookupName MaxSelect Units Index Minimum Maximum Default Required SearchHelpID Unique ModTimeStamp ForeignKeyName ForeignField InKeyindex </COLUMNS>
109
+ <DATA> 7 City City City City 11 Character 0 1 Left 0 0 0 0 0 0 0 City 0 0 1 MemberNumber 0 </DATA>
110
+ <DATA> 9 ContactAddlPhoneType1 Contact Additional Phone Type 1 AddlPhTyp1 Contact Addl Ph Type 1 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
111
+ <DATA> 11 ContactAddlPhoneType2 Contact Additional Phone Type 2 AddlPhTyp2 Contact Addl Ph Type 2 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
112
+ <DATA> 13 ContactAddlPhoneType3 Contact Additional Phone Type 3 AddlPhTyp3 Contact Addl Ph Type 3 1 Character 0 1 Lookup Left 0 ContactAddlPhoneType 0 0 0 0 0 0 ContactAddlPhoneType 0 0 1 MemberNumber 0 </DATA>
113
+ <DATA> 15 ContactPhoneAreaCode1 Contact Phone Area Code 1 ContPhAC1 Contact Phone AC 1 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
114
+ <DATA> 17 ContactPhoneAreaCode2 Contact Phone Area Code 2 ContPhAC2 Contact Phone AC 2 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
115
+ <DATA> 19 ContactPhoneAreaCode3 Contact Phone Area Code 3 ContPhAC3 Contact Phone AC 3 3 Character 0 1 Left 0 0 0 0 0 0 0 ContactPhoneAreaCode 0 0 1 MemberNumber 0 </DATA>
116
+ </METADATA-TABLE>
117
+ </RETS>
118
+ XML
119
+
120
+ SAMPLE_COMPACT_2 = <<XML
121
+ <?xml version="1.0" encoding="utf-8"?>
122
+ <RETS ReplyCode="0" ReplyText="Success">
123
+ <METADATA-TABLE Class="15" Date="2010-10-28T05:41:31Z" Resource="Office" Version="26.27.62891">
124
+ <COLUMNS> ModTimeStamp MetadataEntryID SystemName StandardName LongName DBName ShortName MaximumLength DataType Precision Searchable Interpretation Alignment UseSeparator EditMaskID LookupName MaxSelect Units Index Minimum Maximum Default Required SearchHelpID Unique ForeignKeyName ForeignField InKeyIndex </COLUMNS>
125
+ <DATA> sysid15 sysid sysid sysid sysid 10 Int 0 1 0 0 1 0 0 1 1 </DATA>
126
+ <DATA> 15n1155 OfficePhone_f1155 OfficePhone Phone Offic_1155 Phone 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
127
+ <DATA> 15n1158 AccessFlag_f1158 Office Status Acces_1158 Status 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
128
+ <DATA> 15n1163 MODIFIED_f1163 Modified MODIF_1163 Modified 20 DateTime 0 1 0 0 0 0 0 0 0 </DATA>
129
+ <DATA> 15n1165 DESREALTOR_f1165 DesRealtor DESRE_1165 DesRealtor 75 Character 0 1 0 0 0 0 0 0 0 </DATA>
130
+ <DATA> 15n1166 DESREALTORUID_f1166 Designated Realtor Uid DESRE_1166 RealtorUid 20 Character 0 1 0 0 0 0 0 0 0 </DATA>
131
+ <DATA> 15n1167 INT_NO_f1167 Internet Syndication (Y/N) INT_N_1167 Int.Syn. 1 Character 0 1 Lookup 0 YESNO 1 0 0 0 0 0 </DATA>
132
+ <DATA> 15n1168 STATE_f1168 State STATE_1168 State 50 Character 0 1 Lookup 0 1_523 1 0 0 0 0 0 </DATA>
133
+ <DATA> 15n1169 CITY_f1169 City CITY_1169 City 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
134
+ <DATA> 15n1170 IDX_NO_f1170 IDX (Y/N) IDX_N_1170 IDX 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
135
+ <DATA> 15n1172 ZipCode_f1172 Zip ZipCo_1172 Zip 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
136
+ <DATA> 15n1177 ADDRESS1_f1177 Address Line 1 ADDRE_1177 Address1 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
137
+ <DATA> 15n1182 MLSYN_f1182 MLS Y/N MLSYN_1182 MLSYN 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
138
+ <DATA> 15n1184 OFFICENAME_f1184 Name Office Name OFFIC_1184 Office Name 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
139
+ <DATA> 15n1193 OfficeCode_f1193 OfficeID Office Code Offic_1193 Office Code 12 Character 0 1 0 0 0 0 0 0 1 </DATA>
140
+ </METADATA-TABLE>
141
+ </RETS>
142
+ XML
data/test/helper.rb ADDED
@@ -0,0 +1,6 @@
1
+ require "test/unit"
2
+ require "mocha"
3
+
4
+ require "rets"
5
+
6
+ require "fixtures"
@@ -0,0 +1,571 @@
1
+ require "helper"
2
+
3
+ class TestClient < Test::Unit::TestCase
4
+
5
+ def setup
6
+ @client = Rets::Client.new(:login_url => "http://example.com/login")
7
+ end
8
+
9
+ def test_initialize_adds_escaped_username_to_uri
10
+ client = Rets::Client.new(
11
+ :login_url => "http://example.com",
12
+ :username => "bob@example.com")
13
+
14
+ assert_equal CGI.escape("bob@example.com"), client.uri.user
15
+ assert_nil client.uri.password
16
+ end
17
+
18
+ def test_initialize_adds_escaped_password_to_uri
19
+ client = Rets::Client.new(
20
+ :login_url => "http://example.com",
21
+ :username => "bob",
22
+ :password => "secret@2!")
23
+
24
+ assert_equal CGI.escape("secret@2!"), client.uri.password
25
+ end
26
+
27
+ def test_initialize_merges_default_options
28
+ client = Rets::Client.new(:login_url => "http://example.com", :foo => true)
29
+
30
+ assert client.options.include?(:foo)
31
+ end
32
+
33
+ def test_initialize_allows_default_options_to_be_overridden
34
+ assert Rets::Client::DEFAULT_OPTIONS.include?(:persistent)
35
+
36
+ client = Rets::Client.new(:login_url => "http://example.com")
37
+ assert_equal true, client.options[:persistent]
38
+
39
+ client = Rets::Client.new(:login_url => "http://example.com", :persistent => false)
40
+ assert_equal false, client.options[:persistent]
41
+ end
42
+
43
+
44
+ def test_connection_uses_persistent
45
+ assert_kind_of Net::HTTP::Persistent, @client.connection
46
+ end
47
+
48
+ def test_connection_uses_net_http
49
+ client = Rets::Client.new(:login_url => "http://example.com", :persistent => false)
50
+
51
+ assert_kind_of Net::HTTP, client.connection
52
+ assert_equal "example.com", client.connection.address
53
+ assert_equal 80, client.connection.port
54
+ end
55
+
56
+
57
+ def test_request
58
+ post = mock()
59
+ post.expects(:body=).with("fake body")
60
+
61
+ headers = @client.build_headers
62
+
63
+ Net::HTTP::Post.expects(:new).with("/foo", headers).returns(post)
64
+
65
+ @client.connection.expects(:request).with(@client.uri, post).returns(stub_everything)
66
+
67
+ @client.expects(:handle_cookies)
68
+ @client.expects(:handle_response)
69
+
70
+ @client.stubs(:format_headers)
71
+
72
+ @client.request("/foo", "fake body")
73
+ end
74
+
75
+ def test_request_with_block
76
+ # TODO
77
+ end
78
+
79
+ def test_request_passes_correct_arguments_to_persistent_connection
80
+ @client.connection.expects(:request).with(@client.uri, instance_of(Net::HTTP::Post)).returns(stub_everything)
81
+
82
+ @client.stubs(:handle_cookies)
83
+ @client.stubs(:handle_response)
84
+ @client.stubs(:format_headers)
85
+
86
+ @client.request("/foo")
87
+ end
88
+
89
+ def test_request_passes_correct_arguments_to_net_http_connection
90
+ client = Rets::Client.new(:login_url => "http://example.com", :persistent => false)
91
+
92
+ client.connection.expects(:request).with(instance_of(Net::HTTP::Post)).returns(stub_everything)
93
+
94
+ client.stubs(:handle_cookies)
95
+ client.stubs(:handle_response)
96
+ client.stubs(:format_headers)
97
+
98
+ client.request("/foo")
99
+ end
100
+
101
+
102
+ def test_handle_response_instigates_login_process
103
+ response = Net::HTTPUnauthorized.new("","","")
104
+
105
+ @client.expects(:handle_unauthorized_response)
106
+
107
+ assert_equal response, @client.handle_response(response)
108
+ end
109
+
110
+ def test_handle_response_handles_rets_errors
111
+ response = Net::HTTPSuccess.new("", "", "")
112
+ response.stubs(:body => RETS_ERROR)
113
+
114
+ assert_raise Rets::InvalidRequest do
115
+ @client.handle_response(response)
116
+ end
117
+ end
118
+
119
+ def test_handle_response_handles_rets_valid_response
120
+ response = Net::HTTPSuccess.new("", "", "")
121
+ response.stubs(:body => RETS_REPLY)
122
+
123
+ assert_equal response, @client.handle_response(response)
124
+ end
125
+
126
+ def test_handle_response_handles_empty_responses
127
+ response = Net::HTTPSuccess.new("", "", "")
128
+ response.stubs(:body => "")
129
+
130
+ assert_equal response, @client.handle_response(response)
131
+ end
132
+
133
+ def test_handle_response_handles_non_xml_responses
134
+ response = Net::HTTPSuccess.new("", "", "")
135
+ response.stubs(:body => "<notxml")
136
+
137
+ assert_equal response, @client.handle_response(response)
138
+ end
139
+
140
+ def test_handle_response_raises_on_unknown_response_code
141
+ response = Net::HTTPServerError.new("", "", "")
142
+
143
+ assert_raise Rets::UnknownResponse do
144
+ assert_equal response, @client.handle_response(response)
145
+ end
146
+ end
147
+
148
+
149
+ def test_handle_unauthorized_response_sets_capabilities_on_success
150
+ response = Net::HTTPSuccess.new("","","")
151
+ response.stubs(:body => CAPABILITIES, :get_fields => ["xxx"])
152
+
153
+ @client.stubs(:build_auth)
154
+ @client.expects(:raw_request).with("/login").returns(response)
155
+
156
+ @client.handle_unauthorized_response(response)
157
+
158
+ capabilities = {"abc" => "123", "def" => "ghi=jk"}
159
+
160
+ assert_equal capabilities, @client.capabilities
161
+ end
162
+
163
+ def test_handle_unauthorized_response_raises_on_auth_failure
164
+ response = Net::HTTPUnauthorized.new("","","")
165
+ response.stubs(:body => "", :get_fields => ["xxx"])
166
+
167
+ @client.stubs(:build_auth)
168
+ @client.expects(:raw_request).with("/login").returns(response)
169
+
170
+ assert_raise Rets::AuthorizationFailure do
171
+ @client.handle_unauthorized_response(response)
172
+ end
173
+ end
174
+
175
+
176
+
177
+ def test_extract_capabilities
178
+ assert_equal(
179
+ {"abc" => "123", "def" => "ghi=jk"},
180
+ @client.extract_capabilities(Nokogiri.parse(CAPABILITIES))
181
+ )
182
+ end
183
+
184
+ def test_capability_url_returns_parsed_url
185
+ @client.capabilities = { "foo" => "http://example.com" }
186
+
187
+ assert_equal URI.parse("http://example.com"), @client.capability_url("foo")
188
+ end
189
+
190
+ def test_capability_url_raises_on_malformed_url
191
+ @client.capabilities = { "foo" => "http://e$^&#$&xample.com" }
192
+
193
+ assert_raise Rets::MalformedResponse do
194
+ @client.capability_url("foo")
195
+ end
196
+ end
197
+
198
+ def test_capabilities_calls_login_when_nil
199
+ @client.expects(:login)
200
+ @client.capabilities
201
+ end
202
+
203
+
204
+ def test_cookies?
205
+ assert @client.cookies?({"set-cookie" => "FavoriteFruit=Plum;"})
206
+ assert !@client.cookies?({})
207
+ end
208
+
209
+ def test_cookies=
210
+ @client.cookies = ["abc=123; path=/; HttpOnly", "def=456;", "ghi=789"]
211
+
212
+ assert_equal(
213
+ {"abc" => "123", "def" => "456", "ghi" => "789"},
214
+ @client.instance_variable_get("@cookies")
215
+ )
216
+
217
+ @client.cookies = ["abc=111; blah", "zzz=123"]
218
+
219
+ assert_equal(
220
+ {"abc" => "111", "def" => "456", "ghi" => "789", "zzz" => "123"},
221
+ @client.instance_variable_get("@cookies")
222
+ )
223
+ end
224
+
225
+ def test_cookies
226
+ # Set an array instead of hash for predictable iteration and string construction
227
+ @client.instance_variable_set("@cookies", [%w(abc 123), %w(def 456)])
228
+
229
+ assert_equal "abc=123; def=456", @client.cookies
230
+ end
231
+
232
+
233
+ def test_build_headers_provides_basic_headers
234
+ assert_equal({
235
+ "User-Agent" => "Client/1.0",
236
+ "Host" => "example.com:80",
237
+ "RETS-Version" => "RETS/1.7.2"},
238
+ @client.build_headers)
239
+ end
240
+
241
+ def test_build_headers_provides_authorization
242
+ @client.authorization = "Just trust me"
243
+
244
+ assert_equal({
245
+ "Authorization" => "Just trust me",
246
+ "User-Agent" => "Client/1.0",
247
+ "Host" => "example.com:80",
248
+ "RETS-Version" => "RETS/1.7.2"},
249
+ @client.build_headers)
250
+ end
251
+
252
+ def test_build_headers_provides_cookies
253
+ @client.cookies = ["Allowed=totally"]
254
+
255
+ assert_equal({
256
+ "Cookie" => "Allowed=totally",
257
+ "User-Agent" => "Client/1.0",
258
+ "Host" => "example.com:80",
259
+ "RETS-Version" => "RETS/1.7.2"},
260
+ @client.build_headers)
261
+ end
262
+
263
+
264
+ def test_tries_increments_with_each_call
265
+ assert_equal 1, @client.tries
266
+ assert_equal 2, @client.tries
267
+ end
268
+
269
+ def test_build_auth
270
+ www_authenticate =
271
+ %q(Digest realm="EXAMPLE", nonce="aec306b318feef4c360bc986e06d0a71", opaque="4211001cd29d5a65b3ed99f766a896b0", qop="auth")
272
+
273
+ uri = URI.parse("http://bob:secret@example.com/login")
274
+
275
+ Digest::MD5.stubs(:hexdigest => "heeheehee")
276
+
277
+ expected = <<-DIGEST.gsub(/\n/, "")
278
+ Digest username="bob", realm="EXAMPLE", qop="auth", uri="/login", nonce="aec306b318feef4c360bc986e06d0a71",
279
+ nc=00000000, cnonce="heeheehee", response="heeheehee", opaque="4211001cd29d5a65b3ed99f766a896b0"
280
+ DIGEST
281
+
282
+ assert_equal expected, @client.build_auth(www_authenticate, uri)
283
+ end
284
+
285
+ def test_calculate_digest_with_qop
286
+ Digest::MD5.expects(:hexdigest).with("bob:example:secret").returns("a1")
287
+ Digest::MD5.expects(:hexdigest).with("POST:/login").returns("a2")
288
+
289
+ Digest::MD5.expects(:hexdigest).with("a1:nonce:00000001:cnonce:qop:a2")
290
+
291
+ @client.calculate_digest("bob", "secret", "example", "nonce", "POST", URI.parse("/login"), "qop", "cnonce", 1)
292
+ end
293
+
294
+ def test_calculate_digest_without_qop
295
+ Digest::MD5.expects(:hexdigest).with("bob:example:secret").returns("a1")
296
+ Digest::MD5.expects(:hexdigest).with("POST:/login").returns("a2")
297
+
298
+ Digest::MD5.expects(:hexdigest).with("a1:nonce:a2").returns("hash")
299
+
300
+ assert_equal "hash",
301
+ @client.calculate_digest("bob", "secret", "example", "nonce", "POST", URI.parse("/login"), nil, "cnonce", 1)
302
+ end
303
+
304
+ def test_calculate_user_agent_digest
305
+ Digest::MD5.expects(:hexdigest).with("agent:secret").returns("a1")
306
+ Digest::MD5.expects(:hexdigest).with("a1::session:version").returns("hash")
307
+
308
+ assert_equal "hash",
309
+ @client.calculate_user_agent_digest("agent", "secret", "session", "version")
310
+ end
311
+
312
+
313
+ def test_session_restores_state
314
+ session = Rets::Session.new("Digest auth", {"Foo" => "/foo"}, "sessionid=123")
315
+
316
+ @client.session = session
317
+
318
+ assert_equal("Digest auth", @client.authorization)
319
+ assert_equal({"Foo" => "/foo"}, @client.capabilities)
320
+ assert_equal("sessionid=123", @client.cookies)
321
+ end
322
+
323
+ def test_session_dumps_state
324
+ @client.authorization = "Digest auth"
325
+ @client.capabilities = {"Foo" => "/foo"}
326
+ @client.cookies = "session-id=123"
327
+
328
+ session = @client.session
329
+
330
+ assert_equal("Digest auth", session.authorization)
331
+ assert_equal({"Foo" => "/foo"}, session.capabilities)
332
+ assert_equal("session-id=123", session.cookies)
333
+ end
334
+
335
+ def test_initialize_with_session_restores_state
336
+ session = Rets::Session.new("Digest auth", {"Foo" => "/foo"}, "sessionid=123")
337
+
338
+ client = Rets::Client.new(:login_url => "http://example.com", :session => session)
339
+
340
+ assert_equal("Digest auth", client.authorization)
341
+ assert_equal({"Foo" => "/foo"}, client.capabilities)
342
+ assert_equal("sessionid=123", client.cookies)
343
+ end
344
+
345
+ def test_metadata_when_not_initialized_with_metadata
346
+ client = Rets::Client.new(:login_url => "http://example.com")
347
+ Rets::Metadata::Root.expects(:new)
348
+ client.metadata
349
+ end
350
+
351
+ def test_initialize_with_old_metadata_cached_gets_new_metadata
352
+ metadata = stub(:current? => false)
353
+ new_metadata = stub(:current? => false)
354
+ client = Rets::Client.new(:login_url => "http://example.com", :metadata => metadata)
355
+ client.stubs(:capabilities => {})
356
+ Rets::Metadata::Root.expects(:new => new_metadata).once
357
+
358
+ assert_same new_metadata, client.metadata
359
+ # This second call ensures the expectations on Root are met
360
+ client.metadata
361
+ end
362
+
363
+ def test_initialize_with_current_metadata_cached_return_cached_metadata
364
+ metadata = stub(:current? => true)
365
+ client = Rets::Client.new(:login_url => "http://example.com", :metadata => metadata)
366
+ client.stubs(:capabilities => {})
367
+
368
+ assert_same metadata, client.metadata
369
+ end
370
+
371
+ def test_initialize_takes_logger
372
+ logger = Object.new
373
+
374
+ client = Rets::Client.new(:login_url => "http://example.com", :logger => logger)
375
+
376
+ assert_equal logger, client.logger
377
+ end
378
+
379
+ def test_default_logger_returns_api_compatible_silent_logger
380
+ logger = @client.logger
381
+
382
+ assert_nothing_raised do
383
+ logger.fatal "foo"
384
+ logger.error "foo"
385
+ logger.warn "foo"
386
+ logger.info "foo"
387
+ logger.debug "foo"
388
+ end
389
+ end
390
+
391
+
392
+ def test_find_first_calls_find_every_with_limit_one
393
+ @client.expects(:find_every).with(:limit => 1, :foo => :bar).returns([1,2,3])
394
+
395
+ assert_equal 1, @client.find(:first, :foo => :bar, :limit => 5), "User-specified limit should be ignored"
396
+ end
397
+
398
+ def test_find_all_calls_find_every
399
+ @client.expects(:find_every).with(:limit => 5, :foo => :bar).returns([1,2,3])
400
+
401
+ assert_equal [1,2,3], @client.find(:all, :limit => 5, :foo => :bar)
402
+ end
403
+
404
+ def test_find_raises_on_unknown_quantity
405
+ assert_raise ArgumentError do
406
+ @client.find(:incorrect, :foo => :bar)
407
+ end
408
+ end
409
+
410
+ def test_find_provides_default_values
411
+ @client.expects(:build_key_values).
412
+ with("QueryType" => "DMQL2", "Format" => "COMPACT", "Query" => "x", "Foo" => "bar").
413
+ returns("xxx")
414
+
415
+ @client.stubs(:capability_url => URI.parse("/example"))
416
+ @client.stubs(:request_with_compact_response)
417
+
418
+ @client.find(:all, :query => "x", :foo => "bar")
419
+ end
420
+
421
+ def test_find_allows_defaults_to_be_overridden
422
+ @client.expects(:build_key_values).
423
+ with("QueryType" => "DMQL3000", "Format" => "COMPACT", "Query" => "x", "Foo" => "bar").
424
+ returns("xxx")
425
+
426
+ @client.stubs(:capability_url => URI.parse("/example"))
427
+ @client.stubs(:request_with_compact_response)
428
+
429
+ @client.find(:all, :query => "x", :foo => "bar", :query_type => "DMQL3000")
430
+ end
431
+
432
+ def test_find_returns_undecorated_results
433
+ @client.stubs(:capability_url => URI.parse("/example"))
434
+
435
+ @client.expects(:request_with_compact_response).
436
+ with("/example", instance_of(String), instance_of(Hash)).
437
+ returns([["foo", "bar"]])
438
+
439
+ results = @client.find(:all, :search_type => "Property", :class => "Res", :query => "x", :foo => "bar")
440
+
441
+ assert_equal [["foo", "bar"]], results
442
+ end
443
+
444
+ def test_find_returns_decorated_results
445
+ @client.stubs(:capability_url => URI.parse("/example"))
446
+
447
+ @client.expects(:request_with_compact_response).
448
+ with("/example", instance_of(String), instance_of(Hash)).
449
+ returns([["foo", "bar"]])
450
+
451
+ fake_rets_class = stub(:rets_class)
452
+ fake_result = stub(:result)
453
+
454
+ @client.expects(:find_rets_class).with("Property", "Res").returns(fake_rets_class)
455
+ @client.expects(:decorate_results).with([["foo", "bar"]], fake_rets_class).returns(fake_result)
456
+
457
+ results = @client.find(:all, :search_type => "Property", :class => "Res", :query => "x", :foo => "bar", :resolve => true)
458
+
459
+ assert_equal fake_result, results
460
+ end
461
+
462
+ def test_fixup_keys
463
+ assert_equal({ "Foo" => "bar" }, @client.fixup_keys(:foo => "bar"))
464
+ assert_equal({ "FooFoo" => "bar" }, @client.fixup_keys(:foo_foo => "bar"))
465
+ end
466
+
467
+ def test_all_objects_calls_objects
468
+ @client.expects(:objects).with("*", :foo => :bar)
469
+
470
+ @client.all_objects(:foo => :bar)
471
+ end
472
+
473
+ def test_objects_handles_string_argument
474
+ @client.expects(:fetch_object).with("*", :foo => :bar)
475
+ @client.stubs(:create_parts_from_response)
476
+
477
+ @client.objects("*", :foo => :bar)
478
+ end
479
+
480
+ def test_objects_handle_array_argument
481
+ @client.expects(:fetch_object).with("1,2", :foo => :bar)
482
+ @client.stubs(:create_parts_from_response)
483
+
484
+ @client.objects([1,2], :foo => :bar)
485
+ end
486
+
487
+ def test_objects_raises_on_other_arguments
488
+ assert_raise ArgumentError do
489
+ @client.objects(Object.new, :foo => :bar)
490
+ end
491
+ end
492
+
493
+ def test_create_parts_from_response_returns_multiple_parts_when_multipart_response
494
+ response = {"content-type" => 'multipart; boundary="simple boundary"'}
495
+ response.stubs(:body => MULITPART_RESPONSE)
496
+
497
+ Rets::Parser::Multipart.expects(:parse).
498
+ with(MULITPART_RESPONSE, "simple boundary").
499
+ returns([])
500
+
501
+ @client.create_parts_from_response(response)
502
+ end
503
+
504
+ def test_create_parts_from_response_returns_a_single_part_when_not_multipart_response
505
+ response = {"content-type" => "text/plain"}
506
+ response.stubs(:body => "fakebody")
507
+
508
+ parts = @client.create_parts_from_response(response)
509
+
510
+ assert_equal 1, parts.size
511
+
512
+ part = parts.first
513
+
514
+ assert_equal response, part.headers
515
+ assert_equal "fakebody", part.body
516
+ end
517
+
518
+ def test_object_calls_fetch_object
519
+ response = stub(:body => "foo")
520
+
521
+ @client.expects(:fetch_object).with("1", :foo => :bar).returns(response)
522
+
523
+ assert_equal "foo", @client.object("1", :foo => :bar)
524
+ end
525
+
526
+ def test_fetch_object
527
+ @client.expects(:capability_url).with("GetObject").returns(URI.parse("/obj"))
528
+
529
+ @client.expects(:build_key_values => "fakebody").with(
530
+ "Resource" => "Property",
531
+ "Type" => "Image",
532
+ "ID" => "123:*",
533
+ "Location" => 0
534
+ )
535
+
536
+ @client.expects(:request).with("/obj", "fakebody",
537
+ has_entries(
538
+ "Accept" => "image/jpeg, image/png;q=0.5, image/gif;q=0.1",
539
+ "Content-Type" => "application/x-www-form-urlencoded",
540
+ "Content-Length" => "8")
541
+ )
542
+
543
+ @client.fetch_object("*", :resource => "Property", :object_type => "Image", :resource_id => "123")
544
+ end
545
+
546
+ def test_metadata_caches
547
+ metadata = stub(:current? => true)
548
+ @client.metadata = metadata
549
+ @client.stubs(:capabilities => {})
550
+
551
+ assert_same metadata, @client.metadata, "Should be memoized"
552
+ end
553
+
554
+ def test_retrieve_metadata_type
555
+ @client.expects(:capability_url).with("GetMetadata").returns(URI.parse("/meta"))
556
+
557
+ @client.expects(:build_key_values => "fakebody").with(
558
+ "Format" => "COMPACT",
559
+ "Type" => "METADATA-FOO",
560
+ "ID" => "0"
561
+ )
562
+
563
+ @client.expects(:request => stub(:body => "response")).with("/meta", "fakebody", has_entries(
564
+ "Content-Type" => "application/x-www-form-urlencoded",
565
+ "Content-Length" => "8"
566
+ ))
567
+
568
+ assert_equal "response", @client.retrieve_metadata_type("FOO")
569
+ end
570
+
571
+ end