# frozen_string_literal: true require 'rails_helper' describe DataExplorer::QueryController do def response_json MultiJson.load(response.body) end before do SiteSetting.data_explorer_enabled = true end def make_query(sql, opts = {}, group_ids = []) query = DataExplorer::Query.create!(name: opts[:name] || "Query number", description: "A description for query number", sql: sql, hidden: opts[:hidden] || false) group_ids.each do |group_id| query.query_groups.create!(group_id: group_id) end query end describe "Admin" do routes { ::DataExplorer::Engine.routes } let!(:admin) { log_in_user(Fabricate(:admin)) } describe "when disabled" do before do SiteSetting.data_explorer_enabled = false end it 'denies every request' do get :index expect(response.body).to be_empty get :index, format: :json expect(response.status).to eq(404) get :schema, format: :json expect(response.status).to eq(404) get :show, params: { id: 3 }, format: :json expect(response.status).to eq(404) post :create, params: { id: 3 }, format: :json expect(response.status).to eq(404) post :run, params: { id: 3 }, format: :json expect(response.status).to eq(404) put :update, params: { id: 3 }, format: :json expect(response.status).to eq(404) delete :destroy, params: { id: 3 }, format: :json expect(response.status).to eq(404) end end describe "#index" do before do require_dependency File.expand_path('../../../lib/queries.rb', __FILE__) end it "behaves nicely with no user created queries" do DataExplorer::Query.destroy_all get :index, format: :json expect(response.status).to eq(200) expect(response_json['queries'].count).to eq(Queries.default.count) end it "shows all available queries in alphabetical order" do DataExplorer::Query.destroy_all make_query('SELECT 1 as value', name: 'B') make_query('SELECT 1 as value', name: 'A') get :index, format: :json expect(response.status).to eq(200) expect(response_json['queries'].length).to eq(Queries.default.count + 2) expect(response_json['queries'][0]['name']).to eq('A') expect(response_json['queries'][1]['name']).to eq('B') end it "doesn't show hidden/deleted queries" do DataExplorer::Query.destroy_all make_query('SELECT 1 as value', name: 'A', hidden: false) make_query('SELECT 1 as value', name: 'B', hidden: true) make_query('SELECT 1 as value', name: 'C', hidden: true) get :index, format: :json expect(response.status).to eq(200) expect(response_json['queries'].length).to eq(Queries.default.count + 1) end end describe "#run" do let!(:admin) { log_in(:admin) } def run_query(id, params = {}) params = Hash[params.map { |a| [a[0], a[1].to_s] }] post :run, params: { id: id, _params: MultiJson.dump(params) }, format: :json end it "can run queries" do q = make_query('SELECT 23 as my_value') run_query q.id expect(response.status).to eq(200) expect(response_json['success']).to eq(true) expect(response_json['errors']).to eq([]) expect(response_json['columns']).to eq(['my_value']) expect(response_json['rows']).to eq([[23]]) end it "can process parameters" do q = make_query <<~SQL -- [params] -- int :foo = 34 SELECT :foo as my_value SQL run_query q.id, foo: 23 expect(response.status).to eq(200) expect(response_json['errors']).to eq([]) expect(response_json['success']).to eq(true) expect(response_json['columns']).to eq(['my_value']) expect(response_json['rows']).to eq([[23]]) run_query q.id expect(response.status).to eq(200) expect(response_json['errors']).to eq([]) expect(response_json['success']).to eq(true) expect(response_json['columns']).to eq(['my_value']) expect(response_json['rows']).to eq([[34]]) # 2.3 is not an integer run_query q.id, foo: '2.3' expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/ValidationError/) end it "doesn't allow you to modify the database #1" do p = create_post q = make_query <<~SQL UPDATE posts SET cooked = '

you may already be a winner!

' WHERE id = #{p.id} RETURNING id SQL run_query q.id p.reload # Manual Test - comment out the following lines: # DB.exec "SET TRANSACTION READ ONLY" # raise ActiveRecord::Rollback # This test should fail on the below check. expect(p.cooked).to_not match(/winner/) expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/read-only transaction/) end it "doesn't allow you to modify the database #2" do p = create_post q = make_query <<~SQL SELECT 1 ) SELECT * FROM query; RELEASE SAVEPOINT active_record_1; SET TRANSACTION READ WRITE; UPDATE posts SET cooked = '

you may already be a winner!

