rets-hack 0.11

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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +142 -0
  3. data/Manifest.txt +58 -0
  4. data/README.md +129 -0
  5. data/Rakefile +28 -0
  6. data/bin/rets +202 -0
  7. data/example/connect.rb +19 -0
  8. data/example/get-photos.rb +20 -0
  9. data/example/get-property.rb +16 -0
  10. data/lib/rets/client.rb +373 -0
  11. data/lib/rets/client_progress_reporter.rb +48 -0
  12. data/lib/rets/http_client.rb +133 -0
  13. data/lib/rets/locking_http_client.rb +34 -0
  14. data/lib/rets/measuring_http_client.rb +27 -0
  15. data/lib/rets/metadata/caching.rb +59 -0
  16. data/lib/rets/metadata/containers.rb +89 -0
  17. data/lib/rets/metadata/file_cache.rb +29 -0
  18. data/lib/rets/metadata/json_serializer.rb +27 -0
  19. data/lib/rets/metadata/lookup_table.rb +65 -0
  20. data/lib/rets/metadata/lookup_type.rb +19 -0
  21. data/lib/rets/metadata/marshal_serializer.rb +27 -0
  22. data/lib/rets/metadata/multi_lookup_table.rb +70 -0
  23. data/lib/rets/metadata/null_cache.rb +24 -0
  24. data/lib/rets/metadata/resource.rb +103 -0
  25. data/lib/rets/metadata/rets_class.rb +57 -0
  26. data/lib/rets/metadata/rets_object.rb +41 -0
  27. data/lib/rets/metadata/root.rb +155 -0
  28. data/lib/rets/metadata/table.rb +33 -0
  29. data/lib/rets/metadata/table_factory.rb +19 -0
  30. data/lib/rets/metadata/yaml_serializer.rb +27 -0
  31. data/lib/rets/metadata.rb +18 -0
  32. data/lib/rets/parser/compact.rb +117 -0
  33. data/lib/rets/parser/error_checker.rb +56 -0
  34. data/lib/rets/parser/multipart.rb +39 -0
  35. data/lib/rets.rb +269 -0
  36. data/test/fixtures.rb +324 -0
  37. data/test/helper.rb +14 -0
  38. data/test/test_caching.rb +89 -0
  39. data/test/test_client.rb +307 -0
  40. data/test/test_error_checker.rb +87 -0
  41. data/test/test_file_cache.rb +42 -0
  42. data/test/test_http_client.rb +132 -0
  43. data/test/test_json_serializer.rb +26 -0
  44. data/test/test_locking_http_client.rb +29 -0
  45. data/test/test_marshal_serializer.rb +26 -0
  46. data/test/test_metadata.rb +71 -0
  47. data/test/test_metadata_class.rb +50 -0
  48. data/test/test_metadata_lookup_table.rb +21 -0
  49. data/test/test_metadata_lookup_type.rb +21 -0
  50. data/test/test_metadata_multi_lookup_table.rb +60 -0
  51. data/test/test_metadata_object.rb +33 -0
  52. data/test/test_metadata_resource.rb +148 -0
  53. data/test/test_metadata_root.rb +151 -0
  54. data/test/test_metadata_table.rb +21 -0
  55. data/test/test_metadata_table_factory.rb +24 -0
  56. data/test/test_parser_compact.rb +115 -0
  57. data/test/test_parser_multipart.rb +39 -0
  58. data/test/test_yaml_serializer.rb +26 -0
  59. data/test/vcr_cassettes/unauthorized_response.yml +262 -0
  60. metadata +227 -0
