jschairb-rets4r 1.1.18
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/CHANGELOG +566 -0
- data/CONTRIBUTORS +7 -0
- data/Gemfile +10 -0
- data/LICENSE +29 -0
- data/MANIFEST +62 -0
- data/NEWS +186 -0
- data/README.rdoc +43 -0
- data/RUBYS +56 -0
- data/Rakefile +50 -0
- data/TODO +35 -0
- data/examples/client_get_object.rb +49 -0
- data/examples/client_login.rb +39 -0
- data/examples/client_mapper.rb +17 -0
- data/examples/client_metadata.rb +42 -0
- data/examples/client_parser.rb +9 -0
- data/examples/client_search.rb +49 -0
- data/examples/settings.yml +114 -0
- data/lib/rets4r.rb +14 -0
- data/lib/rets4r/auth.rb +73 -0
- data/lib/rets4r/client.rb +487 -0
- data/lib/rets4r/client/data.rb +14 -0
- data/lib/rets4r/client/dataobject.rb +28 -0
- data/lib/rets4r/client/exceptions.rb +116 -0
- data/lib/rets4r/client/links.rb +32 -0
- data/lib/rets4r/client/metadata.rb +15 -0
- data/lib/rets4r/client/parsers/compact.rb +42 -0
- data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
- data/lib/rets4r/client/parsers/metadata.rb +92 -0
- data/lib/rets4r/client/parsers/response_parser.rb +100 -0
- data/lib/rets4r/client/requester.rb +143 -0
- data/lib/rets4r/client/transaction.rb +31 -0
- data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
- data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
- data/lib/rets4r/core_ext/hash/keys.rb +46 -0
- data/lib/rets4r/core_ext/hash/slice.rb +39 -0
- data/lib/rets4r/listing_mapper.rb +17 -0
- data/lib/rets4r/listing_service.rb +35 -0
- data/lib/rets4r/loader.rb +8 -0
- data/lib/tasks/annotations.rake +121 -0
- data/lib/tasks/coverage.rake +13 -0
- data/rets4r.gemspec +24 -0
- data/spec/rets4r_compact_data_parser_spec.rb +7 -0
- data/test/data/1.5/bad_compact.xml +7 -0
- data/test/data/1.5/count_only_compact.xml +3 -0
- data/test/data/1.5/error.xml +1 -0
- data/test/data/1.5/invalid_compact.xml +4 -0
- data/test/data/1.5/login.xml +16 -0
- data/test/data/1.5/metadata.xml +0 -0
- data/test/data/1.5/search_compact.xml +8 -0
- data/test/data/1.5/search_compact_big.xml +136 -0
- data/test/data/1.5/search_unescaped_compact.xml +8 -0
- data/test/data/listing_service.yml +36 -0
- data/test/test_auth.rb +68 -0
- data/test/test_client.rb +342 -0
- data/test/test_client_links.rb +39 -0
- data/test/test_compact_nokogiri.rb +64 -0
- data/test/test_helper.rb +12 -0
- data/test/test_listing_mapper.rb +112 -0
- data/test/test_loader.rb +24 -0
- data/test/test_parser.rb +96 -0
- data/test/test_quality.rb +57 -0
- metadata +211 -0
@@ -0,0 +1,13 @@
|
|
1
|
+
begin
|
2
|
+
require 'rcov/rcovtask'
|
3
|
+
Rcov::RcovTask.new do |test|
|
4
|
+
test.libs << 'test'
|
5
|
+
test.pattern = "test/test_*.rb"
|
6
|
+
test.verbose = true
|
7
|
+
test.rcov_opts << ' --sort coverage'
|
8
|
+
end
|
9
|
+
rescue LoadError
|
10
|
+
task :rcov do
|
11
|
+
abort "RCov is not available. In order to run rcov, you must: gem install rcov"
|
12
|
+
end
|
13
|
+
end
|
data/rets4r.gemspec
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
lib = File.expand_path("../lib/rets4r.rb", __FILE__)
|
2
|
+
version = File.read(lib)[/^\s*VERSION\s*=\s*(['"])(\d\.\d\.\d+)\1/, 2]
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = 'jschairb-rets4r'
|
6
|
+
spec.authors = ["Scott Patterson", "John Wulff", "bgetting", "Jacob Basham", "Joseph Holsten"]
|
7
|
+
spec.email = ["scott.patterson@digitalaun.com", "john@johnwulff.com", "brian@terra-firma-design.com", "jacob@paperpigeons.net", "joseph@josephholsten.com"]
|
8
|
+
spec.homepage = 'http://rets4r.rubyforge.org/'
|
9
|
+
spec.rubyforge_project = 'rets4r'
|
10
|
+
spec.description = %q{RETS4R is a native Ruby interface to the RETS (Real Estate Transaction Standard). It currently is built for the 1.5 specification, but support for 1.7 and 2.0 are planned. It does not currently implement all of the specification, but the most commonly used portions. Specifically, there is no support for Update transactions.}
|
11
|
+
spec.extra_rdoc_files = %w[CHANGELOG CONTRIBUTORS LICENSE MANIFEST NEWS README.rdoc RUBYS TODO ]
|
12
|
+
spec.rdoc_options << "--charset=UTF-8" <<
|
13
|
+
"--main" << "README.rdoc"
|
14
|
+
spec.version = version
|
15
|
+
spec.summary = spec.description.split(/\.\s+/).first
|
16
|
+
spec.files = File.read("MANIFEST").split(/\r?\n\r?/)
|
17
|
+
spec.test_files = spec.files.grep(/^test\/.*test_.*\.rb$/)
|
18
|
+
|
19
|
+
spec.add_runtime_dependency 'nokogiri', '~>1.4.0'
|
20
|
+
spec.add_development_dependency 'mocha'
|
21
|
+
spec.add_development_dependency 'shoulda'
|
22
|
+
spec.add_development_dependency 'activesupport'
|
23
|
+
end
|
24
|
+
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<RETS ReplyCode="0" ReplyText="Operation Successful">
|
2
|
+
<COUNT Records="38610" />
|
3
|
+
<DELIMITER value="09" />
|
4
|
+
<COLUMNS> MLSNUM AGENTLIST AGENTLIST_FULLNAME OFFICELIST OFFICELIST_OFFICENAM1 INTERNETLIST_ALL LISTSTATUSFLAG PHOTOCOUNT LISTSTATUS BEDS CARPORTCAP NUMDININGAREAS FIREPLACES BATHSFULL BATHSFULLLEVEL1 BATHSFULLLEVEL2 BATHSFULLLEVEL3 BATHSFULLBASEMENT GARAGECAP BATHSHALF BATHSHALFLEVEL1 BATHSHALFLEVEL2 BATHSHALFLEVEL3 BATHSHALFBASEMENT NUMLIVINGAREAS NUMBARNS NUMLAKES NUMPONDS NUMRESIDENCE NUMSTOCKTANKS STORIES NUMWELLS SCHOOLDISTRICT AGENTSELL_FULLNAME OFFICESELL_OFFICENAM2 STATE STREETNAME STREETTYPE STREETNUM STREETNUMDISPLAY STREETDIRSUFFIX STREETDIR STYLE SUBDIVISION ACRES INTERNETDISPLAYYN ROOMBEDBATHDESC STORIESBLDG CITY COMMONFEATURES COUNTY ACRESCULTIVATED INTERIOR LISTPRICE SQFTPRICE LOTNUM LOTDESC LOTDIM LOTSIZE LISTPRICELOW ACRESPRICE MAPBOOK MAPCOORD MAPPAGE LISTPRICEORIG GARAGEDESC PROPTYPE REMARKS PROPSUBTYPE PROPSUBTYPEDISPLAY SQFTTOTAL SQFTSOURCE UTILITIES UID UIDPRP TAXUNEXEMPT UNITNUM YEARBUILT ZIPCODE </COLUMNS>
|
5
|
+
<DATA> 11322886 000533246 CD Boyd, II FMAB McClure, REALTORS OMWS,immo,RCOM,SYND 8 act 4 0 2 1 3 2 0 2 2 Wylie ISD, Taylor Co. TX Thompson PKWY 8317 8317 STYRANCH Belle Vista Estates 0.361 Y BBFJET-TUB,BBFLIN-CLO,BBFSEP-SHO,BBFW+I-CLO Abilene Taylor INFBAY-WIN,INFCABLE-A,INFDECLTNG,INFHIGHSPD,INFLOFT,INFSOUND-W,INFVAUL-CL,INFWIND-CO 239000 84.6 LTDSUBDIV 87X182 LTS.5-.99A 0 662049 OT none 9999 239000 PARATTACHE,PAROPENER,PARREAR,PARSIDE RES Custom home built in 05'. Growing neighborhood convienient to Wylie schools. Abundant living areas and and open space floor plan.Could be 5th BDRM, or upstair BDRM & Bath could be another living area. Perfect office next to Master. Gourmet sized kitchen, granite countertops, barell ceilings, wonderful storage space!!!! S RES-S 2825 TAX STUASPHALT,UTLCITY-SE,UTLCITY-WA,STUCONCRET 3211078 3211078 5876 2005 79606-6652 </DATA>
|
6
|
+
<MAXROWS />
|
7
|
+
</RETS>
|
@@ -0,0 +1 @@
|
|
1
|
+
<RETS ReplyCode="20400" ReplyText="Invalid Invalidness." />
|
@@ -0,0 +1,16 @@
|
|
1
|
+
<RETS ReplyCode="0" ReplyText="Login Request">
|
2
|
+
<RETS-RESPONSE>
|
3
|
+
MemberName=Spiffy Realtor
|
4
|
+
User=srealtor,1,11,11111
|
5
|
+
Broker=1,2
|
6
|
+
MetadataVersion=1.1.1
|
7
|
+
MinMetadataVersion=1.0.0
|
8
|
+
TimeoutSeconds=1200
|
9
|
+
Action=/rets/ActionURL
|
10
|
+
GetMetadata=/rets/GetMetadata
|
11
|
+
GetObject=/rets/GetObject
|
12
|
+
Login=/rets/Login
|
13
|
+
Logout=/rets/Logout
|
14
|
+
Search=/rets/Search
|
15
|
+
</RETS-RESPONSE>
|
16
|
+
</RETS>
|
File without changes
|
@@ -0,0 +1,136 @@
|
|
1
|
+
<RETS ReplyCode="0" ReplyText="SUCCESS">
|
2
|
+
<COUNT Records="4"/>
|
3
|
+
<DELIMITER value="09" />
|
4
|
+
<COLUMNS> First Second Third </COLUMNS>
|
5
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
6
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
7
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
8
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
9
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
10
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
11
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
12
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
13
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
14
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
15
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
16
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
17
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
18
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
19
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
20
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
21
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
22
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
23
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
24
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
25
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
26
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
27
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
28
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
29
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
30
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
31
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
32
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
33
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
34
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
35
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
36
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
37
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
38
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
39
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
40
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
41
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
42
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
43
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
44
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
45
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
46
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
47
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
48
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
49
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
50
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
51
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
52
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
53
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
54
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
55
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
56
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
57
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
58
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
59
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
60
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
61
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
62
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
63
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
64
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
65
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
66
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
67
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
68
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
69
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
70
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
71
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
72
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
73
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
74
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
75
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
76
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
77
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
78
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
79
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
80
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
81
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
82
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
83
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
84
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
85
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
86
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
87
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
88
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
89
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
90
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
91
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
92
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
93
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
94
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
95
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
96
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
97
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
98
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
99
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
100
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
101
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
102
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
103
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
104
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
105
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
106
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
107
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
108
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
109
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
110
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
111
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
112
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
113
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
114
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
115
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
116
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
117
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
118
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
119
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
120
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
121
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
122
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
123
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
124
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
125
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
126
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
127
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
128
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
129
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
130
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
131
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
132
|
+
<DATA> Datum1 Datum2 Datum3</DATA>
|
133
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
134
|
+
<DATA> Datum4 Datum5 Datum6</DATA>
|
135
|
+
<MAXROWS/>
|
136
|
+
</RETS>
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# This file specifies the access information for the listing service #
|
2
|
+
# To access the standard RETS demo server, use:
|
3
|
+
#
|
4
|
+
test:
|
5
|
+
url: http://demo.crt.realtors.org:6103/rets/login
|
6
|
+
username: Joe
|
7
|
+
password: Schmoe
|
8
|
+
version: 1.7.2
|
9
|
+
resource: Property
|
10
|
+
class: RES
|
11
|
+
object_type: Photo
|
12
|
+
select:
|
13
|
+
ListingID: :mls
|
14
|
+
AgentID: :agent_id
|
15
|
+
ModificationTimestamp: :rets_update_at
|
16
|
+
Status: :status
|
17
|
+
# ListDate: :list_date
|
18
|
+
ListPrice: :list_price
|
19
|
+
# ClosePrice: :close_price
|
20
|
+
# ContractDate: :contract_date
|
21
|
+
StreetNumber: :street_number
|
22
|
+
StreetName: :street_name
|
23
|
+
StreetDirection: :street_direction
|
24
|
+
Unit: :unit_number
|
25
|
+
ZipCode: :zip_code
|
26
|
+
City: :city
|
27
|
+
County: :county
|
28
|
+
# Rooms: :rooms
|
29
|
+
# State: :state
|
30
|
+
# Board: :board
|
31
|
+
LivingArea: :living_area
|
32
|
+
Baths: :baths
|
33
|
+
Bedrooms: :beds
|
34
|
+
Garage: :garage
|
35
|
+
SqFt: :square_feet
|
36
|
+
YearBuilt: :year_built
|
data/test/test_auth.rb
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))
|
2
|
+
|
3
|
+
require 'rets4r/auth'
|
4
|
+
require 'test/unit'
|
5
|
+
|
6
|
+
module RETS4R
|
7
|
+
class TestAuth < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@useragent = 'TestAgent/0.00'
|
10
|
+
@username = 'username'
|
11
|
+
@password = 'password'
|
12
|
+
@realm = 'REALM'
|
13
|
+
@nonce = '2006-03-03T17:37:10'
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_authenticate
|
17
|
+
response = {
|
18
|
+
'www-authenticate' => 'Digest qop="auth",realm="'+ @realm +'",nonce="'+ @nonce +'",opaque="",stale="false",domain="\my\test\domain"'
|
19
|
+
}
|
20
|
+
|
21
|
+
Auth.authenticate(response, @username, @password, '/my/rets/url', 'GET', Auth.request_id, @useragent)
|
22
|
+
end
|
23
|
+
|
24
|
+
# test without spacing
|
25
|
+
def test_parse_auth_header_without_spacing
|
26
|
+
header = 'Digest qop="auth",realm="'+ @realm +'",nonce="'+ @nonce +'",opaque="",stale="false",domain="\my\test\domain"'
|
27
|
+
check_header(header)
|
28
|
+
end
|
29
|
+
|
30
|
+
# test with spacing between each item
|
31
|
+
def test_parse_auth_header_with_spacing
|
32
|
+
header = 'Digest qop="auth", realm="'+ @realm +'", nonce="'+ @nonce +'", opaque="", stale="false", domain="\my\test\domain"'
|
33
|
+
check_header(header)
|
34
|
+
end
|
35
|
+
|
36
|
+
# used to check the that the header was processed properly.
|
37
|
+
def check_header(header)
|
38
|
+
results = Auth.parse_header(header)
|
39
|
+
|
40
|
+
assert_equal('auth', results['qop'])
|
41
|
+
assert_equal('REALM', results['realm'])
|
42
|
+
assert_equal('2006-03-03T17:37:10', results['nonce'])
|
43
|
+
assert_equal('', results['opaque'])
|
44
|
+
assert_equal('false', results['stale'])
|
45
|
+
assert_equal('\my\test\domain', results['domain'])
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_calculate_digest
|
49
|
+
# with qop
|
50
|
+
assert_equal('c5f9ef280f0ca78ed7a488158fc2f4cc', Auth.calculate_digest(@username, \
|
51
|
+
@password, @realm, 'test', 'GET', '/my/rets/url', true, 'test'))
|
52
|
+
|
53
|
+
# without qop
|
54
|
+
assert_equal('bceafa34467a3519c2f6295d4800f4ea', Auth.calculate_digest(@username, \
|
55
|
+
@password, @realm, 'test', 'GET', '/my/rets/url', false))
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_request_id
|
59
|
+
assert_not_nil(true, Auth.request_id)
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_cnonce
|
63
|
+
# We call cnonce with a static request ID so that we have a consistent result with which
|
64
|
+
# to test against
|
65
|
+
assert_equal('d5cdfa1acffde590d263689fb40cf53c', Auth.cnonce(@useragent, @password, 'requestId', @nonce))
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/test/test_client.rb
ADDED
@@ -0,0 +1,342 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$:.unshift File.expand_path(File.join(File.dirname(__FILE__), "."))
|
3
|
+
require 'test_helper'
|
4
|
+
|
5
|
+
module RETS4R
|
6
|
+
class Client
|
7
|
+
public :process_content_type
|
8
|
+
end
|
9
|
+
|
10
|
+
class TestClientGetMetadata < Test::Unit::TestCase
|
11
|
+
RETS_PORT = '9080'
|
12
|
+
RETS_URL = "http://localhost:#{RETS_PORT}"
|
13
|
+
RETS_LOGIN = 'login'
|
14
|
+
RETS_PASSWORD = 'password'
|
15
|
+
|
16
|
+
class CustomError < StandardError; end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@logfile = StringIO.open
|
20
|
+
@rets = RETS4R::Client.new(RETS_URL)
|
21
|
+
@rets.logger = Logger.new(@logfile)
|
22
|
+
@rets.logger.level = Logger::DEBUG
|
23
|
+
|
24
|
+
@rets.stubs(:request).returns(@response = mock("response"))
|
25
|
+
@response.stubs(:body).returns(:body)
|
26
|
+
@rets.stubs(:parse).returns(@results = mock("results"))
|
27
|
+
Client::ResponseParser.any_instance.stubs(:parse_metadata).returns(@results = mock("results"))
|
28
|
+
end
|
29
|
+
|
30
|
+
def teardown
|
31
|
+
@logfile.close
|
32
|
+
end
|
33
|
+
|
34
|
+
def test_get_metadata_yields_the_results_if_given_a_block
|
35
|
+
in_block = false
|
36
|
+
@rets.get_metadata do |results|
|
37
|
+
in_block = true
|
38
|
+
assert_equal @results, results
|
39
|
+
end
|
40
|
+
|
41
|
+
assert in_block, "Block was never yielded to"
|
42
|
+
end
|
43
|
+
|
44
|
+
def test_useragent
|
45
|
+
assert_match /^rets4r/, RETS4R::Client::Requester::DEFAULT_USER_AGENT
|
46
|
+
assert_match /#{::RETS4R::VERSION}/, RETS4R::Client::Requester::DEFAULT_USER_AGENT
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_get_metadata_returns_the_metadata_when_no_block_given
|
50
|
+
rval = @rets.get_metadata
|
51
|
+
|
52
|
+
assert_equal @results, rval
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_get_metadata_returns_the_blocks_value_when_given_a_block
|
56
|
+
rval = @rets.get_metadata do |results|
|
57
|
+
:block_value
|
58
|
+
end
|
59
|
+
|
60
|
+
assert_equal :block_value, rval
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class TestClientGetObject < Test::Unit::TestCase
|
65
|
+
RETS_PORT = '9080'
|
66
|
+
RETS_URL = "http://localhost:#{RETS_PORT}"
|
67
|
+
RETS_LOGIN = 'login'
|
68
|
+
RETS_PASSWORD = 'password'
|
69
|
+
|
70
|
+
class CustomError < StandardError; end
|
71
|
+
|
72
|
+
def setup
|
73
|
+
@logfile = StringIO.open
|
74
|
+
@rets = RETS4R::Client.new(RETS_URL)
|
75
|
+
@rets.logger = Logger.new(@logfile)
|
76
|
+
@rets.logger.level = Logger::DEBUG
|
77
|
+
|
78
|
+
@rets.stubs(:request).returns(@response = mock("response"))
|
79
|
+
end
|
80
|
+
|
81
|
+
def test_returns_multipart_parallel_objects_in_a_single_array(boundary = "1231")
|
82
|
+
@response.expects(:[]).with('content-type').at_least_once.returns("multipart/parallel; boundary=#{boundary}")
|
83
|
+
@response.expects(:body).returns("\r\n--1231\r\nContent-ID: 392103\r\nObject-ID: 1\r\nContent-Type: image/jpeg\r\n\r\n" + "\000"*120 + "\r\n--1231\r\nContent-ID: 392103\r\nObject-ID: 2\r\nContent-Type: image/gif\r\n\r\n" + "\000"*140 + "\r\n--1231--")
|
84
|
+
results = @rets.get_object("Property", "Photo", "392103:*")
|
85
|
+
assert_equal 2, results.size, "Client should parse two objects out of the request"
|
86
|
+
assert_kind_of RETS4R::Client::DataObject, results[0], "First result isn't a DataObject"
|
87
|
+
assert_kind_of RETS4R::Client::DataObject, results[1], "Second result isn't a DataObject"
|
88
|
+
assert_equal "image/jpeg", results[0].type["Content-Type"], "First object isn't an image/jpeg"
|
89
|
+
assert_equal 120, results[0].data.size, "First object isn't 120 bytes in length"
|
90
|
+
assert_equal "image/gif", results[1].type["Content-Type"], "Second object isn't an image/gif"
|
91
|
+
assert_equal 140, results[1].data.size, "Second object isn't 140 bytes in length"
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_returns_multipart_parallel_objects_in_a_single_array_boundary_with_double_quotes
|
95
|
+
test_returns_multipart_parallel_objects_in_a_single_array('"1231"')
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_returns_multipart_parallel_objects_in_a_single_array_boundary_with_single_quotes
|
99
|
+
test_returns_multipart_parallel_objects_in_a_single_array("'1231'")
|
100
|
+
end
|
101
|
+
|
102
|
+
def test_returns_single_entity_object_in_a_single_element_array
|
103
|
+
@response.expects(:[]).with('content-type').at_least_once.returns("image/jpeg")
|
104
|
+
@response.expects(:[]).with('Transfer-Encoding').at_least_once.returns("")
|
105
|
+
@response.expects(:[]).with('Content-Length').at_least_once.returns(241)
|
106
|
+
@response.expects(:[]).with('Object-ID').at_least_once.returns("25478")
|
107
|
+
@response.expects(:[]).with('Content-ID').at_least_once.returns("5589")
|
108
|
+
@response.expects(:body).returns("\000"*241)
|
109
|
+
|
110
|
+
results = @rets.get_object("Property", "Photo", "392103:*")
|
111
|
+
assert_equal 1, results.size, "Client parsed one object out of the request"
|
112
|
+
assert_kind_of RETS4R::Client::DataObject, results[0], "First result isn't a DataObject"
|
113
|
+
assert_equal "image/jpeg", results[0].type["Content-Type"], "Content-Type not copied"
|
114
|
+
assert_equal "5589", results[0].type["Content-ID"], "Content-ID not copied"
|
115
|
+
assert_equal "25478", results[0].type["Object-ID"], "Object-ID not copied"
|
116
|
+
assert_equal 241, results[0].data.size, "First object isn't 241 bytes in length"
|
117
|
+
end
|
118
|
+
|
119
|
+
def test_returns_single_entity_object_as_chunked_encoding_in_a_single_element_array
|
120
|
+
@response.expects(:[]).with('content-type').at_least_once.returns("image/jpeg")
|
121
|
+
@response.expects(:[]).with('Transfer-Encoding').at_least_once.returns("chunked")
|
122
|
+
@response.expects(:[]).with('Object-ID').at_least_once.returns("25478")
|
123
|
+
@response.expects(:[]).with('Content-ID').at_least_once.returns("5589")
|
124
|
+
@response.expects(:body).returns("\000"*241)
|
125
|
+
|
126
|
+
results = @rets.get_object("Property", "Photo", "392103:*")
|
127
|
+
assert_equal 1, results.size, "Client parsed one object out of the request"
|
128
|
+
assert_kind_of RETS4R::Client::DataObject, results[0], "First result isn't a DataObject"
|
129
|
+
assert_equal "image/jpeg", results[0].type["Content-Type"], "Content-Type not copied"
|
130
|
+
assert_equal "5589", results[0].type["Content-ID"], "Content-ID not copied"
|
131
|
+
assert_equal "25478", results[0].type["Object-ID"], "Object-ID not copied"
|
132
|
+
assert_equal 241, results[0].data.size, "First object isn't 241 bytes in length"
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_yields_data_objects_to_block_and_returns_blocks_value
|
136
|
+
@response.expects(:[]).with('content-type').at_least_once.returns("image/jpeg")
|
137
|
+
@response.expects(:[]).with('Transfer-Encoding').at_least_once.returns("")
|
138
|
+
@response.expects(:[]).with('Content-Length').at_least_once.returns(241)
|
139
|
+
@response.expects(:[]).with('Object-ID').at_least_once.returns("25478")
|
140
|
+
@response.expects(:[]).with('Content-ID').at_least_once.returns("5589")
|
141
|
+
@response.expects(:body).returns("\000"*241)
|
142
|
+
|
143
|
+
yielded_count = 0
|
144
|
+
|
145
|
+
value = @rets.get_object("Property", "VRImage", "912:0") do |result|
|
146
|
+
assert_kind_of RETS4R::Client::DataObject, result
|
147
|
+
yielded_count += 1
|
148
|
+
:return_value
|
149
|
+
end
|
150
|
+
|
151
|
+
assert_equal yielded_count, value
|
152
|
+
end
|
153
|
+
def test_correcly_handles_location_header_url
|
154
|
+
@response.expects(:[]).with('content-type').at_least_once.returns("multipart/parallel; boundary='1231'")
|
155
|
+
@response.expects(:body).returns(
|
156
|
+
"\r\n--1231\r\nContent-ID: 392103\r\nObject-ID: 1\r\nContent-Type: image/jpeg\r\nLocation: http://example.com/391203-1.jpg\r\n\r\n" +
|
157
|
+
"\000"*120 +
|
158
|
+
"\r\n--1231\r\nContent-ID: 392103\r\nObject-ID: 2\r\nContent-Type: image/gif\r\nLocation: http://example.com/391203-2.gif\r\n\r\n" +
|
159
|
+
"\000"*140 +
|
160
|
+
"\r\n--1231--"
|
161
|
+
)
|
162
|
+
results = @rets.get_object("Property", "Photo", "392103:*", true)
|
163
|
+
|
164
|
+
assert_equal 'http://example.com/391203-1.jpg', results.first.info['Location'], "incorrect location"
|
165
|
+
assert_equal 'http://example.com/391203-2.gif', results.last.info['Location'], "incorrect location"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
class TestClientLogin < Test::Unit::TestCase
|
170
|
+
RETS_PORT = '9080'
|
171
|
+
RETS_URL = "http://localhost:#{RETS_PORT}"
|
172
|
+
RETS_LOGIN = 'login'
|
173
|
+
RETS_PASSWORD = 'password'
|
174
|
+
|
175
|
+
class CustomError < StandardError; end
|
176
|
+
|
177
|
+
def setup
|
178
|
+
@logfile = StringIO.open
|
179
|
+
@rets = RETS4R::Client.new(RETS_URL)
|
180
|
+
@rets.logger = Logger.new(@logfile)
|
181
|
+
@rets.logger.level = Logger::DEBUG
|
182
|
+
|
183
|
+
@rets.stubs(:request).returns(@response = mock("response"))
|
184
|
+
@response.stubs(:body).returns(:body)
|
185
|
+
Client::ResponseParser.any_instance.stubs(:parse_key_value).returns(@results = mock("results"))
|
186
|
+
@results.stubs(:success?).returns(true)
|
187
|
+
@results.stubs(:response).returns(Hash.new)
|
188
|
+
@results.stubs(:secondary_response=)
|
189
|
+
end
|
190
|
+
|
191
|
+
def teardown
|
192
|
+
@logfile.close
|
193
|
+
end
|
194
|
+
|
195
|
+
def test_successful_login_yields_the_results_to_the_block
|
196
|
+
@rets.expects(:request).with {|arg| arg.kind_of?(URI)}.returns(@response)
|
197
|
+
Client::ResponseParser.any_instance.expects(:parse_key_value).returns(@results)
|
198
|
+
@results.expects(:success?).returns(true)
|
199
|
+
@rets.expects(:logout)
|
200
|
+
|
201
|
+
in_block = false
|
202
|
+
@rets.login("user", "pass") do |results|
|
203
|
+
assert_equal @results, results
|
204
|
+
in_block = true
|
205
|
+
end
|
206
|
+
|
207
|
+
assert in_block, "Block was never yielded to"
|
208
|
+
end
|
209
|
+
|
210
|
+
def test_logout_called_after_block_execution_if_block_raises
|
211
|
+
assert_raises(CustomError) do
|
212
|
+
@rets.expects(:logout)
|
213
|
+
@rets.login("user", "pass") do |results|
|
214
|
+
raise CustomError
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def test_login_returns_the_blocks_value
|
220
|
+
rval = @rets.login("user", "pass") do |results|
|
221
|
+
:value
|
222
|
+
end
|
223
|
+
|
224
|
+
assert_equal :value, rval
|
225
|
+
end
|
226
|
+
|
227
|
+
def test_login_without_a_block_returns_the_results
|
228
|
+
results = @rets.login("user", "pass")
|
229
|
+
assert_equal @results, results
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
class TestClient < Test::Unit::TestCase
|
234
|
+
RETS_PORT = '9080'
|
235
|
+
RETS_URL = "http://localhost:#{RETS_PORT}"
|
236
|
+
RETS_LOGIN = 'login'
|
237
|
+
RETS_PASSWORD = 'password'
|
238
|
+
|
239
|
+
def setup
|
240
|
+
@logfile = StringIO.open
|
241
|
+
@rets = RETS4R::Client.new(RETS_URL)
|
242
|
+
@rets.logger = Logger.new(@logfile)
|
243
|
+
@rets.logger.level = Logger::DEBUG
|
244
|
+
end
|
245
|
+
|
246
|
+
def teardown
|
247
|
+
@logfile.close
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_setup
|
251
|
+
assert_nothing_raised() { @rets.user_agent = 'ACK/2.1' }
|
252
|
+
assert_equal('ACK/2.1', @rets.user_agent)
|
253
|
+
|
254
|
+
assert_nothing_raised() { @rets.user_agent = 'SPRETS/0.1' }
|
255
|
+
assert_nothing_raised() { @rets.request_method = 'GET' }
|
256
|
+
|
257
|
+
assert_raise(RETS4R::Client::Unsupported) { @rets.rets_version = '1.4.0' }
|
258
|
+
assert_nothing_raised() { @rets.rets_version = '1.5' }
|
259
|
+
assert_equal("1.5", @rets.rets_version)
|
260
|
+
assert_equal("RETS/1.5", @rets.get_header("RETS-Version"))
|
261
|
+
assert_nothing_raised() { @rets.rets_version = '1.7' }
|
262
|
+
assert_equal("RETS/1.7", @rets.get_header("RETS-Version"))
|
263
|
+
|
264
|
+
assert_equal('SPRETS/0.1', @rets.user_agent)
|
265
|
+
assert_equal('GET', @rets.request_method)
|
266
|
+
assert_equal('1.7', @rets.rets_version)
|
267
|
+
|
268
|
+
assert_nothing_raised() { @rets.request_method = 'POST' }
|
269
|
+
|
270
|
+
assert_equal('POST', @rets.request_method)
|
271
|
+
|
272
|
+
|
273
|
+
# Check that our changes were logged when in debug mode
|
274
|
+
assert @logfile.length > 0
|
275
|
+
end
|
276
|
+
|
277
|
+
# Just to make sure that we're okay when we don't have a logger, we set it to nil and
|
278
|
+
# make a change that would trigger a debug mode log.
|
279
|
+
def test_without_logger
|
280
|
+
@rets.logger = nil
|
281
|
+
|
282
|
+
assert_nothing_raised() { @rets.request_method = 'GET' }
|
283
|
+
end
|
284
|
+
|
285
|
+
def test_content_type_parsing
|
286
|
+
ct = 'multipart/parallel; boundary=cc2631bb.0165.3b32.8a7d.a8453f662101; charset=utf-8'
|
287
|
+
|
288
|
+
results = @rets.process_content_type(ct)
|
289
|
+
|
290
|
+
assert_equal('cc2631bb.0165.3b32.8a7d.a8453f662101', results['boundary'])
|
291
|
+
assert_equal('multipart/parallel', results['content-type'])
|
292
|
+
assert_equal('utf-8', results['charset'])
|
293
|
+
end
|
294
|
+
|
295
|
+
def test_performs_get_request
|
296
|
+
assert_nothing_raised() {@rets.request_method = 'GET'}
|
297
|
+
assert_equal('GET', @rets.request_method)
|
298
|
+
|
299
|
+
http = mock('http')
|
300
|
+
response = mock('response')
|
301
|
+
response.stubs(:to_hash).returns({})
|
302
|
+
response.stubs(:code).returns('500')
|
303
|
+
response.stubs(:message).returns('Move along, nothing to see here.')
|
304
|
+
|
305
|
+
http.expects(:get).with('', {'User-Agent' => @rets.user_agent, 'RETS-Version' => "RETS/#{@rets.rets_version}", 'Accept' => '*/*'}).at_least_once.returns(response)
|
306
|
+
http.expects(:post).never
|
307
|
+
Net::HTTP.any_instance.expects(:start).at_least_once.yields(http)
|
308
|
+
|
309
|
+
assert_raises(RETS4R::Client::HTTPError) {@rets.login('user', 'pass')}
|
310
|
+
end
|
311
|
+
|
312
|
+
def test_performs_post_request
|
313
|
+
assert_nothing_raised() {@rets.request_method = 'POST'}
|
314
|
+
assert_equal('POST', @rets.request_method)
|
315
|
+
|
316
|
+
http = mock('http')
|
317
|
+
response = mock('response')
|
318
|
+
response.stubs(:to_hash).returns({})
|
319
|
+
response.stubs(:code).returns('500')
|
320
|
+
response.stubs(:message).returns('Move along, nothing to see here.')
|
321
|
+
|
322
|
+
http.expects(:post).with('', '', {'User-Agent' => @rets.user_agent, 'RETS-Version' => "RETS/#{@rets.rets_version}", 'Accept' => '*/*'}).at_least_once.returns(response)
|
323
|
+
http.expects(:get).never
|
324
|
+
Net::HTTP.any_instance.expects(:start).at_least_once.yields(http)
|
325
|
+
|
326
|
+
assert_raises(RETS4R::Client::HTTPError) {@rets.login('user', 'pass')}
|
327
|
+
end
|
328
|
+
|
329
|
+
def test_search_without_query_should_not_raise_no_metho_error
|
330
|
+
client = RETS4R::Client.new('http://demo.crt.realtors.org:6103/rets/login')
|
331
|
+
client.login('Joe', 'Schmoe')
|
332
|
+
begin
|
333
|
+
client.search('', '', nil)
|
334
|
+
# search_uri = URI.parse("http://demo.crt.realtors.org:6103/rets/search")
|
335
|
+
# client.send :request, search_uri,
|
336
|
+
# {"Query"=>nil, "Format"=>"COMPACT", "Count"=>"0", "QueryType"=>"DMQL2", "Class"=>"", "SearchType"=>""}
|
337
|
+
rescue Exception => e
|
338
|
+
assert_not_equal "NoMethodError", e.class.to_s
|
339
|
+
end
|
340
|
+
end
|
341
|
+
end
|
342
|
+
end
|