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
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright © 2006 Sun Microsystems, Inc.
1
+ Copyright (c) 2006 Sun Microsystems, Inc.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README CHANGED
@@ -1,12 +1,26 @@
1
- APE stands for Atom Protocol Exerciser, and that’s what it does. It’s written in Ruby, and is
2
- designed to run on the Web, with a nice HTML interface describing its interactions with the
3
- APP server under test.
1
+ == Atom Protocol Exerciser (APE)
2
+
3
+ APE is a sanity-checker for implementations of the Atom Publishing Protocol (AtomPub or APP). It is written in Ruby,
4
+ and provides a Mongrel-based HTML interface describing its interactions with the APP implementation under test.
5
+
6
+ For more information about the history and impetus for the creation of APE, see Tim Bray's account here[http://www.tbray.org/ongoing/When/200x/2006/08/11/Meet-the-Ape].
4
7
 
5
- == Licence
8
+ == License
9
+
10
+ Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved. See the included LICENSE[link:/files/LICENSE.html] file for details.
11
+
12
+ == Quick Start
13
+
14
+ Install APE via RubyGems:
15
+
16
+ $ gem install ape
17
+
18
+ Now, you should have the ape_server command available in your $PATH. Start the server with:
6
19
 
7
- Copyright © 2006 Sun Microsystems, Inc. All rights reserved
8
- Use is subject to license terms - see file "LICENSE"
20
+ $ ape_server
21
+
22
+ This will start the server in the foreground. You can access APE in your browser at http://localhost:4000
9
23
 
10
- == Install
24
+ == The Source
11
25
 
12
- sudo gem install ape
26
+ To access the latest source code for APE, see the project site at https://rubyforge.org/projects/ape
@@ -0,0 +1,66 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/gempackagetask'
4
+ require 'rake/testtask'
5
+
6
+ $:.unshift File.dirname(__FILE__) + '/lib'
7
+ require 'ape'
8
+
9
+ def spec
10
+ spec ||= Gem::Specification.new do |s|
11
+ s.platform = Gem::Platform::RUBY
12
+ s.name = 'ape'
13
+ s.version = Ape::VERSION::STRING
14
+ s.author = 'Tim Bray'
15
+ s.email = 'tim.bray@sun.com'
16
+ s.homepage = 'ape.rubyforge.org'
17
+ s.summary = 'The Atom Protocol Exerciser'
18
+
19
+ s.files = FileList['lib/**/*', 'samples/*', 'test/**/*', 'web/*',
20
+ 'README', 'LICENSE', 'Rakefile'].to_ary
21
+ s.bindir = 'bin'
22
+ s.executable = 'ape_server'
23
+
24
+ s.has_rdoc = false
25
+ s.extra_rdoc_files = ['README', 'LICENSE']
26
+
27
+ s.rubyforge_project = 'ape'
28
+
29
+ s.add_dependency 'rake', '>= 0.8'
30
+ s.add_dependency 'mongrel', '>= 1.1.3'
31
+ s.add_dependency 'erubis', '>= 2.5.0'
32
+ s.add_dependency 'rubyforge', '>= 0.4'
33
+ s.add_dependency 'mocha', '>= 0.9.0'
34
+ end
35
+ end
36
+
37
+ def install_gem(*args)
38
+ cmd = []
39
+ cmd << "#{'sudo ' unless Gem.win_platform?}gem install"
40
+ sh cmd.push(*args.flatten).join(" ")
41
+ end
42
+
43
+ desc 'Install the necessary dependencies'
44
+ task :setup do
45
+ installed = Gem::SourceIndex.from_installed_gems
46
+ dependencies = spec.dependencies
47
+ dependencies.select { |dep|
48
+ installed.search(dep.name, dep.version_requirements).empty? }.each do |dep|
49
+ puts "Installing #{dep} ..."
50
+ install_gem dep.name, "-v '#{dep.version_requirements.to_s}'"
51
+ end
52
+ end
53
+
54
+ # The default task is run if rake is given no explicit arguments.
55
+ desc "Default Task"
56
+ task :default => :test
57
+
58
+ # Test Tasks ---------------------------------------------------------
59
+
60
+ desc "Run all tests"
61
+ task :test => [:test_units]
62
+
63
+ Rake::TestTask.new("test_units") do |t|
64
+ t.test_files = FileList['test/unit/*test.rb']
65
+ t.verbose = false
66
+ end
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
- # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
3
3
  # Use is subject to license terms - see file "LICENSE"
4
4
  $:.unshift File.dirname(__FILE__) + '/../lib'
5
5
 
@@ -16,11 +16,11 @@ OPTIONS = {
16
16
  }
17
17
 
18
18
  parser = OptionParser.new do |opts|
19
- opts.banner = '@@ FIXME'
19
+ opts.banner = 'The ape server options:'
20
20
  opts.separator ''
21
21
  opts.on('-a', '--address ADDRESS', 'Address to bind to', "default: #{OPTIONS[:host]}") { |v| OPTIONS[:host] = v }
22
22
  opts.on('-p', '--port PORT', 'Port to bind to', "default: #{OPTIONS[:port]}") { |v| OPTIONS[:port] = v }
23
- opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{Ape::Samples.home}") { |v| OPTIONS[:home] = v }
23
+ opts.on('-d', '--directory DIRECTORY', 'ape home directory', "default: #{Ape::Ape.home}") { |v| OPTIONS[:home] = v }
24
24
  opts.on('-h', '--help', 'Displays this help') { puts opts; exit }
25
25
  opts.parse!(ARGV)
26
26
  end
