repsheet_visualizer 0.1.6 → 0.1.7

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 0bf1075fbcd0219cc0d7aaf9e414bce1c97f4e82
4
+ data.tar.gz: 07a44a473a4d62fc5ef5266f417f31dbce69f6b8
5
+ SHA512:
6
+ metadata.gz: 307376a60eeea4e67ed8a60b4cdb9c5064ed56281e5199ce8ced8854905149dcbd164540dee2ab0a132e413bb183d667de7dd0803747001bc56195b1fe4ff577
7
+ data.tar.gz: 087dd95a4f7601f36e895f1380dc169b008b269e2c25ba8663939ce15ab0c9fa42370e1b701bf71a596396e49c6988779dfd8a8ddbb4199f8ab872b65e2e579b
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.rvmrc CHANGED
@@ -1 +1 @@
1
- rvm use ruby-1.9.3@repsheet_vizualizer --create
1
+ rvm use ruby-2.0@repsheet_vizualizer --create
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- repsheet_visualizer (0.1.3)
4
+ repsheet_visualizer (0.1.7)
5
5
  geoip
6
6
  json
7
7
  redis
@@ -10,6 +10,7 @@ PATH
10
10
  GEM
11
11
  remote: https://rubygems.org/
12
12
  specs:
13
+ diff-lcs (1.2.4)
13
14
  geoip (1.2.1)
14
15
  json (1.8.0)
15
16
  kgio (2.8.0)
@@ -19,8 +20,16 @@ GEM
19
20
  raindrops (0.11.0)
20
21
  rake (10.0.4)
21
22
  redis (3.0.4)
22
- sinatra (1.4.2)
23
- rack (~> 1.5, >= 1.5.2)
23
+ rspec (2.14.1)
24
+ rspec-core (~> 2.14.0)
25
+ rspec-expectations (~> 2.14.0)
26
+ rspec-mocks (~> 2.14.0)
27
+ rspec-core (2.14.3)
28
+ rspec-expectations (2.14.0)
29
+ diff-lcs (>= 1.1.3, < 2.0)
30
+ rspec-mocks (2.14.1)
31
+ sinatra (1.4.3)
32
+ rack (~> 1.4)
24
33
  rack-protection (~> 1.4)
25
34
  tilt (~> 1.3, >= 1.3.4)
26
35
  tilt (1.4.1)
@@ -36,4 +45,5 @@ DEPENDENCIES
36
45
  bundler
37
46
  rake
38
47
  repsheet_visualizer!
48
+ rspec
39
49
  unicorn
data/Rakefile CHANGED
@@ -1 +1,6 @@
1
1
  require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new('spec')
5
+
6
+ task :default => :spec
@@ -2,9 +2,9 @@ require 'geoip'
2
2
  require 'sinatra'
3
3
  require 'redis'
4
4
  require 'json'
5
+ require_relative 'backend'
5
6
 
6
7
  class RepsheetVisualizer < Sinatra::Base
7
- # Grab the mount point before every request
8
8
  before do
9
9
  @mount = mount
10
10
  end
@@ -20,7 +20,6 @@ class RepsheetVisualizer < Sinatra::Base
20
20
  end
21
21
  end
22
22
 
23
- # Settings methods
24
23
  def redis_connection
25
24
  host = defined?(settings.redis_host) ? settings.redis_host : "localhost"
26
25
  port = defined?(settings.redis_port) ? settings.redis_port : 6379
@@ -42,101 +41,25 @@ class RepsheetVisualizer < Sinatra::Base
42
41
  defined?(settings.redis_expiry) ? (settings.redis_expiry * 60 * 60) : (24 * 60 * 60)
43
42
  end
44
43
 
