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