data/lib/ape.rb CHANGED
@@ -1,982 +1,176 @@
1
- # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
- # Use is subject to license terms - see file "LICENSE"
1
+ # Copyright (c) 2006 Sun Microsystems, Inc. All rights reserved
2
+ # See the included LICENSE[link:/files/LICENSE.html] file for details.
3
3
  $:.unshift File.dirname(__FILE__)
4
-
5
- require 'rubygems'
6
- require 'rexml/document'
7
- require 'builder'
8
-
9
- Dir[File.dirname(__FILE__) + '/ape/*.rb'].each { |l| require l }
10
-
11
4
  module Ape
5
+ require 'rubygems'
6
+
7
+ Dir[File.dirname(__FILE__) + '/ape/*.rb'].each { |l| require l }
8
+
9
+ @CONF = {}
10
+
11
+ def Ape.conf
12
+ @CONF
13
+ end
14
+
12
15
  class Ape
13
- def initialize(args)
14
- @dialogs = (args[:crumbs]) ? {} : []
15
- output = args[:output] || 'html'
16
- if output == 'text' || output == 'html'
17
- @output = output
18
- else
19
- raise ArgumentError, "output must be 'text' or 'html'"
20
- end
21
-
22
- @diarefs = {}
23
- @dianum = 1
24
- @@debugging = args[:debug]
25
- @steps = []
26
- @header = @footer = nil
27
- @lnum = 1
28
- @errors = @warnings = 0
16
+ attr_reader :aperc, :reporter
17
+
18
+ @@home = nil
19
+ def self.home=(home)
20
+ @@home = home
29
21
  end
30
22
 
31
- # Args: APP URI, username/password, preferred entry/media collections
32
- def check(uri, username=nil, password=nil,
33
- requested_e_coll = nil, requested_m_coll = nil)
34
-
35
- # Google athent weirdness
36
- @authent = Authent.new(username, password)
37
- header(uri)
38
- begin
39
- might_fail(uri, requested_e_coll, requested_m_coll)
40
- rescue Exception
41
- error "Ouch! Ape fall down go boom; details: " +
42
- "#{$!}\n#{$!.class}\n#{$!.backtrace}"
43
- puts $!.backtrace.join("\n")
44
- end
23
+ def self.home
24
+ @@home || ENV["APE_HOME"] || File.join(home_directory,".ape")
45
25
  end
46
26
 
47
- def might_fail(uri, requested_e_coll = nil, requested_m_coll = nil)
48
-
49
- info "TESTING: Service document and collections."
50
- name = 'Retrieval of Service Document'
51
- service = check_resource(uri, name, Names::AppMediaType)
52
- return unless service
53
-
54
- # * XML-parse the service doc
55
- text = service.body
56
- begin
57
- service = REXML::Document.new(text, { :raw => nil })
58
- rescue REXML::ParseException
59
- prob = $!.to_s.gsub(/\n/, '<br/>')
60
- error "Service document not well-formed: #{prob}"
61
- return
62
- end
63
-
64
- # RNC-validate the service doc
65
- Validator.validate(Samples.service_RNC, text, 'Service doc', self)
66
-
67
- # * Do we have collections we can post an entry and a picture to?
68
- # the requested_* arguments are the requested collection titles; if
69
- # provided, try to match them, otherwise just pick the first listed
70
- #
71
- begin
72
- collections = Service.collections(service, uri)
73
- rescue Exception
74
- error "Couldn't read collections from service doc: #{$!}"
75
- return
76
- end
77
- entry_coll = media_coll = nil
78
- if collections.length > 0
79
- start_list "Found these collections"
80
- collections.each do |collection|
81
- list_item "'#{collection.title}' " +
82
- "accepts #{collection.accept.join(', ')}"
83
- if (!entry_coll) && collection.accept.index(Names::AtomEntryMediaType)
84
- if requested_e_coll
85
- if requested_e_coll == collection.title
86
- entry_coll = collection
87
- end
88
- else
89
- entry_coll = collection
90
- end
91
- end
92
-
93
- if !media_coll
94
- image_jpeg_ok = false
95
- collection.accept.each do |types|
96
- types.split(/, */).each do |type|
97
-
98
- if type == '*/*' || type == 'image/*' || type == 'image/jpeg'
99
- image_jpeg_ok = true
100
- end
101
- end
102
- end
103
- if image_jpeg_ok
104
- if requested_m_coll
105
- if requested_m_coll == collection.title
106
- media_coll = collection
107
- end
108
- else
109
- media_coll = collection
110
- end
111
- end
112
- end
113
- end
114
- end
115
-
116
- end_list
117
-
118
- if entry_coll
119
- info "Will use collection '#{entry_coll.title}' for entry creation."
120
- test_entry_posts entry_coll
121
- test_sorting entry_coll
122
- test_sanitization entry_coll
123
- else
124
- warning "No collection for 'application/atom+xml;type=entry', won't test entry posting."
125
- end
126
-
127
- if media_coll
128
- info "Will use collection '#{media_coll.title}' for media creation."
129
- test_media_posts media_coll.href
130
- test_media_linkage media_coll
131
- else
132
- warning "No collection for 'image/jpeg', won't test media posting."
133
- end
134
- end
135
-
136
- def test_media_linkage(coll)
137
- info "TESTING: Media collection re-ordering after PUT."
138
-
139
- # We'll post three mini entries to the collection
140
- data = Samples.picture
141
- poster = Poster.new(coll.href, @authent)
142
- ['One', 'Two', 'Three'].each do |num|
143
- slug = "Picture #{num}"
144
- poster.set_header('Slug', slug)
145
- name = "Posting pic #{num}"
146
- worked = poster.post('image/jpeg', data)
147
- save_dialog(name, poster)
148
- if !worked
149
- error("Can't POST Picture #{num}: #{poster.last_error}", name)
150
- return
151
- end
152
- sleep 2
153
- end
154
-
155
- # grab the collection to gather the MLE ids
156
- entries = Feed.read(coll.href, 'Pictures from multi-post', self, true)
157
- if entries.size < 3
158
- error "Pictures apparently not in collection"
159
- return
160
- end
161
-
162
- ids = entries.map { |e| e.child_content('id)') }
163
-
164
- # let's update one of them; have to fetch it first to get the ETag
165
- two_media = entries[1].link('edit-media')
166
- if !two_media
167
- error "Second entry from feed doesn't have an 'edit-media' link."
168
- return
169
- end
170
- two_resp = check_resource(two_media, 'Fetch image to get ETag', 'image/jpeg', true)
171
- unless two_resp
172
- error "Can't fetch image to get ETag"
173
- return
174
- end
175
- etag = two_resp.header 'etag'
176
-
177
- putter = Putter.new(two_media, @authent)
178
- putter.set_header('If-Match', etag)
179
-
180
- name = 'Updating one of three pix with PUT'
181
- if putter.put('image/jpeg', data)
182
- good "Update one of newly posted pictures went OK."
183
- else
184
- save_dialog(name, putter)
185
- error("Can't update picture at #{two_media}", name)
186
- return
187
- end
188
-
189
- # now the order should have changed
190
- wanted = [ ids[2], ids[0], ids[1] ]
191
- entries = Feed.read(coll.href, 'MLEs post-update', self, true)
192
- entries.each do |from_feed|
193
- want = wanted.pop
194
- unless from_feed.child_content('id').eql?(want)
195
- error "Updating bits failed to re-order link entries in media collection."
196
- return
197
- end
198
-
199
- # next to godliness
200
- delete_entry(from_feed)
201
-
202
- break if wanted.empty?
203
- end
204
- good "Entries correctly ordered after update of multi-post."
205
-
27
+ #recipe from cap
28
+ def self.home_directory
29
+ ENV["HOME"] || (ENV["HOMEPATH"] && "#{ENV["HOMEDRIVE"]}#{ENV["HOMEPATH"]}") || "/"
206
30
  end
