ape 1.0.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
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