gbuesing-toadhopper 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.yardopts +5 -0
- data/LICENSE +21 -0
- data/README.md +37 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/gbuesing-toadhopper.gemspec +55 -0
- data/lib/notice.haml +34 -0
- data/lib/notice.xml.erb +50 -0
- data/lib/toadhopper.rb +136 -0
- data/test/helper.rb +3 -0
- data/test/test_filters.rb +30 -0
- data/test/test_posting.rb +21 -0
- metadata +70 -0
data/.gitignore
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2009 Tim Lucas
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
A base library for [Hoptoad](http://www.hoptoadapp.com/) error reporting.
|
2
|
+
|
3
|
+
Toadhopper can be used to report plain old Ruby exceptions, or to build a framework-specific gem such as [toadhopper-sinatra](http://github.com/toolmantim/toadhopper-sinatra).
|
4
|
+
|
5
|
+
begin
|
6
|
+
raise "Kaboom!"
|
7
|
+
rescue => e
|
8
|
+
require 'toadhopper'
|
9
|
+
ToadHopper("YOURAPIKEY").post!(e)
|
10
|
+
end
|
11
|
+
|
12
|
+
You can install it via rubygems:
|
13
|
+
|
14
|
+
gem install toadhopper
|
15
|
+
|
16
|
+
## Development
|
17
|
+
|
18
|
+
Firstly, `gem install bundler`, then:
|
19
|
+
|
20
|
+
% git clone git://github.com/toolmantim/toadhopper.git
|
21
|
+
% cd toadhopper
|
22
|
+
% gem bundle
|
23
|
+
% bin/rake test
|
24
|
+
|
25
|
+
If you set a `HOPTOAD_API_KEY` environment variable it'll test actually posting to the Hoptoad API. For example:
|
26
|
+
|
27
|
+
% bin/rake test HOPTOAD_API_KEY=abc123
|
28
|
+
|
29
|
+
To generate the docs:
|
30
|
+
|
31
|
+
% bin/yardoc
|
32
|
+
|
33
|
+
## Contributors
|
34
|
+
|
35
|
+
* [Tim Lucas](http://github.com/toolmantim)
|
36
|
+
* [Samuel Tesla](http://github.com/stesla)
|
37
|
+
* [atmos](http://github.com/atmos)
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
|
3
|
+
Rake::TestTask.new do |t|
|
4
|
+
t.libs << "test"
|
5
|
+
t.test_files = FileList['test/test*.rb']
|
6
|
+
t.verbose = true
|
7
|
+
end
|
8
|
+
task :default => :test
|
9
|
+
|
10
|
+
begin
|
11
|
+
require 'jeweler'
|
12
|
+
Jeweler::Tasks.new do |gemspec|
|
13
|
+
gemspec.name = "gbuesing-toadhopper"
|
14
|
+
gemspec.summary = "Post error notifications to Hoptoad"
|
15
|
+
gemspec.description = "Fork of toolmantim-toadhopper, with Haml and Nokogiri dependencies removed"
|
16
|
+
gemspec.email = "gbuesing@gmail.com"
|
17
|
+
gemspec.homepage = "http://github.com/gbuesing/toadhopper"
|
18
|
+
gemspec.authors = ["Geoff Buesing"]
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
22
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{gbuesing-toadhopper}
|
8
|
+
s.version = "0.1.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Geoff Buesing"]
|
12
|
+
s.date = %q{2009-12-29}
|
13
|
+
s.description = %q{Fork of toolmantim-toadhopper, with Haml and Nokogiri dependencies removed}
|
14
|
+
s.email = %q{gbuesing@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
".yardopts",
|
22
|
+
"LICENSE",
|
23
|
+
"README.md",
|
24
|
+
"Rakefile",
|
25
|
+
"VERSION",
|
26
|
+
"gbuesing-toadhopper.gemspec",
|
27
|
+
"lib/notice.haml",
|
28
|
+
"lib/notice.xml.erb",
|
29
|
+
"lib/toadhopper.rb",
|
30
|
+
"test/helper.rb",
|
31
|
+
"test/test_filters.rb",
|
32
|
+
"test/test_posting.rb"
|
33
|
+
]
|
34
|
+
s.homepage = %q{http://github.com/gbuesing/toadhopper}
|
35
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
36
|
+
s.require_paths = ["lib"]
|
37
|
+
s.rubygems_version = %q{1.3.5}
|
38
|
+
s.summary = %q{Post error notifications to Hoptoad}
|
39
|
+
s.test_files = [
|
40
|
+
"test/helper.rb",
|
41
|
+
"test/test_filters.rb",
|
42
|
+
"test/test_posting.rb"
|
43
|
+
]
|
44
|
+
|
45
|
+
if s.respond_to? :specification_version then
|
46
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
47
|
+
s.specification_version = 3
|
48
|
+
|
49
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
50
|
+
else
|
51
|
+
end
|
52
|
+
else
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
data/lib/notice.haml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
!!!XML
|
2
|
+
%notice{:version => '2.0.0'}
|
3
|
+
%api-key= api_key
|
4
|
+
%notifier
|
5
|
+
%name= notifier_name
|
6
|
+
%version= notifier_version
|
7
|
+
%url= notifier_url
|
8
|
+
%error
|
9
|
+
%class= error.class.name
|
10
|
+
%message= "#{error.class.name}: #{error.message}"
|
11
|
+
%backtrace
|
12
|
+
- backtrace.each do |line|
|
13
|
+
%line{:method => line.method, :file => line.file, :number => line.number}/
|
14
|
+
%request
|
15
|
+
%url= url
|
16
|
+
%component= component
|
17
|
+
- if action && !action.blank?
|
18
|
+
%action= action
|
19
|
+
- if request && request.params.any?
|
20
|
+
%params
|
21
|
+
- request.params.each do |key,value|
|
22
|
+
%var{:key => key}= value
|
23
|
+
- if session && session.any?
|
24
|
+
%session
|
25
|
+
- session.each do |key,value|
|
26
|
+
%var{:key => key}= value
|
27
|
+
- if environment && environment.any?
|
28
|
+
%cgi-data
|
29
|
+
- environment.each do |key,value|
|
30
|
+
%var{:key => key}= value
|
31
|
+
|
32
|
+
%server-environment
|
33
|
+
%project-root= project_root
|
34
|
+
%environment-name= framework_env
|
data/lib/notice.xml.erb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
+
<notice version="2.0.0">
|
3
|
+
<api-key><%=h defaults[:api_key] %></api-key>
|
4
|
+
<notifier>
|
5
|
+
<name><%=h defaults[:notifier_name] %></name>
|
6
|
+
<version><%=h defaults[:notifier_version] %></version>
|
7
|
+
<url><%=h defaults[:notifier_url] %></url>
|
8
|
+
</notifier>
|
9
|
+
<error>
|
10
|
+
<class><%=h defaults[:error].class.name %></class>
|
11
|
+
<message><%=h defaults[:error].class.name %>: <%=h defaults[:error].message %></message>
|
12
|
+
<backtrace>
|
13
|
+
<% defaults[:backtrace].each do |line| %>
|
14
|
+
<line method="<%=h line.method %>" file="<%=h line.file %>" number="<%=h line.number %>"/>
|
15
|
+
<% end %>
|
16
|
+
</backtrace>
|
17
|
+
</error>
|
18
|
+
<request>
|
19
|
+
<url><%=h defaults[:url] %></url>
|
20
|
+
<component><%=h defaults[:component] %></component>
|
21
|
+
<% if defaults[:action] %>
|
22
|
+
<component><%=h defaults[:action] %></component>
|
23
|
+
<% end %>
|
24
|
+
<% if defaults[:request] && defaults[:request].params.any? %>
|
25
|
+
<params>
|
26
|
+
<% defaults[:request].params.each do |key,value| %>
|
27
|
+
<var key="<%=h key %>"><%=h value %></var>
|
28
|
+
<% end %>
|
29
|
+
</params>
|
30
|
+
<% end %>
|
31
|
+
<% if defaults[:session] && defaults[:session].any? %>
|
32
|
+
<session>
|
33
|
+
<% defaults[:session].each do |key,value| %>
|
34
|
+
<var key="<%=h key %>"><%=h value %></var>
|
35
|
+
<% end %>
|
36
|
+
</session>
|
37
|
+
<% end %>
|
38
|
+
<% if defaults[:environment] && defaults[:environment].any? %>
|
39
|
+
<cgi-data>
|
40
|
+
<% defaults[:environment].each do |key,value| %>
|
41
|
+
<var key="<%=h key %>"><%=h value %></var>
|
42
|
+
<% end %>
|
43
|
+
</cgi-data>
|
44
|
+
<% end %>
|
45
|
+
</request>
|
46
|
+
<server-environment>
|
47
|
+
<project-root><%=h defaults[:project_root] %></project-root>
|
48
|
+
<environment-name><%=h defaults[:framework_env] %></environment-name>
|
49
|
+
</server-environment>
|
50
|
+
</notice>
|
data/lib/toadhopper.rb
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'net/http'
|
2
|
+
require 'erb'
|
3
|
+
|
4
|
+
# Posts errors to the Hoptoad API
|
5
|
+
class ToadHopper
|
6
|
+
VERSION = "0.9.3"
|
7
|
+
include ERB::Util
|
8
|
+
|
9
|
+
# Hoptoad API response
|
10
|
+
class Response < Struct.new(:status, :body, :errors); end
|
11
|
+
|
12
|
+
attr_reader :api_key
|
13
|
+
|
14
|
+
def initialize(api_key)
|
15
|
+
@api_key = api_key
|
16
|
+
end
|
17
|
+
|
18
|
+
# Sets patterns to +[FILTER]+ out sensitive data such as +/password/+, +/email/+ and +/credit_card_number/+
|
19
|
+
def filters=(*filters)
|
20
|
+
@filters = filters.flatten
|
21
|
+
end
|
22
|
+
|
23
|
+
# @private
|
24
|
+
def filters
|
25
|
+
[@filters].flatten.compact
|
26
|
+
end
|
27
|
+
|
28
|
+
# Posts an exception to hoptoad.
|
29
|
+
#
|
30
|
+
# @param [Exception] error the error to post
|
31
|
+
#
|
32
|
+
# @param [Hash] options
|
33
|
+
# @option options [String] url The url for the request, required to post but not useful in a console environment
|
34
|
+
# @option options [String] component Normally this is your Controller name in an MVC framework
|
35
|
+
# @option options [String] action Normally the action for your request in an MVC framework
|
36
|
+
# @option options [#params] request An object that response to #params and returns a hash
|
37
|
+
# @option options [String] notifier_name Say you're a different notifier than ToadHopper
|
38
|
+
# @option options [String] notifier_version Specify the version of your custom notifier
|
39
|
+
# @option options [String] notifier_url Specify the project URL of your custom notifier
|
40
|
+
# @option options [Hash] session A hash of the user session in a web request
|
41
|
+
# @option options [String] framework_env The framework environment your app is running under
|
42
|
+
# @option options [Array] backtrace Normally not needed, parsed automatically from the provided exception parameter
|
43
|
+
# @option options [Hash] environment You MUST scrub your environment if you plan to use this, please do not use it though. :)
|
44
|
+
# @option options [String] project_root The root directory of your app
|
45
|
+
#
|
46
|
+
# @param [Hash] http_headers extra HTTP headers to be sent in the post to the API
|
47
|
+
#
|
48
|
+
# @example
|
49
|
+
# ToadHopper('apikey').post! error,
|
50
|
+
# {:action => 'show', :controller => 'Users'},
|
51
|
+
# {'X-Hoptoad-Client-Name' => 'My Awesome Notifier'}
|
52
|
+
#
|
53
|
+
# @return [Response]
|
54
|
+
def post!(error, options={}, http_headers={})
|
55
|
+
options[:notifier_name] ||= 'ToadHopper'
|
56
|
+
post_document(document_for(error, options), {'X-Hoptoad-Client-Name' => options[:notifier_name]})
|
57
|
+
end
|
58
|
+
|
59
|
+
# @private
|
60
|
+
def post_document(document, headers={})
|
61
|
+
uri = URI.parse("http://hoptoadapp.com:80/notifier_api/v2/notices")
|
62
|
+
Net::HTTP.start(uri.host, uri.port) do |http|
|
63
|
+
http.read_timeout = 5 # seconds
|
64
|
+
http.open_timeout = 2 # seconds
|
65
|
+
begin
|
66
|
+
response = http.post uri.path,
|
67
|
+
document,
|
68
|
+
{'Content-type' => 'text/xml', 'Accept' => 'text/xml, application/xml'}.merge(headers)
|
69
|
+
Response.new response.code.to_i,
|
70
|
+
response.body
|
71
|
+
rescue TimeoutError => e
|
72
|
+
Response.new(500, '', ['Timeout error'])
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @private
|
78
|
+
def document_for(exception, options={})
|
79
|
+
defaults = {
|
80
|
+
:error => exception,
|
81
|
+
:api_key => api_key,
|
82
|
+
:environment => clean(ENV.to_hash),
|
83
|
+
:backtrace => exception.backtrace.map {|l| backtrace_line(l)},
|
84
|
+
:url => 'http://localhost/',
|
85
|
+
:component => 'http://localhost/',
|
86
|
+
:action => nil,
|
87
|
+
:request => nil,
|
88
|
+
:notifier_version => VERSION,
|
89
|
+
:notifier_url => 'http://github.com/toolmantim/toadhopper',
|
90
|
+
:session => {},
|
91
|
+
:framework_env => ENV['RACK_ENV'] || 'development',
|
92
|
+
:project_root => Dir.pwd
|
93
|
+
}.merge(options)
|
94
|
+
|
95
|
+
ERB.new(notice_template).result(binding)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @private
|
99
|
+
def backtrace_line(line)
|
100
|
+
Struct.new(:file, :number, :method).new(*line.match(%r{^([^:]+):(\d+)(?::in `([^']+)')?$}).captures)
|
101
|
+
end
|
102
|
+
|
103
|
+
# @private
|
104
|
+
def notice_template
|
105
|
+
File.read(::File.join(::File.dirname(__FILE__), 'notice.xml.erb'))
|
106
|
+
end
|
107
|
+
|
108
|
+
# @private
|
109
|
+
def clean(hash)
|
110
|
+
hash.inject({}) do |acc, (k, v)|
|
111
|
+
acc[k] = (v.is_a?(Hash) ? clean(v) : filtered_value(k,v)) if serializable?(v)
|
112
|
+
acc
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# @private
|
117
|
+
def filtered_value(key, value)
|
118
|
+
if filters.any? {|f| key.to_s =~ Regexp.new(f)}
|
119
|
+
"[FILTERED]"
|
120
|
+
else
|
121
|
+
value
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
# @private
|
126
|
+
def serializable?(value)
|
127
|
+
[Fixnum, Array, String, Hash, Bignum].any? {|c| value.is_a?(c)}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Convenience method for creating ToadHoppers
|
132
|
+
#
|
133
|
+
# @return [ToadHopper]
|
134
|
+
def ToadHopper(api_key)
|
135
|
+
ToadHopper.new(api_key)
|
136
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class ToadHopper::TestFilters < Test::Unit::TestCase
|
4
|
+
def toadhopper
|
5
|
+
@toadhopper ||= ToadHopper("test api key")
|
6
|
+
end
|
7
|
+
|
8
|
+
def test_no_filters
|
9
|
+
assert_equal( {:id => "myid", :password => "mypassword"},
|
10
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
11
|
+
end
|
12
|
+
|
13
|
+
def test_string_filter
|
14
|
+
toadhopper.filters = "pass"
|
15
|
+
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
16
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_regex_filter
|
20
|
+
toadhopper.filters = /pas{2}/
|
21
|
+
assert_equal( {:id => "myid", :password => "[FILTERED]"},
|
22
|
+
toadhopper.clean(:id => "myid", :password => "mypassword"))
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_multiple_filters
|
26
|
+
toadhopper.filters = "email", /pas{2}/
|
27
|
+
assert_equal( {:id => "myid", :email => "[FILTERED]", :password => "[FILTERED]"},
|
28
|
+
toadhopper.clean(:id => "myid", :email => "myemail", :password => "mypassword"))
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require File.expand_path(File.join(File.dirname(__FILE__), 'helper'))
|
2
|
+
|
3
|
+
class ToadHopper::TestPosting < Test::Unit::TestCase
|
4
|
+
def test_posting
|
5
|
+
toadhopper = ToadHopper("abc123")
|
6
|
+
error = begin; raise "Kaboom!"; rescue => e; e end
|
7
|
+
|
8
|
+
response = toadhopper.post!(error)
|
9
|
+
assert_equal 422, response.status
|
10
|
+
end
|
11
|
+
|
12
|
+
if ENV['HOPTOAD_API_KEY']
|
13
|
+
def test_posting_integration
|
14
|
+
toadhopper = ToadHopper(ENV['HOPTOAD_API_KEY'])
|
15
|
+
error = begin; raise "Kaboom!"; rescue => e; e end
|
16
|
+
|
17
|
+
response = toadhopper.post!(error)
|
18
|
+
assert_equal 200, response.status
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: gbuesing-toadhopper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Geoff Buesing
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-12-29 00:00:00 -06:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Fork of toolmantim-toadhopper, with Haml and Nokogiri dependencies removed
|
17
|
+
email: gbuesing@gmail.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.md
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- .yardopts
|
28
|
+
- LICENSE
|
29
|
+
- README.md
|
30
|
+
- Rakefile
|
31
|
+
- VERSION
|
32
|
+
- gbuesing-toadhopper.gemspec
|
33
|
+
- lib/notice.haml
|
34
|
+
- lib/notice.xml.erb
|
35
|
+
- lib/toadhopper.rb
|
36
|
+
- test/helper.rb
|
37
|
+
- test/test_filters.rb
|
38
|
+
- test/test_posting.rb
|
39
|
+
has_rdoc: true
|
40
|
+
homepage: http://github.com/gbuesing/toadhopper
|
41
|
+
licenses: []
|
42
|
+
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options:
|
45
|
+
- --charset=UTF-8
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
version:
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: "0"
|
59
|
+
version:
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.5
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Post error notifications to Hoptoad
|
67
|
+
test_files:
|
68
|
+
- test/helper.rb
|
69
|
+
- test/test_filters.rb
|
70
|
+
- test/test_posting.rb
|