207
31
 
208
- def test_sanitization(coll)
209
- info "TESTING: Content sanitization"
210
-
211
- poster = Poster.new(coll.href, @authent)
212
- name = 'Posting unclean XHTML'
213
- worked = poster.post(Names::AtomEntryMediaType, Samples.unclean_xhtml_entry)
214
- if !worked
215
- save_dialog(name, poster)
216
- error("Can't POST unclean XHTML: #{poster.last_error}", name)
217
- return
218
- end
219
-
220
- location = poster.header('Location')
221
- name = "Retrieval of unclean XHTML entry"
222
- entry = check_resource(location, name, Names::AtomMediaType)
223
- return unless entry
224
-
225
- begin
226
- entry = Entry.new(entry.body, location)
227
- rescue REXML::ParseException
228
- prob = $!.to_s.gsub(/\n/, '<br/>')
229
- error "New entry is not well-formed: #{prob}"
230
- return
231
- end
232
-
233
- no_problem = true
234
- patterns = {
235
- '//xhtml:script' => "Published entry retains xhtml:script element.",
236
- '//*[@background]' => "Published entry retains 'background' attribute.",
237
- '//*[@style]' => "Published entry retains 'style' attribute.",
238
-
239
- }
240
- patterns.each { |xp, message| warning(message) unless entry.xpath_match(xp).empty? }
241
-
242
- entry.xpath_match('//xhtml:a').each do |a|
243
- if a.attributes['href'] =~ /^([a-zA-Z]+):/
244
- if $1 != 'http'
245
- no_problem = false
246
- warning "Published entry retains dangerous hyperlink: '#{a.attributes['href']}'."
247
- end
248
- end
249
- end
250
-
251
- delete_entry(entry)
252
-
253
- good "Published entry appears to be sanitized." if no_problem
32
+ # Creates an Ape instance with options given in the +args+ Hash.
33
+ #
34
+ # ==== Options
35
+ # * :output - one of 'text' or 'html'. #report will output in this format. Defaults to 'html'.
36
+ # * :debug - enables debug information at each step in the output
37
+ def initialize(args = {})
38
+ output = args[:output] || 'html'
39
+ @reporter = Reporter.instance(output, args)
40
+ load File.join(Ape.home, 'aperc') if File.exist?(File.join(Ape.home, 'aperc'))
254
41
  end
255
-
256
- def test_sorting(coll)
257
-
258
- info "TESTING: Collection re-ordering after PUT."
259
-
260
- # We'll post three mini entries to the collection
261
- poster = Poster.new(coll.href, @authent)
262
- ['One', 'Two', 'Three'].each do |num|
263
- sleep 2
264
- text = Samples.mini_entry.gsub('Mini-1', "Mini #{num}")
265
- name = "Posting Mini #{num}"
266
- worked = poster.post(Names::AtomEntryMediaType, text)
267
- save_dialog(name, poster)
268
- if !worked
269
- error("Can't POST Mini #{name}: #{poster.last_error}", name)
270
- return
271
- end
272
- end
273
42
 