45
- # TODO: These methods should get moved out to another place
46
- def summary(connection)
47
- suspects = {}
48
- blacklisted = {}
49
-
50
- if connection.exists("offenders")
51
- connection.zrevrangebyscore("offenders", "+inf", "0").each do |actor|
52
- next if connection.get("#{actor}:repsheet:blacklist") == "true"
53
- suspects[actor] = Hash.new 0
54
- suspects[actor][:detected] = connection.smembers("#{actor}:detected").join(", ")
55
- suspects[actor][:total] = connection.zscore("offenders", actor).to_i
56
- end
57
-
58
- connection.keys("*:*:blacklist").map {|d| d.split(":").first}.reject {|ip| ip.empty?}.each do |actor|
59
- connection.get("#{actor}:repsheet:blacklist") == "true"
60
- blacklisted[actor] = Hash.new 0
61
- blacklisted[actor][:detected] = connection.smembers("#{actor}:detected").join(", ")
62
- connection.smembers("#{actor}:detected").each do |rule|
63
- blacklisted[actor][:total] += connection.get("#{actor}:#{rule}:count").to_i
64
- end
65
- end
66
- else
67
- connection.keys("*:requests").map {|d| d.split(":").first}.reject {|ip| ip.empty?}.each do |actor|
68
- detected = connection.smembers("#{actor}:detected").join(", ")
69
- blacklist = connection.get("#{actor}:repsheet:blacklist")
70
-
71
- if !detected.empty? && blacklist != "true"
72
- suspects[actor] = Hash.new 0
73
- suspects[actor][:detected] = detected
74
- connection.smembers("#{actor}:detected").each do |rule|
75
- suspects[actor][:total] += connection.get("#{actor}:#{rule}:count").to_i
76
- end
77
- end
78
-
79
- if blacklist == "true"
80
- blacklisted[actor] = Hash.new 0
81
- blacklisted[actor][:detected] = detected
82
- connection.smembers("#{actor}:detected").each do |rule|
83
- blacklisted[actor][:total] += connection.get("#{actor}:#{rule}:count").to_i
84
- end
85
- end
86
- end
87
- end
88
-
89
- [suspects.sort_by{|k,v| -v[:total]}.take(10), blacklisted]
90
- end
91
-
92
- def breakdown(connection)
93
- data = {}
94
- offenders = connection.keys("*:repsheet").map {|o| o.split(":").first}
95
- offenders.each do |offender|
96
- data[offender] = {"totals" => {}}
97
- connection.smembers("#{offender}:detected").each do |rule|
98
- data[offender]["totals"][rule] = connection.get "#{offender}:#{rule}:count"
99
- end
100
- end
101
- aggregate = Hash.new 0
102
- data.each {|ip,data| data["totals"].each {|rule,count| aggregate[rule] += count.to_i}}
103
- [data, aggregate]
104
- end
105
-
106
- def activity(connection)
107
- connection.lrange("#{@ip}:requests", 0, -1)
108
- end
109
-
110
- def worldview(connection, database)
111
- data = {}
112
- offenders = connection.keys("*:repsheet*").map {|o| o.split(":").first}
113
- offenders.each do |address|
114
- details = database.country(address)
115
- next if details.nil?
116
- data[address] = [details.latitude, details.longitude]
117
- end
118
- data
119
- end
120
-
121
44
  # This is the actual application
122
45
  get '/' do
123
- @suspects, @blacklisted = summary(redis_connection)
46
+ @suspects, @blacklisted = Backend.summary(redis_connection)
124
47
  erb :actors
125
48
  end
126
49
 
127
50
  get '/breakdown' do
128
- @data, @aggregate = breakdown(redis_connection)
51
+ @data, @aggregate = Backend.breakdown(redis_connection)
129
52
  erb :breakdown
130
53
  end
131
54
 
132
55
  get '/worldview' do
133
- @data = worldview(redis_connection, geoip_database)
56
+ @data = Backend.worldview(redis_connection, geoip_database)
134
57
  erb :worldview
135
58
  end
136
59
 
137
60
  get '/activity/:ip' do
138
61
  @ip = params[:ip]
139
- @data = activity(redis_connection)
62
+ @data = Backend.activity(redis_connection)
140
63
  erb :activity
141
64
  end
142
65
 
