atom-tools 0.9.0 → 0.9.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. data/README +3 -3
  2. data/Rakefile +1 -1
  3. data/bin/atom-client.rb +13 -10
  4. data/lib/atom/collection.rb +2 -2
  5. data/lib/atom/element.rb +5 -1
  6. data/lib/atom/entry.rb +9 -2
  7. data/lib/atom/feed.rb +11 -6
  8. data/lib/atom/http.rb +157 -38
  9. data/lib/atom/service.rb +170 -0
  10. data/lib/atom/text.rb +15 -2
  11. data/lib/atom/xml.rb +1 -1
  12. data/test/conformance/updated.rb +2 -1
  13. data/test/test_constructs.rb +45 -2
  14. data/test/test_feed.rb +27 -0
  15. data/test/test_http.rb +116 -20
  16. data/test/test_protocol.rb +77 -13
  17. data/test/test_xml.rb +15 -1
  18. metadata +3 -38
  19. data/bin/atom-server.rb~ +0 -71
  20. data/doc/classes/Atom/App.html +0 -217
  21. data/doc/classes/Atom/Author.html +0 -130
  22. data/doc/classes/Atom/Category.html +0 -128
  23. data/doc/classes/Atom/Collection.html +0 -322
  24. data/doc/classes/Atom/Content.html +0 -129
  25. data/doc/classes/Atom/Contributor.html +0 -119
  26. data/doc/classes/Atom/Element.html +0 -325
  27. data/doc/classes/Atom/Entry.html +0 -365
  28. data/doc/classes/Atom/Feed.html +0 -585
  29. data/doc/classes/Atom/HTTP.html +0 -374
  30. data/doc/classes/Atom/Link.html +0 -137
  31. data/doc/classes/Atom/Text.html +0 -229
  32. data/doc/classes/XHTML.html +0 -118
  33. data/doc/created.rid +0 -1
  34. data/doc/files/README.html +0 -213
  35. data/doc/files/lib/atom/app_rb.html +0 -110
  36. data/doc/files/lib/atom/collection_rb.html +0 -110
  37. data/doc/files/lib/atom/element_rb.html +0 -109
  38. data/doc/files/lib/atom/entry_rb.html +0 -111
  39. data/doc/files/lib/atom/feed_rb.html +0 -112
  40. data/doc/files/lib/atom/http_rb.html +0 -109
  41. data/doc/files/lib/atom/text_rb.html +0 -108
  42. data/doc/files/lib/atom/xml_rb.html +0 -110
  43. data/doc/files/lib/atom/yaml_rb.html +0 -109
  44. data/doc/fr_class_index.html +0 -39
  45. data/doc/fr_file_index.html +0 -36
  46. data/doc/fr_method_index.html +0 -62
  47. data/doc/index.html +0 -24
  48. data/doc/rdoc-style.css +0 -208
  49. data/lib/atom/app.rb +0 -87
