sql_safety_net 1.1.11
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/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)
|