atom-tools 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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