@@ -0,0 +1,170 @@
1
+ require "uri"
2
+
3
+ require "atom/http"
4
+ require "atom/element"
5
+ require "atom/collection"
6
+
7
+ module Atom
8
+ PP_NS = "http://purl.org/atom/app#"
9
+
10
+ class WrongNamespace < RuntimeError #:nodoc:
11
+ end
12
+ class WrongMimetype < RuntimeError # :nodoc:
13
+ end
14
+ class WrongResponse < RuntimeError # :nodoc:
15
+ end
16
+
17
+ # an Atom::Workspace has a #title (Atom::Text) and #collections, an Array of Atom::Collection s
18
+ class Workspace < Atom::Element
19
+ element :collections, Atom::Multiple(Atom::Collection)
20
+ element :title, Atom::Text
21
+
22
+ def self.parse(xml, base = "", http = Atom::HTTP.new) # :nodoc:
23
+ ws = Atom::Workspace.new("workspace")
24
+
25
+ rxml = if xml.is_a? REXML::Document
26
+ xml.root
27
+ elsif xml.is_a? REXML::Element
28
+ xml
29
+ else
30
+ REXML::Document.new(xml)
31
+ end
32
+
33
+ xml.fill_text_construct(ws, "title")
34
+
35
+ REXML::XPath.match( rxml,
36
+ "./app:collection",
37
+ {"app" => Atom::PP_NS} ).each do |col_el|
38
+ # absolutize relative URLs
39
+ url = base.to_uri + col_el.attributes["href"].to_uri
40
+
41
+ coll = Atom::Collection.new(url, http)
42
+
43
+ # XXX this is a Text Construct, and should be parsed as such
44
+ col_el.fill_text_construct(coll, "title")
45
+
46
+ accepts = REXML::XPath.first( col_el,
47
+ "./app:accept",
48
+ {"app" => Atom::PP_NS} )
49
+ coll.accepts = (accepts ? accepts.text : "entry")
50
+
51
+ ws.collections << coll
52
+ end
53
+
54
+ ws
55
+ end
56
+
57
+ def to_element # :nodoc:
58
+ root = REXML::Element.new "workspace"
59
+
60
+ # damn you, REXML. Damn you and you bizarre handling of namespaces
61
+ title = self.title.to_element
62
+ title.name = "atom:title"
63
+ root << title
64
+
65
+ self.collections.each do |coll|
66
+ el = REXML::Element.new "collection"
67
+
68
+ el.attributes["href"] = coll.uri
69
+
70
+ title = coll.title.to_element
71
+ title.name = "atom:title"
72
+ el << title
73
+
74
+ unless coll.accepts.nil?
75
+ accepts = REXML::Element.new "accepts"
76
+ accepts.text = coll.accepts
77
+ el << accepts
78
+ end
79
+
80
+ root << el
81
+ end
82
+
83
+ root
84
+ end
85
+ end
86
+
87
+ # Atom::Service represents an Atom Publishing Protocol service
88
+ # document. Its only child is #workspaces, which is an Array of
89
+ # Atom::Workspace s
90
+ class Service < Atom::Element
91
+ element :workspaces, Atom::Multiple(Atom::Workspace)
92
+
93
+ # retrieves and parses an Atom service document.
94
+ def initialize(service_url = "", http = Atom::HTTP.new)
95
+ super("service")
96
+
97
+ @http = http
98
+
99
+ return if service_url.empty?
100
+
101
+ base = URI.parse(service_url)
102
+
103
+ rxml = nil
104
+
105
+ res = @http.get(base)
106
+
107
+ unless res.code == "200" # XXX needs to handle redirects, &c.
108
+ raise WrongResponse, "service document URL responded with unexpected code #{res.code}"
109
+ end
110
+
111
+ unless res.content_type == "application/atomserv+xml"
112
+ raise WrongMimetype, "this isn't an atom service document!"
113
+ end
114
+
115
+ parse(res.body, base)
116
+ end
117
+
118
+ # parse a service document, adding its workspaces to this object
119
+ def parse xml, base = ""
120
+ rxml = if xml.is_a? REXML::Document
121
+ xml.root
122
+ elsif xml.is_a? REXML::Element
123
+ xml
124
+ else
125
+ REXML::Document.new(xml)
126
+ end
127
+
128
+ unless rxml.root.namespace == PP_NS
129
+ raise WrongNamespace, "this isn't an atom service document!"
130
+ end
131
+
132
+ REXML::XPath.match( rxml, "/app:service/app:workspace", {"app" => Atom::PP_NS} ).each do |ws_el|
133
+ self.workspaces << Atom::Workspace.parse(ws_el, base, @http)
134
+ end
135
+
136
+ self
137
+ end
138
+
139
+ # serialize to a (namespaced) REXML::Document
140
+ def to_xml
141
+ doc = REXML::Document.new
142
+
143
+ root = REXML::Element.new "service"
144
+ root.add_namespace Atom::PP_NS
145
+ root.add_namespace "atom", Atom::NS
146
+
147
+ self.workspaces.each do |ws|
148
+ root << ws.to_element
149
+ end
150
+
151
+ doc << root
152
+ doc
153
+ end
154
+ end
155
+
156
+ class Entry
157
+ # the @href of an entry's link[@rel="edit"]
158
+ def edit_url
159
+ begin
160
+ edit_link = self.links.find do |link|
161
+ link["rel"] == "edit"
162
+ end
163
+
164
+ edit_link["href"]
165
+ rescue
166
+ nil
167
+ end
168
+ end
169
+ end
170
+ end
data/lib/atom/text.rb CHANGED
@@ -37,14 +37,27 @@ module Atom
37
37
  end
