# frozen_string_literal: true require "support/application_record" require "support/test_class" require "support/association_test_class" # rubocop:disable Metrics/BlockLength RSpec.shared_examples ".order_as_specified" do # Clean up after each test. This is a lot lighter for these few tests than # trying to wrangle with RSpec-Rails to get transactional tests to work. after :each do TestClass.delete_all AssociationTestClass.delete_all end let(:shuffled_objects) do Array.new(5) { |i| TestClass.create(field: "Field #{i}") }.shuffle end let(:shuffled_object_fields) { shuffled_objects.map(&:field) } let(:shuffled_object_ids) { shuffled_objects.map(&:id) } let(:omitted_object) { TestClass.create(field: "Nothing") } context "with no table name specified" do subject { TestClass.order_as_specified(field: shuffled_object_fields) } it "returns results including unspecified objects" do omitted_object # Build an object that isn't sorted in this list. expect(subject).to include omitted_object end it "returns results in the given order" do omitted_object # Build an object that isn't sorted in this list. expect(subject.map(&:id)). to eq [*shuffled_object_ids, omitted_object.id] end context "when the order is chained with other orderings" do subject do TestClass. order_as_specified(field: shuffled_object_fields.take(3)). order(:id) end it "returns results in the given order by multiple fields" do shuffled_objects # Build these objects first. omitted_object # Build an object that isn't sorted in this list. expect(subject.map(&:id)).to eq [ *shuffled_object_ids.take(3), *shuffled_object_ids.drop(3).sort, omitted_object.id ] end end context "when the order includes nil" do let(:shuffled_objects) do Array.new(5) do |i| TestClass.create(field: (i == 0 ? nil : "Field #{i}")) end.shuffle end it "raises an error" do expect { subject }.to raise_error(OrderAsSpecified::Error) end end end context "with another table name specified" do subject do TestClass. joins(:association_test_class). order_as_specified( association_test_classes: { id: associated_object_ids } ) end let(:associated_objects) do shuffled_objects.map do |object| AssociationTestClass.create(test_class: object) end end let(:associated_object_ids) { associated_objects.map(&:id) } let(:omitted_associated_object) do AssociationTestClass.create(test_class: omitted_object) end it "returns results including unspecified objects" do # Build an object that isn't sorted in this list. omitted_associated_object expect(subject).to include omitted_object end it "returns results in the given order" do # Build an object that isn't sorted in this list. omitted_associated_object expect(subject.map(&:id)). to eq [*shuffled_object_ids, omitted_object.id] end end context "input safety" do before(:each) do 2.times { |i| TestClass.create(field: "foo#{i}") } end it "sanitizes column values" do # Verify that the result set includes two records when using good column # value. good_value = "foo" records = TestClass.order_as_specified(field: [good_value]).to_a expect(records.count).to eq(2) # Attempt to inject a LIMIT clause into the query. If the SQL inputs are # properly sanitized, it will be ignored and the returned result set will # include two records. If not, the LIMIT clause will execute and the # result set will include just one record. bad_value = "' LIMIT 1 --" records = TestClass.order_as_specified(field: [bad_value]).to_a expect(records.count).to eq(2) end it "quotes column and table names" do table = "association_test_classes" quoted_table = AssociationTestClass.connection.quote_table_name(table) column = "id" quoted_column = AssociationTestClass.connection.quote_column_name(column) sql = TestClass.order_as_specified(table => { column => ["foo"] }).to_sql expect(sql).to include("ORDER BY #{quoted_table}.#{quoted_column}") end end end # rubocop:enable Metrics/BlockLength