data/lib/rets.rb ADDED
@@ -0,0 +1,269 @@
1
+ require 'uri'
2
+ require 'digest/md5'
3
+ require 'nokogiri'
4
+
5
+ module Rets
6
+ VERSION = '0.11.0'
7
+
8
+ HttpError = Class.new(StandardError)
9
+ MalformedResponse = Class.new(ArgumentError)
10
+ UnknownResponse = Class.new(ArgumentError)
11
+ NoLogout = Class.new(ArgumentError)
12
+
13
+ class AuthorizationFailure < ArgumentError
14
+ attr_reader :status, :body
15
+ def initialize(status, body)
16
+ @status = status
17
+ @body = body
18
+ super("HTTP status: #{status} (#{body})")
19
+ end
20
+ end
21
+
22
+ class NoRecordsFound < ArgumentError
23
+ ERROR_CODE = 20201
24
+ attr_reader :reply_text
25
+
26
+ def initialize(reply_text)
27
+ @reply_text = reply_text
28
+ super("Got error code #{ERROR_CODE} (#{reply_text})")
29
+ end
30
+ end
31
+
32
+ class NoObjectFound < ArgumentError
33
+ ERROR_CODE = 20403
34
+ attr_reader :reply_text
35
+
36
+ def initialize(reply_text)
37
+ @reply_text = reply_text
38
+ super("Got error code #{ERROR_CODE} (#{reply_text})")
39
+ end
40
+ end
41
+
42
+ class InvalidRequest < ArgumentError
43
+ attr_reader :error_code, :reply_text
44
+ def initialize(error_code, reply_text)
45
+ @error_code = error_code
46
+ @reply_text = reply_text
47
+ super("Got error code #{error_code} (#{reply_text})")
48
+ end
49
+ end
50
+
51
+ class UnknownCapability < ArgumentError
52
+ attr_reader :capability_name, :capabilities
53
+ def initialize(capability_name, capabilities=[])
54
+ @capability_name = capability_name
55
+ @capabilities = capabilities
56
+ super("unknown capability #{capability_name}, available capabilities #{capabilities}")
57
+ end
58
+ end
59
+
60
+ class SystemError < InvalidRequest
61
+ ERROR_CODE = 10000
62
+ end
63
+
64
+ class ZeroBalance < InvalidRequest
65
+ ERROR_CODE = 20003
66
+ end
67
+
68
+ class BrokerCodeRequired < InvalidRequest
69
+ ERROR_CODE = 20012
70
+ end
71
+
72
+ class AdditionalLoginNotPermitted < InvalidRequest
73
+ ERROR_CODE = 20022
74
+ end
75
+
76
+ class ServerLoginError < InvalidRequest
77
+ ERROR_CODE = 20036
78
+ end
79
+
80
+ class ClientAuthenticationFailed < InvalidRequest
81
+ ERROR_CODE = 20037
82
+ end
83
+
84
+ class UserAgentAuthenticationRequired < InvalidRequest
85
+ ERROR_CODE = 20041
86
+ end
87
+
88
+ class ServerTemporarilyDisabled < InvalidRequest
89
+ ERROR_CODE = 20050
90
+ end
91
+
92
+ class InsecurePassword < InvalidRequest
93
+ ERROR_CODE = 20140
94
+ end
95
+
96
+ class SameAsPreviousPassword < InvalidRequest
97
+ ERROR_CODE = 20141
98
+ end
99
+
100
+ class InvalidUserName < InvalidRequest
101
+ ERROR_CODE = 20142
102
+ end
103
+
104
+ class UnknownQueryField < InvalidRequest
105
+ ERROR_CODE = 20200
106
+ end
107
+
108
+ class InvalidSelect < InvalidRequest
109
+ ERROR_CODE = 20202
110
+ end
111
+
112
+ class MiscellaneousSearchError < InvalidRequest
113
+ ERROR_CODE = 20203
114
+ end
115
+
116
+ class InvalidQuerySyntax < InvalidRequest
117
+ ERROR_CODE = 20206
118
+ end
119
+
120
+ class UnauthorizedQuery < InvalidRequest
121
+ ERROR_CODE = 20207
122
+ end
123
+
124
+ class MaximumRecordsExceeded < InvalidRequest
125
+ ERROR_CODE = 20208
126
+ end
127
+
128
+ class RequestTimeout < InvalidRequest
129
+ ERROR_CODE = 20209
130
+ end
131
+
132
+ class TooManyOutstandingQueries < InvalidRequest
133
+ ERROR_CODE = 20210
134
+ end
135
+
136
+ class QueryTooComplex < InvalidRequest
137
+ ERROR_CODE = 20211
138
+ end
139
+
140
+ class InvalidKeyRequest < InvalidRequest
141
+ ERROR_CODE = 20212
142
+ end
143
+
144
+ class InvalidKey < InvalidRequest
145
+ ERROR_CODE = 20213
146
+ end
147
+
148
+ class InvalidParameter < InvalidRequest
149
+ ERROR_CODE = 20301
150
+ end
151
+
152
+ class UnableToSaveRecord < InvalidRequest
153
+ ERROR_CODE = 20301
154
+ end
155
+
156
+ class MiscellaneousUpdateError < InvalidRequest
157
+ ERROR_CODE = 20301
158
+ end
159
+
160
+ class WarningResponseNotGivenForAllWarnings < InvalidRequest
161
+ ERROR_CODE = 20311
162
+ end
163
+
164
+ class WarningResponseGivenForWarningNotRequired < InvalidRequest
165
+ ERROR_CODE = 20312
166
+ end
167
+
168
+ class InvalidResource < InvalidRequest
169
+ ERROR_CODE = 20400
170
+ end
171
+
172
+ class InvalidType < InvalidRequest
173
+ ERROR_CODE = 20401
174
+ end
175
+
176
+ class InvalidIdentifier < InvalidRequest
177
+ ERROR_CODE = 20402
178
+ end
179
+
180
+ class UnsupportedMimeType < InvalidRequest
181
+ ERROR_CODE = 20406
182
+ end
183
+
184
+ class UnauthorizedRetrieval < InvalidRequest
185
+ ERROR_CODE = 20407
186
+ end
187
+
188
+ class ResourceUnavailable < InvalidRequest
189
+ ERROR_CODE = 20408
190
+ end
191
+
192
+ class ObjectUnavailable < InvalidRequest
193
+ ERROR_CODE = 20409
194
+ end
195
+
196
+ class RequestTooLarge < InvalidRequest
197
+ ERROR_CODE = 20410
198
+ end
199
+
200
+ class ExecutionTimeout < InvalidRequest
201
+ ERROR_CODE = 20411
202
+ end
203
+
204
+ class TooManyOutstandingRequests < InvalidRequest
205
+ ERROR_CODE = 20412
206
+ end
207
+
208
+ class InvalidResourceRequested < InvalidRequest
209
+ ERROR_CODE = 20500
210
+ end
211
+
212
+ class InvalidMetadataType < InvalidRequest
213
+ ERROR_CODE = 20501
214
+ end
215
+
216
+ class InvalidIdentifierRequested < InvalidRequest
217
+ ERROR_CODE = 20502
218
+ end
219
+
220
+ class NoMetadataFound < InvalidRequest
221
+ ERROR_CODE = 20503
222
+ end
223
+
224
+ class UnsupportedMetadataMimeType < InvalidRequest
225
+ ERROR_CODE = 20506
226
+ end
227
+
228
+ class UnauthorizedMetadataRetrieval < InvalidRequest
229
+ ERROR_CODE = 20507
230
+ end
231
+
232
+ class MetadataResourceUnavailable < InvalidRequest
233
+ ERROR_CODE = 20508
234
+ end
235
+
236
+ class MetadataUnavailable < InvalidRequest
237
+ ERROR_CODE = 20509
238
+ end
239
+
240
+ class MetadataRequestTooLarge < InvalidRequest
241
+ ERROR_CODE = 20510
242
+ end
243
+
244
+ class MetadataRequestTimeout < InvalidRequest
245
+ ERROR_CODE = 20511
246
+ end
247
+
248
+ class TooManyOutstandingMetadataRequests < InvalidRequest
249
+ ERROR_CODE = 20512
250
+ end
251
+
252
+ class RequestedDTDVersionUnavailable < InvalidRequest
253
+ ERROR_CODE = 20514
254
+ end
255
+
256
+ class NotLoggedIn < InvalidRequest
257
+ ERROR_CODE = 20701
258
+ end
259
+ end
260
+
261
+ require 'rets/http_client'
262
+ require 'rets/client'
263
+ require 'rets/metadata'
264
+ require 'rets/parser/error_checker'
265
+ require 'rets/parser/compact'
266
+ require 'rets/parser/multipart'
267
+ require 'rets/measuring_http_client'
268
+ require 'rets/locking_http_client'
269
+ require 'rets/client_progress_reporter'
data/test/fixtures.rb ADDED
@@ -0,0 +1,324 @@
1
+ RETS_NO_RECORDS_ERROR = <<-XML
2
+ <?xml version="1.0"?>
3
+ <RETS ReplyCode="20201" ReplyText="Error message">
4
+ </RETS>
5
+ XML
6
+
7
+ RETS_NO_OBJECT_ERROR = <<-XML
8
+ <?xml version="1.0"?>
9
+ <RETS ReplyCode="20403" ReplyText="Error message">
10
+ </RETS>
11
+ XML
12
+
13
+ RETS_INVALID_REQUEST_ERROR = <<-XML
14
+ <?xml version="1.0"?>
15
+ <RETS ReplyCode="123" ReplyText="Error message">
16
+ </RETS>
17
+ XML
18
+
19
+ RETS_REPLY = <<-XML
20
+ <?xml version="1.0"?>
21
+ <RETS ReplyCode="0" ReplyText="OK">
22
+ </RETS>
23
+ XML
24
+
25
+ CAPABILITIES = <<-XML
26
+ <RETS ReplyCode="0" ReplyText="OK">
27
+ <RETS-RESPONSE>
28
+
29
+ Abc=123
30
+ Def=ghi=jk
31
+
32
+ </RETS-RESPONSE>
33
+ </RETS>
34
+ XML
35
+
36
+ COUNT_ONLY = <<XML
37
+ <RETS ReplyCode="0" ReplyText="Success">
38
+ <COUNT Records="1234" />
39
+ </RETS>
40
+ XML
41
+
42
+ RETS_STATUS_NO_MATCHING_RECORDS = <<XML
43
+ <?xml version="1.0"?>
44
+ <RETS ReplyCode="0" ReplyText="Operation Successful">
45
+ <RETS-STATUS ReplyCode="20201" ReplyText="No matching records were found" />
46
+ </RETS>
47
+ XML
48
+
49
+ CAPABILITIES_WITH_WHITESPACE = <<XML
50
+ <RETS ReplyCode="0" ReplyText="Operation Successful">
51
+ <RETS-RESPONSE>
52
+ Action = /RETS/Action
53
+ </RETS-RESPONSE>
54
+ </RETS>
55
+ XML
56
+
57
+ # 44 is the ASCII code for comma; an invalid delimiter.
58
+ INVALID_DELIMETER = <<-XML
59
+ <?xml version="1.0"?>
60
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
61
+ <DELIMITER value="44" />
62
+ <COLUMNS>A\tB</COLUMNS>
63
+ <DATA>1\t2</DATA>
64
+ <DATA>4\t5</DATA>
65
+ </METADATA>
66
+ XML
67
+
68
+ CHANGED_DELIMITER = <<-XML
69
+ <?xml version="1.0"?>
70
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
71
+ <DELIMITER value="45" />
72
+ <COLUMNS>A-B</COLUMNS>
73
+ <DATA>1-2</DATA>
74
+ <DATA>4-5</DATA>
75
+ </METADATA>
76
+ XML
77
+
78
+ COMPACT = <<-XML
79
+ <?xml version="1.0"?>
80
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
81
+ <COLUMNS>A\tB</COLUMNS>
82
+ <DATA>1\t2</DATA>
83
+ <DATA>4\t5</DATA>
84
+ </METADATA>
85
+ XML
86
+
87
+
88
+ EMPTY_COMPACT = <<-XML
89
+ <METADATA-TABLE Resource="OpenHouse" Class="OpenHouse" Version="01.01.00000" Date="2011-07-29T12:09:16">
90
+ </METADATA-TABLE>
91
+ XML
92
+
93
+ METADATA_UNKNOWN = <<-XML
94
+ <?xml version="1.0"?>
95
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
96
+ <METADATA-FOO Version="01.72.10306" Date="2011-03-15T19:51:22">
97
+ <UNKNOWN />
98
+ </METADATA-FOO>
99
+ XML
100
+
101
+ METADATA_SYSTEM = <<-XML
102
+ <?xml version="1.0"?>
103
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
104
+ <METADATA-SYSTEM Version="01.72.10306" Date="2011-03-15T19:51:22">
105
+ <SYSTEM />
106
+ <COMMENTS />
107
+ </METADATA-SYSTEM>
108
+ XML
109
+
110
+ METADATA_RESOURCE = <<-XML
111
+ <?xml version="1.0"?>
112
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
113
+ <METADATA-RESOURCE Version="01.72.10306" Date="2011-03-15T19:51:22">
114
+ <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>
115
+ <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>
116
+ <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>
117
+ <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>
118
+ <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>
119
+ <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>
120
+ <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>
121
+ <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>
122
+ <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>
123
+ <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>
124
+ <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>
125
+ <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>
126
+ <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>
127
+ <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>
128
+ </METADATA-RESOURCE>
129
+ </RETS>
130
+ XML
131
+
132
+ 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"
133
+
134
+ MULITPART_RESPONSE = [
135
+ "--simple boundary",
136
+ "Content-Type: image/jpeg",
137
+ "Content-Length: 10",
138
+ "Content-ID: 90020062739",
139
+ "Object-ID: 1",
140
+ "",
141
+ "xxxxxxxx",
142
+ "--simple boundary",
143
+ "Content-Type: image/jpeg",
144
+ "Content-Length: 10",
145
+ "Content-ID: 90020062739",
146
+ "Object-ID: 2",
147
+ "",
148
+ "yyyyyyyy",
149
+ "--simple boundary",
150
+ ""
151
+ ].join("\r\n")
152
+
153
+ MULTIPART_RESPONSE_URLS = [
154
+ '--rets.object.content.boundary.1330546052739',
155
+ 'Content-ID: 38845440',
156
+ 'Object-ID: 1',
157
+ 'Content-Type: text/xml',
158
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-1.jpg',
159
+ '',
160
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
161
+ '',
162
+ '--rets.object.content.boundary.1330546052739',
163
+ 'Content-ID: 38845440',
164
+ 'Object-ID: 2',
165
+ 'Content-Type: text/xml',
166
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-2.jpg',
167
+ '',
168
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
169
+ '',
170
+ '--rets.object.content.boundary.1330546052739',
171
+ 'Content-ID: 38845440',
172
+ 'Object-ID: 3',
173
+ 'Content-Type: text/xml',
174
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-3.jpg',
175
+ '',
176
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
177
+ '',
178
+ '--rets.object.content.boundary.1330546052739',
179
+ 'Content-ID: 38845440',
180
+ 'Object-ID: 4',
181
+ 'Content-Type: text/xml',
182
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-4.jpg',
183
+ '',
184
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
185
+ '',
186
+ '--rets.object.content.boundary.1330546052739',
187
+ 'Content-ID: 38845440',
188
+ 'Object-ID: 5',
189
+ 'Content-Type: text/xml',
190
+ 'Location: http://foobarmls.com/RETS//MediaDisplay/98/hr2890998-5.jpg',
191
+ '',
192
+ '<RETS ReplyCode="0" ReplyText="Operation Successful" />',
193
+ '',
194
+ '--rets.object.content.boundary.1330546052739--'
195
+ ].join("\r\n")
196
+
197
+ SAMPLE_COMPACT = <<XML
198
+ <RETS ReplyCode="0" ReplyText="Operation successful.">
199
+ <METADATA-TABLE Resource="ActiveAgent" Class="MEMB" Version="01.72.10236" Date="2011-03-03T00:29:23">
200
+ <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>
201
+ <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>
202
+ <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>
203
+ <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>
204
+ <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>
205
+ <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>
206
+ <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>
207
+ <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>
208
+ </METADATA-TABLE>
209
+ </RETS>
210
+ XML
211
+
212
+ SAMPLE_COMPACT_2 = <<XML
213
+ <?xml version="1.0" encoding="utf-8"?>
214
+ <RETS ReplyCode="0" ReplyText="Success">
215
+ <METADATA-TABLE Class="15" Date="2010-10-28T05:41:31Z" Resource="Office" Version="26.27.62891">
216
+ <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>
217
+ <DATA> sysid15 sysid sysid sysid sysid 10 Int 0 1 0 0 1 0 0 1 1 </DATA>
218
+ <DATA> 15n1155 OfficePhone_f1155 OfficePhone Phone Offic_1155 Phone 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
219
+ <DATA> 15n1158 AccessFlag_f1158 Office Status Acces_1158 Status 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
220
+ <DATA> 15n1163 MODIFIED_f1163 Modified MODIF_1163 Modified 20 DateTime 0 1 0 0 0 0 0 0 0 </DATA>
221
+ <DATA> 15n1165 DESREALTOR_f1165 DesRealtor DESRE_1165 DesRealtor 75 Character 0 1 0 0 0 0 0 0 0 </DATA>
222
+ <DATA> 15n1166 DESREALTORUID_f1166 Designated Realtor Uid DESRE_1166 RealtorUid 20 Character 0 1 0 0 0 0 0 0 0 </DATA>
223
+ <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>
224
+ <DATA> 15n1168 STATE_f1168 State STATE_1168 State 50 Character 0 1 Lookup 0 1_523 1 0 0 0 0 0 </DATA>
225
+ <DATA> 15n1169 CITY_f1169 City CITY_1169 City 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
226
+ <DATA> 15n1170 IDX_NO_f1170 IDX (Y/N) IDX_N_1170 IDX 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
227
+ <DATA> 15n1172 ZipCode_f1172 Zip ZipCo_1172 Zip 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
228
+ <DATA> 15n1177 ADDRESS1_f1177 Address Line 1 ADDRE_1177 Address1 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
229
+ <DATA> 15n1182 MLSYN_f1182 MLS Y/N MLSYN_1182 MLSYN 1 Character 0 1 0 0 0 0 0 0 0 </DATA>
230
+ <DATA> 15n1184 OFFICENAME_f1184 Office Name Office&#x2019;s Name OFFIC_1184 Office Name 50 Character 0 1 0 0 0 0 0 0 0 </DATA>
231
+ <DATA> 15n1193 OfficeCode_f1193 OfficeID Office Code Offic_1193 Office Code 12 Character 0 1 0 0 0 0 0 0 1 </DATA>
232
+ </METADATA-TABLE>
233
+ </RETS>
234
+ XML
235
+
236
+ HTML_AUTH_FAILURE = <<EOF
237
+ <html><head><title>Apache Tomcat/6.0.26 - Error report</title><style><!--H1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} H2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} H3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} BODY {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} B {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} P {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;}A {color : black;}A.name {color : black;}HR {color : #525D76;}--></style> </head><body><h1>HTTP Status 401 - </h1><HR size="1" noshade="noshade"><p><b>type</b> Status report</p><p><b>message</b> <u></u></p><p><b>description</b> <u>This request requires HTTP authentication ().</u></p><HR size="1" noshade="noshade"><h3>Apache Tomcat/6.0.26</h3></body></html>
238
+ EOF
239
+
240
+ XHTML_AUTH_FAILURE = <<EOF
241
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
242
+ <html xmlns="http://www.w3.org/1999/xhtml">
243
+ <head>
244
+ <title>401 - Unauthorized: Access is denied due to invalid credentials.</title>
245
+ </head>
246
+ <body>
247
+ <h1>401 - Unauthorized: Access is denied due to invalid credentials.</h1>
248
+ <p>You do not have permission to view this directory or page using the credentials that you supplied.</p>
249
+ </body>
250
+ </html>
251
+ EOF
252
+
253
+ SAMPLE_COMPACT_WITH_SPECIAL_CHARS = <<EOF
254
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
255
+ <DELIMITER value=\"09\" />
256
+ <COLUMNS> PublicRemarksNew WindowCoverings YearBuilt Zoning ZoningCompatibleYN </COLUMNS>
257
+ <DATA> porte-coch&amp;#xE8;re welcomes 1999 00 </DATA>
258
+ </RETS>
259
+ EOF
260
+
261
+ SAMPLE_COMPACT_WITH_SPECIAL_CHARS_2 = <<EOF
262
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
263
+ <DELIMITER value=\"09\" />
264
+ <COLUMNS> PublicRemarksNew WindowCoverings YearBuilt Zoning ZoningCompatibleYN </COLUMNS>
265
+ <DATA> text with &lt;tag&gt; 1999 00 </DATA>
266
+ </RETS>
267
+ EOF
268
+
269
+ SAMPLE_COMPACT_WITH_DOUBLY_ENCODED_BAD_CHARACTER_REFERENCES = <<EOF
270
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
271
+ <DELIMITER value=\"09\" />
272
+ <COLUMNS> PublicRemarksNew WindowCoverings YearBuilt Zoning ZoningCompatibleYN </COLUMNS>
273
+ <DATA> foo &amp;#56324; bar 1999 00 </DATA>
274
+ </RETS>
275
+ EOF
276
+
277
+ SAMPLE_PROPERTY_WITH_LOTS_OF_COLUMNS = <<EOF
278
+ <RETS ReplyCode=\"0\" ReplyText=\"Operation Success.\">
279
+ <DELIMITER value=\"09\" />
280
+ <COLUMNS>\t#{800.times.map { |x| "K%03d" % x }.join("\t") }\t</COLUMNS>
281
+ <DATA>\t\t</DATA>
282
+ </RETS>
283
+ EOF
284
+
285
+ EXAMPLE_METADATA_TREE = <<EOF
286
+ # Resource: Properties (Key Field: matrix_unique_key)
287
+ ## Class: T100
288
+ Visible Name: Prop
289
+ Description : some description
290
+ ### Table: L_1
291
+ Resource: Properties
292
+ ShortName: Sq
293
+ LongName: Square Footage
294
+ StandardName: Sqft
295
+ Units: Meters
296
+ Searchable: Y
297
+ Required: N
298
+ ### LookupTable: L_10
299
+ Resource: Properties
300
+ Required: N
301
+ Searchable: Y
302
+ Units:
303
+ ShortName: HF
304
+ LongName: HOA Frequency
305
+ StandardName: HOA F
306
+ #### Types:
307
+ Quarterly -> Q
308
+ Annually -> A
309
+ ### MultiLookupTable: L_11
310
+ Resource: Properties
311
+ Required: N
312
+ Searchable: Y
313
+ Units:
314
+ ShortName: HFs
315
+ LongName: HOA Frequencies
316
+ StandardName: HOA Fs
317
+ Types:
318
+ Quarterly -> Q
319
+ Annually -> A
320
+ Object: HiRes
321
+ Visible Name: Photo
322
+ Mime Type: photo/jpg
323
+ Description: photo description
324
+ EOF
data/test/helper.rb ADDED
@@ -0,0 +1,14 @@
1
+ require "minitest"
2
+ require "mocha/setup"
3
+
4
+ require_relative "../lib/rets"
5
+
6
+ require_relative "fixtures"
7
+
8
+ require 'vcr'
9
+
10
+ VCR.configure do |c|
11
+ c.cassette_library_dir = 'test/vcr_cassettes'
12
+ c.hook_into :webmock
13
+ end
14
+
@@ -0,0 +1,89 @@
1
+ require_relative "helper"
2
+
3
+ require "stringio"
4
+
5
+ class TestCaching < MiniTest::Test
6
+
7
+ class MemoryCache
8
+
9
+ def initialize
10
+ reset
11
+ end
12
+
13
+ def save(&block)
14
+ reset
15
+ yield @io
16
+ end
17
+
18
+ def load(&block)
19
+ @io.rewind
20
+ yield @io
21
+ end
22
+
23
+ private
24
+
25
+ def reset
26
+ @io = StringIO.new
27
+ end
28
+
29
+ end
30
+
31
+ def test_make_defaults
32
+ caching = Rets::Metadata::Caching.make({})
33
+ assert_instance_of Rets::Metadata::NullCache, caching.cache
34
+ assert_instance_of Rets::Metadata::MarshalSerializer, caching.serializer
35
+ end
36
+
37
+ def test_round_trip
38
+ logger = Logger.new("/dev/null")
39
+ sources = {"foo" => "bar"}
40
+ metadata = Rets::Metadata::Root.new(logger, sources)
41
+ caching = Rets::Metadata::Caching.make(metadata_cache: MemoryCache.new)
42
+ caching.save(metadata)
43
+ loaded_metadata = caching.load(logger)
44
+ assert_same logger, loaded_metadata.logger
45
+ assert_equal sources, loaded_metadata.sources
46
+ end
47
+
48
+ def test_load_when_serializer_fails
49
+ serializer = Class.new do
50
+ def load(file)
51
+ nil
52
+ end
53
+ end.new
54
+ logger = Logger.new("/dev/null")
55
+ caching = Rets::Metadata::Caching.make(
56
+ metadata_cache: MemoryCache.new,
57
+ metadata_serializer: serializer,
58
+ )
59
+ loaded_metadata = caching.load(logger)
60
+ assert_nil loaded_metadata
61
+ end
62
+
63
+ def test_load_when_serializer_returns_wrong_type
64
+ serializer = Class.new do
65
+ def load(file)
66
+ Object.new
67
+ end
68
+ end.new
69
+ logger = Logger.new("/dev/null")
70
+ caching = Rets::Metadata::Caching.make(
71
+ metadata_cache: MemoryCache.new,
72
+ metadata_serializer: serializer,
73
+ )
74
+ loaded_metadata = caching.load(logger)
75
+ assert_nil loaded_metadata
76
+ end
77
+
78
+ def test_load_when_cache_fails
79
+ logger = Logger.new("/dev/null")
80
+ cache = stub
81
+ cache.expects(:load).returns(nil)
82
+ caching = Rets::Metadata::Caching.make(
83
+ metadata_cache: cache,
84
+ )
85
+ loaded_metadata = caching.load(logger)
86
+ assert_nil loaded_metadata
87
+ end
88
+
89
+ end