sql_safety_net 1.1.11 → 2.0.0
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/README.rdoc +32 -11
- data/Rakefile +3 -5
- data/lib/sql_safety_net/cache_store.rb +17 -7
- data/lib/sql_safety_net/configuration.rb +35 -46
- data/lib/sql_safety_net/connection_adapter.rb +49 -80
- data/lib/sql_safety_net/explain_plan/mysql.rb +33 -0
- data/lib/sql_safety_net/explain_plan/postgresql.rb +30 -0
- data/lib/sql_safety_net/explain_plan.rb +24 -0
- data/lib/sql_safety_net/formatter.rb +178 -0
- data/lib/sql_safety_net/middleware.rb +41 -0
- data/lib/sql_safety_net/query_analysis.rb +52 -49
- data/lib/sql_safety_net/query_info.rb +33 -0
- data/lib/sql_safety_net.rb +68 -17
- data/spec/cache_store_spec.rb +8 -17
- data/spec/configuration_spec.rb +63 -58
- data/spec/connection_adapter_spec.rb +109 -114
- data/spec/explain_plan/mysql_spec.rb +113 -0
- data/spec/explain_plan/postgresql_spec.rb +44 -0
- data/spec/formatter_spec.rb +46 -0
- data/spec/middleware_spec.rb +90 -0
- data/spec/query_analysis_spec.rb +92 -50
- data/spec/query_info_spec.rb +46 -0
- data/spec/spec_helper.rb +7 -26
- data/spec/sql_safety_net_spec.rb +21 -0
- metadata +112 -89
- data/lib/sql_safety_net/connection_adapter/mysql_adapter.rb +0 -43
- data/lib/sql_safety_net/connection_adapter/postgresql_adapter.rb +0 -48
- data/lib/sql_safety_net/rack_handler.rb +0 -196
- data/spec/example_database.yml +0 -14
- data/spec/mysql_connection_adapter_spec.rb +0 -32
- data/spec/postgres_connection_adapter_spec.rb +0 -43
- data/spec/rack_handler_spec.rb +0 -193
@@ -2,142 +2,137 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe SqlSafetyNet::ConnectionAdapter do
|
4
4
|
|
5
|
-
|
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
|
5
|
+
let(:connection){ SqlSafetyNet::TestModel.connection }
|
44
6
|
|
45
|
-
|
46
|
-
SqlSafetyNet::
|
7
|
+
before :each do
|
8
|
+
SqlSafetyNet::TestModel.delete_all
|
9
|
+
SqlSafetyNet::TestModel.create!(:name => "test", :value => 10)
|
47
10
|
end
|
48
11
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
54
|
-
connection.columns("table", "columns").should == ["col1", "col2"]
|
12
|
+
describe "injection" do
|
13
|
+
it "should analyze queries in the select_rows method" do
|
14
|
+
connection.should_receive(:analyze_query).with("select name, value from test_models", "SQL", []).and_yield
|
15
|
+
connection.select_rows("select name, value from test_models", "SQL").should == [["test", 10]]
|
55
16
|
end
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
connection.should_receive(:active_without_sql_safety_net?).and_return(true)
|
61
|
-
analysis = SqlSafetyNet::QueryAnalysis.analyze do
|
62
|
-
connection.active?.should == true
|
17
|
+
|
18
|
+
it "should analyze queries in the select method" do
|
19
|
+
connection.should_receive(:analyze_query).with("select name, value from test_models", "SQL", []).and_yield
|
20
|
+
connection.send(:select, "select name, value from test_models", "SQL").should == [{"name"=>"test", "value"=>10}]
|
63
21
|
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
22
|
end
|
74
23
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
connection.send(select_method, 'Select sql', 'name').should == [:row1, :row2]
|
80
|
-
end
|
24
|
+
describe "analysis" do
|
25
|
+
it "should not blow up if there is no current QueryAnalysis" do
|
26
|
+
connection.select_rows("select name, value from test_models", "SQL").should == [["test", 10]]
|
27
|
+
end
|
81
28
|
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
29
|
+
context "select statements" do
|
30
|
+
before :each do
|
31
|
+
SqlSafetyNet::TestModel.create!(:name => "foo", :value => 100)
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should analyze select statements" do
|
35
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
36
|
+
results = connection.send(:select, "select name, value from test_models order by name")
|
37
|
+
results.should == [{"name" => "foo", "value" => 100}, {"name" => "test", "value" => 10}]
|
38
|
+
analysis.queries.size.should == 1
|
39
|
+
query_info = analysis.queries.first
|
40
|
+
query_info.sql.should == "select name, value from test_models order by name"
|
41
|
+
query_info.rows.should == 2
|
42
|
+
query_info.result_size.should == 12
|
43
|
+
query_info.elapsed_time.should > 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# ActiveRecord < 3.1 doesn't have the binds parameter
|
48
|
+
if ActiveRecord::VERSION::MAJOR > 3 || (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR >= 1)
|
49
|
+
it "should analyze select statements using bind variables" do
|
50
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
51
|
+
name_column = SqlSafetyNet::TestModel.columns_hash["name"]
|
52
|
+
results = connection.send(:select, "select name, value from test_models where name = ? order by name", "SQL", [[name_column, "foo"]])
|
53
|
+
results.should == [{"name" => "foo", "value" => 100}]
|
54
|
+
analysis.queries.size.should == 1
|
55
|
+
query_info = analysis.queries.first
|
56
|
+
query_info.sql.should == 'select name, value from test_models where name = ? order by name [["name", "foo"]]'
|
57
|
+
query_info.rows.should == 1
|
58
|
+
query_info.result_size.should == 6
|
59
|
+
query_info.elapsed_time.should > 0
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should analyze select_rows statements" do
|
65
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
66
|
+
results = connection.select_rows("select name, value from test_models order by name")
|
67
|
+
results.should == [["foo", 100], ["test", 10]]
|
68
|
+
analysis.queries.size.should == 1
|
69
|
+
query_info = analysis.queries.first
|
70
|
+
query_info.sql.should == "select name, value from test_models order by name"
|
71
|
+
query_info.rows.should == 2
|
72
|
+
query_info.result_size.should == 12
|
73
|
+
query_info.elapsed_time.should > 0
|
86
74
|
end
|
87
|
-
analysis.selects.should == 2
|
88
75
|
end
|
76
|
+
end
|
89
77
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
connection.
|
78
|
+
context "non-select statements" do
|
79
|
+
it "should not analyze schema statements" do
|
80
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
81
|
+
results = connection.select_rows("INSERT INTO test_models (name, value) VALUES ('moo', 1)")
|
82
|
+
analysis.queries.should be_empty
|
94
83
|
end
|
95
|
-
analysis.rows.should == 4
|
96
84
|
end
|
97
|
-
|
98
|
-
it "should analyze
|
99
|
-
|
100
|
-
connection.
|
85
|
+
|
86
|
+
it "should not analyze explain statements" do
|
87
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
88
|
+
results = connection.select_rows("EXPLAIN select * from test_models")
|
89
|
+
analysis.queries.should be_empty
|
101
90
|
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
91
|
end
|
108
|
-
|
109
|
-
it "should analyze
|
110
|
-
|
111
|
-
connection.
|
92
|
+
|
93
|
+
it "should not analyze insert statements" do
|
94
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
95
|
+
results = connection.select_rows("INSERT INTO test_models (name, value) VALUES ('moo', 1)")
|
96
|
+
analysis.queries.should be_empty
|
112
97
|
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
98
|
end
|
118
99
|
|
119
|
-
it "should
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
connection.send(select_method, 'Select * from table')
|
100
|
+
it "should not analyze update statements" do
|
101
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
102
|
+
results = connection.select_rows("UPDATE test_models SET value = 1 WHERE value = 10")
|
103
|
+
analysis.queries.should be_empty
|
124
104
|
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
105
|
end
|
129
|
-
|
130
|
-
it "should not analyze
|
131
|
-
SqlSafetyNet.
|
132
|
-
|
133
|
-
|
106
|
+
|
107
|
+
it "should not analyze delete statements" do
|
108
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
109
|
+
results = connection.select_rows("DELETE FROM test_models WHERE id = 1")
|
110
|
+
analysis.queries.should be_empty
|
134
111
|
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
112
|
end
|
140
113
|
end
|
141
114
|
end
|
142
|
-
|
115
|
+
|
116
|
+
describe "ActiveRecord finders" do
|
117
|
+
it "should analyze the queries" do
|
118
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
119
|
+
model = SqlSafetyNet::TestModel.all.first
|
120
|
+
SqlSafetyNet::TestModel.find(model.id)
|
121
|
+
analysis.total_queries.should == 2
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "explain plan analysis" do
|
127
|
+
it "should do further analysis on queries when the adapter support query plan analysis" do
|
128
|
+
connection.should_receive(:respond_to?).with(:sql_safety_net_analyze_query_plan).and_return(true)
|
129
|
+
connection.should_receive(:sql_safety_net_analyze_query_plan).with("SELECT * from test_models", []).and_return(["table scan"])
|
130
|
+
|
131
|
+
SqlSafetyNet::QueryAnalysis.capture do |analysis|
|
132
|
+
model = connection.select_all("SELECT * from test_models")
|
133
|
+
analysis.queries.first.alerts.should == ["table scan"]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
143
138
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::ExplainPlan::Mysql do
|
4
|
+
|
5
|
+
class MockMysqlConnection; end
|
6
|
+
SqlSafetyNet::ExplainPlan.enable_on_connection_adapter!(MockMysqlConnection, :mysql)
|
7
|
+
|
8
|
+
let(:connection){ MockMysqlConnection.new }
|
9
|
+
let(:sql){ "SELECT * FROM *" }
|
10
|
+
|
11
|
+
it "should detect table scans" do
|
12
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"type" => "ALL", "rows" => 100}])
|
13
|
+
|
14
|
+
SqlSafetyNet.override_config do |config|
|
15
|
+
config.table_scan_limit = 99
|
16
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
17
|
+
alerts.should include("table scan on 100 rows")
|
18
|
+
|
19
|
+
config.table_scan_limit = 100
|
20
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
21
|
+
alerts.should be_empty
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should detect no indexes being used" do
|
26
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"key" => nil, "rows" => 100}])
|
27
|
+
|
28
|
+
SqlSafetyNet.override_config do |config|
|
29
|
+
config.table_scan_limit = 99
|
30
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
31
|
+
alerts.should include("no index used")
|
32
|
+
|
33
|
+
config.table_scan_limit = 100
|
34
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
35
|
+
alerts.should_not include("no index used")
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
it "should detect no indexes possible" do
|
40
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"possible_keys" => nil, "rows" => 100}])
|
41
|
+
|
42
|
+
SqlSafetyNet.override_config do |config|
|
43
|
+
config.table_scan_limit = 99
|
44
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
45
|
+
alerts.should include("no index possible")
|
46
|
+
|
47
|
+
config.table_scan_limit = 100
|
48
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
49
|
+
alerts.should_not include("no index possible")
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
it "should detect dependent subqueries" do
|
54
|
+
connection.should_receive(:select).with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"select_type" => "dependent", "rows" => 100}])
|
55
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
56
|
+
alerts.should include("dependent subquery")
|
57
|
+
end
|
58
|
+
|
59
|
+
it "should detect uncacheable subqueries" do
|
60
|
+
connection.should_receive(:select).with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"select_type" => "uncacheable", "rows" => 100}])
|
61
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
62
|
+
alerts.should include("uncacheable subquery")
|
63
|
+
end
|
64
|
+
|
65
|
+
it "should detect full scan on null key" do
|
66
|
+
connection.should_receive(:select).with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"Extra" => "full scan on null key", "rows" => 100}])
|
67
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
68
|
+
alerts.should include("full scan on null key")
|
69
|
+
end
|
70
|
+
|
71
|
+
it "should detect excess temporary table usage" do
|
72
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"Extra" => "using temporary", "rows" => 100}])
|
73
|
+
|
74
|
+
SqlSafetyNet.override_config do |config|
|
75
|
+
config.temporary_table_limit = 99
|
76
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
77
|
+
alerts.should include("uses temporary table for 100 rows")
|
78
|
+
|
79
|
+
config.temporary_table_limit = 100
|
80
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
81
|
+
alerts.should_not include("uses temporary table for 100 rows")
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
it "should detect excess filesort usage" do
|
86
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"Extra" => "filesort", "rows" => 100}])
|
87
|
+
|
88
|
+
SqlSafetyNet.override_config do |config|
|
89
|
+
config.filesort_limit = 99
|
90
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
91
|
+
alerts.should include("uses filesort for 100 rows")
|
92
|
+
|
93
|
+
config.filesort_limit = 100
|
94
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
95
|
+
alerts.should_not include("uses filesort for 100 rows")
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should detect examining too many rows" do
|
100
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", "EXPLAIN", []).and_return([{"rows" => 100}])
|
101
|
+
|
102
|
+
SqlSafetyNet.override_config do |config|
|
103
|
+
config.examined_rows_limit = 99
|
104
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
105
|
+
alerts.should include("examined 100 rows")
|
106
|
+
|
107
|
+
config.examined_rows_limit = 100
|
108
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
109
|
+
alerts.should be_empty
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::ExplainPlan::Postgresql do
|
4
|
+
class PostgresqlConnection; end
|
5
|
+
SqlSafetyNet::ExplainPlan.enable_on_connection_adapter!(PostgresqlConnection, :postgresql)
|
6
|
+
|
7
|
+
let(:connection){ PostgresqlConnection.new }
|
8
|
+
let(:sql){ "SELECT * FROM *" }
|
9
|
+
|
10
|
+
it "should flag excessive table scans" do
|
11
|
+
query_plan = [{"QUERY PLAN"=>"Seq Scan on records (cost=0.00..12.20 rows=100 width=335)"}]
|
12
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", 'EXPLAIN', []).and_return(query_plan)
|
13
|
+
SqlSafetyNet.override_config do |config|
|
14
|
+
config.table_scan_limit = 99
|
15
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
16
|
+
alerts.should include("table scan on ~100 rows")
|
17
|
+
|
18
|
+
config.table_scan_limit = 100
|
19
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
20
|
+
alerts.should be_empty
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should flag too many rows examined" do
|
25
|
+
query_plan = [{"QUERY PLAN"=>"Index Scan using records_pkey on records (cost=0.00..8.27 rows=100 width=336)"}]
|
26
|
+
connection.should_receive(:select).twice.with("EXPLAIN #{sql}", 'EXPLAIN', []).and_return(query_plan)
|
27
|
+
SqlSafetyNet.override_config do |config|
|
28
|
+
config.examined_rows_limit = 99
|
29
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
30
|
+
alerts.should include("examined ~100 rows")
|
31
|
+
|
32
|
+
config.examined_rows_limit = 100
|
33
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
34
|
+
alerts.should be_empty
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should apply a limit to the rows returned" do
|
39
|
+
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)"}]
|
40
|
+
connection.should_receive(:select).with("EXPLAIN #{sql}", 'EXPLAIN', []).and_return(query_plan)
|
41
|
+
alerts = connection.sql_safety_net_analyze_query_plan(sql, [])
|
42
|
+
alerts.should be_empty
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::Formatter do
|
4
|
+
|
5
|
+
let(:query_info){ SqlSafetyNet::QueryInfo.new("SELECT * FROM *", :elapsed_time => 0.1, :rows => 1, :result_size => 500) }
|
6
|
+
let(:analysis){ SqlSafetyNet::QueryAnalysis.new }
|
7
|
+
|
8
|
+
it "should convert an analysis to valid XHTML" do
|
9
|
+
analysis << query_info
|
10
|
+
formatter = SqlSafetyNet::Formatter.new(analysis)
|
11
|
+
html = formatter.to_html
|
12
|
+
html.should match(/<div [^>]*id="_sql_safety_net_"/)
|
13
|
+
parsed = ActiveSupport::XmlMini.parse(html)
|
14
|
+
parsed["div"]["id"].should == "_sql_safety_net_"
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should convert an analysis to a string" do
|
18
|
+
analysis << query_info
|
19
|
+
formatter = SqlSafetyNet::Formatter.new(analysis)
|
20
|
+
text = formatter.to_s
|
21
|
+
text.should include("1 query, 1 row")
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should give a summary of an analysis" do
|
25
|
+
analysis << query_info
|
26
|
+
formatter = SqlSafetyNet::Formatter.new(analysis)
|
27
|
+
formatter.summary.should == "1 query, 1 row, 0.5K, 100ms"
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should pluralize words in the summary" do
|
31
|
+
analysis << query_info
|
32
|
+
analysis << query_info
|
33
|
+
formatter = SqlSafetyNet::Formatter.new(analysis)
|
34
|
+
formatter.summary.should == "2 queries, 2 rows, 1.0K, 200ms"
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should creat a CSS style for the HTML div" do
|
38
|
+
formatter = SqlSafetyNet::Formatter.new(analysis)
|
39
|
+
style = formatter.div_style("width" => "200px", "left" => "10px", "text-decoration" => "underline", "font-weight" => nil)
|
40
|
+
style.should include("left:10px;")
|
41
|
+
style.should include("top:5px;")
|
42
|
+
style.should include("width:200px")
|
43
|
+
style.should include("text-decoration:underline")
|
44
|
+
style.should_not include("font-weight")
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe SqlSafetyNet::Middleware do
|
4
|
+
|
5
|
+
before :all do
|
6
|
+
SqlSafetyNet::TestModel.delete_all
|
7
|
+
SqlSafetyNet::TestModel.create(:name => "test")
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:app){ lambda{|env| [env[:status] || 200, {"Content-Type" => env[:type] || "text/plain"}, ["<body>Hello</body>"]]} }
|
11
|
+
let(:app_with_query){ lambda{|env| SqlSafetyNet::TestModel.first; [env[:status] || 200, {"Content-Type" => env[:type] || "text/plain"}, ["<body>Hello</body>"]]} }
|
12
|
+
|
13
|
+
it "should return the original response code" do
|
14
|
+
middleware = SqlSafetyNet::Middleware.new(app)
|
15
|
+
response = middleware.call(:status => 301)
|
16
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
17
|
+
response.status.should == 301
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not return the X-SqlSafetyNet header if not queries occurred" do
|
21
|
+
middleware = SqlSafetyNet::Middleware.new(app)
|
22
|
+
response = middleware.call(:status => 200)
|
23
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
24
|
+
response["X-SqlSafetyNet"].should == nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should return the original response headers plus X-SqlSafetyNet" do
|
28
|
+
middleware = SqlSafetyNet::Middleware.new(app_with_query)
|
29
|
+
response = middleware.call(:status => 200)
|
30
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
31
|
+
response["X-SqlSafetyNet"].should include("1 query, 1 row")
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should return the original body if no queries performed" do
|
35
|
+
middleware = SqlSafetyNet::Middleware.new(app)
|
36
|
+
response = middleware.call(:type => "text/html")
|
37
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
38
|
+
body = response.body.join("")
|
39
|
+
body.should == "<body>Hello</body>"
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should return the original body plus the sql analysis if HTML and not Ajax and always show is on" do
|
43
|
+
SqlSafetyNet.override_config do |config|
|
44
|
+
config.always_show = true
|
45
|
+
middleware = SqlSafetyNet::Middleware.new(app_with_query)
|
46
|
+
response = middleware.call(:type => "text/html; charset=UTF-8")
|
47
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
48
|
+
body = response.body.join("")
|
49
|
+
body.should include("<body>Hello</body>")
|
50
|
+
body.should include("_sql_safety_net_")
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should return the original body plus the sql analysis if XHTML and not Ajax and the queries are flagged" do
|
55
|
+
SqlSafetyNet.override_config do |config|
|
56
|
+
config.query_limit = 0
|
57
|
+
middleware = SqlSafetyNet::Middleware.new(app_with_query)
|
58
|
+
response = middleware.call(:type => "text/xhtml; charset=UTF-8")
|
59
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
60
|
+
body = response.body.join("")
|
61
|
+
body.should include("<body>Hello</body>")
|
62
|
+
body.should include("_sql_safety_net_")
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
it "should return the original body only if always_on is false and no flagged query" do
|
67
|
+
middleware = SqlSafetyNet::Middleware.new(app_with_query)
|
68
|
+
response = middleware.call(:type => "text/html; charset=UTF-8")
|
69
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
70
|
+
body = response.body.join("")
|
71
|
+
body.should == "<body>Hello</body>"
|
72
|
+
end
|
73
|
+
|
74
|
+
it "should return the original body only if Ajax" do
|
75
|
+
middleware = SqlSafetyNet::Middleware.new(app)
|
76
|
+
response = middleware.call(:type => "text/html", "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest")
|
77
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
78
|
+
body = response.body.join("")
|
79
|
+
body.should == "<body>Hello</body>"
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should return the original body only if not HTML" do
|
83
|
+
middleware = SqlSafetyNet::Middleware.new(app)
|
84
|
+
response = middleware.call(:type => "text/plain")
|
85
|
+
response = Rack::Response.new(response[2], response[0], response[1])
|
86
|
+
body = response.body.join("")
|
87
|
+
body.should == "<body>Hello</body>"
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|