38
38
  end
39
39
 
40
- # attepts to parse the content and return it as an array of REXML::Elements
40
+ # attempts to parse the content of this element as XML and return it
41
+ # as an array of REXML::Elements.
42
+ #
43
+ # If this self["type"] is "html" and Hpricot is installed, it will
44
+ # be converted to XHTML first.
41
45
  def xml
42
46
  if self["type"] == "xhtml"
43
47
  @content.children
44
48
  elsif self["type"] == "text"
45
49
  [self.to_s]
50
+ elsif self["type"] == "html"
51
+ begin
52
+ require "hpricot"
53
+ rescue
54
+ raise "Turning HTML content into XML requires Hpricot."
55
+ end
56
+
57
+ fixed = Hpricot(self.to_s, :xhtml_strict => true)
58
+ REXML::Document.new("<div>#{fixed}</div>").root.children
46
59
  else
47
- # XXX - hpricot goes here?
60
+ # XXX check that @type is an XML mimetype and parse it
48
61
  raise "I haven't implemented this yet"
49
62
  end
50
63
  end
data/lib/atom/xml.rb CHANGED
@@ -105,7 +105,7 @@ module REXML # :nodoc: all
105
105
  thing.class.attrs.each do |name,req|
106
106
  value = elem.ns_attr name.to_s
107
107
  if value and name == :href
108
- thing[name.to_s] = (URI.parse(top.base) + value).to_s
108
+ thing[name.to_s] = (top.base.to_uri + value).to_s
109
109
  elsif value
110
110
  thing[name.to_s] = value
111
111
  end
@@ -16,7 +16,8 @@ class TestUpdatedConformance < Test::Unit::TestCase
16
16
 
17
17
  # this is an insignificant change,
18
18
  # (ie. atom:updated_1 == atom:updated_2),
19
- # but that's for you to handle
19
+ #
20
+ # the update is applied, your application can handle that however it wants.
20
21
  feed.update!
21
22
  assert_equal "12 of 13 miner<b>s</b> survive mine collapse", feed.entries.first.content.to_s.strip
22
23
 
@@ -2,11 +2,54 @@ require "test/unit"
2
2
  require "atom/entry"
3
3
 
4
4
  class ConstructTest < Test::Unit::TestCase
5
+ def test_text_construct_html_to_xml
6
+ begin
7
+ require "hpricot"
8
+ rescue
9
+ # hpricot isn't installed, just skip this test
10
+ puts "skipping hpricot tests"
11
+ return
12
+ end
13
+
14
+ entry = Atom::Entry.new
15
+
16
+ html = <<END
17
+ <p>Paragraph 1 contains <a href=http://example.org/>a link
18
+ <p>This really is a horrendous mess.
19
+ END
20
+
21
+ entry.content = html
22
+ entry.content["type"] = "html"
23
+
24
+ xhtml = entry.content.xml
25
+
26
+ # Hpricot is imperfect; for now I'll just test that it's parseable
27
+ assert_instance_of Array, xhtml
28
+ assert_instance_of REXML::Element, xhtml.first
29
+
30
+ =begin
31
+ assert_equal 2, xhtml.length
32
+
33
+ first = xhtml.first
34
+ assert_equal "p", first.name
35
+ assert_equal 2, first.children.length
36
+
37
+ a = first.children.last
38
+ assert_equal "a", a.name
39
+ assert_equal "http://example.org/", a.attributes["href"]
40
+ assert_equal "a link", a.text
41
+
42
+ last = xhtml.last
43
+ assert_equal "p", last.name
44
+ assert_equal "This really is a horrendous mess.", last.text
45
+ =end
46
+ end
47
+
5
48
  def test_text_construct_text
6
49
  entry = Atom::Entry.new
7
50
 
8
- assert_nil(entry.title)
9
- assert_equal("", entry.title.to_s)
51
+ assert_nil entry.title
52
+ assert_equal "", entry.title.to_s
10
53
 
11
54
  entry.title = "<3"
12
55
 
data/test/test_feed.rb CHANGED
@@ -81,6 +81,32 @@ END
81
81
  assert_equal 1, feed.entries.length