274
- # now let's grab the collection & check the order
275
- wanted = ['Mini One', 'Mini Two', 'Mini Three']
276
- two = nil
277
- entries = Feed.read(coll.href, 'Entries with multi-post', self, true)
278
- entries.each do |from_feed|
279
- want = wanted.pop
280
- unless from_feed.child_content('title').index(want)
281
- error "Entries feed out of order after multi-post."
282
- return
283
- end
284
- two = from_feed if want == 'Mini Two'
285
- break if wanted.empty?
286
- end
287
- good "Entries correctly ordered after multi-post."
288
-
289
- # let's update one of them; have to fetch it first to get the ETag
290
- link = two.link('edit', self)
291
- unless link
292
- error "Can't check entry without edit link, entry id: #{two.get_child('id/text()')}"
293
- return
294
- end
295
- two_resp = check_resource(link, 'fetch two', Names::AtomMediaType, false)
296
-
297
- correctly_ordered = false
298
- if two_resp
299
- etag = two_resp.header 'etag'
300
-
301
- putter = Putter.new(link, @authent)
302
- putter.set_header('If-Match', etag)
303
-
304
- name = 'Updating mini-entry with PUT'
305
- sleep 2
306
- updated = two_resp.body.gsub('Mini Two', 'Mini-4')
307
- unless putter.put(Names::AtomEntryMediaType, updated)
308
- save_dialog(name, putter)
309
- error("Can't update mini-entry at #{link}", name)
310
- return
311
- end
312
- # now the order should have changed
313
- wanted = ['Mini One', 'Mini Three', 'Mini-4']
314
- correctly_ordered = true
315
- else
316
- error "Mini Two entry not received. Can't assure the correct order after update."
317
- wanted = ['Mini One', 'Mini Two', 'Mini Three']
318
- end
43
+ # Checks the AtomPub server at +uri+ for sanity.
44
+ #
45
+ # ==== Options
46
+ # * uri - the URI of the AtomPub server. Required.
47
+ # * username - an optional username for authentication
48
+ # * password - if a username is provided, a password is required. See Ape::Authent for more information.
49
+ # * requested_e_coll - a preferred entry collection to check
50
+ # * requested_m_coll - a preferred media collection to check
51
+ def check(uri, username=nil, password=nil,
52
+ requested_e_coll = nil, requested_m_coll = nil)
319
53
 
320
- entries = Feed.read(coll.href, 'Entries post-update', self, true)
321
- entries.each do |from_feed|
322
- want = wanted.pop
323
- unless from_feed.child_content('title').index(want)
324
- error "Entries feed out of order after update of multi-post."
325
- return
326
- end
327
-
328
- # next to godliness
329
- delete_entry(from_feed)
330
-
331
- break if wanted.empty?
54
+ @authent = Authent.new(username, password)
55
+ reporter.header = uri
56
+ ::Ape.conf[:REQUESTED_ENTRY_COLLECTION] = requested_e_coll if requested_e_coll
57
+ ::Ape.conf[:REQUESTED_MEDIA_COLLECTION] = requested_m_coll if requested_m_coll
58
+ begin
59
+ might_fail(uri)
60
+ rescue Exception
61
+ reporter.error(self, "Ouch! Ape fall down go boom; details: " +
62
+ "#{$!}\n#{$!.class}\n#{$!.backtrace}")
332
63
  end
333
- good "Entries correctly ordered after update of multi-post." if correctly_ordered
334
-
335
64
  end
336
65
 
337
- def test_entry_posts(entry_collection)
66
+ def might_fail(uri)
67
+ service_validator = Validator.instance(:service_document, @reporter, @authent)
68
+ service_validator.validate(:uri => uri)
69
+ @service = service_validator.service_document
70
+ @entry_collections = service_validator.entry_collections
71
+ @media_collections = service_validator.media_collections
338
72
 