' WHERE id = #{p.id}; SAVEPOINT active_record_1; SET TRANSACTION READ ONLY; WITH query AS ( SELECT 1 SQL run_query q.id p.reload # Manual Test - change out the following line: # # module ::DataExplorer # def self.run_query(...) # if query.sql =~ /;/ # # to # # if false && query.sql =~ /;/ # # Afterwards, this test should fail on the below check. expect(p.cooked).to_not match(/winner/) expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/semicolon/) end it "doesn't allow you to lock rows" do q = make_query <<~SQL SELECT id FROM posts FOR UPDATE SQL run_query q.id expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/read-only transaction/) end it "doesn't allow you to create a table" do q = make_query <<~SQL CREATE TABLE mytable (id serial) SQL run_query q.id expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/read-only transaction|syntax error/) end it "doesn't allow you to break the transaction" do q = make_query <<~SQL COMMIT SQL run_query q.id expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/syntax error/) q.sql = <<~SQL ) SQL run_query q.id expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/syntax error/) q.sql = <<~SQL RELEASE SAVEPOINT active_record_1 SQL run_query q.id expect(response.status).to eq(422) expect(response_json['errors']).to_not eq([]) expect(response_json['success']).to eq(false) expect(response_json['errors'].first).to match(/syntax error/) end it "can export data in CSV format" do q = make_query('SELECT 23 as my_value') post :run, params: { id: q.id, download: 1 }, format: :csv expect(response.status).to eq(200) end context "`limit` parameter" do before do create_post create_post create_post end it "should limit the results in JSON response" do begin original_const = DataExplorer::QUERY_RESULT_DEFAULT_LIMIT DataExplorer.send(:remove_const, "QUERY_RESULT_DEFAULT_LIMIT") DataExplorer.const_set("QUERY_RESULT_DEFAULT_LIMIT", 2) q = make_query <<~SQL SELECT id FROM posts SQL run_query q.id expect(response_json['rows'].count).to eq(2) post :run, params: { id: q.id, limit: 1 }, format: :json expect(response_json['rows'].count).to eq(1) post :run, params: { id: q.id, limit: "ALL" }, format: :json expect(response_json['rows'].count).to eq(3) ensure DataExplorer.send(:remove_const, "QUERY_RESULT_DEFAULT_LIMIT") DataExplorer.const_set("QUERY_RESULT_DEFAULT_LIMIT", original_const) end end it "should limit the results in CSV download" do begin original_const = DataExplorer::QUERY_RESULT_MAX_LIMIT DataExplorer.send(:remove_const, "QUERY_RESULT_MAX_LIMIT") DataExplorer.const_set("QUERY_RESULT_MAX_LIMIT", 2) ids = Post.order(:id).pluck(:id) q = make_query <<~SQL SELECT id FROM posts SQL post :run, params: { id: q.id, download: 1 }, format: :csv expect(response.body.split("\n").count).to eq(3) post :run, params: { id: q.id, download: 1, limit: 1 }, format: :csv expect(response.body.split("\n").count).to eq(2) # The value `ALL` is not supported in csv exports. post :run, params: { id: q.id, download: 1, limit: "ALL" }, format: :csv expect(response.body.split("\n").count).to eq(1) ensure DataExplorer.send(:remove_const, "QUERY_RESULT_MAX_LIMIT") DataExplorer.const_set("QUERY_RESULT_MAX_LIMIT", original_const) end end end end end describe "Non-Admin" do routes { Discourse::Application.routes } let(:user) { Fabricate(:user) } let(:group) { Fabricate(:group, users: [user]) } before do log_in_user(user) end describe "when disabled" do before do SiteSetting.data_explorer_enabled = false end it 'denies every request' do get :group_reports_index, params: { group_name: 1 }, format: :json expect(response.status).to eq(404) get :group_reports_show, params: { group_name: 1, id: 1 }, format: :json expect(response.status).to eq(404) post :group_reports_run, params: { group_name: 1, id: 1 }, format: :json expect(response.status).to eq(404) end end describe "#group_reports_index" do it "only returns queries that the group has access to" do group.add(user) make_query('SELECT 1 as value', { name: 'A' }, ["#{group.id}"]) get :group_reports_index, params: { group_name: group.name }, format: :json expect(response.status).to eq(200) expect(response_json['queries'].length).to eq(1) expect(response_json['queries'][0]['name']).to eq('A') end it "returns a 404 when the user should not have access to the query " do other_user = Fabricate(:user) log_in_user(other_user) get :group_reports_index, params: { group_name: group.name }, format: :json expect(response.status).to eq(404) end it "return a 200 when the user has access the the query" do group.add(user) get :group_reports_index, params: { group_name: group.name }, format: :json expect(response.status).to eq(200) end it "does not return hidden queries" do group.add(user) make_query('SELECT 1 as value', { name: 'A', hidden: true }, ["#{group.id}"]) make_query('SELECT 1 as value', { name: 'B' }, ["#{group.id}"]) get :group_reports_index, params: { group_name: group.name }, format: :json expect(response.status).to eq(200) expect(response_json['queries'].length).to eq(1) expect(response_json['queries'][0]['name']).to eq('B') end end describe "#group_reports_run" do it "calls run on QueryController" do query = make_query('SELECT 1 as value', { name: 'B' }, ["#{group.id}"]) controller.expects(:run).at_least_once get :group_reports_run, params: { group_name: group.name, id: query.id }, format: :json end it "returns a 404 when the user should not have access to the query " do group.add(user) query = make_query('SELECT 1 as value', {}, []) get :group_reports_run, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(404) end it "return a 200 when the user has access the the query" do group.add(user) query = make_query('SELECT 1 as value', {}, [group.id.to_s]) get :group_reports_run, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(200) end it "return a 404 when the query is hidden" do group.add(user) query = make_query('SELECT 1 as value', { hidden: true }, [group.id.to_s]) get :group_reports_run, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(404) end end describe "#group_reports_show" do let(:group) { Fabricate(:group) } it "returns a 404 when the user should not have access to the query " do user = Fabricate(:user) log_in_user(user) group.add(user) query = make_query('SELECT 1 as value', {}, []) get :group_reports_show, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(404) end it "return a 200 when the user has access the the query" do user = Fabricate(:user) log_in_user(user) group.add(user) query = make_query('SELECT 1 as value', {}, [group.id.to_s]) get :group_reports_show, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(200) end it "return a 404 when the query is hidden" do user = Fabricate(:user) log_in_user(user) group.add(user) query = make_query('SELECT 1 as value', { hidden: true }, [group.id.to_s]) get :group_reports_show, params: { group_name: group.name, id: query.id }, format: :json expect(response.status).to eq(404) end end end end