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
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
-