@@ -0,0 +1,97 @@
1
+ class Backend
2
+ def self.summary(connection)
3
+ if connection.exists("offenders")
4
+ suspects, blacklisted = optimized(connection)
5
+ else
6
+ suspects, blacklisted = standard(connection)
7
+ end
8
+
9
+ [suspects.sort_by{|k,v| -v[:total]}.take(10), blacklisted]
10
+ end
11
+
12
+ def self.breakdown(connection)
13
+ # data = {}
14
+ # offenders = connection.keys("*:repsheet").map {|o| o.split(":").first}
15
+ # offenders.each do |offender|
16
+ # data[offender] = {"totals" => {}}
17
+ # connection.smembers("#{offender}:detected").each do |rule|
18
+ # data[offender]["totals"][rule] = connection.get "#{offender}:#{rule}:count"
19
+ # end
20
+ # end
21
+ # aggregate = Hash.new 0
22
+ # data.each {|ip,data| data["totals"].each {|rule,count| aggregate[rule] += count.to_i}}
23
+ # [data, aggregate]
24
+ [{},{}]
25
+ end
26
+
27
+ def self.activity(connection)
28
+ connection.lrange("#{@ip}:requests", 0, -1)
29
+ end
30
+
31
+ def self.worldview(connection, database)
32
+ data = {}
33
+ offenders = connection.keys("*:repsheet*").map {|o| o.split(":").first}
34
+ offenders.each do |address|
35
+ details = database.country(address)
36
+ next if details.nil?
37
+ data[address] = [details.latitude, details.longitude]
38
+ end
39
+ data
40
+ end
41
+
42
+ private
43
+
44
+ def self.triggered_rules(connection, actor)
45
+ connection.zrange("#{actor}:detected", 0, -1)
46
+ end
47
+
48
+ def self.optimized(connection)
49
+ suspects = {}
50
+
51
+ connection.zrevrangebyscore("offenders", "+inf", "0").each do |actor|
52
+ next if connection.get("#{actor}:repsheet:blacklist") == "true"
53
+ suspects[actor] = Hash.new 0
54
+ suspects[actor][:detected] = detected.join(", ")
55
+ suspects[actor][:total] = connection.zscore("offenders", actor).to_i
56
+ end
57
+
58
+ [suspects, blacklist(connection)]
59
+ end
60
+
61
+ def self.standard(connection)
62
+ suspects = {}
63
+
64
+ connection.keys("*:requests").map {|d| d.split(":").first}.reject {|ip| ip.empty?}.each do |actor|
65
+ detected = triggered_rules(connection, actor)
66
+ blacklist = connection.get("#{actor}:repsheet:blacklist")
67
+
68
+ if !detected.empty? && blacklist != "true"
69
+ suspects[actor] = Hash.new 0
70
+ suspects[actor][:detected] = detected.join(", ")
71
+ suspects[actor][:total] = score_actor(connection, actor)
72
+ end
73
+ end
74
+
75
+ [suspects, blacklist(connection)]
76
+ end
77
+
78
+ def self.blacklist(connection)
79
+ blacklisted = {}
80
+
81
+ connection.keys("*:*:blacklist").map {|d| d.split(":").first}.reject {|ip| ip.empty?}.each do |actor|
82
+ next unless connection.get("#{actor}:repsheet:blacklist") == "true"
83
+
84
+ blacklisted[actor] = Hash.new 0
85
+ blacklisted[actor][:detected] = triggered_rules(connection, actor).join(", ")
86
+ blacklisted[actor][:total] = score_actor(connection, actor)
87
+ end
88
+
89
+ blacklisted
90
+ end
91
+
92
+ def self.score_actor(connection, actor)
93
+ connection.zrange("#{actor}:detected", 0, -1).reduce(0) do |memo, rule|
94
+ memo += connection.zscore("#{actor}:detected", rule).to_i
95
+ end
96
+ end
97
+ end
@@ -35,7 +35,7 @@
35
35
  <div class="nav-collapse collapse">
36
36
  <ul class="nav">
