Неправильный анализ массива чисел с плавающей запятой при использовании HTTP-глаголов в тесте Rspecs

Я определил следующие классы:

магазин.рб:

class Shop
    field: :reputation, Float
    embeds_one :location, class_name: "Location"
    accepts_nested_attributes_for :location
end

location.rb:

class Location
  include Mongoid::Document

  field :address, type: String
  field :coordinates, type: Array
  field :place_id, type: String

  validate :coordinates_must_be_pair_of_float

  private

  def coordinates_must_be_pair_of_float
    unless coordinates.is_a?(Array) && coordinates.size == 2
      errors.add(:coordinates, "must be an array with exactly two elements")
      return
    end

    coordinates.each do |coord|
      unless coord.is_a?(Float)
        errors.add(:coordinates, "must contain only integers")
        return
      end
    end
  end
end

В shop_controller.rb:

  def create
    shop = Shop.new(shop_params)

    if shop.save
      render json: { message: 'Shop created successfully', shop: shop }, status: :created
    else
      render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def shop_params
    params.require(:shop).permit(
      :reputation,
      location_attributes: [:address, :place_id, coordinates: []],
    )
  end

Наконец, в shop_spect.rb:

  let(:location) { { address: "St. My Street", coordinates: [-100.0, 100.0], place_id: "12345" } }

  describe "POST /shop" do
    it "creates a new shop" do
      shop_data = {
        reputation: 800
        location_attributes: location,
      }

      post "/shop", params: { shop: shop_data }

      if response.status == 422
        errors = JSON.parse(response.body)["errors"]
        puts "Validation errors: #{errors.join(', ')}" # Display the error messages
      end

      expect(response).to have_http_status(201)

Когда я делаю POST, используя завиток, как показано ниже:

curl -X POST \                   
  -H "Content-Type: application/json" \
  -d '{
    "shop": {
      "reputation": 800,
      "location_attributes": {
        "address": "My Street",
        "coordinates": [-100.0, 100.0],
        "place_id": "12345"
      },
    }
  }' \
  "http://localhost:3000/shop"

Все работает нормально, но тест провалился с кодом ошибки 422, т. е. экземпляр не удалось сохранить. Через некоторое время я понял проблему: массив координат не обрабатывался так же, как обрабатывалась репутация; тип значений, содержащихся в массиве координат, был: Кодировка, UTF8. .

Также это значение параметров в тесте:

{:shop=>{:price=>800, :location_attributes=>{:address=>"My Street", :coordinates=>[-100.0, 100.0], :place_id=>"12345"}}}

и это значение параметров в контроллере:

{"shop"=>{"reputation"=>"800", "location_attributes"=>{"address"=>"My Street", "coordinates"=>["-100.0", "100.0"], "place_id"=>"12345"} }, "price"=>"800"}, "controller"=>"advert", "action"=>"create"}

наконец, это значение параметров в контроллере, когда я делаю запрос с помощью curl:

{"shop"=>{"reputation"=>800, "location_attributes"=>{"address"=>"My Street", "coordinates"=>[-100.0, 100.0], "place_id"=>"12345"}}, "controller"=>"advert", "action"=>"create"}

Очевидно, что теги преобразуются в строки, но почему целые числа и числа с плавающей запятой также преобразуются в строки при использовании post в rspecs?

Таким образом, проверка в классе местоположения не удалась. Чтобы решить эту проблему, мне пришлось изменить контроллер следующим образом: shop_controller.rb:

  def create
    shop = Shop.new(shop_params)
    shop.location.coordinates.map!(&:to_f)

    if shop.save
      render json: { message: 'Shop created successfully', shop: shop }, status: :created
    else
      render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
    end
  end

  private

  def shop_params
    params.require(:shop).permit(
      :reputation,
      location_attributes: [:address, :place_id, coordinates: []],
    )
  end

Я не понимаю, почему это происходит. Почему анализатор интерпретирует содержимое массива как данные в кодировке UTF8, а не как значения с плавающей запятой, как это происходит с полем репутации?

И еще, есть ли способ определить shop_params? Почему следующее определение неверно:

  def shop_params
    params.require(:shop).permit(
      :reputation,
      location_attributes: [:address, :place_id, :coordinates],
    )
  end

@engineersmnky, ты прав. Я исправил пример.

Baltasar del Sol de Haro 02.05.2024 22:26

@engineersmnky, я ответил на вопрос

Baltasar del Sol de Haro 02.05.2024 23:05

Я думаю, что уже знаю, что происходит и как это исправить, но хочу продолжить расследование.

Baltasar del Sol de Haro 03.05.2024 00:36

Вам следует устранить очевидную проблему field: :reputation, вызывающую синтаксическую ошибку, и убедиться, что код вашего примера действительно работает, чтобы нам не пришлось тратить время.

max 03.05.2024 10:50
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
4
71
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Но почему целые числа и числа с плавающей запятой также преобразуются в строки при использовании post в rspecs?

Это имеет мало общего с RSpec.

В вашей спецификации вы на самом деле не отправляете запрос JSON, поскольку по умолчанию для метода post используется application/x-www-form-urlencoded (который в Rails рассматривается как формат :html).

Для отправки JSON используйте:

post "/shop", params: { shop: shop_data }, format: :json

На самом деле это помощник, предоставляемый базовым ActionDispatch::IntegrationTest, который RSpec просто оборачивает.

Причина, по которой вы теперь получаете строки, заключается в том, что параметры данных формы HTTP на самом деле не типизированы. Это просто пары ключей и значений в виде строк.

Более того, ваш контроллер на самом деле не ограничивает формат запроса JSON, что позволяет этой ошибке проскользнуть. Я бы использовал MimeResponds, чтобы вместо этого получить исключение ActionController::UnknownFormat.

class ShopsController < ApplicationController
  # ...
  def create
    shop = Shop.new(shop_params)
    # don't do this - it's just silly to make the controller fix 
    # bad modeling 
    # shop.location.coordinates.map!(&:to_f)

    # this will raise if the client requests HTML
    respond_to :json do 
      if shop.save
        render json: { message: 'Shop created successfully', shop: shop }, status: :created
      else
        render json: { errors: shop.errors.full_messages }, status: :unprocessable_entity
      end
    end
  end
end

Использование типа массива — просто плохая идея. Я бы просто определил два поля типа с плавающей запятой, поскольку это намного менее шатко и дает вам приведение типов и два отдельных атрибута, так что вы действительно можете получить lat или lng в своем коде, не вытаскивая их из массива.

class Location
  include Mongoid::Document

  field :address, type: String
  field :place_id, type: String
  field :latitude, type: Float
  field :longitude, type: Float

  validates :longitude, :latitude, presence: true,
                                   numericality: true

  # Sets the latitude and longitude from an array or list
  def coordinates=(*args)
    self.latitude, self.longitude = *args.flatten
  end

  def coordinates
    [self.latitude, self.longitude]
  end
end

Если вы действительно хотите принять параметр как массив, вам необходимо внести его в белый список как таковой, поскольку массивы не являются разрешенным скалярным типом.

  def shop_params
    params.require(:shop).permit(
      :reputation,
      location_attributes: [
        :address, 
        :place_id, 
        coordinates: []
      ]
    )
  end

Если ОП действительно хочет coordinates в качестве Array, тогда def coordinates=(value); write_attribute(:coordinates,value.map(&:to_f));end тоже подойдет.

engineersmnky 03.05.2024 14:59

@engineersmnky, ты мог бы, но в этом нет никаких преимуществ.

max 03.05.2024 16:11

Другие вопросы по теме