jschairb-rets4r 1.1.18

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. data/.document +5 -0
  2. data/CHANGELOG +566 -0
  3. data/CONTRIBUTORS +7 -0
  4. data/Gemfile +10 -0
  5. data/LICENSE +29 -0
  6. data/MANIFEST +62 -0
  7. data/NEWS +186 -0
  8. data/README.rdoc +43 -0
  9. data/RUBYS +56 -0
  10. data/Rakefile +50 -0
  11. data/TODO +35 -0
  12. data/examples/client_get_object.rb +49 -0
  13. data/examples/client_login.rb +39 -0
  14. data/examples/client_mapper.rb +17 -0
  15. data/examples/client_metadata.rb +42 -0
  16. data/examples/client_parser.rb +9 -0
  17. data/examples/client_search.rb +49 -0
  18. data/examples/settings.yml +114 -0
  19. data/lib/rets4r.rb +14 -0
  20. data/lib/rets4r/auth.rb +73 -0
  21. data/lib/rets4r/client.rb +487 -0
  22. data/lib/rets4r/client/data.rb +14 -0
  23. data/lib/rets4r/client/dataobject.rb +28 -0
  24. data/lib/rets4r/client/exceptions.rb +116 -0
  25. data/lib/rets4r/client/links.rb +32 -0
  26. data/lib/rets4r/client/metadata.rb +15 -0
  27. data/lib/rets4r/client/parsers/compact.rb +42 -0
  28. data/lib/rets4r/client/parsers/compact_nokogiri.rb +91 -0
  29. data/lib/rets4r/client/parsers/metadata.rb +92 -0
  30. data/lib/rets4r/client/parsers/response_parser.rb +100 -0
  31. data/lib/rets4r/client/requester.rb +143 -0
  32. data/lib/rets4r/client/transaction.rb +31 -0
  33. data/lib/rets4r/core_ext/array/extract_options.rb +15 -0
  34. data/lib/rets4r/core_ext/class/attribute_accessors.rb +58 -0
  35. data/lib/rets4r/core_ext/hash/keys.rb +46 -0
  36. data/lib/rets4r/core_ext/hash/slice.rb +39 -0
  37. data/lib/rets4r/listing_mapper.rb +17 -0
  38. data/lib/rets4r/listing_service.rb +35 -0
  39. data/lib/rets4r/loader.rb +8 -0
  40. data/lib/tasks/annotations.rake +121 -0
  41. data/lib/tasks/coverage.rake +13 -0
  42. data/rets4r.gemspec +24 -0
  43. data/spec/rets4r_compact_data_parser_spec.rb +7 -0
  44. data/test/data/1.5/bad_compact.xml +7 -0
  45. data/test/data/1.5/count_only_compact.xml +3 -0
  46. data/test/data/1.5/error.xml +1 -0
  47. data/test/data/1.5/invalid_compact.xml +4 -0
  48. data/test/data/1.5/login.xml +16 -0
  49. data/test/data/1.5/metadata.xml +0 -0
  50. data/test/data/1.5/search_compact.xml +8 -0
  51. data/test/data/1.5/search_compact_big.xml +136 -0
  52. data/test/data/1.5/search_unescaped_compact.xml +8 -0
  53. data/test/data/listing_service.yml +36 -0
  54. data/test/test_auth.rb +68 -0
  55. data/test/test_client.rb +342 -0
  56. data/test/test_client_links.rb +39 -0
  57. data/test/test_compact_nokogiri.rb +64 -0
  58. data/test/test_helper.rb +12 -0
  59. data/test/test_listing_mapper.rb +112 -0
  60. data/test/test_loader.rb +24 -0
  61. data/test/test_parser.rb +96 -0
  62. data/test/test_quality.rb +57 -0
  63. 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
+ require 'spec_helper'
2
+ require 'rets4r/client/parsers/compact'
3
+
4
+ describe RETS4R::Client::CompactDataParser do
5
+ it "should parse results without a delimiter in response"
6
+ it "should parse data without column names in response"
7
+ end
@@ -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&apos;. Growing neighborhood convienient to Wylie schools. Abundant living areas and and open space floor plan.Could be 5th BDRM, or upstair BDRM &amp; 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,3 @@
1
+ <RETS ReplyCode="0" ReplyText="SUCCESS">
2
+ <COUNT Records="1024"/>
3
+ </RETS>
@@ -0,0 +1 @@
1
+ <RETS ReplyCode="20400" ReplyText="Invalid Invalidness." />
@@ -0,0 +1,4 @@
1
+ <RETS ReplyCode="0" ReplyText="SUCCESS">
2
+ <COUNT Records="4"/>
3
+ <DELIMITER value="09" />
4
+ <COLUMNS> First Second Third </COL
@@ -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,8 @@
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
+ <MAXROWS/>
8
+ </RETS>
@@ -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,8 @@
1
+ <RETS ReplyCode="0" ReplyText="SUCCESS">
2
+ <COUNT Records="4"/>
3
+ <DELIMITER value="09" />
4
+ <COLUMNS> First Second Third </COLUMNS>
5
+ <DATA> Datum1 <Datum&2 Datum3</DATA>
6
+ <DATA> Datum4 D>atum5 Datum6</DATA>
7
+ <MAXROWS/>
8
+ </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
@@ -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