pelle-ruby-openid 2.1.8

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 (197) hide show
  1. data/CHANGELOG +215 -0
  2. data/CHANGES-2.1.0 +36 -0
  3. data/INSTALL +47 -0
  4. data/LICENSE +210 -0
  5. data/NOTICE +2 -0
  6. data/README +82 -0
  7. data/UPGRADE +127 -0
  8. data/VERSION +1 -0
  9. data/examples/README +32 -0
  10. data/examples/active_record_openid_store/README +58 -0
  11. data/examples/active_record_openid_store/XXX_add_open_id_store_to_db.rb +24 -0
  12. data/examples/active_record_openid_store/XXX_upgrade_open_id_store.rb +26 -0
  13. data/examples/active_record_openid_store/init.rb +8 -0
  14. data/examples/active_record_openid_store/lib/association.rb +10 -0
  15. data/examples/active_record_openid_store/lib/nonce.rb +3 -0
  16. data/examples/active_record_openid_store/lib/open_id_setting.rb +4 -0
  17. data/examples/active_record_openid_store/lib/openid_ar_store.rb +57 -0
  18. data/examples/active_record_openid_store/test/store_test.rb +212 -0
  19. data/examples/discover +49 -0
  20. data/examples/rails_openid/README +153 -0
  21. data/examples/rails_openid/Rakefile +10 -0
  22. data/examples/rails_openid/app/controllers/application.rb +4 -0
  23. data/examples/rails_openid/app/controllers/consumer_controller.rb +122 -0
  24. data/examples/rails_openid/app/controllers/login_controller.rb +45 -0
  25. data/examples/rails_openid/app/controllers/server_controller.rb +265 -0
  26. data/examples/rails_openid/app/helpers/application_helper.rb +3 -0
  27. data/examples/rails_openid/app/helpers/login_helper.rb +2 -0
  28. data/examples/rails_openid/app/helpers/server_helper.rb +9 -0
  29. data/examples/rails_openid/app/views/consumer/index.rhtml +81 -0
  30. data/examples/rails_openid/app/views/layouts/server.rhtml +68 -0
  31. data/examples/rails_openid/app/views/login/index.rhtml +56 -0
  32. data/examples/rails_openid/app/views/server/decide.rhtml +26 -0
  33. data/examples/rails_openid/config/boot.rb +19 -0
  34. data/examples/rails_openid/config/database.yml +74 -0
  35. data/examples/rails_openid/config/environment.rb +54 -0
  36. data/examples/rails_openid/config/environments/development.rb +19 -0
  37. data/examples/rails_openid/config/environments/production.rb +19 -0
  38. data/examples/rails_openid/config/environments/test.rb +19 -0
  39. data/examples/rails_openid/config/routes.rb +24 -0
  40. data/examples/rails_openid/doc/README_FOR_APP +2 -0
  41. data/examples/rails_openid/public/.htaccess +40 -0
  42. data/examples/rails_openid/public/404.html +8 -0
  43. data/examples/rails_openid/public/500.html +8 -0
  44. data/examples/rails_openid/public/dispatch.cgi +12 -0
  45. data/examples/rails_openid/public/dispatch.fcgi +26 -0
  46. data/examples/rails_openid/public/dispatch.rb +12 -0
  47. data/examples/rails_openid/public/favicon.ico +0 -0
  48. data/examples/rails_openid/public/images/openid_login_bg.gif +0 -0
  49. data/examples/rails_openid/public/javascripts/controls.js +750 -0
  50. data/examples/rails_openid/public/javascripts/dragdrop.js +584 -0
  51. data/examples/rails_openid/public/javascripts/effects.js +854 -0
  52. data/examples/rails_openid/public/javascripts/prototype.js +1785 -0
  53. data/examples/rails_openid/public/robots.txt +1 -0
  54. data/examples/rails_openid/script/about +3 -0
  55. data/examples/rails_openid/script/breakpointer +3 -0
  56. data/examples/rails_openid/script/console +3 -0
  57. data/examples/rails_openid/script/destroy +3 -0
  58. data/examples/rails_openid/script/generate +3 -0
  59. data/examples/rails_openid/script/performance/benchmarker +3 -0
  60. data/examples/rails_openid/script/performance/profiler +3 -0
  61. data/examples/rails_openid/script/plugin +3 -0
  62. data/examples/rails_openid/script/process/reaper +3 -0
  63. data/examples/rails_openid/script/process/spawner +3 -0
  64. data/examples/rails_openid/script/process/spinner +3 -0
  65. data/examples/rails_openid/script/runner +3 -0
  66. data/examples/rails_openid/script/server +3 -0
  67. data/examples/rails_openid/test/functional/login_controller_test.rb +18 -0
  68. data/examples/rails_openid/test/functional/server_controller_test.rb +18 -0
  69. data/examples/rails_openid/test/test_helper.rb +28 -0
  70. data/lib/hmac/hmac.rb +112 -0
  71. data/lib/hmac/sha1.rb +11 -0
  72. data/lib/hmac/sha2.rb +25 -0
  73. data/lib/openid/association.rb +249 -0
  74. data/lib/openid/consumer/associationmanager.rb +344 -0
  75. data/lib/openid/consumer/checkid_request.rb +186 -0
  76. data/lib/openid/consumer/discovery.rb +498 -0
  77. data/lib/openid/consumer/discovery_manager.rb +123 -0
  78. data/lib/openid/consumer/html_parse.rb +134 -0
  79. data/lib/openid/consumer/idres.rb +523 -0
  80. data/lib/openid/consumer/responses.rb +148 -0
  81. data/lib/openid/consumer.rb +395 -0
  82. data/lib/openid/cryptutil.rb +97 -0
  83. data/lib/openid/dh.rb +89 -0
  84. data/lib/openid/extension.rb +39 -0
  85. data/lib/openid/extensions/ax.rb +516 -0
  86. data/lib/openid/extensions/oauth.rb +91 -0
  87. data/lib/openid/extensions/pape.rb +179 -0
  88. data/lib/openid/extensions/sreg.rb +277 -0
  89. data/lib/openid/extras.rb +11 -0
  90. data/lib/openid/fetchers.rb +238 -0
  91. data/lib/openid/kvform.rb +136 -0
  92. data/lib/openid/kvpost.rb +58 -0
  93. data/lib/openid/message.rb +553 -0
  94. data/lib/openid/protocolerror.rb +8 -0
  95. data/lib/openid/server.rb +1544 -0
  96. data/lib/openid/store/filesystem.rb +271 -0
  97. data/lib/openid/store/interface.rb +75 -0
  98. data/lib/openid/store/memcache.rb +107 -0
  99. data/lib/openid/store/memory.rb +84 -0
  100. data/lib/openid/store/nonce.rb +68 -0
  101. data/lib/openid/trustroot.rb +349 -0
  102. data/lib/openid/urinorm.rb +75 -0
  103. data/lib/openid/util.rb +110 -0
  104. data/lib/openid/yadis/accept.rb +148 -0
  105. data/lib/openid/yadis/constants.rb +21 -0
  106. data/lib/openid/yadis/discovery.rb +153 -0
  107. data/lib/openid/yadis/filters.rb +205 -0
  108. data/lib/openid/yadis/htmltokenizer.rb +305 -0
  109. data/lib/openid/yadis/parsehtml.rb +45 -0
  110. data/lib/openid/yadis/services.rb +42 -0
  111. data/lib/openid/yadis/xrds.rb +155 -0
  112. data/lib/openid/yadis/xri.rb +90 -0
  113. data/lib/openid/yadis/xrires.rb +106 -0
  114. data/lib/openid.rb +20 -0
  115. data/setup.rb +1551 -0
  116. data/test/data/accept.txt +124 -0
  117. data/test/data/dh.txt +29 -0
  118. data/test/data/example-xrds.xml +14 -0
  119. data/test/data/linkparse.txt +587 -0
  120. data/test/data/n2b64 +650 -0
  121. data/test/data/test1-discover.txt +137 -0
  122. data/test/data/test1-parsehtml.txt +152 -0
  123. data/test/data/test_discover/malformed_meta_tag.html +19 -0
  124. data/test/data/test_discover/openid.html +11 -0
  125. data/test/data/test_discover/openid2.html +11 -0
  126. data/test/data/test_discover/openid2_xrds.xml +12 -0
  127. data/test/data/test_discover/openid2_xrds_no_local_id.xml +11 -0
  128. data/test/data/test_discover/openid_1_and_2.html +11 -0
  129. data/test/data/test_discover/openid_1_and_2_xrds.xml +16 -0
  130. data/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml +17 -0
  131. data/test/data/test_discover/openid_and_yadis.html +12 -0
  132. data/test/data/test_discover/openid_no_delegate.html +10 -0
  133. data/test/data/test_discover/yadis_0entries.xml +12 -0
  134. data/test/data/test_discover/yadis_2_bad_local_id.xml +15 -0
  135. data/test/data/test_discover/yadis_2entries_delegate.xml +22 -0
  136. data/test/data/test_discover/yadis_2entries_idp.xml +21 -0
  137. data/test/data/test_discover/yadis_another_delegate.xml +14 -0
  138. data/test/data/test_discover/yadis_idp.xml +12 -0
  139. data/test/data/test_discover/yadis_idp_delegate.xml +13 -0
  140. data/test/data/test_discover/yadis_no_delegate.xml +11 -0
  141. data/test/data/test_xrds/=j3h.2007.11.14.xrds +25 -0
  142. data/test/data/test_xrds/README +12 -0
  143. data/test/data/test_xrds/delegated-20060809-r1.xrds +34 -0
  144. data/test/data/test_xrds/delegated-20060809-r2.xrds +34 -0
  145. data/test/data/test_xrds/delegated-20060809.xrds +34 -0
  146. data/test/data/test_xrds/no-xrd.xml +7 -0
  147. data/test/data/test_xrds/not-xrds.xml +2 -0
  148. data/test/data/test_xrds/prefixsometimes.xrds +34 -0
  149. data/test/data/test_xrds/ref.xrds +109 -0
  150. data/test/data/test_xrds/sometimesprefix.xrds +34 -0
  151. data/test/data/test_xrds/spoof1.xrds +25 -0
  152. data/test/data/test_xrds/spoof2.xrds +25 -0
  153. data/test/data/test_xrds/spoof3.xrds +37 -0
  154. data/test/data/test_xrds/status222.xrds +9 -0
  155. data/test/data/test_xrds/subsegments.xrds +58 -0
  156. data/test/data/test_xrds/valid-populated-xrds.xml +39 -0
  157. data/test/data/trustroot.txt +153 -0
  158. data/test/data/urinorm.txt +79 -0
  159. data/test/discoverdata.rb +131 -0
  160. data/test/test_accept.rb +170 -0
  161. data/test/test_association.rb +266 -0
  162. data/test/test_associationmanager.rb +917 -0
  163. data/test/test_ax.rb +648 -0
  164. data/test/test_checkid_request.rb +294 -0
  165. data/test/test_consumer.rb +257 -0
  166. data/test/test_cryptutil.rb +119 -0
  167. data/test/test_dh.rb +86 -0
  168. data/test/test_discover.rb +838 -0
  169. data/test/test_discovery_manager.rb +262 -0
  170. data/test/test_extension.rb +46 -0
  171. data/test/test_extras.rb +35 -0
  172. data/test/test_fetchers.rb +538 -0
  173. data/test/test_filters.rb +270 -0
  174. data/test/test_idres.rb +963 -0
  175. data/test/test_kvform.rb +165 -0
  176. data/test/test_kvpost.rb +65 -0
  177. data/test/test_linkparse.rb +101 -0
  178. data/test/test_message.rb +1116 -0
  179. data/test/test_nonce.rb +89 -0
  180. data/test/test_oauth.rb +175 -0
  181. data/test/test_openid_yadis.rb +178 -0
  182. data/test/test_pape.rb +247 -0
  183. data/test/test_parsehtml.rb +80 -0
  184. data/test/test_responses.rb +63 -0
  185. data/test/test_server.rb +2457 -0
  186. data/test/test_sreg.rb +479 -0
  187. data/test/test_stores.rb +298 -0
  188. data/test/test_trustroot.rb +113 -0
  189. data/test/test_urinorm.rb +35 -0
  190. data/test/test_util.rb +145 -0
  191. data/test/test_xrds.rb +169 -0
  192. data/test/test_xri.rb +48 -0
  193. data/test/test_xrires.rb +63 -0
  194. data/test/test_yadis_discovery.rb +220 -0
  195. data/test/testutil.rb +127 -0
  196. data/test/util.rb +53 -0
  197. metadata +316 -0
