ape 1.0.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 (47) hide show
  1. data/CHANGELOG +1 -0
  2. data/LICENSE +19 -0
  3. data/Manifest +45 -0
  4. data/README +12 -0
  5. data/ape.gemspec +57 -0
  6. data/bin/ape_server +28 -0
  7. data/lib/ape.rb +982 -0
  8. data/lib/ape/atomURI.rb +73 -0
  9. data/lib/ape/auth/google_login_credentials.rb +96 -0
  10. data/lib/ape/auth/wsse_credentials.rb +25 -0
  11. data/lib/ape/authent.rb +42 -0
  12. data/lib/ape/categories.rb +95 -0
  13. data/lib/ape/collection.rb +51 -0
  14. data/lib/ape/crumbs.rb +39 -0
  15. data/lib/ape/entry.rb +151 -0
  16. data/lib/ape/escaper.rb +29 -0
  17. data/lib/ape/feed.rb +117 -0
  18. data/lib/ape/handler.rb +34 -0
  19. data/lib/ape/html.rb +17 -0
  20. data/lib/ape/invoker.rb +54 -0
  21. data/lib/ape/invokers/deleter.rb +31 -0
  22. data/lib/ape/invokers/getter.rb +80 -0
  23. data/lib/ape/invokers/poster.rb +57 -0
  24. data/lib/ape/invokers/putter.rb +46 -0
  25. data/lib/ape/layout/ape.css +56 -0
  26. data/lib/ape/layout/ape_logo.png +0 -0
  27. data/lib/ape/layout/index.html +54 -0
  28. data/lib/ape/layout/info.png +0 -0
  29. data/lib/ape/names.rb +24 -0
  30. data/lib/ape/print_writer.rb +21 -0
  31. data/lib/ape/samples.rb +180 -0
  32. data/lib/ape/samples/atom_schema.txt +338 -0
  33. data/lib/ape/samples/basic_entry.eruby +16 -0
  34. data/lib/ape/samples/categories_schema.txt +69 -0
  35. data/lib/ape/samples/mini_entry.eruby +8 -0
  36. data/lib/ape/samples/service_schema.txt +187 -0
  37. data/lib/ape/samples/unclean_xhtml_entry.eruby +21 -0
  38. data/lib/ape/server.rb +32 -0
  39. data/lib/ape/service.rb +12 -0
  40. data/lib/ape/validator.rb +65 -0
  41. data/lib/ape/version.rb +9 -0
  42. data/scripts/go.rb +29 -0
  43. data/test/test_helper.rb +17 -0
  44. data/test/unit/authent_test.rb +35 -0
  45. data/test/unit/invoker_test.rb +25 -0
  46. data/test/unit/samples_test.rb +36 -0
  47. metadata +111 -0