37
37
  <li><a href="<%= @mount %>">Actors</a></li>
38
- <li><a href="<%= @mount %>breakdown">Breakdown</a></li>
38
+ <!-- <li><a href="<%= @mount %>breakdown">Breakdown</a></li> -->
39
39
  <li><a href="<%= @mount %>worldview">Worldview</a></li>
40
40
  </ul>
41
41
  </div>
@@ -90,7 +90,7 @@
90
90
  <div class="nav-collapse collapse">
91
91
  <ul class="nav">
92
92
  <li class="active"><a href="<%= @mount %>">Actors</a></li>
93
- <li><a href="<%= @mount %>breakdown">Breakdown</a></li>
93
+ <!-- <li><a href="<%= @mount %>breakdown">Breakdown</a></li> -->
94
94
  <li><a href="<%= @mount %>worldview">Worldview</a></li>
95
95
  </ul>
96
96
  </div>
@@ -38,7 +38,7 @@
38
38
  <div class="nav-collapse collapse">
39
39
  <ul class="nav">
40
40
  <li><a href="<%= @mount %>">Actors</a></li>
41
- <li><a href="<%= @mount %>breakdown">Breakdown</a></li>
41
+ <!-- <li><a href="<%= @mount %>breakdown">Breakdown</a></li> -->
42
42
  <li class="active"><a href="<%= @mount %>worldview">Worldview</a></li>
43
43
  </ul>
44
44
  </div>
@@ -1,3 +1,3 @@
1
1
  module RepsheetVisualizer
2
- VERSION = "0.1.6"
2
+ VERSION = "0.1.7"
3
3
  end
@@ -1 +1,2 @@
1
1
  require 'repsheet_visualizer/application/app.rb'
2
+ require 'repsheet_visualizer/application/backend.rb'
@@ -26,4 +26,5 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "bundler"
27
27
  spec.add_development_dependency "rake"
28
28
  spec.add_development_dependency "unicorn"
29
+ spec.add_development_dependency "rspec"
29
30
  end
data/script/fill CHANGED
@@ -6,21 +6,29 @@ r = Redis.new
6
6
  r.flushdb
7
7
 
8
8
  255.times do |i|
9
- r.set("1.1.1.#{i}:950001:count", rand(1000))
10
- r.lpush("1.1.1.#{i}:requests", "request")
11
- r.sadd("1.1.1.#{i}:detected", "950001")
9
+ r.zincrby("1.1.1.#{i}:detected", rand(1000), "950001")
10
+ r.lpush("1.1.1.#{i}:requests", "123, Chrome, GET, foo, bar")
11
+
12
12
  if i > 220
13
13
  r.set("1.1.1.#{i}:repsheet:blacklist", "true")
14
14
  end
15
+
16
+ r.expire("1.1.1.#{i}:requests", (24 * 60 * 60))
17
+ r.expire("1.1.1.#{i}:detected", (24 * 60 * 60))
15
18
  end
16
19
 
17
20
  255.times do |i|
18
- r.lpush("2.2.2.#{i}:requests", "request")
21
+ r.lpush("1.1.1.#{i}:requests", "123, Chrome, POST, foo, bar")
19
22
  end
20
23
 
21
24
 
22
25
  20.times do |i|
23
- r.sadd("1.1.1.1:detected", "9900#{i + 10}")
24
- r.set("1.1.1.1:9900#{i + 10}:count", 1000)
26
+ r.zincrby("1.1.1.1:detected", 1000, "9900#{i + 10}")
25
27
  end
26
28
 
