ape 1.0.0 → 1.5.0

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 (63) hide show
  1. data/LICENSE +1 -1
  2. data/README +22 -8
  3. data/Rakefile +66 -0
  4. data/bin/ape_server +3 -3
  5. data/lib/ape.rb +131 -937
  6. data/lib/ape/atomURI.rb +1 -1
  7. data/lib/ape/authent.rb +11 -17
  8. data/lib/ape/categories.rb +8 -7
  9. data/lib/ape/collection.rb +3 -7
  10. data/lib/ape/crumbs.rb +1 -1
  11. data/lib/ape/entry.rb +1 -1
  12. data/lib/ape/escaper.rb +1 -1
  13. data/lib/ape/feed.rb +26 -14
  14. data/lib/ape/handler.rb +8 -3
  15. data/lib/ape/html.rb +1 -1
  16. data/lib/ape/invoker.rb +1 -1
  17. data/lib/ape/invokers/deleter.rb +1 -1
  18. data/lib/ape/invokers/getter.rb +1 -1
  19. data/lib/ape/invokers/poster.rb +1 -1
  20. data/lib/ape/invokers/putter.rb +1 -1
  21. data/lib/ape/names.rb +1 -1
  22. data/lib/ape/print_writer.rb +4 -6
  23. data/lib/ape/reporter.rb +156 -0
  24. data/lib/ape/reporters/atom_reporter.rb +51 -0
  25. data/lib/ape/reporters/atom_template.eruby +38 -0
  26. data/lib/ape/reporters/html_reporter.rb +53 -0
  27. data/lib/ape/reporters/html_template.eruby +62 -0
  28. data/lib/ape/reporters/text_reporter.rb +37 -0
  29. data/lib/ape/samples.rb +30 -51
  30. data/lib/ape/server.rb +16 -4
  31. data/lib/ape/service.rb +1 -1
  32. data/lib/ape/util.rb +67 -0
  33. data/lib/ape/validator.rb +85 -57
  34. data/lib/ape/validator_dsl.rb +40 -0
  35. data/lib/ape/validators/entry_posts_validator.rb +226 -0
  36. data/lib/ape/validators/media_linkage_validator.rb +78 -0
  37. data/lib/ape/validators/media_posts_validator.rb +104 -0
  38. data/lib/ape/validators/sanitization_validator.rb +57 -0
  39. data/lib/ape/validators/schema_validator.rb +57 -0
  40. data/lib/ape/validators/service_document_validator.rb +64 -0
  41. data/lib/ape/validators/sorting_validator.rb +87 -0
  42. data/lib/ape/version.rb +1 -1
  43. data/{lib/ape/samples → samples}/atom_schema.txt +0 -0
  44. data/{lib/ape/samples → samples}/basic_entry.eruby +4 -4
  45. data/{lib/ape/samples → samples}/categories_schema.txt +0 -0
  46. data/{lib/ape/samples → samples}/mini_entry.eruby +0 -0
  47. data/{lib/ape/samples → samples}/service_schema.txt +0 -0
  48. data/{lib/ape/samples → samples}/unclean_xhtml_entry.eruby +0 -0
  49. data/test/test_helper.rb +33 -1
  50. data/test/unit/ape_test.rb +92 -0
  51. data/test/unit/authent_test.rb +2 -2
  52. data/test/unit/reporter_test.rb +102 -0
  53. data/test/unit/samples_test.rb +2 -2
  54. data/test/unit/validators_test.rb +50 -0
  55. data/{lib/ape/layout → web}/ape.css +5 -1
  56. data/{lib/ape/layout → web}/ape_logo.png +0 -0
  57. data/{lib/ape/layout → web}/index.html +2 -5
  58. data/{lib/ape/layout → web}/info.png +0 -0
  59. metadata +108 -56
  60. data/CHANGELOG +0 -1
  61. data/Manifest +0 -45
  62. data/ape.gemspec +0 -57
  63. data/scripts/go.rb +0 -29