@@ -0,0 +1 @@
1
+ v1. first gem version
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright © 2006 Sun Microsystems, Inc.
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR
17
+ OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
18
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
19
+ OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,45 @@
1
+ bin/ape_server
2
+ CHANGELOG
3
+ lib/ape/atomURI.rb
4
+ lib/ape/auth/google_login_credentials.rb
5
+ lib/ape/auth/wsse_credentials.rb
6
+ lib/ape/authent.rb
7
+ lib/ape/categories.rb
8
+ lib/ape/collection.rb
9
+ lib/ape/crumbs.rb
10
+ lib/ape/entry.rb
11
+ lib/ape/escaper.rb
12
+ lib/ape/feed.rb
13
+ lib/ape/handler.rb
14
+ lib/ape/html.rb
15
+ lib/ape/invoker.rb
16
+ lib/ape/invokers/deleter.rb
17
+ lib/ape/invokers/getter.rb
18
+ lib/ape/invokers/poster.rb
19
+ lib/ape/invokers/putter.rb
20
+ lib/ape/layout/ape.css
21
+ lib/ape/layout/ape_logo.png
22
+ lib/ape/layout/index.html
23
+ lib/ape/layout/info.png
24
+ lib/ape/names.rb
25
+ lib/ape/print_writer.rb
26
+ lib/ape/samples/atom_schema.txt
27
+ lib/ape/samples/basic_entry.eruby
28
+ lib/ape/samples/categories_schema.txt
29
+ lib/ape/samples/mini_entry.eruby
30
+ lib/ape/samples/service_schema.txt
31
+ lib/ape/samples/unclean_xhtml_entry.eruby
32
+ lib/ape/samples.rb
33
+ lib/ape/server.rb
34
+ lib/ape/service.rb
35
+ lib/ape/validator.rb
36
+ lib/ape/version.rb
37
+ lib/ape.rb
38
+ LICENSE
39
+ README
40
+ scripts/go.rb
41
+ test/test_helper.rb
42
+ test/unit/authent_test.rb
43
+ test/unit/invoker_test.rb
44
+ test/unit/samples_test.rb
45
+ Manifest
data/README ADDED
@@ -0,0 +1,12 @@
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.
4
+
5
+ == Licence
6
+
7
+ Copyright © 2006 Sun Microsystems, Inc. All rights reserved
8
+ Use is subject to license terms - see file "LICENSE"
9
+
10
+ == Install
11
+
12
+ sudo gem install ape
@@ -0,0 +1,57 @@
1
+
2
+ # Gem::Specification for Ape-1.0.0
3
+ # Originally generated by Echoe
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = %q{ape}
7
+ s.version = "1.0.0"
8
+
9
+ s.specification_version = 2 if s.respond_to? :specification_version=
10
+
11
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
12
+ s.authors = ["Tim Bray"]
13
+ s.date = %q{2008-02-22}
14
+ s.default_executable = %q{ape_server}
15
+ s.description = %q{A tool to exercice AtomPub server.}
16
+ s.email = %q{tim.bray@sun.com}
17
+ s.executables = ["ape_server"]
18
+ s.files = ["bin/ape_server", "CHANGELOG", "lib/ape/atomURI.rb", "lib/ape/auth/google_login_credentials.rb", "lib/ape/auth/wsse_credentials.rb", "lib/ape/authent.rb", "lib/ape/categories.rb", "lib/ape/collection.rb", "lib/ape/crumbs.rb", "lib/ape/entry.rb", "lib/ape/escaper.rb", "lib/ape/feed.rb", "lib/ape/handler.rb", "lib/ape/html.rb", "lib/ape/invoker.rb", "lib/ape/invokers/deleter.rb", "lib/ape/invokers/getter.rb", "lib/ape/invokers/poster.rb", "lib/ape/invokers/putter.rb", "lib/ape/layout/ape.css", "lib/ape/layout/ape_logo.png", "lib/ape/layout/index.html", "lib/ape/layout/info.png", "lib/ape/names.rb", "lib/ape/print_writer.rb", "lib/ape/samples/atom_schema.txt", "lib/ape/samples/basic_entry.eruby", "lib/ape/samples/categories_schema.txt", "lib/ape/samples/mini_entry.eruby", "lib/ape/samples/service_schema.txt", "lib/ape/samples/unclean_xhtml_entry.eruby", "lib/ape/samples.rb", "lib/ape/server.rb", "lib/ape/service.rb", "lib/ape/validator.rb", "lib/ape/version.rb", "lib/ape.rb", "LICENSE", "README", "scripts/go.rb", "test/test_helper.rb", "test/unit/authent_test.rb", "test/unit/invoker_test.rb", "test/unit/samples_test.rb", "Manifest", "ape.gemspec"]
19
+ s.has_rdoc = true
20
+ s.homepage = %q{http://www.tbray.org/ongoing/misc/Software#p-4}
21
+ s.require_paths = ["lib"]
22
+ s.rubyforge_project = %q{ape}
23
+ s.rubygems_version = %q{1.0.0}
24
+ s.summary = %q{A tool to exercice AtomPub server.}
25
+ s.test_files = ["test/unit/authent_test.rb", "test/unit/invoker_test.rb", "test/unit/samples_test.rb"]
26
+
27
+ s.add_dependency(%q<builder>, [">= 0", "= 2.1.2"])
28
+ end
29
+
30
+
31
+ # # Original Rakefile source (requires the Echoe gem):
32
+ #
33
+ # require File.dirname(__FILE__) + '/lib/ape/version'
34
+ #
35
+ # begin
36
+ # require 'rubygems'
37
+ # require 'echoe'
38
+ # Echoe.new('ape', Ape::VERSION::STRING) do |p|
39
+ # p.rubyforge_name = 'ape'
40
+ # p.summary = 'A tool to exercice AtomPub server.'
41
+ # p.url = 'http://www.tbray.org/ongoing/misc/Software#p-4'
42
+ # p.author = 'Tim Bray'
43
+ # p.email = 'tim.bray@sun.com'
44
+ # p.dependencies << 'builder >= 2.1.2'
45
+ # p.extra_deps = ['mongrel >= 1.1.3', 'erubis >= 2.5.0']
46
+ # p.test_pattern = 'test/unit/*.rb'
47
+ # end
48
+ # rescue LoadError => boom
49
+ # puts 'You are missing a dependency required for meta-operations on this gem.'
50
+ # puts boom.to_s.capitalize
51
+ # end
52
+ #
53
+ # desc 'Install the package as a gem, without generating documentation(ri/rdoc)'
54
+ # task :install_gem_no_doc => [:clean, :package] do
55
+ # sh "#{'sudo ' unless Hoe::WINDOZE }gem install pkg/*.gem --no-rdoc --no-ri"
56
+ # end
57
+ #
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env ruby
2
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
3
+ # Use is subject to license terms - see file "LICENSE"
4
+ $:.unshift File.dirname(__FILE__) + '/../lib'
5
+
6
+ require 'rubygems'
7
+ require 'mongrel'
8
+ require 'optparse'
9
+ require 'ape/server'
10
+ require 'ape/samples'
11
+
12
+ OPTIONS = {
13
+ :host => '0.0.0.0',
14
+ :port => '4000',
15
+ :home => nil
16
+ }
17
+
18
+ parser = OptionParser.new do |opts|
19
+ opts.banner = '@@ FIXME'
20
+ opts.separator ''
21
+ opts.on('-a', '--address ADDRESS', 'Address to bind to', "default: #{OPTIONS[:host]}") { |v| OPTIONS[:host] = v }
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 }
24
+ opts.on('-h', '--help', 'Displays this help') { puts opts; exit }
25
+ opts.parse!(ARGV)
26
+ end
27
+
28
+ Ape::Server.run(OPTIONS)
@@ -0,0 +1,982 @@
1
+ # Copyright © 2006 Sun Microsystems, Inc. All rights reserved
2
+ # Use is subject to license terms - see file "LICENSE"
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
+ module Ape
12
+ 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
29
+ end
30
+
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
45
+ end
46
+
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
+
206
+ end
207
+
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
254
+ 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
+
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
319
+
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?
332
+ end
333
+ good "Entries correctly ordered after update of multi-post." if correctly_ordered
334
+
335
+ end
336
+
337
+ def test_entry_posts(entry_collection)
338
+
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
417
+ end
418
+ end
419
+ if slug_used
420
+ good "Client-provided slug '#{slug}' was used in server-generated URI."
421
+ 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
447
+ end
448
+
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')}'"
474
+ end
475
+ 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
520
+ end
521
+
522
+ good("Post of image file reported success, media link location: " +
523
+ "#{poster.header('Location')}", name)
524
+
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
548
+ end
549
+
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
556
+
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."
578
+ end
579
+
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."
586
+ end
587
+
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}"
805
+ end
806
+ dialog = nil
807
+ end
808
+ end
809
+ end
810
+ 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
+ end
912
+ output.puts @footer if @footer
913
+ 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
946
+ end
947
+ return problem
948
+ end
949
+
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
961
+ end
962
+
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
979
+ end
980
+ end
981
+ end
982
+