require 'spec_helper'
require 'ronin/code/sql/injection'

describe Ronin::Code::SQL::Injection do
  describe "PLACE_HOLDERS" do
    subject { described_class::PLACE_HOLDERS }

    it { should include(integer: 1)   }
    it { should include(decimal: 1.0) }
    it { should include(string: '1')  }
    it { should include(list: [nil])  }
    it { should include(column: :id)  }
  end

  describe "#initialize" do
    context "with no arguments" do
      it { expect(subject.escape).to       eq(:integer) }
      it { expect(subject.place_holder).to eq(1)        }
    end

    context "with :escape" do
      context "with no :place_holder" do
        let(:place_holders) { described_class::PLACE_HOLDERS }
        let(:escape) { :string }

        subject { described_class.new(escape: escape) }

        it "should default the place_holder based on the :escape type" do
          expect(subject.place_holder).to eq(place_holders[escape])
        end
      end
    end

    context "with :place_holder" do
      let(:data) { 'A' }

      subject { described_class.new(place_holder: data) }

      it "should pass it to the InjectionExpr" do
        expect(subject.expression.expression).to eq(data)
      end
    end

    context "when a block is given" do
      subject { described_class.new { @x = 1 } }

      it "should instance_eval the block" do
        expect(subject.instance_variable_get(:@x)).to eq(1)
      end
    end
  end

  describe "#to_sql" do
    context "without an expression" do
      subject { described_class.new(place_holder: 1) }

      it "should still emit the place-holder value" do
        expect(subject.to_sql).to eq('1')
      end

      context "with clauses" do
        subject do
          sqli = described_class.new(place_holder: 1)
          sqli.limit(100).offset(10)
          sqli
        end

        it "should emit the clauses" do
          expect(subject.to_sql).to eq('1 LIMIT 100 OFFSET 10')
        end
      end
    end

    context "with an expression" do
      subject do
        sqli = described_class.new
        sqli.or { 1 == 1 }
        sqli
      end

      it "should emit the expression" do
        expect(subject.to_sql).to eq('1 OR 1=1')
      end

      context "with clauses" do
        subject do
          sqli = described_class.new
          sqli.or { 1 == 1 }.limit(100).offset(10)
          sqli
        end

        it "should emit the clauses" do
          expect(subject.to_sql).to eq('1 OR 1=1 LIMIT 100 OFFSET 10')
        end

        context "with :space" do
          it "should emit the clauses with custom spaces" do
            expect(subject.to_sql(space: '/**/')).to eq('1/**/OR/**/1=1/**/LIMIT/**/100/**/OFFSET/**/10')
          end
        end
      end

      context "with statements" do
        subject do
          sqli = described_class.new
          sqli.or { 1 == 1 }.select(1,2,3)
          sqli
        end

        it "should emit the clauses" do
          expect(subject.to_sql).to eq('1 OR 1=1; SELECT (1,2,3)')
        end

        context "with :space" do
          it "should emit the statements with custom spaces" do
            expect(subject.to_sql(space: '/**/')).to eq('1/**/OR/**/1=1;/**/SELECT/**/(1,2,3)')
          end
        end
      end
    end

    context "when escaping a string value" do
      context "when the place-holder and last operand are Strings" do
        subject do
          sqli = described_class.new(escape: :string)
          sqli.or { string(1) == string(1) }
          sqli
        end 

        it "should balance the quotes" do
          expect(subject.to_sql).to eq("1' OR '1'='1")
        end
      end

      context "when the place-holder and last operand are not both Strings" do
        subject do
          sqli = described_class.new(escape: :string)
          sqli.or { int(1) == int(1) }
          sqli
        end 

        it "should terminate the SQL statement" do
          expect(subject.to_sql).to eq("1' OR 1=1;--")
        end
      end

      context "when terminating" do
        subject do
          sqli = described_class.new(escape: :string)
          sqli.or { string(1) == string(1) }
          sqli
        end 

        it "should terminate the SQL statement" do
          expect(subject.to_sql(terminate: true)).to eq("1' OR '1'='1';--")
        end
      end
    end

    context "when terminating" do
      subject do
        sqli = described_class.new(escape: :integer)
        sqli.or { 1 == 1 }
        sqli
      end 

      it "should terminate the SQL statement" do
        expect(subject.to_sql(terminate: true)).to eq("1 OR 1=1;--")
      end
    end
  end
end