sql_safety_net 1.1.11
Sign up to get free protection for your applications and to get access to all the features.
- data/License.txt +674 -0
- data/README.rdoc +19 -0
- data/Rakefile +40 -0
- data/lib/sql_safety_net/cache_store.rb +19 -0
- data/lib/sql_safety_net/configuration.rb +57 -0
- data/lib/sql_safety_net/connection_adapter/mysql_adapter.rb +43 -0
- data/lib/sql_safety_net/connection_adapter/postgresql_adapter.rb +48 -0
- data/lib/sql_safety_net/connection_adapter.rb +103 -0
- data/lib/sql_safety_net/query_analysis.rb +69 -0
- data/lib/sql_safety_net/rack_handler.rb +196 -0
- data/lib/sql_safety_net.rb +27 -0
- data/spec/cache_store_spec.rb +26 -0
- data/spec/configuration_spec.rb +67 -0
- data/spec/connection_adapter_spec.rb +143 -0
- data/spec/example_database.yml +14 -0
- data/spec/mysql_connection_adapter_spec.rb +32 -0
- data/spec/postgres_connection_adapter_spec.rb +43 -0
- data/spec/query_analysis_spec.rb +71 -0
- data/spec/rack_handler_spec.rb +193 -0
- data/spec/spec_helper.rb +56 -0
- metadata +148 -0
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::Configuration do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
@config = SqlSafetyNet::Configuration.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should be able to set debug" do
|
10
|
+
@config.debug?.should == false
|
11
|
+
@config.debug = true
|
12
|
+
@config.debug?.should == true
|
13
|
+
@config.debug = false
|
14
|
+
@config.debug?.should == false
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should be able to set header" do
|
18
|
+
@config.header?.should == false
|
19
|
+
@config.header = true
|
20
|
+
@config.header?.should == true
|
21
|
+
@config.header = false
|
22
|
+
@config.header?.should == false
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should be able to set table_scan_limit" do
|
26
|
+
@config.table_scan_limit = 500
|
27
|
+
@config.table_scan_limit.should == 500
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should be able to set temporary_table_limit" do
|
31
|
+
@config.temporary_table_limit = 50
|
32
|
+
@config.temporary_table_limit.should == 50
|
33
|
+
end
|
34
|
+
|
35
|
+
it "should be able to set filesort_limit" do
|
36
|
+
@config.filesort_limit = 600
|
37
|
+
@config.filesort_limit.should == 600
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should be able to set examine_rows_limit" do
|
41
|
+
@config.examine_rows_limit = 1000
|
42
|
+
@config.examine_rows_limit.should == 1000
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should be able to set return_rows_limit" do
|
46
|
+
@config.return_rows_limit = 1000
|
47
|
+
@config.return_rows_limit.should == 1000
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should be able to set query_limit" do
|
51
|
+
@config.query_limit = 20
|
52
|
+
@config.query_limit.should == 20
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should be able to set time_limit" do
|
56
|
+
@config.time_limit = 200
|
57
|
+
@config.time_limit.should == 200
|
58
|
+
end
|
59
|
+
|
60
|
+
it "should be able to set always_show" do
|
61
|
+
@config.always_show = true
|
62
|
+
@config.always_show?.should == true
|
63
|
+
@config.always_show = nil
|
64
|
+
@config.always_show?.should == false
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::ConnectionAdapter do
|
4
|
+
|
5
|
+
class SqlSafetyNet::TestConnectionAdapter < ActiveRecord::ConnectionAdapters::AbstractAdapter
|
6
|
+
class Model
|
7
|
+
attr_accessor :id, :name
|
8
|
+
def initialize (attrs)
|
9
|
+
@id = attrs["id"]
|
10
|
+
@name = attrs["name"]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def columns (table_name, name = nil)
|
15
|
+
select_rows("GET columns")
|
16
|
+
["id", "name"]
|
17
|
+
end
|
18
|
+
|
19
|
+
def select_rows (sql, name = nil, binds = [])
|
20
|
+
return [{"id" => 1, "name" => "foo"}, {"id" => 2, "name" => "bar"}]
|
21
|
+
end
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
def analyze_query (sql, name, *args)
|
26
|
+
if sql.match(/table scan/i)
|
27
|
+
{:flags => ["table scan"]}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def select (sql, name = nil, binds = [])
|
32
|
+
select_rows(sql, name).collect do |row|
|
33
|
+
Model.new(row)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
SqlSafetyNet.config.enable_on(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
before(:each) do
|
41
|
+
SqlSafetyNet.config.debug = true
|
42
|
+
SqlSafetyNet::QueryAnalysis.clear
|
43
|
+
end
|
44
|
+
|
45
|
+
after(:each) do
|
46
|
+
SqlSafetyNet::QueryAnalysis.clear
|
47
|
+
end
|
48
|
+
|
49
|
+
let(:connection){ SqlSafetyNet::TestConnectionAdapter.new(:connection) }
|
50
|
+
|
51
|
+
it "should not analyze the SQL select in the columns method" do
|
52
|
+
connection.should_receive(:columns_without_sql_safety_net).with("table", "columns").and_return(["col1", "col2"])
|
53
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
54
|
+
connection.columns("table", "columns").should == ["col1", "col2"]
|
55
|
+
end
|
56
|
+
analysis.selects.should == 0
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should not analyze the SQL select in the active? method" do
|
60
|
+
connection.should_receive(:active_without_sql_safety_net?).and_return(true)
|
61
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
62
|
+
connection.active?.should == true
|
63
|
+
end
|
64
|
+
analysis.selects.should == 0
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should determine if a SQL statement is a select statement" do
|
68
|
+
connection.select_statement?("SELECT * FROM TABLE").should == true
|
69
|
+
connection.select_statement?(" \n SELECT * FROM TABLE").should == true
|
70
|
+
connection.select_statement?("Select * From Table").should == true
|
71
|
+
connection.select_statement?("select * from table").should == true
|
72
|
+
connection.select_statement?("EXECUTE SELECT * FROM TABLE").should == false
|
73
|
+
end
|
74
|
+
|
75
|
+
[:select, :select_rows].each do |select_method|
|
76
|
+
context select_method do
|
77
|
+
it "should proxy the select method to the underlying adapter" do
|
78
|
+
connection.should_receive("#{select_method}_without_sql_safety_net").with('Select sql', 'name').and_return([:row1, :row2])
|
79
|
+
connection.send(select_method, 'Select sql', 'name').should == [:row1, :row2]
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should count selects" do
|
83
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
84
|
+
connection.send(select_method, 'Select * from table')
|
85
|
+
connection.send(select_method, 'Select * from table where whatever')
|
86
|
+
end
|
87
|
+
analysis.selects.should == 2
|
88
|
+
end
|
89
|
+
|
90
|
+
it "should count rows returned" do
|
91
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
92
|
+
connection.send(select_method, 'Select * from table')
|
93
|
+
connection.send(select_method, 'Select * from table where whatever')
|
94
|
+
end
|
95
|
+
analysis.rows.should == 4
|
96
|
+
end
|
97
|
+
|
98
|
+
it "should analyze select statements and keep track of bad queries" do
|
99
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
100
|
+
connection.send(select_method, 'Select * from table doing table scan')
|
101
|
+
end
|
102
|
+
analysis.non_flagged_queries.size.should == 0
|
103
|
+
analysis.flagged_queries.size.should == 1
|
104
|
+
analysis.flagged_queries.first[:sql].should == 'Select * from table doing table scan'
|
105
|
+
analysis.flagged_queries.first[:rows].should == 2
|
106
|
+
analysis.flagged_queries.first[:flags].should == ['table scan']
|
107
|
+
end
|
108
|
+
|
109
|
+
it "should analyze select statements and keep track of good queries" do
|
110
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
111
|
+
connection.send(select_method, 'Select * from table')
|
112
|
+
end
|
113
|
+
analysis.flagged_queries.size.should == 0
|
114
|
+
analysis.non_flagged_queries.size.should == 1
|
115
|
+
analysis.non_flagged_queries.first[:sql].should == 'Select * from table'
|
116
|
+
analysis.non_flagged_queries.first[:rows].should == 2
|
117
|
+
end
|
118
|
+
|
119
|
+
it "should flag queries that exceed the configured time limit" do
|
120
|
+
now = Time.now
|
121
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
122
|
+
Time.stub(:now).and_return(now, now + 100)
|
123
|
+
connection.send(select_method, 'Select * from table')
|
124
|
+
end
|
125
|
+
analysis.flagged_queries.size.should == 1
|
126
|
+
analysis.non_flagged_queries.size.should == 0
|
127
|
+
analysis.flagged_queries.first[:flags].should == ["query time exceeded #{SqlSafetyNet.config.time_limit} ms"]
|
128
|
+
end
|
129
|
+
|
130
|
+
it "should not analyze queries if debug mode disabled" do
|
131
|
+
SqlSafetyNet.config.debug = false
|
132
|
+
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
133
|
+
connection.send(select_method, 'SELECT * from table with table scan')
|
134
|
+
end
|
135
|
+
analysis.selects.should == 1
|
136
|
+
analysis.rows.should == 2
|
137
|
+
analysis.flagged_queries.size.should == 0
|
138
|
+
analysis.non_flagged_queries.size.should == 0
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# Copy this file to database.yml to run the test with connection settings for you environment.
|
2
|
+
# You must create the database in both mysql and postgres.
|
3
|
+
|
4
|
+
mysql:
|
5
|
+
adapter: mysql
|
6
|
+
username: root
|
7
|
+
password: ""
|
8
|
+
database: sql_safety_net_test
|
9
|
+
|
10
|
+
postgresql:
|
11
|
+
adapter: postgresql
|
12
|
+
username: postgres
|
13
|
+
password: postgres
|
14
|
+
database: sql_safety_net_test
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::ConnectionAdapter::MysqlAdapter do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@connection = SqlSafetyNet::MysqlTestModel.connection
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should use query plan analysis to flag queries" do
|
10
|
+
query_plan = [{'type' => 'ALL', 'rows' => 200}]
|
11
|
+
@connection.should_receive(:select_without_sql_safety_net).with('EXPLAIN Select sql', 'EXPLAIN', []).and_return(query_plan)
|
12
|
+
@connection.should_receive(:analyze_query_plan).with(query_plan).and_return(["bad query"])
|
13
|
+
@connection.analyze_query('Select sql', 'name', []).should == {:query_plan => query_plan, :flags => ["bad query"]}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should only analyze query plans for select statements" do
|
17
|
+
@connection.should_not_receive(:select_without_sql_safety_net)
|
18
|
+
@connection.should_not_receive(:analyze_query_plan)
|
19
|
+
@connection.analyze_query('Execute sql', 'name', []).should == nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should translate query plans into flags" do
|
23
|
+
@connection.send(:analyze_query_plan, [{'type' => 'ALL', 'rows' => 500}]).should == ["table scan", "no index used", "no indexes possible"]
|
24
|
+
@connection.send(:analyze_query_plan, [{'Extra' => 'using temporary table; using filesort', 'rows' => 200, 'key' => 'index', 'possible_keys' => 'index'}]).should == ["uses temporary table for 200 rows", "uses filesort for 200 rows"]
|
25
|
+
@connection.send(:analyze_query_plan, [{'select_type' => 'dependent subquery', 'rows' => 20, 'key' => 'index', 'possible_keys' => 'index'}]).should == ["dependent subquery"]
|
26
|
+
@connection.send(:analyze_query_plan, [{'select_type' => 'uncacheable subquery', 'rows' => 20, 'key' => 'index', 'possible_keys' => 'index'}]).should == ["uncacheable subquery"]
|
27
|
+
@connection.send(:analyze_query_plan, [{'rows' => 20000, 'key' => 'index', 'possible_keys' => 'index'}]).should == ["examines 20000 rows"]
|
28
|
+
@connection.send(:analyze_query_plan, [{'select_type' => 'SIMPLE', 'rows' => 1, 'key' => 'index', 'possible_keys' => 'index'}]).should == []
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::ConnectionAdapter::PostgreSQLAdapter do
|
4
|
+
|
5
|
+
before(:each) do
|
6
|
+
@connection = SqlSafetyNet::PostgresqlTestModel.connection
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should use query plan analysis to flag queries" do
|
10
|
+
query_plan = [{"QUERY PLAN"=>"Limit (cost=0.00..0.06 rows=1 width=335)"}, {"QUERY PLAN"=>" -> Seq Scan on records (cost=0.00..12.20 rows=220 width=335)"}]
|
11
|
+
@connection.should_receive(:select_without_sql_safety_net).with('EXPLAIN Select sql', 'EXPLAIN', []).and_return(query_plan)
|
12
|
+
@connection.should_receive(:analyze_query_plan).with(query_plan).and_return(["bad query"])
|
13
|
+
@connection.analyze_query('Select sql', 'name', []).should == {:query_plan => query_plan, :flags => ["bad query"]}
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should only analyze query plans for select statements" do
|
17
|
+
@connection.should_not_receive(:select_without_sql_safety_net)
|
18
|
+
@connection.should_not_receive(:analyze_query_plan)
|
19
|
+
@connection.analyze_query('Execute sql', 'name', []).should == nil
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should not flag a small table scan" do
|
23
|
+
query_plan = [{"QUERY PLAN"=>"Seq Scan on records (cost=0.00..12.20 rows=10 width=335)"}]
|
24
|
+
@connection.send(:analyze_query_plan, query_plan).should == []
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should flag a table scan" do
|
28
|
+
query_plan = [{"QUERY PLAN"=>"Seq Scan on records (cost=0.00..12.20 rows=1000000 width=335)"}]
|
29
|
+
@connection.send(:analyze_query_plan, query_plan).should == ["table scan"]
|
30
|
+
end
|
31
|
+
|
32
|
+
it "should flag too many rows returned" do
|
33
|
+
query_plan = [{"QUERY PLAN"=>"Index Scan using records_pkey on records (cost=0.00..8.27 rows=1000000 width=336)"}]
|
34
|
+
@connection.send(:analyze_query_plan, query_plan).should == ["examines 1000000 rows"]
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should apply a limit to the rows returned" do
|
38
|
+
query_plan = [{"QUERY PLAN"=>"Limit (cost=0.00..0.06 rows=1 width=335)"}, {"QUERY PLAN"=>" -> Seq Scan on records (cost=0.00..12.20 rows=1000000 width=335)"}]
|
39
|
+
@connection.send(:analyze_query_plan, query_plan).should == []
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::QueryAnalysis do
|
4
|
+
|
5
|
+
let(:analysis){ SqlSafetyNet::QueryAnalysis.new }
|
6
|
+
|
7
|
+
it "should add a flagged query" do
|
8
|
+
analysis.add_query("sql", "test", 10, 0.01, :query_plan => "this sucks", :flags => ["write better sql"])
|
9
|
+
analysis.non_flagged_queries.should be_empty
|
10
|
+
query = analysis.flagged_queries.first
|
11
|
+
query[:sql].should == "sql"
|
12
|
+
query[:name].should == "test"
|
13
|
+
query[:rows].should == 10
|
14
|
+
query[:elapsed_time].should == 0.01
|
15
|
+
query[:query_plan].should == "this sucks"
|
16
|
+
query[:flags].should == ["write better sql"]
|
17
|
+
query[:cached].should == false
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should add a non-flagged query" do
|
21
|
+
analysis.add_query("sql", "test", 10, 0.01, nil)
|
22
|
+
analysis.flagged_queries.should be_empty
|
23
|
+
query = analysis.non_flagged_queries.first
|
24
|
+
query[:sql].should == "sql"
|
25
|
+
query[:name].should == "test"
|
26
|
+
query[:rows].should == 10
|
27
|
+
query[:elapsed_time].should == 0.01
|
28
|
+
query[:cached].should == false
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should determine if any query is flagged" do
|
32
|
+
analysis.add_query("sql", "test", 10, 0.01, :query_plan => "this sucks", :flags => ["write better sql"])
|
33
|
+
analysis.add_query("sql", "test", 10, 0.01, nil)
|
34
|
+
analysis.should be_flagged
|
35
|
+
analysis.flagged_queries?.should == true
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should determine if the rows selected is flagged" do
|
39
|
+
analysis.rows = 1000000
|
40
|
+
analysis.should be_flagged
|
41
|
+
analysis.too_many_rows?.should == true
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should determine if the number of selects is flagged" do
|
45
|
+
analysis.selects = 100000
|
46
|
+
analysis.should be_flagged
|
47
|
+
analysis.too_many_selects?.should == true
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should set the analysis object within a block" do
|
51
|
+
SqlSafetyNet::QueryAnalysis.analyze do
|
52
|
+
SqlSafetyNet::QueryAnalysis.current.should be_a(SqlSafetyNet::QueryAnalysis)
|
53
|
+
end
|
54
|
+
SqlSafetyNet::QueryAnalysis.current.should == nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should determine if a query is happening in a cache block" do
|
58
|
+
cache = ActiveSupport::Cache::MemoryStore.new
|
59
|
+
analysis = nil
|
60
|
+
SqlSafetyNet::QueryAnalysis.analyze do
|
61
|
+
val = cache.fetch("key") do
|
62
|
+
analysis = SqlSafetyNet::QueryAnalysis.current
|
63
|
+
analysis.add_query("sql", "test", 10, 0.01, nil)
|
64
|
+
"woot"
|
65
|
+
end
|
66
|
+
val.should == "woot"
|
67
|
+
end
|
68
|
+
query = analysis.non_flagged_queries.first
|
69
|
+
query[:cached].should == true
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::RackHandler do
|
4
|
+
|
5
|
+
class SqlSafetyNet::TestApp
|
6
|
+
attr_accessor :response, :block
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@response = Rack::Response.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def call (env)
|
13
|
+
@block.call(env) if @block
|
14
|
+
@response["Content-Type"] = env["response_content_type"] if env["response_content_type"]
|
15
|
+
@response.finish
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
before(:each) do
|
20
|
+
SqlSafetyNet.config.debug = true
|
21
|
+
SqlSafetyNet::QueryAnalysis.clear
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:logger) do
|
25
|
+
logger = Object.new
|
26
|
+
def logger.warn (message)
|
27
|
+
# noop
|
28
|
+
end
|
29
|
+
logger
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:app){ SqlSafetyNet::TestApp.new }
|
33
|
+
|
34
|
+
let(:handler){ SqlSafetyNet::RackHandler.new(app, logger) }
|
35
|
+
|
36
|
+
let(:env) do
|
37
|
+
{
|
38
|
+
"rack.url_scheme" => "http",
|
39
|
+
"PATH_INFO" => "/test",
|
40
|
+
"SERVER_PORT" => 80,
|
41
|
+
"HTTP_HOST" => "example.com",
|
42
|
+
"REQUEST_METHOD" => "GET"
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should append the bad queries to the HTML when debug is enabled and always_show is false" do
|
47
|
+
SqlSafetyNet.config.always_show = false
|
48
|
+
|
49
|
+
app.block = lambda do |env|
|
50
|
+
SqlSafetyNet::QueryAnalysis.current.flagged_queries << {:sql => 'sql', :query_plan => 'bad plan', :flags => ['bad query'], :rows => 4, :elapsed_time => 0.1}
|
51
|
+
end
|
52
|
+
app.response.write("Monkeys are neat.")
|
53
|
+
|
54
|
+
r = handler.call(env)
|
55
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
56
|
+
|
57
|
+
response.body.join("").should include('<div id="sql_safety_net_warning"')
|
58
|
+
response.body.join("").should include("Monkeys are neat.")
|
59
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=1"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should not append the bad queries to the HTML when there are none and always_show is false" do
|
63
|
+
SqlSafetyNet.config.always_show = false
|
64
|
+
|
65
|
+
app.response.write("Monkeys are neat.")
|
66
|
+
|
67
|
+
r = handler.call(env)
|
68
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
69
|
+
|
70
|
+
response.body.join("").should == "Monkeys are neat."
|
71
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=0"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should append the queries to the HTML when there are none and always_show is true" do
|
75
|
+
SqlSafetyNet.config.always_show = true
|
76
|
+
|
77
|
+
app.response.write("Monkeys are neat.")
|
78
|
+
|
79
|
+
r = handler.call(env)
|
80
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
81
|
+
|
82
|
+
response.body.join("").should include('<div id="sql_safety_net_warning"')
|
83
|
+
response.body.join("").should include("Monkeys are neat.")
|
84
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=0"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should append the bad queries to XML when debug is enabled" do
|
88
|
+
SqlSafetyNet.config.always_show = false
|
89
|
+
|
90
|
+
app.block = lambda do |env|
|
91
|
+
SqlSafetyNet::QueryAnalysis.current.flagged_queries << {:sql => 'sql', :query_plan => 'bad plan', :flags => ['bad query'], :rows => 4, :elapsed_time => 0.1}
|
92
|
+
end
|
93
|
+
app.response.write("<woot>Monkeys are neat.</woot>")
|
94
|
+
|
95
|
+
r = handler.call(env.merge("response_content_type" => "application/xml"))
|
96
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
97
|
+
|
98
|
+
response.body.join("").should_not include('<div id="sql_safety_net_warning"')
|
99
|
+
response.body.join("").should include("<!-- SqlSafetyNet")
|
100
|
+
response.body.join("").should include("<woot>Monkeys are neat.</woot>")
|
101
|
+
Hash.from_xml(response.body.join('')).should == {"woot" => "Monkeys are neat."}
|
102
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=1"
|
103
|
+
end
|
104
|
+
|
105
|
+
it "should not append the bad queries to the HTML or add a response header if debug is not enabled" do
|
106
|
+
SqlSafetyNet.config.debug = false
|
107
|
+
|
108
|
+
app.block = lambda do |env|
|
109
|
+
SqlSafetyNet::QueryAnalysis.current.flagged_queries << {:sql => 'sql', :query_plan => 'bad plan', :flags => ['bad query'], :rows => 4, :elapsed_time => 0.1}
|
110
|
+
end
|
111
|
+
app.response.write("Monkeys are neat.")
|
112
|
+
|
113
|
+
r = handler.call(env)
|
114
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
115
|
+
|
116
|
+
response.body.join("").should == "Monkeys are neat."
|
117
|
+
response["X-SqlSafetyNet"].should == nil
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should not append the bad queries to the HTML if not text/html" do
|
121
|
+
app.block = lambda do |env|
|
122
|
+
SqlSafetyNet::QueryAnalysis.current.flagged_queries << {:sql => 'sql', :query_plan => 'bad plan', :flags => ['bad query'], :rows => 4, :elapsed_time => 0.1}
|
123
|
+
end
|
124
|
+
app.response.write("Monkeys are neat.")
|
125
|
+
app.response["Content-Type"] = "text/plain"
|
126
|
+
|
127
|
+
r = handler.call(env)
|
128
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
129
|
+
|
130
|
+
response.body.join("").should == "Monkeys are neat."
|
131
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=1"
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should not append the bad queries to the HTML if Ajax request" do
|
135
|
+
app.block = lambda do |env|
|
136
|
+
SqlSafetyNet::QueryAnalysis.current.flagged_queries << {:sql => 'sql', :query_plan => 'bad plan', :flags => ['bad query'], :rows => 4, :elapsed_time => 0.1}
|
137
|
+
end
|
138
|
+
app.response.write("Monkeys are neat.")
|
139
|
+
app.response["Content-Type"] = "text/plain"
|
140
|
+
|
141
|
+
r = handler.call(env.merge("HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"))
|
142
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
143
|
+
|
144
|
+
response.body.join("").should == "Monkeys are neat."
|
145
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=0; elapsed_time=0; flagged_queries=1"
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should log too many selects even if debug is not enabled" do
|
149
|
+
SqlSafetyNet.config.debug = false
|
150
|
+
app.block = lambda do |env|
|
151
|
+
SqlSafetyNet::QueryAnalysis.current.selects = 1000
|
152
|
+
end
|
153
|
+
logger.should_receive(:warn).with("Excess database usage: request generated 1000 queries and returned 0 rows [GET http://example.com/test]")
|
154
|
+
r = handler.call(env)
|
155
|
+
end
|
156
|
+
|
157
|
+
it "should log too many rows even if debug is not enabled" do
|
158
|
+
SqlSafetyNet.config.debug = false
|
159
|
+
SqlSafetyNet.config.header = false
|
160
|
+
app.block = lambda do |env|
|
161
|
+
SqlSafetyNet::QueryAnalysis.current.rows = 10000
|
162
|
+
end
|
163
|
+
app.response.write("Monkeys are neat.")
|
164
|
+
|
165
|
+
logger.should_receive(:warn).with("Excess database usage: request generated 0 queries and returned 10000 rows [GET http://example.com/test]")
|
166
|
+
r = handler.call(env)
|
167
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
168
|
+
|
169
|
+
response.body.join("").should == "Monkeys are neat."
|
170
|
+
response["X-SqlSafetyNet"].should == nil
|
171
|
+
end
|
172
|
+
|
173
|
+
it "should log too many rows even if debug is not enabled" do
|
174
|
+
SqlSafetyNet.config.debug = false
|
175
|
+
SqlSafetyNet.config.header = true
|
176
|
+
app.block = lambda do |env|
|
177
|
+
SqlSafetyNet::QueryAnalysis.current.rows = 10
|
178
|
+
end
|
179
|
+
app.response.write("Monkeys are neat.")
|
180
|
+
r = handler.call(env)
|
181
|
+
response = Rack::Response.new(r[2], r[0], r[1])
|
182
|
+
|
183
|
+
response.body.join("").should == "Monkeys are neat."
|
184
|
+
response["X-SqlSafetyNet"].should == "selects=0; rows=10; elapsed_time=0; flagged_queries=0"
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should not log warnings if there are none" do
|
188
|
+
SqlSafetyNet.config.debug = false
|
189
|
+
logger.should_not_receive(:warn)
|
190
|
+
r = handler.call(env)
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
|
3
|
+
active_record_version = ENV['ACTIVE_RECORD_VERSION'] || ">=2.2.2"
|
4
|
+
gem 'rails', active_record_version
|
5
|
+
gem 'activerecord', active_record_version
|
6
|
+
gem 'activesupport', active_record_version
|
7
|
+
require 'active_support/all'
|
8
|
+
require 'active_record'
|
9
|
+
puts "Testing against #{ActiveRecord::VERSION::STRING}"
|
10
|
+
|
11
|
+
require 'mysql'
|
12
|
+
require 'pg'
|
13
|
+
|
14
|
+
begin
|
15
|
+
require 'simplecov'
|
16
|
+
SimpleCov.start do
|
17
|
+
add_filter "/spec/"
|
18
|
+
end
|
19
|
+
rescue LoadError
|
20
|
+
# simplecov not installed
|
21
|
+
end
|
22
|
+
|
23
|
+
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'sql_safety_net'))
|
24
|
+
|
25
|
+
module SqlSafetyNet
|
26
|
+
class TestModel < ActiveRecord::Base
|
27
|
+
def self.create_tables
|
28
|
+
connection.create_table(table_name) do |t|
|
29
|
+
t.string :name
|
30
|
+
end unless table_exists?
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.drop_tables
|
34
|
+
connection.drop_table(table_name)
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.database_config
|
38
|
+
database_yml = File.expand_path(File.join(File.dirname(__FILE__), 'database.yml'))
|
39
|
+
raise "You must create a database.yml file in the spec directory (see example_database.yml)" unless File.exist?(database_yml)
|
40
|
+
YAML.load_file(database_yml)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
class MysqlTestModel < TestModel
|
45
|
+
establish_connection(database_config['mysql'])
|
46
|
+
end
|
47
|
+
|
48
|
+
class PostgresqlTestModel < TestModel
|
49
|
+
establish_connection(database_config['postgresql'])
|
50
|
+
end
|
51
|
+
|
52
|
+
SqlSafetyNet.config.enable_on(SqlSafetyNet::MysqlTestModel.connection.class)
|
53
|
+
SqlSafetyNet.config.enable_on(SqlSafetyNet::PostgresqlTestModel.connection.class)
|
54
|
+
end
|
55
|
+
|
56
|
+
ActiveSupport::Cache::Store.send(:include, SqlSafetyNet::CacheStore)
|