339
- collection_uri = entry_collection.href
340
- entries = Feed.read(collection_uri, 'Entry collection', self)
341
-
342
- # * List the current entries, remember which IDs we've seen
343
- info "TESTING: Entry-posting basics."
344
- ids = []
345
- unless entries.empty?
346
- start_list "Now in the Entries feed"
347
- entries.each do |entry|
348
- list_item entry.summarize
349
- ids << entry.child_content('id')
350
- end
351
- end_list
352
- end
353
-
354
- # Setting up to post a new entry
355
- poster = Poster.new(collection_uri, @authent)
356
- if poster.last_error
357
- error("Unacceptable URI for '#{entry_collection.title}' collection: " +
358
- poster.last_error)
359
- return
360
- end
361
-
362
- my_entry = Entry.new(Samples.basic_entry)
363
-
364
- # ask it to use this in the URI
365
- slug_num = rand(100000)
366
- slug = "ape-#{slug_num}"
367
- slug_re = %r{ape.?#{slug_num}}
368
- poster.set_header('Slug', slug)
369
-
370
- # add some categories to the entry, and remember which
371
- @cats = Categories.add_cats(my_entry, entry_collection, @authent, self)
372
-
373
- # * OK, post it
374
- worked = poster.post(Names::AtomEntryMediaType, my_entry.to_s)
375
- name = 'Posting new entry'
376
- save_dialog(name, poster)
377
- if !worked
378
- error("Can't POST new entry: #{poster.last_error}", name)
379
- return
380
- end
381
-
382
- location = poster.header('Location')
383
- unless location
384
- error("No Location header upon POST creation", name)
385
- return
386
- end
387
- good("Posting of new entry to the Entries collection " +
388
- "reported success, Location: #{location}", name)
389
-
390
- info "Examining the new entry as returned in the POST response"
391
- check_new_entry(my_entry, poster.entry, "Returned entry") if poster.entry
392
-
393
- # * See if the Location uri can be retrieved, and check its consistency
394
- name = "Retrieval of newly created entry"
395
- new_entry = check_resource(location, name, Names::AtomMediaType)
396
- return unless new_entry
397
-
398
- # Grab its etag
399
- etag = new_entry.header 'etag'
400
-
401
- info "Examining the new entry as retrieved using Location header in POST response:"
402
-
403
- begin
404
- new_entry = Entry.new(new_entry.body, location)
405
- rescue REXML::ParseException
406
- prob = $!.to_s.gsub(/\n/, '<br/>')
407
- error "New entry is not well-formed: #{prob}"
408
- return
409
- end
410
-
411
- # * See if the slug was used
412
- slug_used = false
413
- new_entry.alt_links.each do |a|
414
- href = a.attributes['href']
415
- if href && href.index(slug_re)
416
- slug_used = true
73
+ if @entry_collections && true != ::Ape.conf[:ENTRY_VALIDATION_DISABLED]
74
+ [:entry_posts, :sorting, :sanitization].each do |option|
75
+ check_validator(option)
417
76
  end
418
- end
419
- if slug_used
420
- good "Client-provided slug '#{slug}' was used in server-generated URI."
421
77
  else
422
- warning "Client-provided slug '#{slug}' not used in server-generated URI."
423
- end
424
-
425
- check_new_entry(my_entry, new_entry, "Retrieved entry")
426
-
427
- entry_id = new_entry.child_content('id')
428
-
429
- # * fetch the feed again and check that version
430
- from_feed = find_entry(collection_uri, "entry collection", entry_id)
431
- if from_feed.class == String
432
- good "About to check #{collection_uri}"
433
- Feed.read(collection_uri, "Can't find entry in collection", self)
434
- error "New entry didn't show up in the collections feed."
435
- return
436
- end
437
-
438
- info "Examining the new entry as it appears in the collection feed:"
439
-
440
- # * Check the entry from the feed
441
- check_new_entry(my_entry, from_feed, "Entry from collection feed")
442
-
443
- edit_uri = new_entry.link('edit', self)
444
- if !edit_uri
445
- error "Entry from Location header has no edit link."
446
- return
78
+ reporter.warning(self, "No collection for 'application/atom+xml;type=entry', won't test entry posting.")
447
79
  end
448
80
 
449
- # * Update the entry, see if the update took
450
- name = 'In-place update with put'
451
- putter = Putter.new(edit_uri, @authent)
452
-
453
- # Conditional PUT if an etag
454
- putter.set_header('If-Match', etag) if etag
455
-
456
- new_title = "Let’s all do the Ape!"
457
- new_text = Samples.retitled_entry(new_title, entry_id)
458
- response = putter.put(Names::AtomEntryMediaType, new_text)
459
- save_dialog(name, putter)
460
-
461
- if response
462
- good("Update of new entry reported success.", name)
463
- from_feed = find_entry(collection_uri, "entry collection", entry_id)
464
- if from_feed.class == String
465
- check_resource(collection_uri, "Check collection after lost update", nil, true)
466
- error "Updated entry ID #{entry_id} not found in entries collection."
467
- return
468
- end
469
- if from_feed.child_content('title') == new_title
470
- good "Title of new entry successfully updated."
471
- else
472
- warning "After PUT update of title, Expected " +
473
- "'#{new_title}', but saw '#{from_feed.child_content('title')}'"
81
+ if @media_collections && true != ::Ape.conf[:MEDIA_VALIDATION_DISABLED]
82
+ [:media_posts, :media_linkage].each do |option|
83
+ check_validator(option)
474
84
  end
475
85
  else
476
- warning("Can't update new entry with PUT: #{putter.last_error}", name)
477
- end
478
-
479
- # the edit-uri might have changed
480
- return unless delete_entry(from_feed, 'New Entry deletion')
481
-
482
- # See if it's gone from the feed
483
- still_there = find_entry(collection_uri, "entry collection", entry_id)
484
- if still_there.class != String
485
- error "Entry is still in collection post-deletion."
486
- else
487
- good "Entry not found in feed after deletion."
488
- end
489
-
490
- end
491
-
492
- def test_media_posts media_collection
493
-
494
- info "TESTING: Posting to media collection."
495
-
496
- # * Post a picture to the media collection
497
- #
498
- poster = Poster.new(media_collection, @authent)
499
- if poster.last_error
500
- error("Unacceptable URI for '#{media_coll.title}' collection: " +
501
- poster.last_error)
502
- return
503
- end
504
-
505
- name = 'Post image to media collection'
506
-
507
- # ask it to use this in the URI
508
- slug_num = rand(100000)
509
- slug = "apix-#{slug_num}"
510
- slug_re = %r{apix.?#{slug_num}}
511
- poster.set_header('Slug', slug)
512
-
513
- #poster.set_header('Slug', slug)
514
- worked = poster.post('image/jpeg', Samples.picture)
515
- save_dialog(name, poster)
516
- if !worked
517
- error("Can't POST picture to media collection: #{poster.last_error}",
518
- name)
519
- return
86
+ reporter.warning(self, "No collection for 'image/jpeg', won't test media posting.")
520
87
  end
521
-
522
- good("Post of image file reported success, media link location: " +
523
- "#{poster.header('Location')}", name)
524
88
 
525
- # * Retrieve the media link entry
526
- mle_uri = poster.header('Location')
527
-
528
- media_link_entry = check_resource(mle_uri, 'Retrieval of media link entry', Names::AtomMediaType)
529
- return unless media_link_entry
530
-
531
- if media_link_entry.last_error
532
- error "Can't proceed with media-post testing."
533
- return
534
- end
535
-
536
- # * See if the <content src= is there and usable
537
- begin
538
- media_link_entry = Entry.new(media_link_entry.body, mle_uri)
539
- rescue REXML::ParseException
540
- prob = $!.to_s.gsub(/\n/, '<br/>')
541
- error "Media link entry is not well-formed: #{prob}"
542
- return
543
- end
544
- content_src = media_link_entry.content_src
545
- if (!content_src) || (content_src == "")
546
- error "Media link entry has no content@src pointer to media resource."
547
- return
89
+ #custom validators
90
+ Validator.custom_validators(@reporter, @authent).each do |validator|
91
+ opts = check_manifest(validator)
92
+ break if !validator.validate(opts) && validator.deterministic?
548
93
  end
94
+ end
95
+
96
+ def report(output=STDOUT)
97
+ @reporter.report(output)
98
+ end
549
99
 
550
- # see if slug was used in media URI
551
- if content_src =~ slug_re
552
- good "Client-provided slug '#{slug}' was used in Media Resource URI."
553
- else
554
- warning "Client-provided slug '#{slug}' not used in Media Resource URI."
555
- end
100
+ def check_manifest(validator)
101
+ variables = {}
102
+ manifest = validator.manifest
103
+ variables[:service_doc] = @service if (manifest.include?(:service_doc))
104
+ manifest.delete(:service_doc)
556
105
 
557
- media_link_id = media_link_entry.child_content('id')
558
-
559
- name = 'Retrieval of media resource'
560
- picture = check_resource(content_src, name, 'image/jpeg')
561
- return unless picture
562
-
563
- if picture.body == Samples.picture
564
- good "Media resource was apparently stored and retrieved properly."
565
- else
566
- warning "Media resource differs from posted picture"
567
- end
568
-
569
- # * Delete the media link entry
570
- return unless delete_entry(media_link_entry, 'Deletion of media link entry')
571
-
572
- # * media link entry still in feed?
573
- still_there = find_entry(media_collection, "media collection", media_link_id)
574
- if still_there.class != String
575
- error "Media link entry is still in collection post-deletion."
576
- else
577
- good "Media link entry no longer in feed."
106
+ if (manifest.include?(:entry_collection))
107
+ variables[:entry_collection] = ::Ape.conf[:REQUESTED_ENTRY_COLLECTION].nil? ? @entry_collections.first :
108
+ get_collection(@entry_collections, ::Ape.conf[:REQUESTED_ENTRY_COLLECTION])
109
+ manifest.delete(:entry_collection)
578
110
  end
579
111
 
580
- # is the resource there any more?
581
- name = 'Check Media Resource deletion'
582
- if check_resource(content_src, name, 'image/jpeg', false)
583
- error "Media resource still there after media link entry deletion."
584
- else
585
- good "Media resource no longer fetchable."
112
+ if (manifest.include?(:media_collection))
113
+ variables[:media_collection] = ::Ape.conf[:REQUESTED_MEDIA_COLLECTION].nil? ? @media_collections.first :
114
+ get_collection(@media_collections, ::Ape.conf[:REQUESTED_MEDIA_COLLECTION])
115
+ manifest.delete(:media_collection)
586
116
  end
587
117
 
588
- end
589
-
590
- def check_new_entry(as_posted, new_entry, desc)
591
-
592
- if compare_entries(as_posted, new_entry, "entry as posted", desc)
593
- good "#{desc} is consistent with posted entry."
594
- end
595
-
596
- # * See if the categories we sent made it in
597
- cat_probs = false
598
- @cats.each do |cat|
599
- if !new_entry.has_cat(cat)
600
- cat_probs = true
601
- warning "Provided category not in #{desc}: #{cat}"
602
- end
603
- end
604
- good "Provided categories included in #{desc}." unless cat_probs
605
-
606
- # * See if the dc:subject survived
607
- dc_subject = new_entry.child_content(Samples.foreign_child, Samples.foreign_namespace)
608
- if dc_subject
609
- if dc_subject == Samples.foreign_child_content
610
- good "Server preserved foreign markup in #{desc}."
611
- else
612
- warning "Server altered content of foreign markup in #{desc}."
613
- end
614
- else
615
- warning "Server discarded foreign markup in #{desc}."
616
- end
617
- end
618
-
619
- #
620
- # End of tests; support functions from here down
621
- #
622
-
623
- # Fetch a feed and look up an entry by ID in it
624
- def find_entry(feed_uri, name, id, report=false)
625
- entries = Feed.read(feed_uri, name, self, report)
626
- entries.each do |from_feed|
627
- return from_feed if id == from_feed.child_content('id')
628
- end
629
-
630
- return "Couldn't find id #{id} in feed #{feed_uri}"
631
- end
632
-
633
- # remember the dialogue that the get/put/post/delete actor recorded
634
- def save_dialog(name, actor)
635
- @dialogs[name] = actor.crumbs if @dialogs
636
- end
637
-
638
- # Get a resource, optionally check its content-type
639
- def check_resource(uri, name, content_type, report=true)
640
- resource = Getter.new(uri, @authent)
641
-
642
- # * Check the URI
643
- if resource.last_error
644
- error("Unacceptable #{name} URI: " + resource.last_error, name) if report
645
- return nil
646
- end
647
-
648
- # * Get it, make sure it has the right content-type
649
- worked = resource.get(content_type)
650
- @dialogs[name] = resource.crumbs if @dialogs
651
-
652
- if (resource.security_warning and not @security_warning)
653
- @security_warning = true
654
- warning("Sending authentication information over a open channel is not a good security practice.", name)
655
- end
656
-
657
- if !worked
658
- # oops, couldn't even get get it
659
- error("#{name} failed: " + resource.last_error, name) if report
660
- return nil
661
-
662
- elsif resource.last_error
663
- # oops, media-type problem
664
- error("#{name}: #{resource.last_error}", name) if report
665
-
666
- else
667
- # resource fetched and is of right type
668
- good("#{name}: it exists and is served properly.", name) if report
669
- end
670
-
671
- return resource
672
- end
673
-
674
- def header(uri)
675
- @header = "APP Service doc: #{uri}"
676
- end
677
-
678
- def footer(message)
679
- @footer = message
680
- end
681
-
682
- def show_crumbs key
683
- @dialogs[key].each do |d|
684
- puts "D: #{d}"
685
- end
686
- end
687
-
688
- def warning(message, crumb_key=nil)
689
- @warnings += 1
690
- if @dialogs
691
- step "D#{crumb_key}" if crumb_key
692
- show_crumbs(crumb_key) if crumb_key && @@debugging
693
- end
694
- step "W" + message
695
- end
696
-
697
- def error(message, crumb_key=nil)
698
- @errors += 1
699
- if @dialogs
700
- step "D#{crumb_key}" if crumb_key
701
- show_crumbs(crumb_key) if crumb_key && @@debugging
702
- end
703
- step "E" + message
704
- end
705
-
706
- def good(message, crumb_key=nil)
707
- if @dialogs
708
- step "D#{crumb_key}" if crumb_key
709
- show_crumbs(crumb_key) if crumb_key && @@debugging
710
- end
711
- step "G" + message
712
- end
713
-
714
- def info(message)
715
- step "I" + message
716
- end
717
-
718
- def step(message)
719
- puts "PROGRESS: #{message[1..-1]}" if @@debugging
720
- @steps << message
721
- end
722
-
723
- def start_list(message)
724
- step [ message + ":" ]
725
- end
726
-
727
- def list_item(message)
728
- @steps[-1] << message
729
- end
730
-
731
- def end_list
732
- end
733
-
734
- def line
735
- printf "%2d. ", @lnum
736
- @lnum += 1
737
- end
738
-
739
- def report(output=STDOUT)
740
- if @output == 'text'
741
- report_text output
742
- else
743
- report_html output
744
- end
745
- end
746
-
747
- def report_html(output=STDOUT)
748
- dialog = nil
749
-
750
- if output == STDOUT
751
- output.puts "Status: 200 OK\r"
752
- output.puts "Content-type: text/html; charset=utf-8\r"
753
- output.puts "\r"
754
- end
755
-
756
- @w = Builder::XmlMarkup.new(:target => output)
757
- @w.html do
758
- @w.head do
759
- @w.title { @w.text! 'Atom Protocol Exerciser Report' }
760
- @w.text! "\n"
761
- @w.link(:rel => 'stylesheet', :type => 'text/css',:href => '../ape/ape.css' )
762
- end
763
- @w.text! "\n"
764
- @w.body do
765
- @w.h2 { @w.text! 'The Ape says:' }
766
- @w.text! "\n"
767
- if @header
768
- @w.p { @w.text! @header }
769
- @w.p do
770
- @w.text! "Summary: "
771
- @w.text!((@errors == 1) ? '1 error, ' : "#{@errors} errors, ")
772
- @w.text!((@warnings == 1) ? '1 warning.' : "#{@warnings} warnings.")
773
- end
774
- @w.text! "\n"
775
- end
776
- @w.ol do
777
- @w.text! "\n"
778
- @steps.each do |step|
779
- if step.kind_of? Array
780
- # it's a list; no dialog applies
781
- @w.li do
782
- @w.p do
783
- write_mark :info
784
- @w.text! " #{step[0]}\n"
785
- end
786
- @w.ul do
787
- step[1 .. -1].each { |li| report_li(nil, nil, li) }
788
- end
789
- @w.text! "\n"
790
- end
791
- else
792
- body = step[1 .. -1]
793
- opcode = step[0,1]
794
- if opcode == "D"
795
- dialog = body
796
- else
797
- case opcode
798
- when "W" then report_li(dialog, :question, body)
799
- when "E" then report_li(dialog, :exclamation, body)
800
- when "G" then report_li(dialog, :check, body)
801
- when "I" then report_li(dialog, :info, body)
802
- else
803
- line
804
- puts "HUH? #{step}"
118
+ manifest.each do |option|
119
+ if (option.instance_of?(Hash))
120
+ all_collections = @entry_collections + @media_collections
121
+ option.each do |key, value|
122
+ unless (value.instance_of?(Hash))
123
+ #request a collection by its title, i.e: :entry_collection => 'Posts'
124
+ variables[key] = get_collection(all_collections, value)
125
+ else
126
+ #request the first collection that matches the options,
127
+ # i.e: :entry_collection => {:accept => 'image/png'}
128
+ # :entry_collection => {:title => 'Atachments', :accept => 'video/*'}
129
+ hash = value
130
+ variables[key] = all_collections.select do |collection|
131
+ matches = nil
132
+ hash.each do |k, v|
133
+ begin
134
+ matches = eval("collection.#{k.to_s}", binding, __FILE__, __LINE__).index(v)
135
+ rescue
136
+ raise ValidationError, "collection attribute not found: #{k.to_s}"
805
137
  end
806
- dialog = nil
807
138
  end
808
- end
139
+ collection if matches
140
+ end.first
809
141
  end
810
142
  end
811
-
812
- @w.text! "\n"
813
- if @footer then @w.p { @w.text! @footer } end
814
- @w.text! "\n"
815
-
816
- #unless @dialog.nil?
817
- if @dialogs
818
- @w.h2 { @w.text! 'Recorded client/server dialogs' }
819
- @w.text! "\n"
820
- @diarefs.each do |k, v|
821
- dialog = @dialogs[k]
822
- @w.h3(:id => "dia-#{v}") do
823
- @w.text! k
824
- end
825
- @w.div(:class => 'dialog') do
826
-
827
- @w.div(:class => 'dialab') do
828
- @w.text! "\nTo server:\n"
829
- dialog.grep(/^>/).each { |crumb| show_message(crumb, :to) }
830
- end
831
- @w.div( :class => 'dialab' ) do
832
- @w.text! "\nFrom Server:\n"
833
- dialog.grep(/^</).each { |crumb| show_message(crumb, :from) }
834
- end
835
- end
836
- end
837
- end
838
- end
839
- end
840
- end
841
-
842
- def report_li(dialog, marker, text)
843
- @w.li do
844
- @w.p do
845
- if marker
846
- write_mark marker
847
- @w.text! ' '
848
- end
849
- # preserve line-breaks in output
850
- lines = text.split("\n")
851
- lines[0 .. -2].each do |line|
852
- @w.text! line
853
- @w.br
854
- end
855
- @w.text! lines[-1] if lines[-1]
856
-
857
- if dialog
858
- @w.a(:class => 'diaref', :href => "#dia-#{@dianum}") do
859
- @w.text! ' [Dialog]'
860
- end
861
- @diarefs[dialog] = @dianum
862
- @dianum += 1
863
- end
864
- end
865
- end
866
- @w.text! "\n"
867
- end
868
-
869
- def show_message(crumb, tf)
870
- message = crumb[1 .. -1]
871
- message.gsub!(/^\s*"/, '')
872
- message.gsub!(/"\s*$/, '')
873
- message.gsub!(/\\"/, '"')
874
- message = Escaper.escape message
875
- message.gsub!(/\\n/, "\n<br/>")
876
- message.gsub!(/\\t/, '&#xa0;&#xa0;&#xa0;&#xa0;')
877
- @w.div(:class => tf) { @w.target! << message }
878
- end
879
-
880
- def report_text(output=STDOUT)
881
- output.puts @header if @header
882
- @steps.each do |step|
883
- if step.class == Crumbs
884
- output.puts " Dialog:"
885
- step.each { |crumb| output.puts " #{crumb}" }
886
- else
887
- body = step[1 .. -1]
888
- case step[0,1]
889
- when "W"
890
- line
891
- output.puts "WARNING: #{body}"
892
- when "E"
893
- line
894
- output.puts "ERROR: #{body}"
895
- when "G"
896
- line
897
- output.puts body
898
- when "L"
899
- line
900
- output.puts body
901
- when "e"
902
- # no-op
903
- when "I"
904
- output.puts " #{body}"
905
- when "D"
906
- # later, dude
907
- else
908
- line
909
- output.puts "HUH? #{body}"
910
- end
911
143
  end
912
- output.puts @footer if @footer
913
144
  end
914
- end
915
-
916
- def compare_entries(e1, e2, e1Name, e2Name)
917
- problems = 0
918
- [ 'title', 'summary', 'content' ].each do |field|
919
- problems += 1 if compare1(e1, e2, e1Name, e2Name, field)
920
- end
921
- return problems == 0
922
- end
923
-
924
- def compare1(e1, e2, e1Name, e2Name, field)
925
- c1 = e1.child_content(field)
926
- c2 = e2.child_content(field)
927
- if c1 != c2
928
- problem = true
929
- if c1 == nil
930
- warning "'#{field}' absent in #{e1Name}."
931
- elsif c2 == nil
932
- warning "'#{field}' absent in #{e2Name}."
933
- else
934
- t1 = e1.child_type(field)
935
- t2 = e2.child_type(field)
936
- if t1 != t2
937
- warning "'#{field}' has type='#{t1}' " +
938
- "in #{e1Name}, type='#{t2}' in #{e2Name}."
939
- else
940
- c1 = Escaper.escape(c1)
941
- c2 = Escaper.escape(c2)
942
- warning "'#{field}' in #{e1Name} [#{c1}] " +
943
- "differs from that in #{e2Name} [#{c2}]."
944
- end
945
- end
145
+
146
+ #ensure all variables are setted
147
+ raise ValidationError, "#{manifest.join("\n")} haven't been setted" if variables.empty?
148
+ variables.each do |k, v|
149
+ raise ValidationError, "#{k} haven't been setted" unless v
946
150
  end
947
- return problem
151
+
152
+ variables
948
153
  end
154
+
155
+ private
949
156
 
950
- def write_mark(mark)
951
- case mark
952
- when :check
953
- @w.span(:class => 'good') { @w.target << '&#x2713;' }
954
- when :question
955
- @w.span(:class => 'warning') { @w.text! '?' }
956
- when :exclamation
957
- @w.span(:class => 'error') { @w.text! '!' }
958
- when :info
959
- @w.img(:align => 'top', :src => '../ape/info.png')
960
- end
157
+ def check_validator(option)
158
+ validator = Validator.instance(option, @reporter, @authent)
159
+ opts = check_manifest(validator)
160
+ validator.validate(opts)
961
161
  end
962
162
 
963
- def delete_entry(entry, name = nil)
964
- link = entry.link('edit', self)
965
- unless link
966
- error "Can't delete entry without edit link"
967
- return false
968
- end
969
- deleter = Deleter.new(link, @authent)
970
- worked = deleter.delete
971
-
972
- save_dialog(name, deleter) if name
973
- if worked
974
- good("Entry deletion reported success.", name)
975
- else
976
- error("Couldn't delete the entry: " + deleter.last_error, name)
977
- end
978
- return worked
163
+ def get_collection(collections, requested)
164
+ if (requested.instance_of?(Integer))
165
+ return collections[requested]
166
+ elsif (requested.to_sym == :first)
167
+ return collections.first
168
+ elsif (requested.to_sym == :last)
169
+ return collections.last
170
+ end
171
+ collections.select do |coll|
172
+ coll if (coll.title == requested)
173
+ end.first
979
174
  end
980
175
  end
981
176
  end
982
-