@@ -0,0 +1,104 @@
1
+ module Ape
2
+ class MediaPostsValidator < Validator
3
+ disabled
4
+ requires_presence_of :media_collection
5
+
6
+ def validate(opts = {})
7
+ reporter.info(self, "TESTING: Posting to media collection.")
8
+ media_collection = opts[:media_collection]
9
+ reporter.info(self, "Will use collection '#{media_collection.title}' for media creation.")
10
+
11
+ # * Post a picture to the media collection
12
+ #
13
+ poster = Poster.new(media_collection.href, @authent)
14
+ if poster.last_error
15
+ reporter.error(self, "Unacceptable URI for '#{media_coll.title}' collection: " +
16
+ poster.last_error)
17
+ return
18
+ end
19
+
20
+ name = 'Post image to media collection'
21
+
22
+ # ask it to use this in the URI
23
+ slug_num = rand(100000)
24
+ slug = "apix-#{slug_num}"
25
+ slug_re = %r{apix.?#{slug_num}}
26
+ poster.set_header('Slug', slug)
27
+
28
+ #poster.set_header('Slug', slug)
29
+ worked = poster.post('image/jpeg', Samples.picture)
30
+ reporter.save_dialog(name, poster)
31
+ if !worked
32
+ reporter.error(self, "Can't POST picture to media collection: #{poster.last_error}",
33
+ name)
34
+ return
35
+ end
36
+
37
+ reporter.success(self, "Post of image file reported success, media link location: " +
38
+ "#{poster.header('Location')}", name)
39
+
40
+ # * Retrieve the media link entry
41
+ mle_uri = poster.header('Location')
42
+
43
+ media_link_entry = check_resource(mle_uri, 'Retrieval of media link entry', Names::AtomMediaType)
44
+ return unless media_link_entry
45
+
46
+ if media_link_entry.last_error
47
+ reporter.error(self, "Can't proceed with media-post testing.")
48
+ return
49
+ end
50
+
51
+ # * See if the <content src= is there and usable
52
+ begin
53
+ media_link_entry = Entry.new(media_link_entry.body, mle_uri)
54
+ rescue REXML::ParseException
55
+ prob = $!.to_s.gsub(/\n/, '<br/>')
56
+ reporter.error(self, "Media link entry is not well-formed: #{prob}")
57
+ return
58
+ end
59
+ content_src = media_link_entry.content_src
60
+ if (!content_src) || (content_src == "")
61
+ reporter.error(self, "Media link entry has no content@src pointer to media resource.")
62
+ return
63
+ end
64
+
65
+ # see if slug was used in media URI
66
+ if content_src =~ slug_re
67
+ reporter.success(self, "Client-provided slug '#{slug}' was used in Media Resource URI.")
68
+ else
69
+ reporter.warning(self, "Client-provided slug '#{slug}' not used in Media Resource URI.")
70
+ end
71
+
72
+ media_link_id = media_link_entry.child_content('id')
73
+
74
+ name = 'Retrieval of media resource'
75
+ picture = check_resource(content_src, name, 'image/jpeg')
76
+ return unless picture
77
+
78
+ if picture.body == Samples.picture
79
+ reporter.success(self, "Media resource was apparently stored and retrieved properly.")
80
+ else
81
+ reporter.warning(self, "Media resource differs from posted picture")
82
+ end
83
+
84
+ # * Delete the media link entry
85
+ return unless delete_entry(media_link_entry, 'Deletion of media link entry')
86
+
87
+ # * media link entry still in feed?
88
+ still_there = find_entry(media_collection.href, "media collection", media_link_id)
89
+ if still_there.class != String
90
+ reporter.error(self, "Media link entry is still in collection post-deletion.")
91
+ else
92
+ reporter.success(self, "Media link entry no longer in feed.")
93
+ end
94
+
95
+ # is the resource there any more?
96
+ name = 'Check Media Resource deletion'
97
+ if check_resource(content_src, name, 'image/jpeg', false)
98
+ reporter.error(self, "Media resource still there after media link entry deletion.")
99
+ else
100
+ reporter.success(self, "Media resource no longer fetchable.")
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,57 @@
1
+ module Ape
2
+ class SanitizationValidator < Validator
3
+ disabled
4
+ requires_presence_of :entry_collection
5
+
6
+ def validate(opts = {})
7
+ reporter.info(self, "TESTING: Content sanitization")
8
+ coll = opts[:entry_collection]
9
+
10
+ poster = Poster.new(coll.href, @authent)
11
+ name = 'Posting unclean XHTML'
12
+ worked = poster.post(Names::AtomEntryMediaType, Samples.unclean_xhtml_entry)
13
+ if !worked
14
+ reporter.save_dialog(name, poster)
15
+ reporter.error(self, "Can't POST unclean XHTML: #{poster.last_error}", name)
16
+ return
17
+ end
18
+
19
+ location = poster.header('Location')
20
+ name = "Retrieval of unclean XHTML entry"
21
+ entry = check_resource(location, name, Names::AtomMediaType)
22
+ return unless entry
23
+
24
+ begin
25
+ entry = Entry.new(entry.body, location)
26
+ rescue REXML::ParseException
27
+ prob = $!.to_s.gsub(/\n/, '<br/>')
28
+ reporter.error(self, "New entry is not well-formed: #{prob}")
29
+ return
30
+ end
31
+
32
+ no_problem = true
33
+ patterns = {
34
+ '//xhtml:script' => "Published entry retains xhtml:script element.",
35
+ '//*[@background]' => "Published entry retains 'background' attribute.",
36
+ '//*[@style]' => "Published entry retains 'style' attribute.",
37
+
38
+ }
39
+ patterns.each { |xp, message|
40
+ reporter.warning(self, message) unless entry.xpath_match(xp).empty?
41
+ }
42
+
43
+ entry.xpath_match('//xhtml:a').each do |a|
44
+ if a.attributes['href'] =~ /^([a-zA-Z]+):/
45
+ if $1 != 'http'
46
+ no_problem = false
47
+ reporter.warning(self, "Published entry retains dangerous hyperlink: '#{a.attributes['href']}'.")
48
+ end
49
+ end
50
+ end
51
+
52
+ delete_entry(entry)
53
+
54
+ reporter.success(self, "Published entry appears to be sanitized.") if no_problem
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,57 @@
1
+ if RUBY_PLATFORM =~ /java/
2
+ require 'java'
3
+ CompactSchemaReader = com.thaiopensource.validate.rng.CompactSchemaReader
4
+ ValidationDriver = com.thaiopensource.validate.ValidationDriver
5
+ StringReader = java.io.StringReader
6
+ StringWriter = java.io.StringWriter
7
+ InputSource = org.xml.sax.InputSource
8
+ ErrorHandlerImpl = com.thaiopensource.xml.sax.ErrorHandlerImpl
9
+ PropertyMapBuilder = com.thaiopensource.util.PropertyMapBuilder
10
+ ValidateProperty = com.thaiopensource.validate.ValidateProperty
11
+ end
12
+
13
+ module Ape
14
+ class SchemaValidator < Validator
15
+ disabled
16
+ def validate(opts = {})
17
+ if RUBY_PLATFORM =~ /java/
18
+ rcn_validate(opts[:schema], opts[:doc].body, opts[:title])
19
+ else
20
+ reporter.add(self, :info, "Schema validation is just available building the ape with jruby.")
21
+ true
22
+ end
23
+ end
24
+
25
+ def rnc_validate(schema, text, name, ape)
26
+ schemaError = StringWriter.new
27
+ schemaEH = ErrorHandlerImpl.new(schemaError)
28
+ properties = PropertyMapBuilder.new
29
+ properties.put(ValidateProperty::ERROR_HANDLER, schemaEH)
30
+ error = nil
31
+ driver = ValidationDriver.new(properties.toPropertyMap, CompactSchemaReader.getInstance)
32
+ if driver.loadSchema(InputSource.new(StringReader.new(schema)))
33
+ begin
34
+ if !driver.validate(InputSource.new(StringReader.new(text)))
35
+ error = schemaError.toString
36
+ end
37
+ rescue org.xml.sax.SAXParseException
38
+ error = $!.to_s.sub(/\n.*$/, '')
39
+ end
40
+ else
41
+ error = schemaError.toString
42
+ end
43
+
44
+ if !error
45
+ reporter.add(self, :success, "#{name} passed schema validation.")
46
+ true
47
+ else
48
+ # this kind of sucks, but I spent a looong time lost in a maze of twisty
49
+ # little passages without being able to figure out how to
50
+ # tell jing what name I'd like to call the InputSource
51
+ reporter.add(self, :error, "#{name} failed schema validation:\n" + error.gsub('(unknown file):', 'Line '))
52
+ false
53
+ end
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,64 @@
1
+ module Ape
2
+ class ServiceDocumentValidator < Validator
3
+ disabled
4
+ deterministic
5
+ attr_reader :service_document, :entry_collections, :media_collections
6
+
7
+ def validate(opts = {})
8
+ init_service_document(opts[:uri])
9
+ raise ValidationError, "service document not found in: #{opts[uri]}" unless @service
10
+ init_service_collections(opts[:uri])
11
+ raise ValidationError unless @entry_collections && @media_collections
12
+ end
13
+
14
+ def init_service_document(uri)
15
+ reporter.info(self, "TESTING: Service document and collections.")
16
+ name = 'Retrieval of Service Document'
17
+ service = check_resource(uri, name, Names::AppMediaType)
18
+ return unless service
19
+
20
+ # * XML-parse the service doc
21
+ text = service.body
22
+ begin
23
+ @service = REXML::Document.new(text, { :raw => nil })
24
+ rescue REXML::ParseException
25
+ prob = $!.to_s.gsub(/\n/, '<br/>')
26
+ reporter.error(self, "Service document not well-formed: #{prob}")
27
+ return
28
+ end
29
+
30
+ # RNC-validate the service doc
31
+ Validator.instance(:schema, @reporter).validate(:schema => Samples.service_RNC,
32
+ :title => 'Service doc', :doc => @service)
33
+ end
34
+
35
+ def init_service_collections(uri)
36
+ # * Do we have collections we can post an entry and a picture to?
37
+ # the requested_* arguments are the requested collection titles; if
38
+ # provided, try to match them, otherwise just pick the first listed
39
+ #
40
+ begin
41
+ @service_collections = Service.collections(@service, uri)
42
+ rescue Exception
43
+ reporter.error(self, "Couldn't read collections from service doc: #{$!}")
44
+ return
45
+ end
46
+
47
+ if @service_collections.length > 0
48
+ reporter.start_list(self, "Found these collections")
49
+ @service_collections.each do |collection|
50
+ reporter.list_item("'#{collection.title}' " +
51
+ "accepts #{collection.accept.join(', ')}")
52
+
53
+ if (collection.accept.index(Names::AtomEntryMediaType))
54
+ @entry_collections ||= []
55
+ @entry_collections << collection
56
+ else
57
+ @media_collections ||= []
58
+ @media_collections << collection
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,87 @@
1
+ module Ape
2
+ class SortingValidator < Validator
3
+ disabled
4
+ requires_presence_of :entry_collection
5
+
6
+ def validate(opts = {})
7
+ coll = opts[:entry_collection]
8
+ reporter.info(self, "TESTING: Collection re-ordering after PUT.")
9
+
10
+ # We'll post three mini entries to the collection
11
+ poster = Poster.new(coll.href, @authent)
12
+ ['One', 'Two', 'Three'].each do |num|
13
+ sleep 2
14
+ text = Samples.mini_entry.gsub('Mini-1', "Mini #{num}")
15
+ name = "Posting Mini #{num}"
16
+ worked = poster.post(Names::AtomEntryMediaType, text)
17
+ reporter.save_dialog(name, poster)
18
+ if !worked
19
+ reporter.error(self, "Can't POST Mini #{name}: #{poster.last_error}", name)
20
+ return
21
+ end
22
+ end
23
+
24
+ # now let's grab the collection & check the order
25
+ wanted = ['Mini One', 'Mini Two', 'Mini Three']
26
+ two = nil
27
+ entries = Feed.read(coll.href, 'Entries with multi-post', reporter)
28
+ entries.each do |from_feed|
29
+ want = wanted.pop
30
+ unless from_feed.child_content('title').index(want)
31
+ reporter.error(self, "Entries feed out of order after multi-post.")
32
+ return
33
+ end
34
+ two = from_feed if want == 'Mini Two'
35
+ break if wanted.empty?
36
+ end
37
+ reporter.success(self, "Entries correctly ordered after multi-post.")
38
+
39
+ # let's update one of them; have to fetch it first to get the ETag
40
+ link = two.link('edit', self)
41
+ unless link
42
+ reporter.error(self, "Can't check entry without edit link, entry id: #{two.get_child('id/text()')}")
43
+ return
44
+ end
45
+ two_resp = check_resource(link, 'fetch two', Names::AtomMediaType, false)
46
+
47
+ correctly_ordered = false
48
+ if two_resp
49
+ etag = two_resp.header 'etag'
50
+
51
+ putter = Putter.new(link, @authent)
52
+ putter.set_header('If-Match', etag)
53
+
54
+ name = 'Updating mini-entry with PUT'
55
+ sleep 2
56
+ updated = two_resp.body.gsub('Mini Two', 'Mini-4')
57
+ unless putter.put(Names::AtomEntryMediaType, updated)
58
+ reporter.save_dialog(name, putter)
59
+ reporter.error(self, "Can't update mini-entry at #{link}", name)
60
+ return
61
+ end
62
+ # now the order should have changed
63
+ wanted = ['Mini One', 'Mini Three', 'Mini-4']
64
+ correctly_ordered = true
65
+ else
66
+ reporter.error(self, "Mini Two entry not received. Can't assure the correct order after update.")
67
+ wanted = ['Mini One', 'Mini Two', 'Mini Three']
68
+ end
69
+
70
+ entries = Feed.read(coll.href, 'Entries post-update', reporter)
71
+ entries.each do |from_feed|
72
+ want = wanted.pop
73
+ unless from_feed.child_content('title').index(want)
74
+ reporter.error(self, "Entries feed out of order after update of multi-post.")
75
+ return
76
+ end
77
+
78
+ # next to godliness
79
+ delete_entry(from_feed)
80
+
81
+ break if wanted.empty?
82
+ end
83
+ reporter.success(self, "Entries correctly ordered after update of multi-post.") if correctly_ordered
84
+
85
+ end
86
+ end
87
+ end
@@ -1,7 +1,7 @@
1
1
  module Ape
2
2
  module VERSION
3
3
  MAJOR = 1
4
- MINOR = 0
4
+ MINOR = 5
5
5
  TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
@@ -1,16 +1,16 @@
1
1
  <?xml version="1.0" ?>
2
2
  <entry xmlns="http://www.w3.org/2005/Atom">
3
3
  <id><%= id %></id>
4
- <title><%= title %></title>
4
+ <title><%= @title %></title>
5
5
  <author><name>The Atom Protocol Exerciser</name></author>
6
6
  <updated><%= now %></updated>
7
7
  <link href='http://www.tbray.org/ape'/>
8
- <summary type='html'><%= summary %></summary>
8
+ <summary type='html'><%= @summary %></summary>
9
9
  <content type='xhtml'><div xmlns='http://www.w3.org/1999/xhtml'>
10
- <p>A test post from the &lt;APE&gt; at #{updated}</p>
10
+ <p>A test post from the &lt;APE&gt; at <%= now %></p>
11
11
  <p>If you see this in an entry, it's probably a left-over from an
12
12
  unsuccessful Ape run; feel free to delete it.</p>
13
13
  </div>
14
14
  </content>
15
- <dc:subject xmlns:dc='<%=subject%>'>Simians</dc:subject>
15
+ <dc:subject xmlns:dc='<%= @subject%>'>Simians</dc:subject>
16
16
  </entry>
@@ -1,6 +1,7 @@
1
1
  $:.unshift File.dirname(__FILE__) + '/../lib'
2
2
  require 'test/unit'
3
3
  require 'ape'
4
+ require 'mocha'
4
5
 
5
6
  def load_test_dir(dir)
6
7
  Dir[File.join(File.dirname(__FILE__), dir, "*.rb")].each do |file|
@@ -13,5 +14,36 @@ module Writer
13
14
  @response = response
14
15
  end
15
16
  end
16
-
17
17
  Ape::Invoker.send(:include, Writer)
18
+
19
+ module ApeAccessors
20
+ def service=(service)
21
+ @service = service
22
+ end
23
+
24
+ def entry_collections=(colls)
25
+ @entry_collections = colls
26
+ end
27
+
28
+ def media_collections=(colls)
29
+ @media_collections = colls
30
+ end
31
+
32
+ def service
33
+ @service
34
+ end
35
+
36
+ def entry_collections
37
+ @entry_collections
38
+ end
39
+
40
+ def media_collections
41
+ @media_collections
42
+ end
43
+ end
44
+ Ape::Ape.send(:include, ApeAccessors)
45
+
46
+ class ValidatorMock < Ape::Validator
47
+ deterministic
48
+ requires_presence_of :entry_collection
49
+ end