82
82
  end
83
83
 
84
+ def test_media_types
85
+ c = proc do |c_t|
86
+ @s.mount_proc("/") do |req,res|
87
+ res.content_type = c_t
88
+ res.body = @test_feed
89
+
90
+ @s.stop
91
+ end
92
+ one_shot
93
+ end
94
+
95
+ feed = Atom::Feed.new "http://localhost:#{@port}/"
96
+
97
+ # even if it looks like a feed, the server's word is law
98
+ c.call("text/plain")
99
+ assert_raise(Atom::HTTPException) { feed.update! }
100
+
101
+ # text/xml isn't the preferred mimetype, but we'll accept it
102
+ c.call("text/xml")
103
+ assert_nothing_raised { feed.update! }
104
+
105
+ # same goes for application/xml
106
+ # XXX c.call("application/xml")
107
+ # assert_nothing_raised { feed.update! }
108
+ end
109
+
84
110
  def test_conditional_get
85
111
  @s.mount_proc("/") do |req,res|
86
112
  assert_nil req["If-None-Match"]
@@ -124,5 +150,6 @@ END
124
150
  assert_equal 1, feed.entries.length
125
151
  end
126
152
 
153
+ # prepares the server for a single request
127
154
  def one_shot; Thread.new { @s.start }; end
128
155
  end
data/test/test_http.rb CHANGED
@@ -1,7 +1,14 @@
1
+ require "test/unit"
2
+
1
3
  require "atom/http"
2
4
  require "webrick"
3
5
 
4
6
  class AtomProtocolTest < Test::Unit::TestCase
7
+ REALM = "test authentication"
8
+ USER = "test_user"
9
+ PASS = "aoeuaoeu"
10
+ SECRET_DATA = "I kissed a boy once"
11
+
5
12
  def setup
6
13
  @http = Atom::HTTP.new
7
14
  @port = rand(1024) + 1024
@@ -11,13 +18,21 @@ class AtomProtocolTest < Test::Unit::TestCase
11
18
  end
12
19
 
13
20
  def test_parse_wwwauth
14
- header = 'Basic realm="SokEvo"'
21
+ header = 'realm="SokEvo"'
15
22
 
16
- # parse_wwwauth is a private method
17
- auth_type, auth_params = @http.send :parse_wwwauth, header
23
+ params = @http.send :parse_quoted_wwwauth, header
24
+ assert_equal "SokEvo", params[:realm]
25
+
26
+ header = 'opaque="07UrfUiCYac5BbWJ", algorithm=MD5-sess, qop="auth", stale=TRUE, nonce="MDAx0Mzk", realm="test authentication"'
18
27
 
19
- assert_equal "Basic", auth_type
20
- assert_equal "SokEvo", auth_params["realm"]
28
+ params = @http.send :parse_wwwauth_digest, header
29
+
30
+ assert_equal "test authentication", params[:realm]
31
+ assert_equal "MDAx0Mzk", params[:nonce]
32
+ assert_equal true, params[:stale]
33
+ assert_equal "auth", params[:qop]
34
+ assert_equal "MD5-sess", params[:algorithm]
35
+ assert_equal "07UrfUiCYac5BbWJ", params[:opaque]
21
36
  end
22
37
 
23
38
  def test_GET
@@ -32,11 +47,11 @@ class AtomProtocolTest < Test::Unit::TestCase
32
47
 
33
48
  one_shot
34
49
 
35
- res = get_root
50
+ get_root
36
51
 
37
- assert_equal("200", res.code)
38
- assert_equal("text/plain", res.content_type)
39
- assert_equal("just junk", res.body)
52
+ assert_equal("200", @res.code)
53
+ assert_equal("text/plain", @res.content_type)
54
+ assert_equal("just junk", @res.body)
40
55
  end
41
56
 
42
57
  def test_GET_headers
@@ -48,39 +63,120 @@ class AtomProtocolTest < Test::Unit::TestCase
48
63
 
49
64
  one_shot
50
65
 
51
- res = get_root("User-Agent" => "tester agent")
66
+ get_root("User-Agent" => "tester agent")
52
67
 
53
- assert_equal("200", res.code)
68
+ assert_equal("200", @res.code)
54
69
  end
