ape 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +1 -0
- data/LICENSE +19 -0
- data/Manifest +45 -0
- data/README +12 -0
- data/ape.gemspec +57 -0
- data/bin/ape_server +28 -0
- data/lib/ape.rb +982 -0
- data/lib/ape/atomURI.rb +73 -0
- data/lib/ape/auth/google_login_credentials.rb +96 -0
- data/lib/ape/auth/wsse_credentials.rb +25 -0
- data/lib/ape/authent.rb +42 -0
- data/lib/ape/categories.rb +95 -0
- data/lib/ape/collection.rb +51 -0
- data/lib/ape/crumbs.rb +39 -0
- data/lib/ape/entry.rb +151 -0
- data/lib/ape/escaper.rb +29 -0
- data/lib/ape/feed.rb +117 -0
- data/lib/ape/handler.rb +34 -0
- data/lib/ape/html.rb +17 -0
- data/lib/ape/invoker.rb +54 -0
- data/lib/ape/invokers/deleter.rb +31 -0
- data/lib/ape/invokers/getter.rb +80 -0
- data/lib/ape/invokers/poster.rb +57 -0
- data/lib/ape/invokers/putter.rb +46 -0
- data/lib/ape/layout/ape.css +56 -0
- data/lib/ape/layout/ape_logo.png +0 -0
- data/lib/ape/layout/index.html +54 -0
- data/lib/ape/layout/info.png +0 -0
- data/lib/ape/names.rb +24 -0
- data/lib/ape/print_writer.rb +21 -0
- data/lib/ape/samples.rb +180 -0
- data/lib/ape/samples/atom_schema.txt +338 -0
- data/lib/ape/samples/basic_entry.eruby +16 -0
- data/lib/ape/samples/categories_schema.txt +69 -0
- data/lib/ape/samples/mini_entry.eruby +8 -0
- data/lib/ape/samples/service_schema.txt +187 -0
- data/lib/ape/samples/unclean_xhtml_entry.eruby +21 -0
- data/lib/ape/server.rb +32 -0
- data/lib/ape/service.rb +12 -0
- data/lib/ape/validator.rb +65 -0
- data/lib/ape/version.rb +9 -0
- data/scripts/go.rb +29 -0
- data/test/test_helper.rb +17 -0
- data/test/unit/authent_test.rb +35 -0
- data/test/unit/invoker_test.rb +25 -0
- data/test/unit/samples_test.rb +36 -0
- metadata +111 -0
data/CHANGELOG
ADDED
@@ -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.
|
data/Manifest
ADDED
@@ -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
|
data/ape.gemspec
ADDED
@@ -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
|
+
#
|
data/bin/ape_server
ADDED
@@ -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)
|
data/lib/ape.rb
ADDED
@@ -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/, '    ')
|
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 << '✓' }
|
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
|
+
|