29
+ r.lpush("1.1.1.1:requests", "123, Chrome, PUT, foo, bar")
30
+ r.lpush("1.1.1.1:requests", "123, Chrome, DELETE, foo, bar")
31
+ r.lpush("1.1.1.1:requests", "123, Chrome, HEAD, foo, bar")
32
+ r.lpush("1.1.1.1:requests", "123, Chrome, TRACE, foo, bar")
33
+ r.lpush("1.1.1.1:requests", "123, Chrome, OPTIONS, foo, bar")
34
+ r.lpush("1.1.1.1:requests", "123, Chrome, CONNECT, foo, bar")
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Backend do
4
+ before(:each) {@connection = Redis.new}
5
+ after(:each) {@connection.flushdb}
6
+
7
+ describe ".summary" do
8
+ it "call the optimized routine if the proper keys exist" do
9
+ @connection.zincrby("offenders", 10, "1.1.1.1")
10
+ @connection.sadd("detected", "950001")
11
+ Backend.should_receive(:optimized) { [{},{}] }
12
+ Backend.summary(@connection)
13
+ end
14
+
15
+ it "calls the standard routine if they don't" do
16
+ @connection.del("offenders")
17
+ @connection.sadd("detected", "950001")
18
+ Backend.should_receive(:standard) { [{},{}] }
19
+ Backend.summary(@connection)
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'redis'
4
+ require_relative '../lib/repsheet_visualizer/application/backend'
5
+
6
+ RSpec.configure do |config|
7
+ config.treat_symbols_as_metadata_keys_with_true_values = true
8
+ config.run_all_when_everything_filtered = true
9
+ config.filter_run :focus
10
+ config.order = 'random'
11
+ end
metadata CHANGED
@@ -1,126 +1,125 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: repsheet_visualizer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.6
5
- prerelease:
4
+ version: 0.1.7
6
5
  platform: ruby
7
6
  authors:
8
7
  - Aaron Bedra
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2013-06-20 00:00:00.000000000 Z
11
+ date: 2013-07-25 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: geoip
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
- - - ! '>='
17
+ - - '>='
20
18
  - !ruby/object:Gem::Version
21
19
  version: '0'
22
20
  type: :runtime
23
21
  prerelease: false
24
22
  version_requirements: !ruby/object:Gem::Requirement
25
- none: false
26
23
  requirements:
27
- - - ! '>='
24
+ - - '>='
28
25
  - !ruby/object:Gem::Version
29
26
  version: '0'
30
27
  - !ruby/object:Gem::Dependency
31
28
  name: json
32
29
  requirement: !ruby/object:Gem::Requirement
33
- none: false
34
30
  requirements:
35
- - - ! '>='
31
+ - - '>='
36
32
  - !ruby/object:Gem::Version
37
33
  version: '0'
38
34
  type: :runtime
39
35
  prerelease: false
40
36
  version_requirements: !ruby/object:Gem::Requirement
41
- none: false
42
37
  requirements:
43
- - - ! '>='
38
+ - - '>='
44
39
  - !ruby/object:Gem::Version
45
40
  version: '0'
46
41
  - !ruby/object:Gem::Dependency
47
42
  name: redis
48
43
  requirement: !ruby/object:Gem::Requirement
49
- none: false
50
44
  requirements:
51
- - - ! '>='
45
+ - - '>='
52
46
  - !ruby/object:Gem::Version
53
47
  version: '0'
54
48
  type: :runtime
55
49
  prerelease: false
56
50
  version_requirements: !ruby/object:Gem::Requirement
57
- none: false
58
51
  requirements:
59
- - - ! '>='
52
+ - - '>='
60
53
  - !ruby/object:Gem::Version
61
54
  version: '0'
62
55
  - !ruby/object:Gem::Dependency
63
56
  name: sinatra
64
57
  requirement: !ruby/object:Gem::Requirement
65
- none: false
66
58
  requirements:
67
- - - ! '>='
59
+ - - '>='
68
60
  - !ruby/object:Gem::Version
69
61
  version: '0'
70
62
  type: :runtime
71
63
  prerelease: false
72
64
  version_requirements: !ruby/object:Gem::Requirement
73
- none: false
74
65
  requirements:
75
- - - ! '>='
66
+ - - '>='
76
67
  - !ruby/object:Gem::Version
77
68
  version: '0'
78
69
  - !ruby/object:Gem::Dependency
79
70
  name: bundler
80
71
  requirement: !ruby/object:Gem::Requirement
81
- none: false
82
72
  requirements:
83
- - - ! '>='
73
+ - - '>='
84
74
  - !ruby/object:Gem::Version
85
75
  version: '0'
86
76
  type: :development
87
77
  prerelease: false
88
78
  version_requirements: !ruby/object:Gem::Requirement
89
- none: false
90
79
  requirements:
91
- - - ! '>='
80
+ - - '>='
92
81
  - !ruby/object:Gem::Version
93
82
  version: '0'
94
83
  - !ruby/object:Gem::Dependency
95
84
  name: rake
96
85
  requirement: !ruby/object:Gem::Requirement
97
- none: false
98
86
  requirements:
99
- - - ! '>='
87
+ - - '>='
100
88
  - !ruby/object:Gem::Version
101
89
  version: '0'
102
90
  type: :development
103
91
  prerelease: false
104
92
  version_requirements: !ruby/object:Gem::Requirement
105
- none: false
106
93
  requirements:
107
- - - ! '>='
94
+ - - '>='
108
95
  - !ruby/object:Gem::Version
109
96
  version: '0'
110
97
  - !ruby/object:Gem::Dependency
111
98
  name: unicorn
112
99
  requirement: !ruby/object:Gem::Requirement
113
- none: false
114
100
  requirements:
115
- - - ! '>='
101
+ - - '>='
116
102
  - !ruby/object:Gem::Version
117
103
  version: '0'
118
104
  type: :development
119
105
  prerelease: false
120
106
  version_requirements: !ruby/object:Gem::Requirement
121
- none: false
122
107
  requirements:
123
- - - ! '>='
108
+ - - '>='
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rspec
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - '>='
124
123
  - !ruby/object:Gem::Version
125
124
  version: '0'
126
125
  description: Visualizer for Repsheet
@@ -132,6 +131,7 @@ extensions: []
132
131
  extra_rdoc_files: []
133
132
  files:
134
133
  - .gitignore
134
+ - .rspec
135
135
  - .rvmrc
136
136
  - Gemfile
137
137
  - Gemfile.lock
@@ -141,6 +141,7 @@ files:
141
141
  - bin/repsheet_visualizer
142
142
  - lib/repsheet_visualizer.rb
143
143
  - lib/repsheet_visualizer/application/app.rb
144
+ - lib/repsheet_visualizer/application/backend.rb
144
145
  - lib/repsheet_visualizer/application/public/css/blue/asc.gif
145
146
  - lib/repsheet_visualizer/application/public/css/blue/bg.gif
146
147
  - lib/repsheet_visualizer/application/public/css/blue/desc.gif
@@ -162,29 +163,32 @@ files:
162
163
  - lib/repsheet_visualizer/version.rb
163
164
  - repsheet_visualizer.gemspec
164
165
  - script/fill
166
+ - spec/backend_spec.rb
167
+ - spec/spec_helper.rb
165
168
  homepage: https://github.com/Repsheet/visualizer
166
169
  licenses:
167
170
  - MIT
171
+ metadata: {}
168
172
  post_install_message:
169
173
  rdoc_options: []
170
174
  require_paths:
171
175
  - lib
172
176
  required_ruby_version: !ruby/object:Gem::Requirement
173
- none: false
174
177
  requirements:
175
- - - ! '>='
178
+ - - '>='
176
179
  - !ruby/object:Gem::Version
177
180
  version: '0'
178
181
  required_rubygems_version: !ruby/object:Gem::Requirement
179
- none: false
180
182
  requirements:
181
- - - ! '>='
183
+ - - '>='
182
184
  - !ruby/object:Gem::Version
183
185
  version: '0'
184
186
  requirements: []
185
187
  rubyforge_project:
186
- rubygems_version: 1.8.23
188
+ rubygems_version: 2.0.3
187
189
  signing_key:
188
- specification_version: 3
190
+ specification_version: 4
189
191
  summary: A visualization package for Repsheet
190
- test_files: []
192
+ test_files:
193
+ - spec/backend_spec.rb
194
+ - spec/spec_helper.rb