@@ -0,0 +1,165 @@
1
+ require 'test/unit'
2
+ require 'openid/kvform'
3
+ require 'openid/util'
4
+ require 'util'
5
+
6
+ include OpenID
7
+
8
+ class KVFormTests < Test::Unit::TestCase
9
+ include OpenID::TestUtil
10
+
11
+ def test_kvdict
12
+ [
13
+ # (kvform, parsed dictionary, expected warnings)
14
+ ["", {}, 0],
15
+ ["\n \n \n", {}, 0],
16
+ ["college:harvey mudd\n", {"college" => "harvey mudd"}, 0],
17
+ ["city:claremont\nstate:CA\n",
18
+ {"city" => "claremont", "state" => "CA"}, 0],
19
+ ["is_valid:true\ninvalidate_handle:{HMAC-SHA1:2398410938412093}\n",
20
+ {"is_valid" => "true",
21
+ "invalidate_handle" => "{HMAC-SHA1:2398410938412093}"}, 0],
22
+
23
+ # Warnings from lines with no colon:
24
+ ["x\n", {}, 1],
25
+ ["x\nx\n", {}, 2],
26
+ ["East is least\n", {}, 1],
27
+
28
+ # But not from blank lines (because LJ generates them)
29
+ ["x\n\n", {}, 1],
30
+
31
+ # Warning from empty key
32
+ [":\n", {''=>''}, 1],
33
+ [":missing key\n", {''=>'missing key'}, 1],
34
+
35
+ # Warnings from leading or trailing whitespace in key or value
36
+ [" street:foothill blvd\n", {"street"=>"foothill blvd"}, 1],
37
+ ["major: computer science\n", {"major"=>"computer science"}, 1],
38
+ [" dorm : east \n", {"dorm"=>"east"}, 2],
39
+
40
+ # Warnings from missing trailing newline
41
+ ["e^(i*pi)+1:0", {"e^(i*pi)+1" => "0"}, 1],
42
+ ["east:west\nnorth:south", {"east"=>"west", "north"=>"south"}, 1],
43
+ ].each { |case_|
44
+ _run_kvdictTest(case_)
45
+ }
46
+ end
47
+
48
+ def _run_kvdictTest(case_)
49
+ kv, dct, warnings = case_
50
+
51
+ d = nil
52
+ d2 = nil
53
+ assert_log_line_count(warnings) {
54
+ # Convert KVForm to dict
55
+ d = Util.kv_to_dict(kv)
56
+
57
+ # Strict mode should raise KVFormError instead of logging
58
+ # messages
59
+ if warnings > 0
60
+ assert_raise(KVFormError) do
61
+ Util.kv_to_seq(kv, true)
62
+ end
63
+ end
64
+
65
+ # make sure it parses to expected dict
66
+ assert_equal(dct, d)
67
+ }
68
+
69
+ # Convert back to KVForm and round-trip back to dict to make sure
70
+ # that *** dict -> kv -> dict is identity. ***
71
+ kv = Util.dict_to_kv(d)
72
+
73
+ silence_logging {
74
+ d2 = Util.kv_to_dict(kv)
75
+ }
76
+
77
+ assert_equal(d, d2)
78
+ end
79
+
80
+ def test_kvseq
81
+ [
82
+ [[], "", 0],
83
+
84
+ [[["openid", "useful"], ["a", "b"]], "openid:useful\na:b\n", 0],
85
+
86
+ # Warnings about leading whitespace
87
+ [[[" openid", "useful"], ["a", "b"]], " openid:useful\na:b\n", 2],
88
+
89
+ # Warnings about leading and trailing whitespace
90
+ [[[" openid ", " useful "],
91
+ [" a ", " b "]], " openid : useful \n a : b \n", 8],
92
+
93
+ # warnings about leading and trailing whitespace, but not about
94
+ # internal whitespace.
95
+ [[[" open id ", " use ful "],
96
+ [" a ", " b "]], " open id : use ful \n a : b \n", 8],
97
+
98
+ [[["foo", "bar"]], "foo:bar\n", 0],
99
+ ].each { |case_|
100
+ _run_kvseqTest(case_)
101
+ }
102
+ end
103
+
104
+ def _cleanSeq(seq)
105
+ # Create a new sequence by stripping whitespace from start and end
106
+ # of each value of each pair
107
+ seq.collect { |k, v| [k.strip(), v.strip()] }
108
+ end
109
+
110
+ def _run_kvseqTest(case_)
111
+ seq, kvform, warnings = case_
112
+
113
+ assert_log_line_count(warnings) {
114
+ # seq serializes to expected kvform
115
+ actual = Util.seq_to_kv(seq)
116
+
117
+ assert_equal(kvform, actual)
118
+ assert actual.is_a?(String)
119
+
120
+ # Strict mode should raise KVFormError instead of logging
121
+ # messages
122
+ if warnings > 0
123
+ assert_raise(KVFormError) do
124
+ Util.seq_to_kv(seq, true)
125
+ end
126
+ end
127
+
128
+ # Parse back to sequence. Expected to be unchanged, except
129
+ # stripping whitespace from start and end of values
130
+ # (i. e. ordering, case, and internal whitespace is preserved)
131
+ seq = Util.kv_to_seq(actual)
132
+ clean_seq = _cleanSeq(seq)
133
+
134
+ assert_equal(seq, clean_seq)
135
+ }
136
+ end
137
+
138
+ def test_kvexc
139
+ [
140
+ [["openid", "use\nful"]],
141
+ [["open\nid", "useful"]],
142
+ [["open\nid", "use\nful"]],
143
+ [["open:id", "useful"]],
144
+ [["foo", "bar"], ["ba\n d", "seed"]],
145
+ [["foo", "bar"], ["bad:", "seed"]],
146
+ ].each { |case_|
147
+ _run_kvexcTest(case_)
148
+ }
149
+ end
150
+
151
+ def _run_kvexcTest(case_)
152
+ seq = case_
153
+
154
+ assert_raise(KVFormError) do
155
+ Util.seq_to_kv(seq)
156
+ end
157
+ end
158
+
159
+ def test_convert
160
+ assert_log_line_count(2) {
161
+ result = Util.seq_to_kv([[1, 1]])
162
+ assert_equal(result, "1:1\n")
163
+ }
164
+ end
165
+ end
@@ -0,0 +1,65 @@
1
+ require "openid/kvpost"
2
+ require "openid/kvform"
3
+ require "openid/message"
4
+ require "test/unit"
5
+ require 'testutil'
6
+
7
+ module OpenID
8
+ class KVPostTestCase < Test::Unit::TestCase
9
+ include FetcherMixin
10
+
11
+ def mk_resp(status, resp_hash)
12
+ return MockResponse.new(status, Util.dict_to_kv(resp_hash))
13
+ end
14
+
15
+ def test_msg_from_http_resp_success
16
+ resp = mk_resp(200, {'mode' => 'seitan'})
17
+ msg = Message.from_http_response(resp, 'http://invalid/')
18
+ assert_equal({'openid.mode' => 'seitan'}, msg.to_post_args)
19
+ end
20
+
21
+ def test_400
22
+ args = {'error' => 'I ate too much cheese',
23
+ 'error_code' => 'sadness'}
24
+ resp = mk_resp(400, args)
25
+ begin
26
+ val = Message.from_http_response(resp, 'http://invalid/')
27
+ rescue ServerError => why
28
+ assert_equal(why.error_text, 'I ate too much cheese')
29
+ assert_equal(why.error_code, 'sadness')
30
+ assert_equal(why.message.to_args, args)
31
+ else
32
+ fail("Expected exception. Got: #{val}")
33
+ end
34
+ end
35
+
36
+ def test_500
37
+ args = {'error' => 'I ate too much cheese',
38
+ 'error_code' => 'sadness'}
39
+ resp = mk_resp(500, args)
40
+ assert_raises(HTTPStatusError) {
41
+ Message.from_http_response(resp, 'http://invalid')
42
+ }
43
+ end
44
+
45
+ def make_kv_post_with_response(status, args)
46
+ resp = mk_resp(status, args)
47
+ mock_fetcher = Class.new do
48
+ define_method(:fetch) do |url, body, xxx, yyy|
49
+ resp
50
+ end
51
+ end
52
+ fetcher = mock_fetcher.new
53
+
54
+ with_fetcher(mock_fetcher.new) do
55
+ OpenID.make_kv_post(Message.from_openid_args(args), 'http://invalid/')
56
+ end
57
+ end
58
+
59
+ def test_make_kv_post
60
+ assert_raises(HTTPStatusError) {
61
+ make_kv_post_with_response(500, {})
62
+ }
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,101 @@
1
+ require 'test/unit'
2
+ require 'testutil'
3
+ require 'openid/consumer/html_parse'
4
+
5
+ class LinkParseTestCase < Test::Unit::TestCase
6
+ include OpenID::TestDataMixin
7
+
8
+ def attr_cmp(expected, found)
9
+ e = expected.to_a.sort
10
+ f = found.to_a.sort
11
+ while (ep = e.shift)
12
+ ek, ev = ep
13
+ fk, fv = f.shift
14
+ ok = false
15
+ while ek[-1] == '*'[0] # optional entry detected
16
+ if fk == ek[0...-1] and fv==ev # optional entry found
17
+ ok = true
18
+ break
19
+ else # not found. okay, move on to next expected pair
20
+ ek, ev = e.shift
21
+ end
22
+ if ek.nil?
23
+ if fk == nil
24
+ ok = true
25
+ end
26
+ break
27
+ end
28
+ end
29
+ next if ok
30
+ next if fk == ek and fv == ev
31
+ return false
32
+ end
33
+ return f.empty?
34
+ end
35
+
36
+ def test_attrcmp
37
+ good = [
38
+ [{'foo' => 'bar'},{'foo' => 'bar'}],
39
+ [{'foo*' => 'bar'},{'foo' => 'bar'}],
40
+ [{'foo' => 'bar', 'bam*' => 'baz'},{'foo' => 'bar'}],
41
+ [{'foo' => 'bar', 'bam*' => 'baz', 'tak' => 'tal'},
42
+ {'foo' => 'bar', 'tak' => 'tal'}],
43
+ ]
44
+ bad = [
45
+ [{},{'foo' => 'bar'}],
46
+ [{'foo' => 'bar'}, {'bam' => 'baz'}],
47
+ [{'foo' => 'bar'}, {}],
48
+ [{'foo*' => 'bar'},{'foo*' => 'bar'}],
49
+ [{'foo' => 'bar', 'tak' => 'tal'}, {'foo' => 'bar'}]
50
+ ]
51
+ good.each{|c|assert(attr_cmp(c[0],c[1]),c.inspect)}
52
+ bad.each{|c|assert(!attr_cmp(c[0],c[1]),c.inspect)}
53
+
54
+ end
55
+
56
+ def test_linkparse
57
+ cases = read_data_file('linkparse.txt', false).split("\n\n\n")
58
+
59
+ numtests = nil
60
+ testnum = 0
61
+ cases.each {|c|
62
+ headers, html = c.split("\n\n",2)
63
+ expected_links = []
64
+ name = ""
65
+ testnum += 1
66
+ headers.split("\n").each{|h|
67
+ k,v = h.split(":",2)
68
+ v = '' if v.nil?
69
+ if k == "Num Tests"
70
+ assert(numtests.nil?, "datafile parsing error: there can be only one NumTests")
71
+ numtests = v.to_i
72
+ testnum = 0
73
+ next
74
+ elsif k == "Name"
75
+ name = v.strip
76
+ elsif k == "Link" or k == "Link*"
77
+ attrs = {}
78
+ v.strip.split.each{|a|
79
+ kk,vv = a.split('=')
80
+ attrs[kk]=vv
81
+ }
82
+ expected_links << [k== "Link*", attrs]
83
+ else
84
+ assert(false, "datafile parsing error: bad header #{h}")
85
+ end
86
+ }
87
+ links = OpenID::parse_link_attrs(html)
88
+
89
+ found = links.dup
90
+ expected = expected_links.dup
91
+ while(fl = found.shift)
92
+ optional, el = expected.shift
93
+ while optional and !attr_cmp(el, fl) and not expected.empty?
94
+ optional, el = expected.shift
95
+ end
96
+ assert(attr_cmp(el,fl), "#{name}: #{fl.inspect} does not match #{el.inspect}")
97
+ end
98
+ }
99
+ assert_equal(numtests, testnum, "Number of tests")
100
+ end
101
+ end