rets 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gemtest +0 -0
- data/CHANGELOG.md +9 -0
- data/Manifest.txt +23 -0
- data/README.md +42 -0
- data/Rakefile +18 -0
- data/bin/rets +194 -0
- data/lib/rets.rb +24 -0
- data/lib/rets/authentication.rb +59 -0
- data/lib/rets/client.rb +473 -0
- data/lib/rets/metadata.rb +6 -0
- data/lib/rets/metadata/containers.rb +84 -0
- data/lib/rets/metadata/lookup_type.rb +17 -0
- data/lib/rets/metadata/resource.rb +73 -0
- data/lib/rets/metadata/rets_class.rb +42 -0
- data/lib/rets/metadata/root.rb +155 -0
- data/lib/rets/metadata/table.rb +98 -0
- data/lib/rets/parser/compact.rb +46 -0
- data/lib/rets/parser/multipart.rb +36 -0
- data/test/fixtures.rb +142 -0
- data/test/helper.rb +6 -0
- data/test/test_client.rb +571 -0
- data/test/test_metadata.rb +452 -0
- data/test/test_parser_compact.rb +71 -0
- data/test/test_parser_multipart.rb +21 -0
- metadata +162 -0
@@ -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
data/test/test_client.rb
ADDED
@@ -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
|