55
70
 
56
71
  def test_basic_auth
57
72
  @s.mount_proc("/") do |req,res|
58
- WEBrick::HTTPAuth.basic_auth(req, res, "test authentication") do |u,p|
59
- u == "test" and p == "pass"
73
+ WEBrick::HTTPAuth.basic_auth(req, res, REALM) do |u,p|
74
+ u == USER and p == PASS
60
75
  end
61
76
 
62
- res.body = "sooper-secret!"
77
+ res.body = SECRET_DATA
63
78
  @s.stop
64
79
  end
65
80
 
66
81
  one_shot
67
-
82
+
83
+ # with no credentials
84
+ assert_raises(Atom::Unauthorized) { get_root }
85
+
86
+ @http.user = USER
87
+ @http.pass = "incorrect_password"
88
+
89
+ # with incorrect credentials
68
90
  assert_raises(Atom::Unauthorized) { get_root }
69
91
 
70
92
  @http.when_auth do |abs_url,realm|
71
93
  assert_equal "http://localhost:#{@port}/", abs_url
72
- assert_equal "test authentication", realm
94
+ assert_equal REALM, realm
73
95
 
74
- ["test", "pass"]
96
+ [USER, PASS]
75
97
  end
76
98
 
77
99
  one_shot
78
100
 
101
+ get_root
102
+ assert_equal "200", @res.code
103
+ assert_equal SECRET_DATA, @res.body
104
+ end
105
+
106
+ def test_digest_auth
107
+ # a dummy userdb
108
+ userdb = {}
109
+ userdb[USER] = PASS
110
+
111
+ def userdb.get_passwd(realm, user, reload)
112
+ Digest::MD5::hexdigest([user, realm, self[user]].join(":"))
113
+ end
114
+
115
+ authenticator = WEBrick::HTTPAuth::DigestAuth.new(
116
+ :UserDB => userdb,
117
+ :Realm => REALM,
118
+ :Algorithm => "MD5"
119
+ )
120
+
121
+ @s.mount_proc("/") do |req,res|
122
+ authenticator.authenticate(req, res)
123
+ res.body = SECRET_DATA
124
+ end
125
+
126
+ one_shot
127
+
128
+ # no credentials
129
+ assert_raises(Atom::Unauthorized) { get_root }
130
+
131
+ @http.user = USER
132
+ @http.pass = PASS
133
+
134
+ # correct credentials
79
135
  res = get_root
80
- assert_equal("200", res.code)
81
- assert_equal("sooper-secret!", res.body)
136
+ assert_equal SECRET_DATA, res.body
137
+
138
+ @s.stop
139
+ end
140
+
141
+ def test_wsse_auth
142
+ @s.mount_proc("/") do |req,res|
143
+ assert_equal 'WSSE profile="UsernameToken"', req["Authorization"]
144
+
145
+ xwsse = req["X-WSSE"]
146
+
147
+ p = @http.send :parse_quoted_wwwauth, xwsse
148
+
149
+ assert_equal USER, p[:Username]
150
+ assert_match /^UsernameToken /, xwsse
151
+
152
+ # un-base64 in preparation for SHA1-ing
153
+ nonce = p[:Nonce].unpack("m").first
154
+
155
+ # Base64( SHA1( Nonce + CreationTimestamp + Password ) )
156
+ pd_string = nonce + p[:Created] + PASS
157
+ password_digest = [Digest::SHA1.digest(pd_string)].pack("m").chomp
158
+
159
+ assert_equal password_digest, p[:PasswordDigest]
160
+
161
+ res.body = SECRET_DATA
162
+ @s.stop
163
+ end
164
+
165
+ one_shot
166
+
167
+ @http.always_auth = :wsse
168
+ @http.user = USER
169
+ @http.pass = PASS
170
+
171
+ get_root
172
+
173
+ assert_equal "200", @res.code
174
+ assert_equal SECRET_DATA, @res.body
175
+ end
176
+
177
+ def get_root(*args)
178
+ @res = @http.get("http://localhost:#{@port}/", *args)
82
179
  end
83
180
 
84
- def get_root(*args); @http.get("http://localhost:#{@port}/", *args); end
85
181
  def one_shot; Thread.new { @s.start }; end
86
182
  end